iOS-RunLoop浅析

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

基础实现

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

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

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());
}

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

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

Runloop运行:

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

运行机制

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定义如下:

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 */
};
iOS-RunLoop浅析_第1张图片
RunLoop.jpg

基础知识

RunLoop运行状态通过CFRunLoopActivity可以查看:

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
};
iOS-RunLoop浅析_第2张图片
运行时.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事件和观察者事件.

iOS-RunLoop浅析_第3张图片
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时退出.

NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
while (!self.isCancelled && !self.isFinished) {
    @autoreleasepool {
            [runLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:3]];
    }
}

2.创建常驻线程,执行一些会一直存在的任务,该线程的生命周期跟App相同:

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

创建常驻线程最经典的例子是AFNetWorking 2.x版本中代码:

+ (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;
}

3.在一定时间内监听某种事件,或执行某种任务的线程
如下代码,在30分钟内,每隔30s执行onTimerFired.这种场景一般会出现在,如我需要在应用启动之后,在一定时间内持续更新某项数据,如果用来监控屏幕的卡顿也可以.

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

4.UITableView滚动加载图片
当tableView的cell上有需要从网络获取的图片的时候,滚动tableView,异步线程回去加载图片,加载完成后主线程会设置cell的图片,但是会造成卡顿。可以设置图片的任务在CFRunloopDefaultMode下进行,当滚动tableView的时候,Runloop切换到UITrackingRunLoopMode,不去设置图片,而是而是当停止的时候,再去设置图片.

[self performSelector:@selector(download:) withObject:url afterDelay:0 inModes:NSDefaultRunLoopMode];

5.NSTimer失效
如果页面有计时器同时有滑动视图的时候,需要注意NSTimer的模式,视图滑动的过程会切换至UITrackingMode模式下,造成Timer短暂失效,将Timer的模式设置为CommonMode即可.

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

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

你可能感兴趣的:(iOS-RunLoop浅析)