GCD实践之(一)使用GCD保护property

写在前面

作为一个iOS开发者,必须要熟练使用GCD,本文是站在实际应用的角度总结GCD的用法之一:使用barrier保护property。

在多线程环境下,如果有多个线程要执行同一份代码,那么有时会出现问题,为了保证某些资源操作的可控性,需要一些手段来对这些公共资源进行保护,无论是什么语言,只要支持多线程,都得面临这个问题,即所谓的线程同步问题。本文围绕property讨论各种同步工具的保护效果,这同样可以延伸到其他需要保护的公共资源上。

atomic保护property

维持property原子操作的最简单的保护措施是为其添加atomic修饰词,这样编译器在为其生成setter和getter时对其进行原子保护。问题来了,对于使用atomic修饰的property,编译器为其生成的getter和setter是什么样子的呢?在很长时间里,由于受到不靠谱网友的误导,以为是这样:

1
2
3
4
5
6
7
8
9
10
11
12
- (NSString *)name {
{lock} // 上锁
_ret = _name; // get name
{unlock} // 释放锁
return _ret; // 返回
}
- (void)setName:(NSString *)newName {
{lock} // 上锁
{set name} // set name
{unlock} // 释放锁
}

看来认真看了Apple官方文档Threading Programming Guide,意识到这种说法是错误的。Apple的原子操作的底层实现并不是基于锁的,具体是什么样子呢?我也不知道,但肯定不是如上这样的(以后有时间深入了解一下,希望能够找到答案吧)。

那么修饰词atomic靠谱吗?它能保证相关属性getter和setter的原子性吗?能信赖它吗?

根据我的理解,不太靠谱。对于某个被atomic修饰的属性,当完全依赖于编译器自动合成getter和setter时,相信它们的原子性能够得到保证(不管它是如何合成实现的);但是我们常常免不了自己动手写setter或者getter,此时会将系统默认合成的setter或getter给覆盖掉,我们自己的代码能够保证原子性吗?如果我们只是自己实现setter和getter中的其中一个,另外一个依赖于编译器合成,那么自定义的这个该如何实现呢?

种种问题外加种种不确定,导致了在iOS开发中几乎不使用atomic修饰符(至少没在著名第三方库中看到使用它修饰属性的案例)。

P.S: 这种说法纯属一家之言,以后补充更靠谱的佐证吧!

NSLock保护property

上文的代码块恰好是互斥锁(NSLock)或者递归锁(NSRecursiveLock)保护property的基本套路,为了方便说明,再次罗列如下:

1
2
3
4
5
6
7
8
9
10
11
12
- (NSString *)name {
{lock} // 上锁
_ret = _name; // get name
{unlock} // 释放锁
return _ret; // 返回
}
- (void)setName:(NSString *)newName {
{lock} // 上锁
{set name} // set name
{unlock} // 释放锁
}

根据我的理解,在一般情况下,NSLock是能够保证property原子性的。但根据《Effective Objective-C 2.0》的描述:一旦遇到死锁,就会非常麻烦。

什么情况下会出现死锁呢?我认为至少有这么一种情况:保护name属性的锁在其他地方也被使用了,即当前线程正在持有该锁,此时正在访问别处的某个公共资源,保护该资源的锁正被另外一个线程持有,而那个线程正在获取当前线程持有的这把锁…

当然,对于属性而言,这种情况过于极端,但如果NSLock保护的不是属性而是别的更复杂的公共资源,那么这种极端情况就不是那么极端了;再者,如果保护name属性的这把锁也被用来保护其他的资源,那么问题就变得更复杂了。

总之,根据我的理解,如果确实需要使用保护property,要做到:

  • 尽量使用NSRecursiveLock,避免多次持有该锁造成死锁
  • 每个属性有一个单独的锁为之服务,不可与别的资源共用,否则问题会变得更复杂

P.S: 《Effective Objective-C 2.0》中关于NSLock保护property的这部分内容讲得非常不到位,以上纯属个人理解!

@synchronized块保护属性

