iOS 透过CFRunloop源码分析runloop底层原理及应用场景

一、runloop是什么?

从字面是理解就是循环,无限循环,app应用从启动到退出,这个循环一直存在,启动就会自动创建一个runloop,这也是app保持运行不退出的关键所在。无限循环可以从CFRunloop的源码可以看出:

 do{

///....

  }while(0== retVal);

二、runloop在干什么?

我们看下runloop的定义,在源码中,runloop是一个结构体,而结构体中有一个重要的成员CFRunloopModelRef:

CFRunloop结构体

我们再看看CFRunloopModelRef的数据结构:

CFRunloopModelRef数据结构

看到CFRunloopModelRef的结构体的成员中有,source0,source1,observers,timer,port等成员。这个跟runloop是什么样的关系呢?我们看看runloop的运行原理了,苹果官方有张图:

runloop运行原理

其实NSRunLoop的本质是一个消息机制的处理模式,runloop的运行,其实就是不停的通过observer监听各种事件,包含各种source事件,timer,port等等,如果有这些事件,那就处理,没有事件,就会进入休眠,不停的重复上述过程。由此形成了运行->检测->休眠 ->运行 的循环状态。我们注意到source事件,timer,port,observer这些都是放在mode中,所以还有下面一种图,才能完整描述runloop:

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的工作过程:

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的异步提交到主队列,上面提到过的自动释放池等等,在这里就不一一叙述了。

以上,欢迎各位指正。

你可能感兴趣的:(iOS 透过CFRunloop源码分析runloop底层原理及应用场景)