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

浏览: 161 发布日期: 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的运行状态(它包含一个函数指针_call