iOS并发编程Tips(二)

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