iOS-RunLoop浅析

作者: FlyElephant | 来源:发表于2017-05-19 12:35 被阅读113次

    RunLoop是iOS事件响应与任务处理最核心的机制,它贯穿iOS整个系统,自动释放池,延迟处理,触摸事件,屏幕刷新都是通过RunLoop实现的.Foundation中的NSRunLoop和Core Foundation中CFRunLoop 是RunLoop的主要实现.

    基础实现

    RunLoop通过do-while循环保持整个App的持续运行,同时能在运行和睡眠状态之间切换,节省CPU资源.Android中的Looper跟iOS中的RunLoop类似,接收异步消息,控制应用程序的生命周期.一般情况一个线程只能执行一个任务,执行完成后就会退出.我们可以通过Runloop保证线程能随时处理事件,并不退出.

    Apple不允许直接创建 RunLoop,提供了两个自动获取的函数:CFRunLoopGetMain() 和 CFRunLoopGetCurrent().

    <pre><code>`CFRunLoopRef CFRunLoopGetMain(void) {
    CHECK_FOR_FORK();
    static CFRunLoopRef __main = NULL; // no retain needed
    if (!__main) __main = _CFRunLoopGet0(pthread_main_thread_np()); // no CAS needed
    return __main;
    }

    CFRunLoopRef CFRunLoopGetCurrent(void) {
    CHECK_FOR_FORK();
    CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
    if (rl) return rl;
    return _CFRunLoopGet0(pthread_self());
    }`</code></pre>

    pthread_self获取当前线程,pthread_main_thread_np获取主线程,通过线程获取当前的Runloop.线程和Runloop被保存在全局字典__CFRunLoops中,如果字典中存在则会取出,如果不存在则会创建.

    <pre><code>// should only be called by Foundation // t==0 is a synonym for "main thread" that always works CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) { if (pthread_equal(t, kNilPthreadT)) { t = pthread_main_thread_np(); } __CFLock(&loopsLock); if (!__CFRunLoops) { __CFUnlock(&loopsLock); CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks); CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np()); CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop); if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) { CFRelease(dict); } CFRelease(mainLoop); __CFLock(&loopsLock); } CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t)); __CFUnlock(&loopsLock); if (!loop) { CFRunLoopRef newLoop = __CFRunLoopCreate(t); __CFLock(&loopsLock); loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t)); 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; }</code></pre>

    Runloop运行:

    <pre><code>void CFRunLoopRun(void) { /* DOES CALLOUT */ int32_t result; do { result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false); CHECK_FOR_FORK(); } while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result); }</code></pre>

    运行机制

    RunLoop是线程中的一个循环,在循环中不断检测通过Input sources(输入源)和Timer sources(定时源)两种来源等待接受事件,然后对接受到的事件通知线程进行处理,并在没有事件的时候进行睡眠.

    RunLoop与线程之间的关系密不可分:
    1.线程与RunLoop是一一对应的,一个线程对应一个RunLoop对象,根RunLoop可以嵌套子RunLoop.
    2.主线程的RunLoop在应用启动的时候会自动创建,非主线程的RunLoop需要在该线程自己启动.
    3.RunLoop对象在第一次获取RunLoop时创建,销毁则是在线程结束的时候.
    4.不能自己创建RunLoop.
    5.RunLoop并不是线程安全的,只能在当前线程中操作当前线程的RunLoop,而不能去操作其他线程的RunLoop,同时也需要避免在其他线程上调用当前线程的RunLoop.

    一个 RunLoop 对象包含若干个 Mode 对象,每个 Mode 又包含若干个 Source/Timer/Observer,RunLoop 一次运行只能在一个 Mode 之下,如果需要切换 Mode,需要退出 Loop 才能重新指定一个 Mode.这样做主要是为了分隔开不同组Source/Timer/Observer,让其互不影响.

    一个 Source 对象是一个事件,Source 有两个版本:Source0 和 Source1,Source0 只包含一个函数指针,并不能主动触发,需要将 Source0 标记为待处理,在 RunLoop 运转的时候,才会处理这个事件(如果 RunLoop 处于休眠状态,则不会被唤醒去处理),而 Source1 包含了一个 mach_port 和一个函数指针,mach_port 是 iOS 系统提供的基于端口的输入源,可用于线程或进程间通讯。而 RunLoop 支持的输入源类型中就包括基于端口的输入源,可以做到对 mach_port 端口源事件的监听。所以监听到 source1 端口的消息时,RunLoop 就会自己醒来去执行 Source1 事件(也能称为被消息唤醒)。也就是 Source0 是直接添加给 RunLoop 处理的事件,而 Source1 是基于端口的,进程或线程之间传递消息触发的事件.

    Timer 是基于时间的触发器,CFRunLoopTimerRef 和 NSTimer 可以通过 Toll-free bridging 技术混用,Toll-free bridging 是一种允许某些 ObjC 类与其对应的 CoreFoundation 类之间可以互换使用的机制,当将 Timer 加入到 RunLoop 时,RunLoop 会注册对应的时间点,当时间点到时,RunLoop 会被唤醒以执行 Timer 回调.

    __CFRunLoopMode定义如下:
    <pre><code>`struct __CFRunLoopMode {
    CFRuntimeBase _base;
    pthread_mutex_t _lock; /* must have the run loop locked before locking this */
    CFStringRef _name;
    Boolean _stopped;
    char _padding[3];
    CFMutableSetRef _sources0;
    CFMutableSetRef _sources1;
    CFMutableArrayRef _observers;
    CFMutableArrayRef _timers;
    CFMutableDictionaryRef _portToV1SourceMap;
    __CFPortSet _portSet;
    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;
    Boolean _mkTimerArmed;
    

    endif

    if DEPLOYMENT_TARGET_WINDOWS

    DWORD _msgQMask;
    void (*_msgPump)(void);
    

    endif

    uint64_t _timerSoftDeadline; /* TSR */
    uint64_t _timerHardDeadline; /* TSR */
    

    };`</code></pre>

    RunLoop.jpg

    基础知识

    RunLoop运行状态通过CFRunLoopActivity可以查看:
    <pre><code>typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) { kCFRunLoopEntry = (1UL << 0), kCFRunLoopBeforeTimers = (1UL << 1), kCFRunLoopBeforeSources = (1UL << 2), kCFRunLoopBeforeWaiting = (1UL << 5), kCFRunLoopAfterWaiting = (1UL << 6), kCFRunLoopExit = (1UL << 7), kCFRunLoopAllActivities = 0x0FFFFFFFU };</code></pre>

    运行时.png
    1. kCFRunLoopEntry -- 进入runloop循环
    2. kCFRunLoopBeforeTimers -- 处理定时调用前回调
    3. kCFRunLoopBeforeSources -- 处理input sources的事件
    4. kCFRunLoopBeforeWaiting -- runloop睡眠前调用
    5. kCFRunLoopAfterWaiting -- runloop唤醒后调用
    6. kCFRunLoopExit -- 退出runloop

    CoreFoundation中关于RunLoop的5个重要的类:

    1. CFRunLoopRef:运行循环对象,也就是它自身.
    2. CFRunLoopModeRef:指定runloop的运行模式.给事件源分组,避免互相影响.一个runLoop可以有很多个Mode,1个Mode可以有很多个Source,Observer,Timer,但是在同一时刻只能同时执行一种Mode.
    3. CFRunLoopSourceRef:输入源.
    4. CFRunLoopTimerRef:定时源,定时器,必须加入到runloop.
    5. CFRunLoopObserverRef(观察者,观察是否有事件).

    关于五个主要的类可以描述为一个RunLoop对象中包含若干个运行模式(CFRunLoopModeRef),而每一个运行模式下又包含若干个输入源(CFRunLoopSourceRef)、定时源(CFRunLoopTimerRef)、观察者(CFRunLoopObserverRef).

    单次Runloop可以处理Source1(触摸/锁屏/摇晃),Source0事件(需要手动触发),Timer事件和观察者事件.

    ibireme.png

    Runloop 模式

    系统默认定义了多种运行模式(CFRunLoopModeRef),常见的有五种:

    1. kCFRunLoopDefaultMode:App的默认Mode,通常主线程是在这个 Mode 下运行的.

    2. UITrackingRunLoopMode:界面跟踪 Mode,用于ScrollView 追踪触摸滑动,保证界面滑动时不受其他Mode 影响.

    3. UIInitializationRunLoopMode:在刚启动 App 时第进入的第一个Mode,启动完成之后就不再使用.

    4. GSEventReceiveRunLoopMode:接收系统事件的内部 Mode,通常用不到.

    5. kCFRunLoopCommonModes:占位用的 Mode,不是一种真正的 Mode.

    RunLoop与自动释放池:
    自动释放池寄生于Runloop,程序启动后,主线程注册了两个Observer监听runloop的进出和睡眠.一个最高优先级OB监测Entry状态,一个最低优先级OB监听BeforeWaiting状态和Exit状态.

    RunLoop 实战

    1.维护线程的生命周期,让线程不自动退出,isFinished为Yes时退出.
    <pre><code>NSRunLoop *runLoop = [NSRunLoop currentRunLoop]; [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode]; while (!self.isCancelled && !self.isFinished) { @autoreleasepool { [runLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:3]]; } }</code></pre>

    2.创建常驻线程,执行一些会一直存在的任务,该线程的生命周期跟App相同:
    <pre><code>@autoreleasepool { NSRunLoop *runLoop = [NSRunLoop currentRunLoop]; [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode]; [runLoop run]; }</code></pre>

    创建常驻线程最经典的例子是AFNetWorking 2.x版本中代码:
    <pre><code>`+ (void)networkRequestThreadEntryPoint:(id)__unused object {
    @autoreleasepool {
    [[NSThread currentThread] setName:@"AFNetworking"];

        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        [runLoop run];
    }
    

    }

    • (NSThread *)networkRequestThread {
      static NSThread *_networkRequestThread = nil;
      static dispatch_once_t oncePredicate;
      dispatch_once(&oncePredicate, ^{
      _networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
      [_networkRequestThread start];
      });

      return _networkRequestThread;
      }`</code></pre>

    3.在一定时间内监听某种事件,或执行某种任务的线程
    如下代码,在30分钟内,每隔30s执行onTimerFired.这种场景一般会出现在,如我需要在应用启动之后,在一定时间内持续更新某项数据,如果用来监控屏幕的卡顿也可以.
    <pre><code>@autoreleasepool { NSRunLoop * runLoop = [NSRunLoop currentRunLoop]; NSTimer * udpateTimer = [NSTimer timerWithTimeInterval:30 target:self selector:@selector(onTimerFired:) userInfo:nil repeats:YES]; [runLoop addTimer:udpateTimer forMode:NSRunLoopCommonModes]; [runLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:60*30]]; }</code></pre>

    4.UITableView滚动加载图片
    当tableView的cell上有需要从网络获取的图片的时候,滚动tableView,异步线程回去加载图片,加载完成后主线程会设置cell的图片,但是会造成卡顿。可以设置图片的任务在CFRunloopDefaultMode下进行,当滚动tableView的时候,Runloop切换到UITrackingRunLoopMode,不去设置图片,而是而是当停止的时候,再去设置图片.
    <pre><code>[self performSelector:@selector(download:) withObject:url afterDelay:0 inModes:NSDefaultRunLoopMode];</code></pre>

    5.NSTimer失效
    如果页面有计时器同时有滑动视图的时候,需要注意NSTimer的模式,视图滑动的过程会切换至UITrackingMode模式下,造成Timer短暂失效,将Timer的模式设置为CommonMode即可.
    <pre><code>` self.upTimer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(upTimeUpdate) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:self.upTimer forMode:NSRunLoopCommonModes];
    [self.upTimer fire];

    self.bottomTimer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(bottomTimeUpdate) userInfo:nil repeats:YES];`</code></pre>
    

    参考资料:
    CF框架源码
    RunLoop 原理和核心机制
    CoreFoundation
    深入理解RunLoop
    滑动卡顿优化
    官方文档
    CF源码
    http://blog.raozhizhen.com/post/2016-08-18

    相关文章

      网友评论

        本文标题:iOS-RunLoop浅析

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