一.Runloop是什么

如果说RunTime是OC语言的动态特性的实现技术,那么Runloop则是iOS中线程内的事件调度模型

Runloop的本质就是一个循环代码,只是这个循环会根据是事件驱动的。意味着在没有事件触发的时候,app是不会占用cpu资源的。由此来达到app内部的事件的有效调度。

在Runloop中接受两种事件源:Input SourceTimer Source

其中 Input Source又可以分为三类:

  • Port-Based Sources,系统底层的 Port 事件,例如 CFSocketRef ,在应用层基本用不到。
  • Custom Input Sources,用户手动创建的 Source。
  • Cocoa Perform Selector Sources,Cocoa 提供的performSelector系列方法,也是一种事件源。

Timer Source顾名思义就是指定时器事件了(NSTimer)。

二.Runloop 与线程的关系

  • Runloop和线程是绑定在一起的。
  • 每个线程(包括主线程)都有一个对应的 Runloop 对象。
  • 线程执行了这个函数后,就会一直处于这个函数内部 “接受消息->等待->处理” 的循环中,直到这个循环结束(比如传入 quit 的消息),函数返回。
  • 我们并不能自己创建 Runloop 对象,但是可以获取到系统提供的Runloop对象(系统帮我们为每个线程创建了Runloop,除了主线程以外默认是不运行的,所以在其他线程使用NSTimer就需要手动开启该线程的Runloop)。
/// 全局的Dictionary,key 是 pthread_t, value 是 CFRunLoopRef
static CFMutableDictionaryRef loopsDic;
/// 访问 loopsDic 时的锁
static CFSpinLock_t loopsLock;

/// 获取一个 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;
}

CFRunLoopRef CFRunLoopGetMain() {
    return _CFRunLoopGet(pthread_main_thread_np());
}

CFRunLoopRef CFRunLoopGetCurrent() {
    return _CFRunLoopGet(pthread_self());
}

二.Runloop的执行单位:Runloop Mode

一个RunLoop以单个Mode为执行单位,一个 RunLoop 包含若干个 Mode,每个 Mode 又包含若干个Source/Timer/Observer。每次调用 RunLoop 的主函数时,只能指定其中一个Mode,这个Mode被称作CurrentMode

如果需要切换 Mode,只能退出 Loop,再重新指定一个 Mode 进入。这样做主要是为了分隔开不同组的Source/Timer/Observer,让其互不影响。

苹果文档中提到的 Mode 有五个,分别是:

  • NSDefaultRunLoopMode
  • NSConnectionReplyMode
  • NSModalPanelRunLoopMode
  • NSEventTrackingRunLoopMode
  • NSRunLoopCommonModes

iOS 中公开暴露出来的只有 NSDefaultRunLoopMode NSRunLoopCommonModes。NSRunLoopCommonModes 实际上是一个 Mode 的集合,默认包括 NSDefaultRunLoopMode 和 NSEventTrackingRunLoopMode。

系统提供的很多API是基于上诉的几种Mode来实现的,用户是无法实现一个完整的Mode(也没有必要)。使用事件调度模型的好处在于提高线程的利用率,我们要实现添加一个自定义事件到Runloop中有两种方法:自定义Input Source和使用performSelector系列API。

performSelector:OnThread

performSelectorOnMainThread:withObject:waitUntilDone:  
performSelectorOnMainThread:withObject:waitUntilDone:modes:

performSelector:onThread:withObject:waitUntilDone:  
performSelector:onThread:withObject:waitUntilDone:modes:

performSelector:withObject:afterDelay:  
performSelector:withObject:afterDelay:inModes:

cancelPreviousPerformRequestsWithTarget:  
cancelPreviousPerformRequestsWithTarget:selector:object:

perform selector请求会在目标线程上序列化,减缓在单个线程上容易引起的同步问题。perform selector执行完后会自动清除出run loop。值得注意的是,由于它是运行在runloop之上的所以在runloop在未开始运行的时候是不会去执行的。

自定义Input Source

参考:Runloop文档

二.Runloop Mode的内部逻辑

实际上 RunLoop 内部是一个 do-while 循环。当你调用 CFRunLoopRun() 时,线程就会一直停留在这个循环里,直到超时或被手动停止,该函数才会返回。

三.Runloop 的常见应用

NSTimer

NSTimer是一个使用Timer Source作为输入源来实现的定时器(底层基于使用mk_timer实现)。NSTimer定时器的触发正是基于RunLoop运行的,所以使用NSTimer之前必须注册到RunLoop,但是RunLoop为了节省资源并不会在非常准确的时间点调用定时器,如果一个任务执行时间较长,那么当错过一个时间点后只能等到下一个时间点执行,并不会延后执行(NSTimer提供了一个tolerance属性用于设置宽容度,如果确实想要使用NSTimer并且希望尽可能的准确,则可以设置此属性)。

我们日常使用的时候,通常就是加入到当前的 Runloop的default mode中,而 ScrollView 在用户滑动时,主线程 RunLoop会转到 UITrackingRunLoopMode,也就是Runloop的Mode进行了切换,位于default mode的定时器就不能工作了。

有如下两种解决方案:

  • 第一种: 设置RunLoop Mode,例如NSTimer,我们指定它运行于 NSRunLoopCommonModes,这是一个Mode的集合。注册到这个 Mode 下后,无论当前 runLoop 运行哪个 mode ,事件都能得到执行。
  • 第二种: 另一种解决Timer的方法是,我们在另外一个线程执行和处理 Timer 事件,然后在主线程更新UI。

AutoreleasePool

AutoreleasePool是Objective-C中内存管理的一种方法。AutoreleasePool与RunLoop并没有直接的关系,之所以将两个话题放到一起讨论最主要的原因是因为在iOS应用启动后会注册两个Observer管理和维护AutoreleasePool。也就是AutoreleasePool所实现的内存自动释放是运行在Runloop之上的。

UI更新

App在启动的时候会向Runloop里面注册一个Observer专门用于监听UI变化的更新。比如修改了frame、调整了UI层(UIView/CALayer)或者手动设置了setNeedsDisplay/setNeedsLayout之后就会将这些操作提交到全局容器。而这个Observer监听了主线程RunLoop的即将进入休眠和退出状态,一旦进入这两种状态则会遍历所有的UI更新并提交进行实际绘制更新。

NSURLConnection

一旦NSURLConnection设置了delegate会立即创建一个线程com.apple.NSURLConnectionLoader,同时内部启动RunLoop并在NSDefaultMode模式下添加4个Source0。其中CFHTTPCookieStorage用于处理cookie ;CFMultiplexerSource负责各种delegate回调并在回调中唤醒delegate内部的RunLoop(通常是主线程)来执行实际操作。

results matching ""

    No results matching ""