所谓@synchronized块在中文世界里常被称为同步块,根据Threading Programming Guide的描述,同步块是使用同步锁的简写形式,本质仍然是使用同步锁保护公共资源。同步块保护属性的一般形式是:

1
2
3
4
5
6
7
8
9
10
11
- (void)setSomeString:(NSString *)someString {
@synchronized(self) {
_someString = someString;
}
}
- (NSString *)someString {
@synchronized(self) {
return _someString;
}
}

这种做法有什么问题呢?相对于NSLock,这种处理问题更大!主要问题是:@synchronized(self)效率极低。

根据《Effective Objective-C 2.0》的说法。因为@synchronized(){}的本质是根据给定的对象,自动创建一个锁,并等待块中的代码执行完毕。执行到这段代码结尾处,锁就被释放了。通常传给@synchronized块的对象是self。这意味着同步锁将self整个对象都保护起来了,如果滥用,其他属性也都用@synchronized(self){}这种方式确保原子性,这样在self对象上频繁加锁,那么程序可能要等待另一段与此无关的代码执行完毕,才能继续执行当前代码,这显然会降低代码效率。

GCD串行队列保护property

将对property的读写方法都安排在同一个队列中,即可保证数据同步,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@property (nonatomic, strong) NSString *name;
@property (nonatomic, strong) dispatch_queue_t serialQueue;
@synthesize name = _name;
// create a serial dispatch queue
_serialQueue = dispatch_queue_create("com.zhangbuhuai.test", nil);
// getter
- (NSString *)name {
__block NSString *localName;
dispatch_sync(_serialQueue, ^{
localName = _name;
});
return localName;
}
// setter
- (void)setName:(NSString *)name {
dispatch_sync(_serialQueue, ^{
_name = name;
});
}

此模式的思路是:把setter和getter都安排在序列化的队列里执行,这样的话,所有针对属性的访问就都同步了。为了使代码块能够设置局部变量,getter中用到了__block关键字,若是抛开这一点,这种写法比之前的那些更为整洁。全部加锁任务都在GCD中处理,而GCD是在相当深的底层来实现的,于是能够做许多优化。因此,开发者无需担心那些事,只要专心把访问方法写好就行了。

然而,还可以进一步优化,毕竟setter方法不一定非得是同步的。设置实例变量所用的block,并不需要向setter返回什么值

也就是说,setter代码可以改成下面这样:

1
2
3
4
5
6
// setter
- (void)setName:(NSString *)name {
dispatch_async(_serialQueue, ^{
_name = name;
});
}

这次只是把同步派发改成了异步派发,从调用者的角度来看,这个小改动可以提升设置方法的执行速度(毕竟直接返回而不用等待block执行完成),而读取操作与写入操作依然会按顺序执行。但是这么改有一个坏处:如果测试一下程序的性能,那么可能发现这种写法比原来慢,因为执行异步派发时,需要拷贝block。若拷贝block所用的时间明显超过执行块所用时间,则这种做法将比原来更慢。

所以,setter的block设置为asynchronous或者synchronous,得看setter的block的复杂度。

GCD并行队列和barrier保护property

其实在更多的时候,调用getter可以并发执行,而getter和setter之前不能并发执行。利用这个特点,还能写一些更快一些的代码。此时正可以体现出GCD写法的好处。用同步块或锁对象,是无法轻易实现出如下这种方案的,这次不用serial dispatch queue,而改用并发队列:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@property (nonatomic, strong) NSString *name;
@property (nonatomic, strong) dispatch_queue_t concurrentQueue;
@synthesize name = _name;
// create a concurrent dispatch queue
_concurrentQueue = dispatch_queue_create("com.zhangbuhuai.test", 0);
// getter
- (NSString *)name {
__block NSString *localName;
dispatch_sync(_concurrentQueue, ^{
localName = _name;
});
return localName;
}
// setter
- (void)setName:(NSString *)name {
dispatch_async(_concurrentQueue, ^{
_name = name;
});
}

