博文链接: http://ifujun.com/iosbing-fa-bian-cheng-tips-er/
在 iOS并发编程Tips(一) 中,我们提到了三点,分别是线程、原子属性和并发同步。在本文中,你将会看到以下几点:
线程安全
锁
使用主线程
GCD 还是 NSOperationQueue
线程安全是编程中的术语,指某个函数、函数库在多线程环境中被调用时,能够正确地处理多个线程之间的共享变量,使程序功能正确完成。 — 维基百科
举个例子。
我们定义一个 NSInteger
型的全局变量 count
,我们使用三个异步线程将它自增100000,那么,我们希望的输出结果是300000。但是,它的真实结果是多少呢?
#import "ViewController.h" @interface ViewController () @property (assign, nonatomic) NSInteger count; @end @implementation ViewController -(void)viewDidLoad { [super viewDidLoad]; for (int i = 0; i < 3; i++) { [self startThread]; } } -(void)startThread { dispatch_async(dispatch_get_global_queue(0, 0), ^{ [self addCount]; }); } -(void)addCount { for (int i = 0; i < 100000; i++) { self.count++; } NSLog(@"count = %ld", self.count); } -(void)addCountWithLock { @synchronized (self) { for (int i = 0; i < 100000; i++) { self.count++; } NSLog(@"lock count = %ld", self.count); } } @end
运行结果显然不是我们想要的,而且,每次的结果都不一定一致,这就是我们所要说的线程安全。
很多时候,我们为了效率,会编写多线程的代码。多线程除了会带来效率的提升之外,也会提高控制的复杂程度。我们有很多解决办法,比如说,使用锁、不可变变量、尽量使用主线程(单线程)等等。
在上述例子中,我们如果加一个最简单的互斥锁( addCountWithLock
方法),就可以达到线程安全的目的。
运行结果正是我们想要的。
还有一点想提及一下的是, 苹果有个文档 列出了部分框架的部分安全和非安全的类和函数,可以适当看一下。
上面提到了锁,我们常用的锁有很多,比如,互斥锁、条件锁、递归锁、信号量、自旋锁等等。网上有很多关于这方面的资料,我就不再赘述了,毕竟篇幅很大,而我这篇只是Tips。
网上也有很多关于这些锁性能对比的文章,比如说 ibireme的文章 等等。
这么多锁,除了比较特殊的递归锁等,如果你想要一个高性能的锁的话,可以使用 pthread_mutex
或者 dispatch_semaphore
,如果想使用比较方便的话,以直接使用 @synchronized
和 NSLock
。
在性能优化的时候,我们很容易陷入过度优化的误区。现在的设备性能越来越好,我们可以在主线程中做越来越多的事情。
如果某个函数或者方法只有主线程去访问,那它必然是多线程安全的,因为只有单线程访问,不存在多线程的情况。
我们知道 NSMutableArray
、 NSMutableDictionary
这种的是非线程安全的类,在我的使用过程中,我一般不会对这些东西加锁,因为我基本只用主线程去访问,而如果涉及到多线程的话,我会使用不可变的数组和字典。
在大多数情况下,使用多线程只存在于某一个部分,比如网络等,那么在多线程执行完成之后,一定要交由主线程回调。比如,我们常用的 AFNetworking
中,在回调 success
和 failure
的block块的过程中,就会回调到主线程上:
dispatch_group_async(self.completionGroup ?: http_request_operation_completion_group(), self.completionQueue ?: dispatch_get_main_queue(), ^{ success(self, responseObject); });
除了我们自己设计的库需要这么做以外,也有一些系统上的方法需要我们注意。比如, NSNotification
。
NSNotification
是哪个线程去post就是哪个线程去调用 selector
。我们来测试一下:
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(test) name:kTestNotification object:nil]; dispatch_async(dispatch_get_global_queue(0, 0), ^{ [[NSNotificationCenter defaultCenter] postNotificationName:kTestNotification object:nil]; });
我们在 test
方法上打个断点,我们会看到:
这样就会有问题,如果 test
方法内是执行UI操作或者某些需要主线程的操作的话,那么有可能会造成UI无响应,或者很长时间才变化,甚至是崩溃。
所以,我建议一定要在主线程上post,因为你不知道你所发出的 NSNotification
谁会去接收,它又要去干什么,但是你知道,主线程是肯定没错的。
实现这个的方法有很多,比如继承、category、hook等。
前段时间在写指纹解锁的时候碰到一个问题。在我的App中需要验证指纹或者手势密码才可以进入主页,而验证指纹需要用到这么一个方法:
-(void)evaluatePolicy:(LAPolicy)policy localizedReason:(NSString *)localizedReason reply:(void(^)(BOOL success, NSError * __nullable error))reply;
测试的时候,我发现一个问题,在用户验证通过之后, alertView
消失之后,页面并没有跳到主页。有时候需要过好久才会跳到主页,但是页面并没有卡死,手势解锁依旧可用。这就奇了怪了,我找了一圈才发现,这个方法是在子线程上回调回来的,而我并不知道。所以我用这个子线程去初始化页面的时候,就会出现长时间无响应的问题。
所以,系统异步回调的接口一定要去检查一下是不是主线程的。
我们知道,在 iOS 4 以上, NSOperationQueue
是在GCD上封装上来的,相比起GCD, NSOperationQueue
具有如下一些优点:
提供cancel操作。
更细粒度的优先级控制。
支持继承,方便封装。
支持KVO。
而GCD相比起 NSOperationQueue
的优点是:
使用方便、简单。
速度可能更快一点。
我相信,对于大部分好的封装来说,会优先选择 NSOperationQueue
。而如果你只是一个很小的项目,以使用方便为主,那么,使用GCD也是一种不错的选择。