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标记为待处理,在每次迭代即将结束时会执行实际的绘制。