[译文]如何才能不崩溃 #4: Threading

原文地址:

http://inessential.com/2015/05/22/how_not_to_crash_4_threading

这有一个简单的规则:做任何事情都在主线程上。随着机器和设备的日益发展,你可以在主线程中做比你想象中更多的事情。

当你不需要考虑任何并发的时候,这就太美好了。

但是...

我是一个性能狂。或者,更重要的是,我是一个用户体验狂。唯一一个比缓慢更糟的是明显的阻塞主线程。所以不要这么做。

好,我们先从主线程开始。

主线程规则

所有我期望运行在主线程上的代码只在主线程上运行,除少数之外。(我们在一分钟内获得例外。)

这解决了一串问题。例如,我以前写了一个文章关于在dealloc中注销通知。很多人指出,你不能保证dealloc将会在哪一个线程中被调用 - 但是你能,实际上,对于任何在主线程上运行的对象,都只能在主线程上被引用。

这意味着,任何KVO的改变都被发送到主线程上,所有观察者也依然运行在主线程上,并在主线程上期待通知。

不处理并发的好处是极大的。我强烈建议你使用这种方式去编写你的app,然后去测试是否会有东西阻塞主线程。如果没有,那就太棒了(原文:then it’s golden, and you should ship)。(当然,适当测试一些更大的数据。)

对象只活在他们自己的小世界中

在且只在你发现主线程明显被阻塞的情况下,你应该寻找方法去解除阻塞。

第一个候选者就是可以完全独立于你的app的转换。我举一个处理JSON的例子。

当我从服务器获取一个JSON,我喜欢将它转变为中间对象,然后并入对象模型。理由是:

  1. 我不想模型对象了解JSON。
  2. 我想要处理NSNull值,数据转换,和在所有其他对象看到数据之前的转换。

所以我使用一个NSOperationQueue或者GCD队列(这段时间,经常使用后者)去将NSData转换成中间对象。

(一般使用队列。不要使用detachThreadSelector或者performSelectorInBackground。)

这些中间对象只会在同一时间被一个线程访问。它们在后台线程被创建,然后传递到主线程上,它们被用于更新模型,然后被丢弃。

它们在它们的生命周期中将会被不同的线程引用,我确信这些线程并不想了解除它们自己之外的其他事情,和传递到它们初始化方法中的东西。一旦在队列中被创建,它们是不可变的。它们不观察任何东西,并且任何人也不应该观察它们(毕竟,它们不会改变)。

(这使得这些对象是线程安全的,不能被改变的对象是线程安全的。然而,没有必要去强调线程安全,重要的是,它们在同一时间在一个线程中可以安全的使用,而不是同一时间在多个线程中。)

对象的朋友们

有时候,大量的对象一起工作。我们用RSS解析器来替代JSON。在这个例子中,涉及到三个主要对象:一个SAX解析包装,它的代理,代理创建的中间对象。(概念上和上面的例子是一样的。)

SAX解析包装和它的代理为操作而活。它们不需要线程安全,即使代码运行在一个分离的线程上 - 因为它们只允许那个线程访问。当它们工作的时候,它们不了解外面的世界,并且外面的世界也不了解它们。

  1. SAX解析包装了解它所初始化的NSData,并且知道它拥有一个代理。
  2. SAX解析包装的代理知道它创建的中间对象。
  3. 中间对象什么都不知道。

这些对象一起工作,但是,重要的是,它们无论如何都不会使用KVO或者通知。它们使用代理模式来替代(是否通过block或者方法在概念上都不重要)。

这些对象一起工作,但是它们尽可能保持松散,并保持它们的任务在组之间的隔离。

最后,只要中间对象活下来 - 它们被传递到主线程上,它们被用于更新模型。然后被丢弃。

最坏的情况

我使用了很多次短句 “更新模型(update the model)”,并且提到要在主线程上做这个。几年前,我怎么也不会想到这一点 - 但是计算机和设备进步如此快速,以至于我们可以在第一时间使用主线程,只有在处理可以也应该安全的移到队列上操作的时候,我们才考虑替代。

实际上,你并不想在后台线程中更新模型对象。这是一个崩溃制造机。但是测试和分析也许会告诉你,你应该这么做。

尝试去打破这个问题。如果更新模型是正常的,除了这一件事情 - 一些事情包含将 NSData 转换为 UIImage 或者 NSImage的操作。例如 - 只移动运行缓慢的部分到后台任务。(将从数据或者文件来创建图像的操作从主线程上移出,是一个非常好的事情。这很容易分离。)

数据库是一个问题:也许你发现在内存中创建对象、更新属性很快,甚至是很多这种操作。既然这样,你可能会像我一样在主线程中连接数据库。(这并不难:数据库的代码应该运行在一个串行的后台队列上,它应该要处理所有主线程给予它的命令。)

也就是说:它们是可选的。

但是如果你仍然发现你不得不在后台线程上更新模型,那你就这么做吧。记住,你的app寄托于主线程,所以当你发送通知等等,你需要使用主线程。

总概

在主线程上做所有事情。不要去考虑队列和后台线程。享受这个天堂!

如果,在测试和分析之后,你发现你不得不将一些事情移到后台线程上去,那么你可以提取一些可以完全独立的东西,并确保它们是完全独立的。要使用代理,不要使用KVO或者通知。

如果,最后,你仍然要做一些棘手的事情 - 像是在后台线程上更新模型 - 记住,你的app依赖于主线程或者很少的独立的事情,所以你不需要去考虑编写这些棘手的代码。然后:要仔细,不要盲目乐观。(盲目乐观会编写出崩溃的代码。)

Article by 付军