iOS 模块详解—「Runloop 面试、工作」看我就 ???? 了 ^_^.

浏览: 186 发布日期: 2017-09-15 分类: objective-c

引导


上图内容释义


  • Run loops 是线程相关底层基础的一部分。它的本质和字面意思一样运行着的循环(事件处理的循环),作用:接受循环事件和安排线程的工作。目的:让线程在有任务的时候忙于工作,而没任务的时候处于休眠状态。
  • Run loop 的管理并非完全自动。你仍然需要设置线程代码在合适的时候启动 run loop 来帮助你处理输入事件。iOS 中 Cocoa 和 CoreFoundation 框架中各有完整的一套关于 runloop 对象的操作api,在主线程中 run loop 是自动创建并运行(在子线程开启RunLoop 需要手动创建且手动开启)。

译文&源码


Runloop 尽管在平时多数开发者很少直接使用,但是理解 RunLoop 可以帮助开发者更好的利用多线程编程模型,同时也可以帮助开发者解答面试套路的一些疑惑,对于 iOS 编程 熟知它是必不可少的,下面是我对 Runloop 的整理,将以一劳永逸的心态,渐进式学习的目地,并且带有几个实战开发场景。 --> 大神可选择性路过「思想」。

RunLoop 该模块学习将续更 ~

在「时间 & 知识 」有限内,总结的文章难免有「未全、不足 」的地方,还望各位好友指出,可留言指正或是补充,以提高文章质量@白开水ln原著;

目录

  1. Runloop 概念
  2. Runloop 作用
  3. Runloop 开启&退出
  4. Runloop 和线程关系
    1.如何创建子线程对应的 Runloop ?
  5. Runloop 获取
  6. Runloop 源码
  7. Runloop 相关5个类
  8. Runloop 相关类(Mode)
  9. Runloop 相关类(Source)
  10. Runloop 相关类(Timer)
  11. Runloop 相关类(Observer)
  12. Runloop 相关5个类代码示例
  13. Runloop 应用场景
    1.Runloop 经典应用:常驻线程

2.AutoreleasePool 自动释放池
3.UI更新
4.UIImageView 延迟加载图片
5.UITableView 与 NSTimer 冲突

  1. Runtime & Runloop 面试最常问到的题整理【建议看】
  2. Runloop 模块博文推荐(❤️数量较多)
  3. Demo 重要的部分代码中都有相应的注解和文字打印,运行程序可以很直观的表现。
  4. SourceCodeToolsClassWechatPublic-Codeidea
  5. Runtime 模块详解「面试、工作」看我就 ???? 了 ^_^. &version=12020810&nettype=WIFI&fontScale=100&pass_ticket=SIRvaekdiT4fq8ggN%2BA3g10mJj26kBRPmRGlur5%2B3Cc3Vi32Q9Ioz66TmKoztAmW)

Runloop 概念


  • 【Runloop 释义】:"运行循环"、"跑圈"
  • 【注解1】:iOS 中通常所说的 RunLoop 指的是 NSRunloop (Foundation框架) 或者 CFRunloopRef (CoreFoundation 框架)CFRunloopRef 是纯C的函数,而 NSRunloop 仅仅是 CFRunloopRef 的一层OC封装,并未提供额外的其他功能,因此要了解 RunLoop 内部结构,需要多研究 CFRunLoopRef API(Core Foundation \ 更底层)。
  • 【注解2】:CFRunloopRef 其实就是 __CFRunloop 这个结构体指针(按照OC的思路我们可以将RunLoop看成一个对象),这个对象的运行才是我们通常意义上说的运行循环,核心方法是 __CFRunloopRun() 查看下(附:源码)

Runloop 作用


  • 1、保持程序的持续运行(如:程序一启动就会开启一个主线程(中的 runloop 是自动创建并运行),runloop 保证主线程不会被销毁,也就保证了程序的持续运行)。
  • 2、处理App中的各种事件(如:touches 触摸事件、NSTimer 定时器事件、Selector事件(选择器 performSelector))。
  • 3、节省CPU资源,提高程序性能(有事情就做事情,没事情就休息 (其资源释放))。
  • 4、负责渲染屏幕上的所有UI。

附:CFRunLoop.c 源码