然而,如上这样的代码,还无法正确实现同步。所有读取操作与写入操作都会在同一个队列上执行,不过由于是并发队列,所以读取与写入操作可能随时执行。而我们恰恰不想让这些操作随意执行。此问题用一个简单的GCD功能即可解决,它就是栅栏(barrier)。下列函数可以向队列中派发块,将其作为栅栏使用:

1
2
void dispatch_barrier_sync(dispatch_queue_t queue, dispatch_block_t block);
void dispatch_barrier_async(dispatch_queue_t queue, dispatch_block_t block);

在队列中,栅栏块必须单独执行,不能与其他块并行。这只对并发队列有意义,因为串行队列中的块总是按顺序逐个来执行的。并发队列如果发现接下来的要处理的block是barrier block,那么就一直要等当前所有并发块都执行完毕,才会单独执行这个栅栏块。待栅栏块执行完成后,再按正常方式继续向下执行。

在本例中,可以用栅栏块来实现属性的setter方法。在设置方法中使用了栅栏块之后,对属性的读取操作依然可以并发执行,但是写入操作却必须单独执行了,如下图所示:

代码实现很简单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@property (nonatomic, strong) NSString *name;
@property (nonatomic, strong) dispatch_queue_t concurrentQueue;
@synthesize name = _name;
// create a concurrent dispatch queue
_concurrentQueue = dispatch_queue_create("com.zhangbuhuai.test", 0);
// getter
- (NSString *)name {
__block NSString *localName;
dispatch_sync(_concurrentQueue, ^{
localName = _name;
});
return localName;
}
// setter
- (void)setName:(NSString *)name {
dispatch_barrier_async(_concurrentQueue, ^{
_name = name;
});
}

测试一下性能,就会发现,这种做法肯定比使用串行队列要快。当然,将上述代码中的dispatch_barrier_async改为dispatch_barrier_sync也是没问题的,也可能会更高效,至于原因上文已经讲到了。在实际使用时,最好还是测一测每种做法的性能,然后从中选出最适合当前场景的方案。

本文参考

GCD基础知识

并行和并发

在英文世界里,「并行」和「并发」的区别比较清晰,「并行」对应「parallelism」,「并发」对应「concurrency」;但在中文世界里二者仅一字之差,两个概念非常容易弄混淆。

各种资料对并行并发有各种各样的解释和比喻。我比较喜欢的一种,是播客节目内核恐慌中的主播Rio的描述,大概意思是:

「并发」和「并行」是一种计算模型,使得计算机能够在同一时间处理多个任务;「并发」表示逻辑概念上的「同时」,「并行」表示物理概念上的「同时」。

简单来说,若说两个任务A和B并发执行,则表示任务A和任务B在同一时间段里被执行(更多的可能是二者交替执行);若说任务A和B并行执行,则表示任务A和任务B在同时被执行(这要求计算机有多个运算器);

一句话:并行要求并发,但并发并不能保证并行。

P.S: 关于并发和并行,《Grand Central Dispatch In-Depth: Part 1/2》中有更详细的图文解释。

Dispatch Queues介绍

Dispatch Queues是GCD处理异步任务和并发任务的关键载体,简而言之,在GCD中,将task放入某个Dispatch Queue中,然后等待系统去处理之。

Dispatch queue是object-like structure,也就是说Dispatch queue在Objective-C中不是类结构,而是类类结构。dispatch queue对task的管理都遵循FIFO。GCD提供了一些公共的dispatch queue,但是用户也可以自定义一些dispatch queue;iOS对dispatch queue做了归类,分为三类:

  • Serial Dispatch Queue
  • Concurrent Dispatch Queue
  • Main Dispatch Queue

Serial Dispatch Queue

顾名思义,serial dispatch queue中的block按照先进先出(FIFO)的顺序去执行,实际上为单线程执行。即每次从queue中取出一个task进行处理;用户可以根据需要创建任意多的serial dispatch queue,serial dispatch queue彼此之间是并发的;

创建serial dispatch queue使用dispatch_queue_create方法,指定其第二个参数为DISPATCH_QUEUE_SERIAL(即NULL)即可:

