ios RunLoop详解
一、概述
一般来说,一个线程只能执行一个任务,执行完就会退出。如果我们需要一种机制,让线程能随时处理任务但不退出,那么RunLoop就是这样的一种机制。RunLoop是事件接收和分发机制的一个实现。RunLoop实际上是一个对象,这个对象在循环中用来处理程序运行过程中的各种事件(比如说触摸事件、UI刷新事件、定时器事件、Selector事件),从而保持程序的持续运行。而且在没有事件处理的时候,会进入睡眠模式,从而节省CPU资源,提高程序性能。
二、RunLoop基本运用
1.保持程序持续运行
程序一启动就会开启一个主线程,主线程一开起来就会跑一个主线程对应的RunLoop,RunLoop保证主线程不会被销毁,也就保证了程序的持续运行。
2.处理程序的各种事件
比如:触摸事件、UI刷新事件、定时器事件、Selector事件。
3.节省CPU资源,提高程序性能
程序运行起来时,当什么操作都没有的时候,RunLoop就告诉CPU,这时CPU就会将其资源释放出来去做其他事情,当有事情做的时候RunLoop就会立马起来去做事情。我们通过一张图来简单看一下RunLoop内部原理:
可以看出,RunLoop在跑圈的过程中,当接收到input sources或者Timer sources时就会交给对应的处理方式去处理。当没有事件传入时,RunLoop就休息了。
三、RunLoop的开启
大家应该都知道程序的入口是main函数,ios程序的入口也是main函数:
程序主线程一开起来,就会跑一个和主线程对应的RunLoop,那么RunLoop一定是在程序的入口main函数中开启。
其中main函数中的UIApplicationMain函数:
我们发现它返回的是一个int类型的值,那么我们对main函数做一些修改:
运行程序,我们看打印台:
我们发现只会打印开始,不会打印结束。说明在UIApplicationMain函数中,自动开启了一个与主线程相关的RunLoop,导致UIApplicationMain不会返回,一直处在运行中,也就保证了程序的持续运行。
四、RunLoop对象
1.获得RunLoop对象
五、RunLoop和线程
1.RunLoop和线程之间的关系
(1)每条线程都有唯一的一个与之对应的RunLoop对象。
(2)主线程的RunLoop系统会自动创建,子线程的RunLoop需要手动创建。
(3)RunLoop在第一次获取时创建,销毁于线程结束时。
2.主线程相关联的RunLoop的创建(系统自动创建)
主线程创建RunLoop源码:
3.创建与子线程相关联的RunLoop(手动创建)
苹果不允许直接创建RunLoop,它只提供了四个自动获取的函数,如上述Tip四所示。
子线程创建RunLoop源码:
可以看出,线程和RunLoop之间是一一对应的,其关系是保存在一个全局的NSDictionary里。子线程刚创建时并没有RunLoop,需主动创建获取。RunLoop的创建是发生在第一次获取时,其销毁于线程结束时。
六、RunLoop相关类
CoreFundation中关于RunLoop的五个类:
CFRunLoopRef //获得当前RunLoop和主RunLoop
CFRunLoopModeRef //运行模式,只能选择一种,在不同模式中做不同的操作
CFRunLoopSourceRef //事件源,输入源
CFRunLoopTimerRef //定时器时间
CFRunLoopObserverRef //观察者
该五大类的关系如图所示:
1.CFRunLoopModeRef
一个RunLoop包含若干个Mode,每个Mode又包含Source/Observer/Timer。每次调用RunLoop的主函数时,只能指定其中一个Mode,这个Mode被称为currentMode。如果要切换Mode,只能先退出RunLoop,再重新指定一个Mode进入。
在CoreFundation中,系统默认注册了5个Mode,其中常见的有第一种和第二种:
1. kCFRunLoopDefaultMode:App的默认Mode,通常主线程是在这个Mode下运行
2. UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响
3. UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用
4. GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到
5. kCFRunLoopCommonModes: 这是一个占位用的Mode,作为标记kCFRunLoopDefaultMode和UITrackingRunLoopMode用,并不是一种真正的Mode
Mode间的切换:
我们平时在开发中一定遇到过,当我们使用NSTimer每一段时间调用一个方法时,此时若滑动UIScrollView,NSTimer就会暂停。当我们停止滑动以后,NSTimer又会重新恢复。我们通过一段代码来看一下:
可以看出,当我们滑动UIScrollView时,NSTimer的计时器方法不管用了。是因为Mode的切换导致的,我们在主线程中的RunLoop中添加NSTimer,虽然设置了其Mode为NSRunLoopCommonModes(我们知道该Mode只是一个占位用的),但此时主线程中的RunLoop的Mode仍为kCFRunLoopDefaultMode(默认)。那么当我们滑动UIScrollView时,RunLoop的Mode会切换到UITrackingRunLoopMode,因此之前Mode中的Timer就停止调用了,进而处理新Mode中的Source。当停止滑动时,RunLoop中的Mode又切换为老的Mode,继续处理其中的Timer。
2.CFRunLoopSourceRef
Source分为两种:
第一种:非基于Port的,用于用户主动触发的事件(如UIButton点击)。
第二种:基于Port的,通过内核与其他线程相互发送消息。
3.CFRunLoopObserverRef
能够监听RunLoop的运行状态,通过一段代码来看一下:
七、RunLoop退出
(1)主线程销毁,RunLoop退出。
(2)当Mode中的Timer,Source,Observer为空时,RunLoop会立刻退出。
(3)在启动RunLoop的时候可以设置什么时候停止。
八、与RunLoop相关的常见问题
1.事件响应和手势识别底层处理是否一致?
事件响应:
苹果注册了一个Source1用来接收系统事件,其回调函数为__IOHIDEventSystemClientQueueCallBack()。当一个硬件事件(触摸,锁屏,摇晃)发生后,首先由IOKit.framework生成一个IOHIDEvent事件,并由SpringBoard接收。SpringBoard只接收按键,触摸,加速,传感器等几种Event,随后SpringBoard用mach port转发给需要的App进程。随后苹果注册过的Source1就会先触发回调函数,然后调用函数_UIApplicationHandleEventQueue()进行应用内部的分发。_UIApplicationHandleEventQueue()会把IOHIDEvent事件处理并包装成UIEvent处理或分发。通常事件比如UIButton点击,touchBegin/Move/End/Cancel事件都是在这个回调中完成的。
手势识别:
当_UIApplicationHandleEventQueue()接收到一个手势(Gesture)时,首先会调用Cancel将当前的touchBegin/Move/End系列回调打断,随后系统将手势标记为待处理。苹果注册了一个Observer监测BeforeWaiting(Loop即将进入休眠),这个Observer的回调函数是_UIGestureRecognizerUpdateObserver(),该函数内部会获取所有被标记为等待处理的手势,并执行手势的回调。
2.界面刷新时,是在什么时候真正进行刷新,为什么会刷新不及时?
当在操作UI时,比如改变了Frame、更新了UIView/CALayer的层次时、或者是调用了UIView/CALayer的setNeedsLayout/setNeedsDisplay方法后,这个UIView/CALayer就会被标记为待处理,并被提交到一个全局的容器去。
苹果注册了一个Observer监听BeforeWaiting(即将进入休眠)和Exit(即将退出Loop),回调去执行一个很长的函数_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()。这个函数里会遍历所有等待处理的UIView/CALayer以执行实际的绘制和调整,并更新UI界面。因此界面刷新不是在setNeedsLayout后就会立即执行的。
3.程序运行过程中,总是伴随着多次自动释放池的创建和销毁,这些是在什么时候发生的?
App启动后,苹果在主线程RunLoop里注册了2个Observer,其回调都是_wrapRunLoopWithAutoreleasePoolHandle(),
第一个Observer监视的事件是Entry(即将进入Loop), 其回调会调用_objc_autoreleasePoolPush()自动创建释放池。其order是-2147483647,优先级最高,