iOS 模块分解—「Runloop 面试工作」看我就 🐒 了 ^_^.

Posted plainboiledwater

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了iOS 模块分解—「Runloop 面试工作」看我就 🐒 了 ^_^.相关的知识,希望对你有一定的参考价值。

技术分享图片
释义:

  • Run loops 是线程相关底层基础的一部分。它的本质和字面意思一样运行着的循环(事件处理的循环),作用:接受循环事件和安排线程的工作。目的:让线程在有任务的时候忙于工作,而没任务的时候处于休眠状态。

  • Run loop 的管理并非完全自动。你仍然需要设置线程代码在合适的时候启动 run loop 来帮助你处理输入事件。ios 中 Cocoa 和 CoreFoundation 框架中各有完整的一套关于 runloop 对象的操作api,在主线程中 run loop 是自动创建并运行(在子线程开启RunLoop 需要手动创建且手动开启)。

译文&源码


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

目录

  1. Runloop 概念 & 作用
  2. Runloop 开启&退出
  3. Runloop 和线程关系
  4. Runloop 获取 & 源码
  5. Runloop 相关5个类
  6. Runloop 场景
    1.Runloop 经典应用:常驻线程
    2.AutoreleasePool 自动释放池
    3.UI更新
    4.UIImageView 延迟加载图片
    5.UITableView 与 NSTimer 冲突
  7. Runloop 模块博文推荐(??数量较多)
  8. Demo 重要的部分代码中都有相应的注解和文字打印,运行程序可以很直观的表现。
  9. Runtime 模块详解「面试、工作」看我就 ?? 了 ^_^.

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 里。

  • 更多系统或框架 Mode查看这里

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/kenshincui/p/6823841.html
其实对于 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-定时器事件
技术分享图片

技术分享图片

技术分享图片

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];
    }  
}

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

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

参考文章:

Reading


  • 各位厂友,由于「时间 & 知识」有限,总结的文章难免有「未全、不足」,该模块将系统化学习,后替换、补充文章内容 ~
  • 熬夜写者不易,不知名开发者

















以上是关于iOS 模块分解—「Runloop 面试工作」看我就 🐒 了 ^_^.的主要内容,如果未能解决你的问题,请参考以下文章

深入理解Runloop,看我一篇就够了

深入理解Runloop,看我一篇就够了

iOS 常见面试题--runloop

iOS面试题之runloop

iOS开发RunLoop学习:四:RunLoop的应用和RunLoop的面试题

2021年,大厂常问iOS面试题--Runloop篇