#【用DefaultMode启动,具体实现查看 CFRunLoopRunSpecific Line2704】
#【RunLoop的主函数,是一个死循环 dowhile】
void CFRunLoopRun(void) {    /* DOES CALLOUT */
    int32_t result;
    do {
        /*
         参数一:CFRunLoopRunSpecific   具体处理runloop的运行情况
         参数二:CFRunLoopGetCurrent()  当前runloop对象
         参数三:kCFRunLoopDefaultMode  runloop的运行模式的名称
         参数四:1.0e10                 runloop默认的运行时间,即超时为10的九次方
         参数五:returnAfterSourceHandled 回调处理
         */
        result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
        CHECK_FOR_FORK();
        
        //【判断】:如果runloop没有停止 且 没有结束则继续循环,相反侧退出。
    } while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}

#【直观表现】
RunLoop 其实内部就是do-while循环,在这个循环内部不断地处理各种任务(`比如Source、Timer、Observer`),
通过判断result的值实现的。所以 可以看成是一个死循环。
如果没有RunLoop,UIApplicationMain 函数执行完毕之后将直接返回,就是说程序一启动然后就结束;

Runloop 开启&退出


我们来验证 Runloop 是在那开启的?答案:UIApplicationMain 中开启;

#【验证 Runloop 的开启】。

# int 类型返回值
UIKIT_EXTERN int UIApplicationMain(int argc, char *argv[], NSString * __nullable principalClassName, NSString * __nullable delegateClassName);

int main(int argc, char * argv[]) {
    @autoreleasepool {
        NSLog(@"开始");
        int number = UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
        NSLog(@"结束");
        return number;
    }
}

#【验证结果】:只会打印开始,并不会打印结束。

----
#【Runloop 的退出条件】。
App退出;线程关闭;设置最大时间到期;

【注解】:说明在UIApplicationMain函数内部开启了一个和主线程相关的RunLoop (保证主线程不会被销毁),导致 UIApplicationMain 不会返回,一直在运行中,也就保证了程序的持续运行。

Runloop和线程关系


【附】:CFRunLoop.c 源码

# NOTE: 获得runloop实现 (创建runloop)

CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
    if (pthread_equal(t, kNilPthreadT)) {// ✔️【主线程相关联的RunLoop创建】,如果为空,默认是主线程
        t = pthread_main_thread_np();
    }
    __CFLock(&loopsLock);
    if (!__CFRunLoops) { // 如果 RunLoop 不存在
        __CFUnlock(&loopsLock);
        // 创建字典
        CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
        // 创建主线程对应的runloop
        CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
        // 使用字典保存(KEY:线程 -- Value:线程对应的runloop), 以保证一一对应关系。
        CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
        if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
            CFRelease(dict);
        }
        CFRelease(mainLoop);
        __CFLock(&loopsLock);
    }
    
    // ✔️【创建与子线程相关联的RunLoop】,从字典中获取 子线程的runloop
    CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
    __CFUnlock(&loopsLock);
    if (!loop) {
        // 如果子线程的runloop不存在,那么就为该线程创建一个对应的runloop
        CFRunLoopRef newLoop = __CFRunLoopCreate(t);
        __CFLock(&loopsLock);
        loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
        // 把当前子线程和对应的runloop保存到字典中
        if (!loop) {
            CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
            loop = newLoop;
        }
        // don't release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it
        __CFUnlock(&loopsLock);
        CFRelease(newLoop);
    }
    if (pthread_equal(t, pthread_self())) {
        _CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL);
        if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) {
            _CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop);
        }
    }
    return loop;
}
  • 【由上源码可得】:RunLoop 和 线程关系

    • 1.每条线程都有唯一的一个与之对应的RunLoop对象。
    • 2.主线程的RunLoop已经自动创建,子线程的RunLoop需要主动创建。
    • 3.RunLoop在第一次获取时创建,在线程结束时销毁。
  • 【注解】:Runloop 对象是利用字典来进行存储,而且 Key:线程 -- Value:线程对应的 runloop。
  • iOS开发过程中对于开发者而言更多的使用的是NSRunloop,它默认提供了三个常用的run方法

如何创建子线程对应的 Runloop ?

  • 【解决】:开一个子线程创建 runloop ,不是通过 [alloc init] 方法创建,而是直接通过调用 currentRunLoop 方法来创建。
  • 【原因】:currentRunLoop 本身是懒加载的,当第一次调用currentRunLoop 方法获得该子线程对应的 Runloop 的时候,它会先去判断(去字典中查找)这个线程的Runloop 是否存在,如果不存在就会自己创建并且返回,如果存在直接返回。

