共计 1936 个字符,预计需要花费 5 分钟才能阅读完成。
iOS 知识梳理 – runloop
runloop 其实是个很普遍的东西,基本上是个应用框架都有类似的东西,比如 js 或 flutter 里的 event loop,Android 的 looper。
下面我们从最简单的例子来看一下,runloop 的本质
溯源
最初学编程的时候,有个经典题目是用 c /c++ 实现四则运算。大体上要下面这个可以一直输入的效果:
代码大概像这样:
int main()
{
string str;
double result;// 保存结果
while(getline(cin,str))
{
// calculate
cout << "Result is:" << result << endl;
}
}
由于可以一直输入新的算式,我们自然地使用了一个 while 循环来处理输入,有新的一行输入就去计算结果并输出,没有新的输入时其实就是阻塞等待。
这其实就是最简单的 runloop。
实际应用程序中,无非就是输入的形式更多了一些,执行的任务更复杂了一些,基本框架,其实还是一个 while 循环。
网上看到一个伪代码挺清晰的:
function loop() {initialize();
do {var message = get_next_message();
process_message(message);
} while (message != quit);
}
深入理解 runloop
runloop 的实际实现还是有大量细节的,core foundation 框架也是开源的,这里其实可以挖得很深。
iOS 多线程:『RunLoop』详尽总结,这篇文章讲得挺好,推荐阅读。很多细节本文就不再做太多展开了。
Mode
runloop 在运行时有个 mode 的概念,runloop 只会处理当前 mode 的 event。
比如,我们有 mode1 和 mode2,mode1 对应了 3 个 event,mode2 对应了 2 个 event,如果当前 runloop 处于 mode1,就只会处理 mode1 对应的 3 个 event。
具体而言,runloop 暴露出来的是 kCFRunLoopDefaultMode 和 UITrackingRunLoopMode 两个 mode,通常主线程处于 DefaultMode,但是当滑动 scrollview 时,主线程处于 TrackingMode。而默认地,我们的 NSTimer 事件是绑定在 DefaultMode 的,这就会导致,当滑动 scrollview 时,就不会触发 NSTimer 事件。
为了解决这种问题,runloop 给出了一个 CommonMode 的概念,CommonMode 不是一个具体的 Mode,可以理解成一组 Mode 的集合,这里实际上就是 DefaultMode 和 TrackingMode。把 Timer 加入到 CommonMode,则在 DefaultMode 和 TrackingMode 都会执行这个 Timer 了。
Mode 这个东西,在实际使用中,除了给 Timer 的使用带来困扰之外,几乎从未被使用过。
那么,为什么苹果会在 Runloop 中设计 Mode 这个东西?
网上并没有找到有价值的讨论,不过可以简单揣测一下,目的可能是,在某些场景下,可以只处理对应场景需要的事件。这可能是基于一种特殊情况的假设:在某种场景下,CPU 忙不过来,因此需要停掉其它任务,专注于当前场景的任务。比如,任务 A 需要 90% 的 CPU,其它杂七杂八事件处理需要 40% 的 CPU,那么,停掉那些杂事专注当前场景,可能是有意义的行为。
如果 CPU 并不会跑满的话,那么 Mode 的拆分就意义不大,充其量是副作用极大的优先级调整(低优先级的直接被卡住了诶)。
参考其它平台的 EventLoop 实现,通常只是提供简单的优先级设置能力即可。
总的来说,个人觉得 Mode 的设计是比较鸡肋的。
子线程的 Runloop
默认地,子线程并不会开启 Runloop。想一想,这是非常合理的。开启了 Runloop 后,即使在没有事件时,线程也会进入阻塞等待的状态,需要的时候那叫保活,不需要的时候就是白占资源了。因此,子线程默认情况下是肯定不能开启 Runloop 的。而没有开启 Runloop 的情况下,涉及到 Runloop 的异步调用就都不能用了,主要就是 NSTimer,performSelector:afterDelay:
,网络请求的回调等。
解决方案是通过 [[NSRunLoop currentRunLoop] run]
启动当前线程的 runloop。
跟 Runloop 相关的特性
- autoreleasepool:每次 runloop 开始时进入 pool,结束时离开 pool。从而在未显式声明 autoreleasepool 的时候释放 autorelease 对象。
- 界面更新:操作 UI 时会把对应的 UIView/CALayer 标记为待处理,在每次迭代即将结束时会执行实际的绘制。