一、runloop是什么?
从字面是理解就是循环,无限循环,app应用从启动到退出,这个循环一直存在,启动就会自动创建一个runloop,这也是app保持运行不退出的关键所在。无限循环可以从CFRunloop的源码可以看出:
do{
///....
}while(0== retVal);
二、runloop在干什么?
我们看下runloop的定义,在源码中,runloop是一个结构体,而结构体中有一个重要的成员CFRunloopModelRef:
我们再看看CFRunloopModelRef的数据结构:
看到CFRunloopModelRef的结构体的成员中有,source0,source1,observers,timer,port等成员。这个跟runloop是什么样的关系呢?我们看看runloop的运行原理了,苹果官方有张图:
其实NSRunLoop的本质是一个消息机制的处理模式,runloop的运行,其实就是不停的通过observer监听各种事件,包含各种source事件,timer,port等等,如果有这些事件,那就处理,没有事件,就会进入休眠,不停的重复上述过程。由此形成了运行->检测->休眠 ->运行 的循环状态。我们注意到source事件,timer,port,observer这些都是放在mode中,所以还有下面一种图,才能完整描述runloop:
对于mode的类型,iOS系统给出了5种,分别是:
、、、
NSDefaultRunLoopMode // App默认Mode通常主线程是在这个mode下运行
UITrackingRunloopMode //界面跟踪Mode用于scrollView追踪触摸界面滑动时不受其他Mode影响
UIinitializationRunloopMode //在app一启动进入的第一个Mode,启动完成后就不再使用
GSEventRecieveRunloopMode //苹果使用绘图相关,系统内核调用,开发者使用不到
NSRunLoopCommonModes //占位模式
、、、
开发者能使经常使用的就三种模式(NSDefaultRunLoopMode、UITrackingRunloopMode、NSRunLoopCommonModes),runloop会根据事件在不同模式之间自动切换,例如当scrollView的滑动事件,系统的主runloop会切换到UITrackingRunloopMode模式下,这是NSDefaultRunLoopMode中的事件就会停止,直到scrollView的滑动停止,才会切换回NSDefaultRunLoopMode模式下。当然还可自定义mode,并添加runloop中,这里就不深入讲解了。
三、工作过程:
下图是runloop的工作过程:
我们可以看下源码的__CFRunLoopRun()函数:
/// 用DefaultMode启动
void CFRunLoopRun(void) {
CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
}
/// 用指定的Mode启动,允许设置RunLoop超时时间
int CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean stopAfterHandle) {
return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}
/// RunLoop的实现
int CFRunLoopRunSpecific(runloop, modeName, seconds, stopAfterHandle) {
/// 首先根据modeName找到对应mode
CFRunLoopModeRef currentMode = __CFRunLoopFindMode(runloop, modeName, false);
/// 如果mode里没有source/timer/observer, 直接返回。
if (__CFRunLoopModeIsEmpty(currentMode)) return;
/// 1. 通知 Observers: RunLoop 即将进入 loop。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopEntry);
/// 内部函数,进入loop
__CFRunLoopRun(runloop, currentMode, seconds, returnAfterSourceHandled) {
Boolean sourceHandledThisLoop = NO;
int retVal = 0;
do {
/// 2. 通知 Observers: RunLoop 即将触发 Timer 回调。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers);
/// 3. 通知 Observers: RunLoop 即将触发 Source0 (非port) 回调。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources);
/// 执行被加入的block
__CFRunLoopDoBlocks(runloop, currentMode);
/// 4. RunLoop 触发 Source0 (非port) 回调。
sourceHandledThisLoop = __CFRunLoopDoSources0(runloop, currentMode, stopAfterHandle);
/// 执行被加入的block
__CFRunLoopDoBlocks(runloop, currentMode);
/// 5. 如果有 Source1 (基于port) 处于 ready 状态,直接处理这个 Source1 然后跳转去处理消息。
if (__Source0DidDispatchPortLastTime) {
Boolean hasMsg = __CFRunLoopServiceMachPort(dispatchPort, &msg)
if (hasMsg) goto handle_msg;
}
/// 通知 Observers: RunLoop 的线程即将进入休眠(sleep)。
if (!sourceHandledThisLoop) {
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);
}
/// 7. 调用 mach_msg 等待接受 mach_port 的消息。线程将进入休眠, 直到被下面某一个事件唤醒。
/// ? 一个基于 port 的Source 的事件。
/// ? 一个 Timer 到时间了
/// ? RunLoop 自身的超时时间到了
/// ? 被其他什么调用者手动唤醒
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort) {
mach_msg(msg, MACH_RCV_MSG, port); // thread wait for receive msg
}
/// 8. 通知 Observers: RunLoop 的线程刚刚被唤醒了。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopAfterWaiting);
/// 收到消息,处理消息。
handle_msg:
/// 9.1 如果一个 Timer 到时间了,触发这个Timer的回调。
if (msg_is_timer) {
__CFRunLoopDoTimers(runloop, currentMode, mach_absolute_time())
}
/// 9.2 如果有dispatch到main_queue的block,执行block。
else if (msg_is_dispatch) {
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
}
/// 9.3 如果一个 Source1 (基于port) 发出事件了,处理这个事件
else {
CFRunLoopSourceRef source1 = __CFRunLoopModeFindSourceForMachPort(runloop, currentMode, livePort);
sourceHandledThisLoop = __CFRunLoopDoSource1(runloop, currentMode, source1, msg);
if (sourceHandledThisLoop) {
mach_msg(reply, MACH_SEND_MSG, reply);
}
}
/// 执行加入到Loop的block
__CFRunLoopDoBlocks(runloop, currentMode);
if (sourceHandledThisLoop && stopAfterHandle) {
/// 进入loop时参数说处理完事件就返回。
retVal = kCFRunLoopRunHandledSource;
} else if (timeout) {
/// 超出传入参数标记的超时时间了
retVal = kCFRunLoopRunTimedOut;
} else if (__CFRunLoopIsStopped(runloop)) {
/// 被外部调用者强制停止了
retVal = kCFRunLoopRunStopped;
} else if (__CFRunLoopModeIsEmpty(runloop, currentMode)) {
/// source/timer/observer一个都没有了
retVal = kCFRunLoopRunFinished;
}
/// 如果没超时,mode里没空,loop也没被停止,那继续loop。
} while (retVal == 0);
}
/// 10. 通知 Observers: RunLoop 即将退出。
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
}
四、runloop与线程的关系:
runloop与线程是一一对应的关系,我应用运行的主线程对应着主runloop,上面讲了默认就是开启的,否则我们的应用就不能保持运行;而对于后台线程,它所对应的runloop则是没有开启的,这也就是后台线程执行完任务就会自动销毁回收的,如果如果需要则手动开启。
苹果开发的接口中并没有直接创建Runloop的接口,如果需要使用Runloop通常CFRunLoopGetMain()和CFRunLoopGetCurrent()两个方法来获取(通过上面的源代码也可以看到,核心逻辑在_CFRunLoopGet_当中),通过代码并不难发现其实只有当我们使用线程的方法主动get Runloop时才会在第一次创建该线程的Runloop,同时将它保存在全局的Dictionary中(线程和Runloop二者一一对应),默认情况下线程并不会创建Runloop(主线程的Runloop比较特殊,任何线程创建之前都会保证主线程已经存在Runloop),同时在线程结束的时候也会销毁对应的Runloop。
五、runloop与autoReleasePool的关系:
AutoreleasePool是另一个与RunLoop相关讨论较多的话题。其实从RunLoop源代码分析,AutoreleasePool与RunLoop并没有直接的关系,之所以将两个话题放到一起讨论最主要的原因是因为在iOS应用启动后会注册两个Observer管理和维护AutoreleasePool。不妨在应用程序刚刚启动时打印currentRunLoop可以看到系统默认注册了很多个Observer,其中有两个Observer的callout都是** _ wrapRunLoopWithAutoreleasePoolHandler**,这两个是和自动释放池相关的两个监听。
第一个Observer会监听RunLoop的进入,它会回调objc_autoreleasePoolPush()向当前的AutoreleasePoolPage增加一个哨兵对象标志创建自动释放池。这个Observer的order是-2147483647优先级最高,确保发生在所有回调操作之前。
第二个Observer会监听RunLoop的进入休眠和即将退出RunLoop两种状态,在即将进入休眠时会调用objc_autoreleasePoolPop() 和 objc_autoreleasePoolPush() 根据情况从最新加入的对象一直往前清理直到遇到哨兵对象。而在即将退出RunLoop时会调用objc_autoreleasePoolPop() 释放自动自动释放池内对象。这个Observer的order是2147483647,优先级最低,确保发生在所有回调操作之后。
主线程的其他操作通常均在这个AutoreleasePool之内(main函数中),以尽可能减少内存维护操作(当然你如果需要显式释放【例如循环】时可以自己创建AutoreleasePool否则一般不需要自己创建)。
在子线程你创建了 Pool 的话,产生的 Autorelease 对象就会交给 pool 去管理。如果你没有创建 Pool ,但是产生了 Autorelease 对象,就会调用 autoreleaseNoPage 方法。在这个方法中,会自动帮你创建一个 hotpage(hotPage 可以理解为当前正在使用的 AutoreleasePoolPage,如果你还是不理解,可以先看看 Autoreleasepool 的源代码,再来看这个问题 ),并调用page->add(obj)将对象添加到 AutoreleasePoolPage 的栈中,也就是说你不进行手动的内存管理,也不会内存泄漏啦!我们看看autoreleaseNoPage的源码:
static __attribute__((noinline))
id *autoreleaseNoPage(id obj)
{
// No pool in place.
// hotPage 可以理解为当前正在使用的 AutoreleasePoolPage。
assert(!hotPage());
// POOL_SENTINEL 只是 nil 的别名
if(obj != POOL_SENTINEL && DebugMissingPools) {
// We are pushing an object with no pool in place,
// and no-pool debugging was requested by environment.
_objc_inform("MISSING POOLS: Object %p of class %s "
"autoreleased with no pool in place - "
"just leaking - break on "
"objc_autoreleaseNoPool() to debug",
(void*)obj, object_getClassName(obj));
objc_autoreleaseNoPool(obj);
returnnil;
}
// Install the first page.
// 帮你创建一个 hotpage(hotPage 可以理解为当前正在使用的 AutoreleasePoolPage
AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
setHotPage(page);
// Push an autorelease pool boundary if it wasn't already requested.
// POOL_SENTINEL 只是 nil 的别名,哨兵对象
if(obj != POOL_SENTINEL) {
page->add(POOL_SENTINEL);
}
// Push the requested object.
// 把对象添加到 自动释放池 进行管理
returnpage->add(obj);
}
六、runloop在开发中的应用
1、线程常驻:
我们知道,让一个后台线程常驻,需要让它对应的runloop run起来,并且还要往里面添加一个timer或者一个port,不然线程执行完任务就会立马销毁。
我们讲线程对应的runloop开启,代码执行:
2、UITableView滑动卡顿优化
UITableView滑动卡顿的原因一般主要有以下几种:
1、每次重新计算cell布局和高度,导致计算量大;
2、cell上加载多张超大高清图片,导致计算量大,渲染超时;
3、动态创建添加子视图;
4、过多使用透明视图,导致离屏渲染。
网上可以找到很多对应的解决方案,这里就不多描述了,主要讲下第2点的解决方案(runloop方案),思路如下:
tableView加载过多的高清大图,Runloop不只处理iOS事件,渲染图形也是runloop处理的。
而渲染图形的UI操作必须在主线程中,不能开辟线程进行图形处理。
在拖动tableView的时候,Runloop要处理拖动事件,还要处理过多图片渲染,而造成卡顿。
解决卡顿分析:
1、Runloop在一次循环渲染图片过多,那就减少Runloop一次处理图片的数量,最多一次三张;
2、将处理图片的代码放在block中,然后加入数组中,处理几次加入几次。
3、我们只需要渲染,tableView显示的图片,显示图片有最大个数。移开屏幕或者不处理的从队列数组里删去;
4、滑动的时候,不处理渲染任务;
代码如下,具体见demo:
///保持runloop一直运转
-(NSTimer*)taskTimer{
if(!_taskTimer) {
_taskTimer = [NSTimer scheduledTimerWithTimeInterval:0.001 repeats:YES block:^(NSTimer * _Nonnull timer) {
}];
}
return _taskTimer;
}
///添加到任务队列中
-(void)addTask:(RunloopTask)task{
[self.tasksaddObject:task];
if(self.tasks.count>MAXTASKS) {
[self.tasks removeObjectAtIndex:0];
}
}
#pragma mark 向runloop中注册observer
- (void)addObserver{
//拿到当前的Runloop
CFRunLoopRef runloop = CFRunLoopGetCurrent();
CFRunLoopObserverContext context = {
0,
(__bridgevoid*)(self),
&CFRetain,
&CFRelease,
NULL
};
//定义一个观察者,static内存中只存在一个
static CFRunLoopObserverRef obverser;
//创建一个观察者
obverser =CFRunLoopObserverCreate(NULL, kCFRunLoopAfterWaiting, YES, 0, &callBack, &context);
//添加观察者!!!默认模式下,滑动的时候不处理渲染任务
CFRunLoopAddObserver(runloop, obverser, kCFRunLoopDefaultMode);
//release
CFRelease(obverser);
}
///runloop即将休眠的observer回调
void callBack (CFRunLoopObserverRef observer,CFRunLoopActivity activity,void*info){
HXWRunloopTask*runloopTask = (__bridgeHXWRunloopTask*)info;
if(runloopTask.tasks.count==0){
return;
}
///从任务队列中去任务执行
RunloopTask task = runloopTask.tasks.firstObject;
task();
[runloopTask.tasks removeObjectAtIndex:0];
}
runloop应用很广泛,比如还有NSTimer的,需要添加到runloop中才能有效,GCD的异步提交到主队列,上面提到过的自动释放池等等,在这里就不一一叙述了。
以上,欢迎各位指正。