Runloop 获取


    // Foundation框架
    NSRunLoop *mainRunloop = [NSRunLoop mainRunLoop]; // 获得主线程对应的 runloop对象
    NSRunLoop *currentRunloop = [NSRunLoop currentRunLoop]; // 获得当前线程对应的runloop对象
    
    // Core Foundation框架
    CFRunLoopRef maiRunloop = CFRunLoopGetMain(); // 获得主线程对应的 runloop对象
    CFRunLoopRef maiRunloop = CFRunLoopGetCurrent(); // 获得当前线程对应的runloop对象

    // NSRunLoop <--> CFRunLoopRef 相互转化
    NSLog(@"NSRunLoop <--> CFRunloop == %p--%p",CFRunLoopGetMain() , [NSRunLoop mainRunLoop].getCFRunLoop);

#【打印结果】:内存地址相同
0000-00-13 00:30:16.527 MultiThreading[57703:1217113] NSRunLoop <--> CFRunloop == 0x60000016a680--0x60000016a680

Runloop 源码


Runloop 相关内部实现源码,代码量甚多,其核心方法是 【__CFRunLoopRun】 ,为了不影响文章的可读性,这里就不再直接贴源代码,放一段伪代码方便大家阅读【转】:

int32_t __CFRunLoopRun()
{
    // 通知即将进入runloop
    __CFRunLoopDoObservers(KCFRunLoopEntry);
    
    do
    {
        // 通知将要处理timer和source
        __CFRunLoopDoObservers(kCFRunLoopBeforeTimers);
        __CFRunLoopDoObservers(kCFRunLoopBeforeSources);
        
        // 执行被加入的Block(处理非延迟的主线程调用)
        __CFRunLoopDoBlocks();
        // 处理Source0事件
        __CFRunLoopDoSource0();
        
        if (sourceHandledThisLoop) {
            __CFRunLoopDoBlocks();
         }
        // 如果有 Source1 (基于port) 处于 ready 状态,直接处理这个 Source1 然后跳转去处理消息。
        if (__Source0DidDispatchPortLastTime) {
            Boolean hasMsg = __CFRunLoopServiceMachPort();
            if (hasMsg) goto handle_msg;
        }
            
        // 通知 Observers: RunLoop 的线程即将进入休眠(sleep)。
        if (!sourceHandledThisLoop) {
            __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);
        }
            
        // GCD dispatch main queue
        CheckIfExistMessagesInMainDispatchQueue();
        
        // 即将进入休眠
        __CFRunLoopDoObservers(kCFRunLoopBeforeWaiting);
        
        // 等待内核mach_msg事件
        mach_port_t wakeUpPort = SleepAndWaitForWakingUpPorts();
        
        // 等待。。。
        
        // 从等待中醒来
        __CFRunLoopDoObservers(kCFRunLoopAfterWaiting);
        
        // 处理因timer的唤醒
        if (wakeUpPort == timerPort)
            __CFRunLoopDoTimers();
        
        // 处理异步方法唤醒,如dispatch_async
        else if (wakeUpPort == mainDispatchQueuePort)
            __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__()
            
        // 处理Source1
        else
            __CFRunLoopDoSource1();
        
        // 再次确保是否有同步的方法需要调用
        __CFRunLoopDoBlocks();
        
    } while (!stop && !timeout);
    
    // 通知即将退出runloop
    __CFRunLoopDoObservers(CFRunLoopExit);
}

线程执行了这个函数 (__CFRunLoopRun) 后,就会一直处于这个函数内部 "接受消息->等待->处理" 的循环中,直到这个循环结束函数才返回,当然Runloop精华在于在休眠时几乎不会占用系统资源(系统内核负责)。

面对上面的一段伪代码不知道做什么的函数调用 , 这里你如果想结合上段的伪代码看源码的话,可以 CFRunLoop.c 源码 (c) 2015 上面的每一步都有相应的注解,打开对照查看可以很直观的表现。

【NOTE】:
当然我 也整理了一张图,描述了 Runloop 内部实现流程(版1 & 版2 基本描述了 Runloop 的核心流程,当然可还是对照查看官方文档或源码)。

【注意】:尽管 CFRunLoopPerformBlock 在上图中作为唤醒机制有所体现,但事实上执行 CFRunLoopPerformBlock 只是入队,下次 RunLoop 运行才会执行,而如果需要立即执行则必须调用 CFRunLoopWakeUp 。

Runloop 相关类


Core Foundation 中关于 RunLoop 的5个类

  • 1、CFRunloopRef【RunLoop本身】
  • 2、CFRunloopModeRef【Runloop的运行模式】
  • 3、CFRunloopSourceRef【Runloop要处理的事件源】
  • 4、CFRunloopTimerRef【Timer事件】
  • 5、CFRunloopObserverRef【Runloop的观察者(监听者)】