1
2
dispatch_queue_t queue;
queue = dispatch_queue_create("com.example.MySerialQueue", DISPATCH_QUEUE_SERIAL);

注意:如果不算“Main Dispatch Queue”,系统中不存在所谓的global serial dispatch queue。

P.S: main dispatch queue其实也算serial dispatch queue,后文有述。

Concurrent Dispatch Queue

相对于Serial Dispatch Queue,Concurrent Dispatch Queue一次性并发执行一个或者多个task;和Serial Dispatch Queue不同,系统提供了四个global concurrent queue,使用dispatch_get_global_queue函数就可以获取这些global concurrent queue;

和Serial Dispatch Queue一样,用户也可以根据需要自己定义concurrent queue;创建concurrent dispatch queue也使用dispatch_queue_create方法,所不同的是需要指定其第二个参数为DISPATCH_QUEUE_CONCURRENT

1
2
dispatch_queue_t queue;
queue = dispatch_queue_create("com.example.MyConcurrentQueue", DISPATCH_QUEUE_CONCURRENT);

P.S: 根据我的理解,对于concurrent queue,其管理的task可能在多个不同thread上执行,至于dispatch queue管理多少个thread是未知的,这要视系统资源而定,用户无需为此烦扰。

Main Dispatch Queue

关于Main Dispatch Queue,《Concurrency Programming Guide》(Apple官方文档)的描述如下:

The main dispatch queue is a globally available serial queue that executes tasks on the application’s main thread.

根据我的理解,application的主要任务(譬如UI管理之类的)都在main dispatch queue中完成;根据文档的描述,main dispatch queue中的task都在一个thread中运行,即application’s main thread(thread 1)。

所以,如果想要更新UI,则必须在main dispatch queue中处理,获取main dispatch queue也很容易,调用dispatch_get_main_queue()函数即可。

关于Dispatch Queues的一些误解

在学习GCD过程中,我一路上有许多关于dispatch的错误理解,如下是总结:

  • 不存在所谓的「同步队列」和「异步队列」

同步或异步描述的是task与其上下文之间的关系,所以,我觉得「同步队列」和「异步队列」对于Objective-C的GCD而言是不靠谱的概念。

P.S: 补充!虽然没有「同步队列」和「异步队列」的说法,但是有「同步串行队列」和「同步并发队列」的概念。

  • Serial Dispatch Queue上的tasks并非只在同一个thread上执行

吾尝以为serial queue上的tasks都是在同一个thread上运行,后来明白了不是这样的,对于那些同步请求的任务,譬如使用dispatch_sync函数添加到serial dispatch queue中的任务,其运行的task往往与所在的上下文是同一个thread;对于那些异步请求的任务,譬如使用dispatch_async函数添加到serial dispatch queue中的任务,其运行的task往往是另一个的thread。举例说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
- (void)viewDidLoad {
[super viewDidLoad];
dispatch_queue_t aSerialQueue = dispatch_queue_create("haha", DISPATCH_QUEUE_SERIAL);
dispatch_sync(aSerialQueue, ^{
// block 1
NSLog(@"current 1: %@", [NSThread currentThread]);
});
dispatch_async(aSerialQueue, ^{
// block 2
NSLog(@"current 2: %@", [NSThread currentThread]);
});
}
//
// 执行结果:
// current 1: <NSThread: 0x7f8f397152f0>{number = 1, name = main}
// current 2: <NSThread: 0x7f8f39464db0>{number = 2, name = (null)}

block 1和block 2都由同一个serial dispatch queue管理,但它们的执行线程显然不同,前者的执行线程是thread 1,后者的执行线程是thread 2。

  • dispatch queue和thread并不存在一对一或者一对多的关系

通过设置断点等测试手段可以知道可能多个dispatch queue共用一个thread,也可能一个dispatch queue中的tasks在多个不同threads上执行。

总之,根据我的理解,thread和dispatch queue之间没有从属关系。

dispatch_sync和dispatch_async

