美文网首页
iOS 透过CFRunloop源码分析runloop底层原理及应

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

作者: huxinwen | 来源:发表于2019-06-06 14:25 被阅读0次

    一、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底层原理及应

          本文链接:https://www.haomeiwen.com/subject/udqvtctx.html