CFRunLoop 的5个相关类关系图解:

【图解直观得知】:

  • 一条线程 对应一个 Runloop,Runloop 总是运行在某种特定的CFRunLoopModeRef(运行模式)下。
  • 每个 Runloop 都可以包含若干个 Mode ,每个 Mode 又包含Source源 / Timer事件 / Observer观察者。
  • 在 Runloop 中有多个运行模式,每次调用 RunLoop 的主函数【__CFRunloopRun()】时,只能指定其中一个 Mode(称 CurrentMode )运行, 如果需要切换 Mode,只能是退出 CurrentMode 切换到指定的 Mode 进入,目的以保证不同 Mode 下的 Source / Timer / Observer 互不影响。
  • Runloop 有效,mode 里面 至少 要有一个timer(定时器事件) 或者是source(源);
  • 附:源码
   struct __CFRunLoop {
        CFRuntimeBase _base;
        pthread_mutex_t _lock;          /* locked for accessing mode list */
        __CFPort _wakeUpPort;           // used for CFRunLoopWakeUp 
        Boolean _unused;
        volatile _per_run_data *_perRunData;              // reset for runs of the run loop
        pthread_t _pthread;
        uint32_t _winthread;
        CFMutableSetRef _commonModes;
        CFMutableSetRef _commonModeItems;
        CFRunLoopModeRef _currentMode;
        CFMutableSetRef _modes;
        struct _block_item *_blocks_head;
        struct _block_item *_blocks_tail;
        CFAbsoluteTime _runTime;
        CFAbsoluteTime _sleepTime;
        CFTypeRef _counterpart;
    };

    struct __CFRunLoopMode {
        CFRuntimeBase _base;
        pthread_mutex_t _lock;  /* must have the run loop locked before locking this */
        CFStringRef _name; // mode名
        Boolean _stopped;
        char _padding[3];
        CFMutableSetRef _sources0; // source0 源
        CFMutableSetRef _sources1; // source1 源
        CFMutableArrayRef _observers; // observer 源
        CFMutableArrayRef _timers; // timer 源
        CFMutableDictionaryRef _portToV1SourceMap;// mach port 到 mode的映射,为了在runloop主逻辑中过滤runloop自己的port消息。
        __CFPortSet _portSet;// 记录了所有当前mode中需要监听的port,作为调用监听消息函数的参数。
        CFIndex _observerMask;
    #if USE_DISPATCH_SOURCE_FOR_TIMERS
        dispatch_source_t _timerSource;
        dispatch_queue_t _queue;
        Boolean _timerFired; // set to true by the source when a timer has fired
        Boolean _dispatchTimerArmed;
    #endif
    #if USE_MK_TIMER_TOO
        mach_port_t _timerPort;// 使用 mk timer, 用到的mach port,和source1类似,都依赖于mach port
        Boolean _mkTimerArmed;
    #endif
    #if DEPLOYMENT_TARGET_WINDOWS
        DWORD _msgQMask;
        void (*_msgPump)(void);
    #endif
        uint64_t _timerSoftDeadline; /* TSR timer触发的理想时间*/
        uint64_t _timerHardDeadline; /* TSR timer触发的实际时间,理想时间加上tolerance(偏差*/
    };

Runloop 相关类(Mode)


CFRunLoopModeRef 代表 RunLoop 的运行模式;系统默认提供了5个 Mode 。

  • 1.【kCFRunLoopDefaultMode (NSDefaultRunLoopMode)】: App的默认Mode,通常主线程是在这个Mode下运行。
  • 2.【UITrackingRunLoopMode】: 界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响。
  • 3.【UIInitializationRunLoopMode】: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用。
  • 4.【GSEventReceiveRunLoopMode】: 接受系统事件的内部 Mode,通常用不到。
  • 5.【kCFRunLoopCommonModes (NSRunLoopCommonModes)】: 这个并不是某种具体的 Mode, 可以说是一个占位用的Mode(一种模式组合)。
  • CFRunLoop 对外暴露的管理 Mode 接口:
# CFRunLoop
CF_EXPORT void CFRunLoopAddCommonMode(CFRunLoopRef rl, CFRunLoopMode mode);
CF_EXPORT CFRunLoopRunResult CFRunLoopRunInMode(CFRunLoopMode mode, CFTimeInterval seconds, Boolean returnAfterSourceHandled);

