系列:如何不闪退之线程

501 查看

如何不闪退

一个简单的准则就是:一切都放在主线程中进行。现如今的机器设备处理速度已经非常快,以致于在主线程中你可以做的远比你想象中的要多。

一个不需要考虑并发的世界就是天堂,因为没有了并发会引起的问题

但是……

我是一个性能迷,或者,更确切一点,我是一个用户体验主义者。在我看来最糟糕的事情就是运行缓慢和明显阻塞主线程。千万不要这样做。

后面我会讲述如何避免这个问题。现在让我们从主线程讲起。

主线程准则

我编写的所有代码都应当并且只能在主线程运行,当然也有些例外(我们稍后会讲一下这些例外)。

这样做可以解决很多问题。例如,我在之前的博客中写到过在dealloc中移除消息注册。有一些人指出,你无法确定dealloc在哪一个线程中被调用。但实际上你是可以的,只要对象只在主线程工作,并且也只在主线程被引用。

这也意味着任何KVO变化都会在主线程发出,所有观察者都在主线程中,并且期望通知在主线程传递过来。

无需处理并发的好处非常多。我强烈建议用这种方式写你的app,然后测试是否有阻塞主线程的现象存在。如果没有,那就棒极了,你可以提交你的 APP 了(当然,应该使用适当大规模的数据来测试。)

拥有自己独立空间的对象

当且仅当你发现主线程发生了明显的阻塞,这时你就应当想办法解决阻塞。

第一个考虑的对象是那些可以独立于其他部分的转换过程。我会用解析JSON的例子说明。

当我从服务器获取到JSON数据,我喜欢把它转换成之后会合并进 model 的中间对象,原因如下:

  1. 我不想让model 对象直接处理JSON数据。
  2. 我想在其他对象使用到这个数据之前,完成处理NSNull值,数据转换,以及其他转换工作。

所以我用NSOperationQueue或者GCD 队列(最近经常使用后者)将服务器返回的NSData转换成中间对象。(一定要使用队列。不要使用 detachThreadSelector或者performSelectorInBackground。

一次只能有一个线程访问这些中间对象。他们是由后台线程创建,之后传递到主线程,主线程用它们来更新model,然后将他们删除

由于在这些中间对象的生命周期中,会有不同的线程引用它们,所以我要确保这些对象对于除了它们自身和它们的初始化数据之外的世界一无所知。一旦在队列中被创建,它们将是不可变的。它们不会成为观察者也不会成为被观察对象(毕竟它们是不可变的)。

不可变对象是线程安全的,因此这些中间对象也是线程安全的。然而,没有必要强调线程安全,因为我们讨论的重点在于一次只有一个线程访问的情况下它们是安全的,而不是同时有多个线程同时访问的情况。

多个对象

有时,一些对象要放在一起工作。抛开JSON的例子,考虑一下RSS解析器。在这个例子中,包含三个主要的对象:SAX 解析封装器,它的委托对象,以及委托对象创建的中间对象。(理论上来说应该与上面例子中的对象相似)

SAX 解析封装器和他的委托对象在整个操作期间都存活。尽管代码运行在一个独立的线程上,他们不需要是线程安全的。因为它们只会被那一个线程访问。运行期间,它们不了解外界,外界对它们也是一无所知。

  1. SAX parser wrapper了解初始化它的NSData,并且知道自己有一个委托对象。
  2. SAX解析器代理了解自己创建的中间对象。
  3. 中间对象什么都不了解。

这些对象在一起工作,但重要的是,它们不会使用KVO或者是通知,而是使用委托模式。(理论上来说通过block还是委托方法实现并不重要)

这些对象在一起工作,但是在保持整体独立的同时,这些对象内部也要尽可能低耦合。

在最后,只有中间数据对象还存在——他们被传递给主线程,主线程使用它们来更新model,然后将它们删除

最差的情景

我多次用到了这个词“更新 model ”,并且提到要在主线程中做这件事。几年之前,我还无法想象可以这么做——但是随着计算机和设备运行越来越快,我们应当首先考虑只在主线程工作,对于那些可以并且能够被安全加入到另一个队列中的任务,再考虑其它线程。

真的不要试图在后台进程中更新model对象。因为这很容易产生崩溃。但是在进行testingprofiling时可以会提示你需要在后台线程更新 model

我们试着分解这个问题。如果更新model一般不会出现问题,除了涉及到一类操作——例如,涉及到将NSData转换为UIImage或者NSImage的操作——那么把处理慢的部分作为后台任务就可以了。(利用数据或者文件创建图像是最适合在主线程之外做的,因为这个操作很容易分离出来)

还有可能出问题的是数据库:或许你发现在内存中创建对象或者更新属性并不快,甚至非常慢。在这种情况下,你可能采用和我一样的做法,将数据库调用从主线程中分离出来(这并不困难:数据库代码需要在串行后台队列运行,而且应该精确按照主线程中事件的发生顺序处理一切问题。)

也就是说:有不同的方案可选。

如果你还是觉得你必须在后台线程中更新model,那么你就只能那么做了。记住,你app中剩余的部分在主线程运行,所以发出通知等等的事情,要在主线程中进行。

总结

一切都在主线程中处理。不考虑队列和后台线程。享受在天堂的感觉。

如果testing和profiling之后,你发现确实需要把一些事情移动到后台线程中,那么把那些独立的模块移走,并且保证它们能够很好的独立。使用代理;不要使用KVO和通知。

如果最后,你还是需要做一些复杂的工作——例如在后台队列中更新model——记住:你app中的其他代码要么放在主线程中,要么是一些当你写这些复杂代码时无需要考虑的独立代码。但是:一定要仔细,不要过分乐观。(乐观主义者的代码容易出现崩溃。)