在GCD中,dispatch_syncdispatch_async是两个函数,前者用于派发同步任务,后者用于派发异步任务,二者使用格式如下:

1
2
3
4
5
// dispatch task synchronously
dispatch_sync(someQueue1, ^{
// do something 1
});
// do something 2
1
2
3
4
5
// dispatch task asynchronously
dispatch_async(someQueue2, ^{
// do something 3
});
// do something 4

do something 2一定会在do something 1完成之后执行,即所谓的同步。当执行到dispatch_sync(...)时,其上下文被阻塞,直到dispatch_sync派发的block被执行完毕。

根据我的理解:dispatch_sync派发的block的执行线程和dispatch_sync上下文线程是同一个线程

P.S: 这个说法还没有找到权威的、直接明了的佐证。

do something 4会立即执行,而不会等到do something 3执行完,即所谓异步。当执行到dispatch_async(...)时,其上下文不被阻塞,继续运行。

根据我的理解:do something 3do something 4的执行线程往往不是同一个,即dispatch_async派发的block的执行线程和dispatch_async上下文线程不是同一个线程

来看一个示例,如下有一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 1. create a serial dispatch queue
dispatch_queue_t serial_queue=
dispatch_queue_create("com.zhangbuhuai.test", DISPATCH_QUEUE_SERIAL); // Thread 1
// 2. add tasks to serial dispatch queue
// 1) add a task synchronously
dispatch_sync(serial_queue, ^{
sleep(3); // 休眠3秒
NSLog(@"task 1"); // Thread 1
});
// 2) add a task synchronously too
dispatch_sync(serial_queue, ^{
NSLog(@"task 2"); // Thread 1
});
// 3) add a task asynchronously
dispatch_async(serial_queue, ^{
NSLog(@"task 3"); // Thread x (x != 1)
});
// 4) add a task asynchronously too
dispatch_async(serial_queue, ^{
NSLog(@"task 4"); // Thread x (x != 1)
});
NSLog(@"test end"); // Thread 1

假设创建serial_queue所在的上下文的执行线程为Thread 1,则测试结果是:NSLog(@"task 1")NSLog(@"task 2")也都在Thread 1中执行,而NSLog(@"task 3")NSLog(@"task 4")在别的Thread中执行。

执行结果:

1
2
3
4
5
task 1
task 2
test end
task 3
task 4

结果说明,对于serial dispatch queue中的tasks,无论是同步派发还是异步派发,其执行顺序都遵循FIFO;同样,这个示例也可以直观阐述dispatch_syncdispatch_async的不同效果。

dispatch_sync和dispatch_async的使用时机

在大多数时候,dispatch_syncdispatch_async的使用时机非常清晰的:

  • 如果派发的task耗时长,不想让上下文线程被阻塞,就用dispatch_async
  • 如果要处理的代码比较短,想要实现代码保护(线程安全),选用dispatch_sync

P.S: 关于dispatch_sync与线程同步(代码保护)之间的关系,以后补充

但有些时候,使用dispatch_sync或者dispatch_async都可以的情况下(譬如实现setter),就不是那么好选择了。

在《Effective Objective-C 2.0》Item 41(中文版P169)中看到非常重要的一句话:

…,因为在执行异步派发时,需要拷贝块。

我对这句话的理解是:

  • 执行同步派发(dispatch_sync)时,是不需要拷贝block的,这是因为dispatch_sync中所派发的task往往和当前上下文所处同一个Thread
  • 执行异步派发(dispatch_async)时,需要拷贝block,这是因为dispatch_async中所派发的task往往和当前上下文不同于一个Thread

所以,当选择dispatch_sync或者dispatch_async都可以的情况下,站在效率的角度,如果拷贝block的时间成本过高,则使用dispatch_sync;如果拷贝block的时间成本远低于执行block的时间成本,则使用dispatch_async

如上所引用的「…,因为在执行异步派发时,需要拷贝块」这句话,在某种程度上佐证了上文提到的两个说法:

  • dispatch_sync派发的block的执行线程和dispatch_sync上下文线程是同一个线程;
  • dispatch_async派发的block的执行线程和dispatch_async上下文线程不是同一个线程;