# NSRunLoop.h
FOUNDATION_EXPORT NSRunLoopMode const NSDefaultRunLoopMode;// (默认):同一时间只能执行一个任务
FOUNDATION_EXPORT NSRunLoopMode const NSRunLoopCommonModes NS_AVAILABLE(10_5, 2_0); // (公用):可以分配一定的时间处理定时器

【注】:对照上面贴的源码,关于 CommonModes ;

  • 【关于 _commonModes】:一个 mode 可以标记为 common 属性(用于 CFRunLoopAddCommonMode函数),然后它就会保存在_commonModes。主线程 CommonModes 默认已有两个modek CFRunLoopDefaultMode 和 UITrackingRunLoopMode,当然你也可以通过调用 CFRunLoopAddCommonMode() 方法将自定义mode 放到 kCFRunLoopCommonModes 组合)。
  • 【关于 _commonModeItems】:_commonModeItems 里面存放的source, observer, timer等,在每次 runLoop 运行的时候都会被同步到具有 Common 标记的 Modes 里。如:[[NSRunLoop currentRunLoop] addTimer:_timer forMode:NSRunLoopCommonModes]; 就是把timer放到commonModeItems 里。

Runloop 相关类(Source)


CFRunloopSourceRef 事件源 输入源,有两种分类模式

  • 【官方版】:
    • 【Port-Based Sources】: 基于端口的源 (对应的是source1):与内核端口相关,只需要简单的创建端口对象,并使用 NSPort 的方法将端口对象加入到runloop,端口对象会处理创建以及配置输入源;。
    • 【Custom Input Sources】:自定义源:使用CFRunLoopSourceRef 类型相关的函数 (线程) 来创建自定义输入源。
    • 【Perform Selector Sources】:performSelector:OnThread:delay:
    • 【源码版】:按照函数调用栈的分类 source0 和 source1

      • Source0:非基于端口Port的事件;(用于用户主动触发的事件,如:点击按钮 或点击屏幕)。
      • Source1:基于端口Port的事件;(通过内核和其他线程相互发送消息,与内核相关)。
      • 补充:Source1 事件在处理时会分发一些操作给 Source0 去处理。

    Runloop 相关类(Timer)


    • CFRunLoopTimerRef是基于时间的触发器。
    • 基本上说的就是NSTimer(CADisplayLink也是加到RunLoop),它受RunLoop的Mode影响。
    • 而与NSTimer相比,GCD定时器不会受Runloop影响。

    Runloop 相关类(Observer)


    相对来说CFRunloopObserverRef理解起来并不复杂,它相当于消息循环中的一个监听器,随时通知外部当前RunLoop的运行状态(它包含一个函数指针_callout_将当前状态及时告诉观察者)。具体的Observer状态如下:

    /* jianshu:白开水ln Run Loop Observer Activities */
    typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
        kCFRunLoopEntry = (1UL << 0),           //即将进入Runloop
        kCFRunLoopBeforeTimers = (1UL << 1),    //即将处理NSTimer
        kCFRunLoopBeforeSources = (1UL << 2),   //即将处理Sources
        kCFRunLoopBeforeWaiting = (1UL << 5),   //即将进入休眠
        kCFRunLoopAfterWaiting = (1UL << 6),    //从休眠装填中唤醒
        kCFRunLoopExit = (1UL << 7),            //退出runloop
        kCFRunLoopAllActivities = 0x0FFFFFFFU   //所有状态改变
    };

    Runloop 休眠


    摘录:http://www.cnblogs.com/kenshi...
    其实对于 Event Loop 而言 RunLoop 最核心的事情就是保证线程在没有消息时休眠以避免占用系统资源,有消息时能够及时唤醒。 RunLoop 的这个机制完全依靠系统内核来完成,具体来说是苹果操作系统核心组件 Darwin 中的 Mach 来完成的。可以从下图最底层 Kernel 中找到 Mach:

    Mach 是 Darwin 的核心,可以说是内核的核心,提供了进程间通信(IPC)、处理器调度等基础服务。在 Mach 中,进程、线程间的通信是以消息的方式来完成的,消息在两个 Port 之间进行传递(这也正是 Source1 之所以称之为 Port-based Source 的原因,因为它就是依靠系统发送消息到指定的Port来触发的)。消息的发送和接收使用<mach/message.h>中的mach_msg()函数(事实上苹果提供的Mach API 很少,并不鼓励我们直接调用这些API):

      /*
         *  Routine:    mach_msg
         *  Purpose:
         *      Send and/or receive a message.  If the message operation
         *      is interrupted, and the user did not request an indication
         *      of that fact, then restart the appropriate parts of the
         *      operation silently (trap version does not restart).
         */
        __WATCHOS_PROHIBITED __TVOS_PROHIBITED
        extern mach_msg_return_t    mach_msg(
                            mach_msg_header_t *msg,
                            mach_msg_option_t option,
                            mach_msg_size_t send_size,
                            mach_msg_size_t rcv_size,
                            mach_port_name_t rcv_name,
                            mach_msg_timeout_t timeout,
                            mach_port_name_t notify);

    而 mach_msg() 的本质是一个调用 mach_msg_trap(),这相当于一个系统调用,会触发内核状态切换。当程序静止时,RunLoop停留在__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy),而这个函数内部就是调用了mach_msg 让程序处于休眠状态。

    Runloop 相关5个类代码示例


    Mode-Runloop的运行模式、Source-Runloop要处理的事件源、Timer-定时器事件

    ![[NSTimer timerWithTimeInterval: ].gif](http://upload-images.jianshu....

    ![ [NSTimer scheduledTimerWithTimeInterval: ].gif](http://upload-images.jianshu....

    Runloop 应用场景


    1、NSTimer
    2、ImageView显示:控制方法在特定的模式下可用
    3、PerformSelector
    4、常驻线程:在子线程中开启一个runloop
    5、AutoreleasePool 自动释放池
    6、UI更新

    Runloop 经典应用:常驻线程

    【注解】:常驻线程:线程创建出来就处于等待状态(有或无任务),想用它的时候就用它执行任务,不想用的时候就处于等待状态。

    【场景】:如:1.聊天发送语音消息,可能会专门开一个子线程来处理;2.在后台记录用户的停留时间或某个按钮点击次数,这些用主线程做可能不太方便,可能会开启一个子线程后台默默收集;

    【需求】:让线程持续存在,可以切换执行其他任务。

    【解决】:开启 Runloop循环。

    Demo & 效果图:

    AutoreleasePool 自动释放池

    AutoreleasePool 是另一个与 RunLoop 相关讨论较多的话题。其实从RunLoop 源代码分析,AutoreleasePoolRunLoop 并没有直接的关系,之所以将两个话题放到一起讨论最主要的原因是因为在iOS应用启动后会注册两个 Observer 管理和维护 AutoreleasePool。不妨在应用程序刚刚启动时打印 currentRunLoop可以看到系统默认注册了很多个Observer,其中有两个Observercallout 都是 _ wrapRunLoopWithAutoreleasePoolHandler,这两个是和自动释放池相关的两个监听。

          <CFRunLoopObserver 0x6080001246a0 [0x101f81df0]>{valid = 
    Yes, activities = 0x1, repeats = Yes, order = -2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x1020e07ce), 
    context = <CFArray 0x60800004cae0 [0x101f81df0]>{type = mutable-small, count = 0, values = ()}}
        '' <CFRunLoopObserver 0x608000124420 [0x101f81df0]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2147483647, 
    callout = _wrapRunLoopWithAutoreleasePoolHandler (0x1020e07ce), context = <CFArray 0x60800004cae0 [0x101f81df0]>
    {type = mutable-small, count = 0, values = ()}}
    
    • 第一个 Observer 会监听 RunLoop 的进入,它会回调objc_autoreleasePoolPush() 向当前的 AutoreleasePoolPage 增加一个哨兵对象标志创建自动释放池。这个 Observer 的 order 是 -2147483647 优先级最高,确保发生在所有回调操作之前。
    • 第二个 Observer 会监听 RunLoop 的进入休眠和即将退出 RunLoop 两种状态,在即将进入休眠时会调用 objc_autoreleasePoolPop() 和 objc_autoreleasePoolPush() 根据情况从最新加入的对象一直往前清理直到遇到哨兵对象。而在即将退出 RunLoop 时会调用objc_autoreleasePoolPop() 释放自动自动释放池内对象。这个Observer 的 order 是 2147483647 ,优先级最低,确保发生在所有回调操作之后。
    • 主线程的其他操作通常均在这个 AutoreleasePool 之内(main函数中),以尽可能减少内存维护操作(当然你如果需要显式释放【例如循环】时可以自己创建 AutoreleasePool 否则一般不需要自己创建)。

    UI更新

    如果打印App启动之后的主线程RunLoop可以发现另外一个callout为_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv 的 Observer,这个监听专门负责UI变化后的更新,比如修改了frame、调整了UI层级(UIView/CALayer)或者手动设置了setNeedsDisplay/setNeedsLayout 之后就会将这些操作提交到全局容器。而这个Observer监听了主线程RunLoop的即将进入休眠和退出状态,一旦进入这两种状态则会遍历所有的UI更新并提交进行实际绘制更新。

    通常情况下这种方式是完美的,因为除了系统的更新,还可以利用 setNeedsDisplay 等方法手动触发下一次 RunLoop 运行的更新。但是如果当前正在执行大量的逻辑运算可能UI的更新就会比较卡,因此facebook 推出了 AsyncDisplayKit 来解决这个问题。AsyncDisplayKit 其实是将UI排版和绘制运算尽可能放到后台,将UI的最终更新操作放到主线程(这一步也必须在主线程完成),同时提供一套类 UIView 或 CALayer 的相关属性,尽可能保证开发者的开发习惯。这个过程中 AsyncDisplayKit 在主线程 RunLoop 中增加了一个Observer 监听即将进入休眠和退出 RunLoop 两种状态,收到回调时遍历队列中的待处理任务一一执行。

    UIImageView 延迟加载图片

    Demo & 效果图

    UITableView 与 NSTimer 冲突

    【描述】:由于 UItabelView 在滑动的时候,会从当前的 RunLoop 默认的模式 kCFRunLoopDefaultMode (NSDefaultRunLoopMode) 自动切换到 UITrackingRunLoopMode界面追踪模式。这个时候,处于 NSDefaultRunLoopMode 里面的 NSTimer 由于切换了模式造成计时器无法继续运行。

    【解决】:

    • 1、更改RunLoop运行Mode(NSRunLoopCommonModes
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
    • 2、将NSTimer放到新的线程中
    NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(newThread) object:nil];
        [thread start];
    
    - (void)newThread{
        @autoreleasepool{
            //在当前Run Loop中添加timer,模式是默认的NSDefaultRunLoopMode
            timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(incrementCounter:) userInfo: nil repeats:YES];
            //开始执行新线程的Run Loop,如果不启动run loop,timer的事件是不会响应的
            [[NSRunLoop currentRunLoop] run];
        }  
    }
    

    Runtime & Runloop 面试最常问到的题整理【建议看】

    说明:此面试题针对性的摘录整理,只为方便 在面试路上准备的你 ,会注有原文。


    1、整理原文:2017年5月iOS招人心得(附面试题)

    Runtime
    1. objc在向一个对象发送消息时,发生了什么?
    2. 什么时候会报unrecognized selector错误?iOS有哪些机制来避免走到这一步?
    3. 能否向编译后得到的类中增加实例变量?能否向运行时创建的类中添加实例变量?为什么?
    4. runtime如何实现weak变量的自动置nil?
    5. 给类添加一个属性后,在类结构体里哪些元素会发生变化?
    RunLoop
    1. runloop是来做什么的?runloop和线程有什么关系?主线程默认开启了runloop么?子线程呢?
    2. runloop的mode是用来做什么的?有几种mode
    3. 为什么把NSTimer对象以NSDefaultRunLoopMode(kCFRunLoopDefaultMode)添加到主运行循环以后,滑动scrollview的时候NSTimer却不动了?
    4. 苹果是如何实现Autorelease Pool的?

    //-------------------- 【我是分割线】 ---------------------//

    整理原文:2017年iOS面试题总结,附上答案

    Runtime

    | 01 |
    | :- |
    | 问题: objc在向一个对象发送消息时,发生了什么? |
    | 解答: 根据对象的 isa 指针找到类对象 id,在查询类对象里面的 methodLists 方法函数列表,如果没有在好到,在沿着 superClass ,寻找父类,再在父类 methodLists 方法列表里面查询,最终找到 SEL ,根据 id 和 SEL 确认 IMP(指针函数),在发送消息;|

    | 03 |
    | :- |
    | 问题: 什么时候会报unrecognized selector错误?iOS有哪些机制来避免走到这一步? |
    | 解答: 当发送消息的时候,我们会根据类里面的 methodLists 列表去查询我们要动用的SEL,当查询不到的时候,我们会一直沿着父类查询,当最终查询不到的时候我们会报 unrecognized selector 错误,当系统查询不到方法的时候,会调用 +(BOOL)resolveInstanceMethod:(SEL)sel 动态解释的方法来给我一次机会来添加,调用不到的方法。或者我们可以再次使用 -(id)forwardingTargetForSelector:(SEL)aSelector 重定向的方法来告诉系统,该调用什么方法,一来保证不会崩溃。|

    | 04 |
    | :- |
    | 问题: 能否向编译后得到的类中增加实例变量?能否向运行时创建的类中添加实例变量?为什么? |
    | 解答: 1、不能向编译后得到的类增加实例变量 2、能向运行时创建的类中添加实例变量。【解释】:1. 编译后的类已经注册在 runtime 中,类结构体中的 objc_ivar_list 实例变量的链表和 instance_size 实例变量的内存大小已经确定,runtime会调用 class_setvarlayout 或 class_setWeaklvarLayout 来处理strong weak 引用.所以不能向存在的类中添加实例变量。2. 运行时创建的类是可以添加实例变量,调用class_addIvar函数. 但是的在调用 objc_allocateClassPair 之后,objc_registerClassPair 之前,原因同上. |

    | 05 |
    | :- |
    | 问题: runtime如何实现weak变量的自动置nil? |
    | 解答: runtime 对注册的类, 会进行布局,对于 weak 对象会放入一个 hash 表中。 用 weak 指向的对象内存地址作为 key,当此对象的引用计数为0的时候会 dealloc,假如 weak 指向的对象内存地址是a,那么就会以a为键, 在这个 weak 表中搜索,找到所有以a为键的 weak 对象,从而设置为 nil。|

    | 06 |
    | :- |
    | 问题: 给类添加一个属性后,在类结构体里哪些元素会发生变化?|
    | 解答: instance_size :实例的内存大小;objc_ivar_list *ivars:属性列表|

    RunLoop

    | 01 |
    | :- |
    | 问题: runloop是来做什么的?runloop和线程有什么关系?主线程默认开启了runloop么?子线程呢?|
    | 解答: runloop: 从字面意思看:运行循环、跑圈,其实它内部就是do-while循环,在这个循环内部不断地处理各种任务(比如Source、Timer、Observer)事件。runloop和线程的关系:一个线程对应一个RunLoop,主线程的RunLoop默认创建并启动,子线程的RunLoop需手动创建且手动启动(调用run方法)。RunLoop只能选择一个Mode启动,如果当前Mode中没有任何Source(Sources0、Sources1)、Timer,那么就直接退出RunLoop。 |

    | 02 |
    | :- |
    | 问题: runloop的mode是用来做什么的?有几种mode? |
    | 解答: model:是runloop里面的运行模式,不同的模式下的runloop处理的事件和消息有一定的差别。系统默认注册了5个Mode:(1)kCFRunLoopDefaultMode: App的默认 Mode,通常主线程是在这个 Mode 下运行的。(2)UITrackingRunLoopMode: 界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响。(3)UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用。(4)GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到。(5)kCFRunLoopCommonModes: 这是一个占位的 Mode,没有实际作用。注意iOS 对以上5中model进行了封装 NSDefaultRunLoopMode、NSRunLoopCommonModes |

    | 03 |
    | :- |
    | 问题: 为什么把NSTimer对象以NSDefaultRunLoopMode(kCFRunLoopDefaultMode)添加到主运行循环以后,滑动scrollview的时候NSTimer却不动了? |
    | 解答: nstime对象是在 NSDefaultRunLoopMode下面调用消息的,但是当我们滑动scrollview的时候,NSDefaultRunLoopMode模式就自动切换到UITrackingRunLoopMode模式下面,却不可以继续响应nstime发送的消息。所以如果想在滑动scrollview的情况下面还调用nstime的消息,我们可以把nsrunloop的模式更改为NSRunLoopCommonModes. |

    | 04 |
    | :- |
    | 问题: 苹果是如何实现Autorelease Pool的? |
    | 解答: Autorelease Pool作用:缓存池,可以避免我们经常写relase的一种方式。其实就是延迟release,将创建的对象,添加到最近的autoreleasePool中,等到autoreleasePool作用域结束的时候,会将里面所有的对象的引用计数器 - autorelease. |

    后续遇到针对 runtime & runloop 常面相关,会及时在这里补充;

    Runloop 模块博文推荐(❤️数量较多)


    | 分享者 | Runloop 模块推荐阅读博文 |
    |:-:| :-:|
    | [xx_cc]() | 充满灵性的死循环 http://www.jianshu.com/p/b942... |
    | [WeiHing]() | 原理探究及基本使用 http://www.jianshu.com/p/9115... |
    | [续更]() | -- |

    参考文章:


    效果图


    Reading


    • 如果在阅读过程中遇到 error || new ideas,希望你能 issue 我,我会及时补充谢谢。
    • 喜欢可 赞赏 or Star一波;点击左上角关注 或 微众『Codeidea』,在 Demo 更新时收到邮件通知,便捷你的阅读。
    返回顶部