RunLoop 梳理

2018-04-03  本文已影响70人  扬仔360

简介

存在的必要性:应用程序需要一直运行,所以需要一个以事件驱动为基础的事件循环机制。

RunLoop 的架构主题就是一个死循环,一个事件循环,没有退出就一直运行。这个循环具体的实现后文有详细的描述。

int main() {
    init();
    do {
        var message = get_next_message();
        prcess_mesage(message);
    } while (message != quit);
}

调用解耦:主调方把需要执行的逻辑放入消息队列中。执行方从消息队列中拿到并执行,与主调方实现解耦。

RunLoop 系统上的应用

框架结构

两个对象:

CFRunLoopRef 在 CoreFoundation 框架中,提供纯 C 的函数供调用,线程安全,并且开源的
NSRunLoop 基于 CFRunLoopRef 的对象封装,体面面向对象的 API。不做具体的事情。

创建过程

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

用于获得 mainRunLoop 以及 currentRunLoop

下面代码这里来自:YY 的 RunLoop 博客 -- 深入理解RunLoop

/// 全局的Dictionary,key 是 pthread_t, value 是 CFRunLoopRef
static CFMutableDictionaryRef loopsDic;
/// 访问 loopsDic 时的锁
static CFSpinLock_t loopsLock;
 
CFRunLoopRef CFRunLoopGetMain() {
    return _CFRunLoopGet(pthread_main_thread_np());
}
 
CFRunLoopRef CFRunLoopGetCurrent() {
    return _CFRunLoopGet(pthread_self());
}

/// 获取一个 pthread 对应的 RunLoop。
CFRunLoopRef _CFRunLoopGet(pthread_t thread) {
    OSSpinLockLock(&loopsLock);
    
    if (!loopsDic) {
        // 第一次进入时,初始化全局Dic,并先为主线程创建一个 RunLoop。
        loopsDic = CFDictionaryCreateMutable();
        CFRunLoopRef mainLoop = _CFRunLoopCreate();
        CFDictionarySetValue(loopsDic, pthread_main_thread_np(), mainLoop);
    }
    
    /// 直接从 Dictionary 里获取。
    CFRunLoopRef loop = CFDictionaryGetValue(loopsDic, thread));
    
    if (!loop) {
        /// 取不到时,创建一个
        loop = _CFRunLoopCreate();
        CFDictionarySetValue(loopsDic, thread, loop);
        /// 注册一个回调,当线程销毁时,顺便也销毁其对应的 RunLoop。
        _CFSetTSD(..., thread, loop, __CFFinalizeRunLoop);
    }
    
    OSSpinLockUnLock(&loopsLock);
    return loop;
}

通过创建的过程,分析 RunLoop 与线程的关系

创建 RunLoop 的方法表明了 RunLoop 与线程一定是一一对应的关系,尤其体现在 loopsDic 这个字典,字典中 key 是 pthread_t, value 是 CFRunLoopRef

在任何线程中,只能获取当前线程的 runLoop 和主线程的 runLoop。即上文中的 mainRunLoopcurrentRunLoop

内部结构

runloop_img01.png

CFRunLoop 与线程是一一对应的,一个 Thread 里面可以有很多 RunLoop。

CFRunLoopMode:CFRunLoop 对应一个或多个 Mode,CFRunTimeMode

Mode 之下有 Timer Sources Observer

可以理解成: Source timer 以及 Observer 才是外部真正关心的事情,Mode 是对他们做的区分,最外通过 CFRunLoopRef 进行封装。

CFRunLoopTimer

是个基于事件的触发器,有时间和回调的地址,时间到达的时候,会执行回调地址的函数。

具体的应用有:NSTime、延迟执行performSelectorAfterDelay、displayLink。

CFRunLoopSource

Source 是 RunLoop 的数据输入源,是一个抽象的 Protocol,符合这个 Source 的对象,才可以在 RunLoop 上面去执行,具体的看下面的 CFRunLoopSourceContext

Source 有两个版本:Source0 Source1

Source0:属于 App 内部事件,App 自己去负责管理和触发,比如:UIEvent、CFSocket。

Source0 中只有回调的指函数针,不能主动出发事件。

Source1:由 RunLoop 和 内核管理,Mach port 驱动(进程直接通讯的方式),比如:CFMachPort、CFMessagePort

可以基于 这个 Source Protool 自己实现一个 Source,基本不会去实现,不过没想到什么应用场景

struct __CFRunLoopSource {
    CFRuntimeBase _base;
    uint32_t _bits;
    pthread_mutex_t _lock;
    CFIndex _order;         
    CFMutableBagRef _runLoops;
    union {
        CFRunLoopSourceContext version0;
        CFRunLoopSourceContext1 version1;
    } _context;
};