———————-如下是补充内容———————-

使用串行同步队列保护代码

先来看一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
- (NSURLSessionDataTask *)dataTaskWithRequest:(NSURLRequest *)request
completionHandler:(void (^)(NSURLResponse *response,
id responseObject,
NSError *error))completionHandler
{
__block NSURLSessionDataTask *dataTask = nil;
dispatch_sync(url_session_manager_creation_queue(), ^{ // mark 1
dataTask = [self.session dataTaskWithRequest:request];
});
[self addDelegateForDataTask:dataTask completionHandler:completionHandler]; // mark 2
return dataTask;
}

这段代码是在AFNetworking框架的AFURLSessionManager.m中定义的,用于创建Data Task(NSURLSessionDataTask实例),这短短几行代码的逻辑是:

  1. mark 1以同步派发的方式提交任务 – 创建一个NSURLSessionDataTask实例;
  2. mark 2处被阻塞;
  3. NSURLSessionDataTask实例创建完成,继续执行mark 2及后续代码;

曾经对这几行代码比较疑惑:为什么使用dispatch_sync派发任务呢?

现在是能够理解了,上述url_session_manager_creation_queue()函数返回的其实是一个serial dispatch queue,这种组合(dispatch_sync+串行队列)即所谓的串行同步队列。串行同步队列常被用来保护临界资源,确保临界资源的线程安全,作用类似于同步锁,估摸着dataTaskWithRequest:并不是一个线程安全方法。

使用dispatch_sync的注意事项

Concurrency Programming Guide)有包括关于使用dispatch_sync的提示:

Important: You should never call the dispatch_sync or dispatch_sync_f function from a task that is executing in the same queue that you are planning to pass to the function. This is particularly important for serial queues, which are guaranteed to deadlock, but should also be avoided for concurrent queues.

简单来说,在dispatch_sync嵌套使用时要注意:不能在一个嵌套中使用同一个serial dispatch queue,因为会发生死锁;

假设有如下这么一段代码要执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
- (void)test {
dispatch_queue_t aSerialDispatchQueue =
dispatch_queue_create("I.am.an.iOS.developer", DISPATCH_QUEUE_SERIAL);
dispatch_sync(aSerialDispatchQueue, ^{
// block 1
NSLog(@"Good Night, Benjamin");
dispatch_sync(aSerialDispatchQueue, ^{
// block 2
NSLog(@"Good Night, Daisy");
});
});
}

自己试着执行以下就会发现:「Good Night, Daisy」这一句永远都无法被打印出来,原因很简单,程序产生了死锁。为什么会产生死锁呢?

可以想象aSerialDispatchQueue在底层实现中有一把锁,这把锁确保serial dispatch queue中只有一个block被执行,当执行到block 1代码时,这把锁为block 1所持有,当block 1执行完了,会释放之;然而block 1同步派发了一个任务block 2,同步派发意味着block 1会被阻塞,直到block 2被执行完成;但是这里产生了矛盾,block 2顺利执行的前提是aSerialDispatchQueue的这把锁被block 1释放,但是block 1释放这把锁的前提是block 1执行完成…这就是典型的dead lock。

这一段代码还好,比较容易避免,但是如果对GCD理解不深,更多的时候容易写出如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
- (void)viewDidLoad {
[super viewDidLoad];
// 巴拉巴拉,做了很多事情
NSLog(@"Good Night, Benjamin");
dispatch_sync(dispatch_get_main_queue(), ^{
// refresh UI
NSLog(@"Good Night, Daisy");
});
}

这段代码的问题其实和上一段代码类似,只不过这里的serial dispatch queue恰好是main queue。

上述的死锁问题主要针对同步串行队列,对于同步并发队列,根据我的理解应该不存在这个deadlock问题,但是《Concurrency Programming Guide》明确说了:

…This is particularly important for serial queues, which are guaranteed to deadlock, but should also be avoided for concurrent queues.