// union 中的 CFRunLoopSourceContect version0:
typedef struct {
    CFIndex version;
    void *  info;
    const void *(*retain)(const void *info);
    void    (*release)(const void *info);
    CFStringRef (*copyDescription)(const void *info);
    Boolean (*equal)(const void *info1, const void *info2);
    CFHashCode  (*hash)(const void *info);
    void    (*schedule)(void *info, CFRunLoopRef rl, CFStringRef mode);
    void    (*cancel)(void *info, CFRunLoopRef rl, CFStringRef mode);
    void    (*perform)(void *info);
} CFRunLoopSourceContext;

union 中的很多都是函数指针,需要实现体自己去实现,比如 内存管理的 retain release copy equal hash。最重要的是最后的一个 perform 方法,真正去调用的方法。

CFRunLoopObserver

RunLoop 开放给外部的 观察者,相当于 Delegate,每个 Observer 都包含了一个回调(函数指针),当 RunLoop 的状态发生变化时,观察者就能通过回调接受到这个变化:

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry         = (1UL << 0), // 即将进入 Loop
    kCFRunLoopBeforeTimers  = (1UL << 1), // 即将处理 Timer
    kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Source
    kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠
    kCFRunLoopAfterWaiting  = (1UL << 6), // 刚从休眠中唤醒
    kCFRunLoopExit          = (1UL << 7), // 即将退出 Loop
};

框架中的一些机制结合了 RunLoopObserver,比如 CAAnimation,会有一些动画的延迟机制

CFRunLoopMode 比较重要

RunLoop 一定有且仅有一个 Mode

如果要切换,这个 runLoop 会 quit,重新走一个循环

iOS 滑动的时候,是 Mode: UITrackingRunLoopMode

所有的Mode:

NSDefaultRunLoopMode          默认状态、空闲状态
UITrackingRunLoopMode         滑动 ScrollView 时,iOS 流程的关键
UIInitializationRunLoopMode   私有,App 启动时是这个 Mode,成功后切换成第一个 Mode
NSRunLoopCommonModes          []是一个数组,第一个 Mode 和第二个结合,都可以执行

Timer 与 Mode

滑动的时候,Time 是不会跑的,因为 RunLoop 在 UITrackingRunLoopMode 上。

解决方法就是加入到 NSRunLoopCommonModes 中去:

NSTimer *timer = [NSTimer timerWithTimeInterval:1.0
                                             target:self
                                           selector:@selector(timerTick:)
                                           userInfo:nil
                                            repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

注意:GCD 的 Timer 是内核单独维护的,跟 RunLoop 平级,只是落地点是在 RunLoop 上。

RunLoop 执行过程简化记录

    //进入循环之前,调用GCDTimer,设置过期时间,否则就真的变成死循环了
    SetupThisRunLoopRunTimeoutTimer(); // by GCD timer

    do {
        //进入循环 告诉 Observer 告诉要进入 timers sources
        __CFRunLoopDoObservers(kCFRunLoopBeforeTimers);
        __CFRunLoopDoObservers(kCFRunLoopBeforeSources);
        
        __CFRunLoopDoBlocks();
        //遍历 Source 0 执行
        __CFRunLoopDoSource0();
        
        //需要跑的代码直执行:__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__         
        CheckIfExistMessagesInMainDispatchQueue(); // GCD
        
        //调用 Observers BeforeWaiting
        __CFRunLoopDoObservers(kCFRunLoopBeforeWaiting);
        
        //挂起方法,进入 trap,卡在这里,等待唤醒,获得唤醒的端口号
        var wakeUpPort = SleepAndWaitForWakingUpPorts();
        // mach_msg_trap

        // Received mach_msg, wake up
        
        __CFRunLoopDoObservers(kCFRunLoopAfterWaiting);
        
        // Handle msgs   如果是Timer 唤醒的,就跑 Timer
        if (wakeUpPort == timerPort) {
            __CFRunLoopDoTimers();
            
            // 如果是 GCD 唤醒的
        } else if (wakeUpPort == mainDispatchQueuePort) {
            __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__()
            
            // 基于 port 唤醒的,比如网络来数据了,就去处理数据
        } else {
            __CFRunLoopDoSource1();
        }
       __CFRunLoopDoBlocks();
       
       // 判断是不是停止了, timeout 了没有
   } while (!stop && !timeout);

RunLoop 的底层

OS X / iOS 中最底层是 Drawin 层,这个层面中包括了 mach port,在 <mach/message.h> 中有 mach 的定义:

typedef struct {
  mach_msg_header_t header;
  mach_msg_body_t body;
} mach_msg_base_t;
 
typedef struct {
  mach_msg_bits_t msgh_bits;
  mach_msg_size_t msgh_size;
  mach_port_t msgh_remote_port;
  mach_port_t msgh_local_port;
  mach_port_name_t msgh_voucher_port;
  mach_msg_id_t msgh_id;
} mach_msg_header_t;

发送和接受消息的 API 如下,option 标记了消息传递的方向:

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

RunLoop 的挂起和唤醒

RunLoop 的另一个核心就在于 mach_msg(),RunLoop 调用这个方法后去等待唤醒,并获得唤醒源进行判断。

调用 mach_msg 监听唤醒端口,被唤醒前,系统内核将这个线程挂起,停留在 mach_msg_trap 状态。

由另一个线程(或另一个进程中的某个线程)向内核发送这个端口的 msg 后,trap 状态被唤醒,RunLoop 继续执行。

RunLoop 中进入 trap 等待唤醒其实就是一个 mach_msg()。例如你在模拟器里跑起一个 iOS 的 App,然后在 App 静止时点击暂停,你会看到主线程调用栈是停留在 mach_msg_trap() 这个地方。

RunLoop Callouts 调用外部方法的途径

当 RunLoop 调用 modeItems 中的外部指针的时候,都是通过一个很长的函数调的。

所以几乎所有的函数都是由这些方法调起的:

1. __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__
2. __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__
3. __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__
4. __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__
5. __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__
6. __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__
  1. 回调 Observer
  2. Source 0 的时候调用 block
  3. GCD 调用主线程,分发到 mainRunLoop 中执行
  4. Time 唤醒 RunLoop 的话,回调 Timer
  5. 触发 Source0 (非基于 port 的) 回调
  6. 触发 Source1 (mach_port 的) 回调

命名成这样也是为了在调用栈里面可以自解释。

与 GCD 的关系

__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ 如果在 GCD 中派发到主线程,那么就会分发到 mainRunLoop 中执行。

dispatch_after 同理。

RunLoopObserver 与 Autorelease Pool

Objective-C 高级编程那本书中有提到:

RunLoop 的每次循环过程中,NSAutoreleasePool 对象被生成或废弃

准确的说,RunLoop 的下两次 Sleep/Trap 过程之间,上一次的 NSAutoreleasePool 有了时机去执行 drain()。

Apple 在 mainRunLoop 中注册两个 Observer:

当进入 Loop 的时候,调用 _objc_autoreleasePoolPush() 创建 自动释放池,这个操作的优先级是最小的 Int,确保发生在其他所有的回调之前。

第二个 Observer 发生在 Trap 休眠发成之前,会释放旧池,并创建新池。Quit 的时候也会释放旧池,这个操作优先级是最大的 Int,确保发生在其他所有的回调之后。

事件响应 与 手势的应用

事件响应上,iOS 的实现是注册一个基于 mach_port 的 Source1 去接收系统事件,回调函数为__IOHIDEventSystemClientQueueCallback()

IOKit 生成 IOHIDEvent,仅能由 SpringBoard 接收(iOS 6.1加入的限制),然后发送给各个需要的 App 的 mach_port,_UIApplicationHandleEventQueue() 会包装成 UIEvent,包括了 gesture、屏幕旋转、点击等,发送给 UIWindow。这里罗列了目前支持的 service 以及 keyboard events

事件响应过程图示:
Touch发生 ----> IOKit 感知
                 |
                 |
                 V
           生成 IOHIDEvent
                 |
                 |
                 V
---------------     ---------------
|                                 |
|          SpringBoard            |
|                                 |                 
-----------------------------------
            |
            |  传递到需要的 App 的 mach_port ---> 
            |         入口:RunLoop 的 Source 1:
            |            _IOHIDEventSystemClientQueueCallback()
            V
       ----   ----
       |         |   _UIApplicationHandleEventQueue()
       |   App   |     接收并生成 UIEvent --> UIWindow
       |         |
       -----------

像 UIButton 的点击,touchBegin 等也都是在这个回调中完成的。

手势的实现:在 RunLoop beforeWaiting (即将休眠的时候)注册了,_UIGestureRecognizerUpdateObserver() 获得所有待处理的 GestureRecognizer,并执行回调。

关于网络请求

    CFSocket
    CFNetWorking     -> ASIHttpRequest
    NSURLConnection  -> AFNetworking
    NSURLSession     -> AFNetworking 2, Alamofire

NSURLConnection 工作的时候,创建一个 自己的线程 C 和 CFSocket 的线程 S。

底层的 S 通过 C 的 Source 1 通知到 C,C 再通过 currentRunLoop 的 Source 0 去 通知到最上层的 Delegate。

实践 1:AFN 中对于 RunLoop 的实践 -- 常驻线程

下面是 AFN 的两端初始化的代码,第二个方法是 AFN 创建线程的方法,其中调用第一个方法,其结果是 AFN 这个单例创建持有了一个常驻的线程 + RunLoop,给 RunLoop 添加一个 mach port,一直去监听,这个 port 不会发送东西,让这个 RunLoop 一直不被销毁。

这是 AFN 线程常驻的一个很好的方法。

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

实践 2:TableView 加载图片的优化

滑动的时候设置图片会影响帧数,通过下面的代码,避开 trackingMode 的 RunLoop,在 default 中执行:

    UIImage *downloadedImage = ...;
    [self.avatarImageView performSelector:@selector(setImage:)
                               withObject:downloadedImage
                               afterDelay:0
                                  inModes:@[NSDefaultRunLoopMode]];

参考资料:

RunLoop文章:

其他:

上一篇 下一篇

猜你喜欢

热点阅读