P.S: 目前还不理解这个说辞。

本文参考

NSError分析

写在前面

在iOS开发中,NSError的使用非常常见,使用也比较简单,也正因为简单,所以对这一部分知识不甚注重。但是近期在做app底层网络封装时发现了一些问题。我使用的网络框架是AFNetworking,AFNetworking常常会返回一些错误信息,有时需要将这些错误信息告诉用户,通常做法是将NSError#localizedDescription以弹框的形式原原本本展现给用户(譬如“网络不畅”之类的),但是这样非常不友好,一是这些信息往往是英文,二是这些信息过于冗长,不够简洁。所以自然想到了对这些error信息进行包装。这就迫使我不得不去了解更多关于NSError相关的信息,本文着重叙述Error Domain和Error Code。

Error Domain

首先,error domain是一个字符串。因为历史原因,在OS X中将errors分为不同的domains。譬如,对于Carbon框架的error,归于OSStatus domain(NSOSStatusErrorDomain),对于POSIX error,归于NSPOSIXErrorDomain,而对于我们的iOS开发,一般使用NSCocoaErrorDomain。NSError.h定义了四个domain,如下:

1
2
3
4
5
6
7
// Predefined domain for errors from most AppKit and Foundation APIs.
FOUNDATION_EXPORT NSString *const NSCocoaErrorDomain;
// Other predefined domains; value of "code" will correspond to preexisting values in these domains.
FOUNDATION_EXPORT NSString *const NSPOSIXErrorDomain;
FOUNDATION_EXPORT NSString *const NSOSStatusErrorDomain;
FOUNDATION_EXPORT NSString *const NSMachErrorDomain;

除了上述的四个domain之外,不同的framework甚至一些classes group(相关的几个classes)也定义了自己的domain,譬如对于Web Kit framework,定义了WebKitErrorDomain,而更常见的,URL相关的classes定义了NSURLErrorDomain

Domains非常有用,特别当程序非常复杂庞大时,官方文档是这么说的:

Domains serve several useful purposes. They give Cocoa programs a way to identify the OS X subsystem that is detecting an error. They also help to prevent collisions between error codes from different subsystems with the same numeric value. In addition, domains allow for a causal relationship between error codes based on the layering of subsystems; for example, an error in the NSOSStatusErrorDomain may have an underlying error in the NSMachErrorDomain.

用户也可以为自己的framework或者app定义自己的domain,官方推荐的domain命名规则是:
com.company.framework_or_app.ErrorDomain

Error Code

Error Code的类型是signed integer。Error Code指定了特殊的错误。这个信息对于程序开发来说极为有用。比如访问URL资源timeout错误对应的是NSURLErrorTimedOut-1001)。

那么如何知道各个error code对应什么样的值呢?iOS开发中常用的error code所对应的头文件如下:

  • Foundation/FoundationErrors.h - Generic Foundation error codes
  • CoreData/CoreDataErrors.h - Core Data error codes
  • Foundation/NSURLError.h - URL error codes

以Foundation/NSURLError.h为例,其中的URLError Code值从NSURLErrorDataLengthExceedsMaximumNSURLErrorCancelled,二者分别对应-1103-999。如果对所有网络error笼统处理,这两个值可以为我所用。

The User Info Dictionary

Every NSError object has a “user info” dictionary to hold error information beyond domain and code.

User info可以包含很多自定义信息,最常用的或许是localized error information。访问localized error information有两种方式,其一是访问NSError的localizedDescription属性,其二是访问NSError#userInfoNSLocalizedDescriptionKey域。

关于user info dictionary,比较常见,这里不多讲了,更多内容参考《Error Handling Programming Guide》。

约定

为了保证一致性以及叙述方便,本博客的博文遵循一些约束。

属性/方法表示法

  • A#p表示A的实例对象的实例属性p,其中A是类型名;
  • A.p表示A的类型实例p,其中A是类型名;
  • a.p表示对象a的实例属性p,其中a是一个实例对象;