Runloop 总结

一、概念

简单点说Runloop就是一个do while的运行循环。主要的作用就是保持程序的持续运行。比如主线程的runloop从程序打开就一直在运行。一个线程对应着一个runloopRunLoop在第一次获取时创建,在线程结束时销毁。
OSX/iOS 系统中,提供了两个这样的对象:NSRunLoopCFRunLoopRef

  • CFRunLoopRef 是在 CoreFoundation 框架内的,它提供了纯 C 函数的 API,所有这些 API 都是线程安全的。
  • NSRunLoop 是基于 CFRunLoopRef 的一层OC包装,提供了面向对象的 API,但是这些 API 不是线程安全

苹果不允许直接创建RunLoop,获取Runloop的两种方式

NSRunLoop
    [NSRunLoop currentRunLoop]; // 获得当前线程的RunLoop对象
    [NSRunLoop mainRunLoop]; // 获得主线程的RunLoop对象
CFRunLoopRef
    CFRunLoopGetCurrent(); // 获得当前线程的RunLoop对象
    CFRunLoopGetMain(); // 获得主线程的RunLoop对象

二、Runloop 与线程的关系

线程和 RunLoop 之间是一一对应的,其关系是保存在一个全局的 Dictionary 里。线程刚创建时并没有 RunLoop,如果你不主动获取,那它一直都不会有。RunLoop 的创建是发生在第一次获取时,RunLoop 的销毁是发生在线程结束时。你只能在一个线程的内部获取其 RunLoop(主线程除外)

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 总结_第1张图片
runloop组成.jpg

Runloop 总结_第2张图片
image.png

两张图都描述了 Runloop的结构 ,从图中可以看出一个 Runloop中由多个 Mode组成,一个 Mode可以包含多个 Source, Timer, Observer.

Source/Timer/Observer 被统称为 mode item,一个item 可以被同时加入多个 mode。但一个item 被重复加入同一个mode 时是不会有效果的。如果一个 mode 中一个 item 都没有,则 RunLoop 会直接退出,不进入循环。

Runloop 总结_第3张图片
1877784-6ab632fc118e31f3.jpg

CoreFoundation 里面关于RunLoop 的这5个类:

  • CFRunLoopRef Runloop
  • CFRunLoopModeRef Mode
  • CFRunLoopSourceRef Source
  • CFRunLoopTimerRef Timer
  • CFRunLoopObserverRef Observer
3.1、CFRunLoopMode

一个 RunLoop 包含若干个 Mode,每个 Mode 又包含若干个 Source/Timer/Observer。
每次RunLoop启动时,只能指定其中一个 Mode,这个Mode被称作 CurrentMode,RunLoop在同一时段只能且必须在一种特定Mode下Run
如果需要切换 Mode,只能退出 Loop,再重新指定一个 Mode 进入。这样做主要是为了分隔开不同组的 Source/Timer/Observer,让其互不影响。

  • kCFRunLoopDefaultMode: App的默认 Mode,通常主线程是在这个 Mode 下运行的。
  • UITrackingRunLoopMode: 界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响。
  • UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用。
  • GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到。
  • kCFRunLoopCommonModes: 这是一个占位的 Mode,没有实际作用。
3.2、CFRunLoopSource

CFRunLoopSource是RunLoop的数据源抽象类,类似iOS中的protocol,RunLoop定义两Source

  • Source0:处理App内部事件,App自己负责管理(触发),如UIEvent,CFSocket,Source0包含了一个回调(函数指针),它并不能主动触发事件。使用时,你需要先调用 CFRunLoopSourceSignal(source),将这个 Source 标记为待处理,然后手动调用CFRunLoopWakeUp(runloop) 来唤醒 RunLoop,让其处理这个事件。
  • Source1:由RunLoop和内核管理,Mach port驱动 如CFMach、CFMessage,Source1包含了一个 mach_port和一个回调(函数指针),被用于通过内核和其他线程相互发送消息。这种 Source 能主动唤醒 RunLoop 的线程。
3.3、CFRunLoopTimerRef

CFRunLoopTimerRef 是基于时间的触发器,它和 NSTimertoll-free bridged的,可以混用。其包含一个时间长度和一个回调(函数指针)。当其加入到 RunLoop 时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行那个回调。

+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
- (void)performSelector:(SEL)aSelector withObject:(id)anArgument afterDelay:(NSTimeInterval)delay inModes:(NSArray *)modes;
+ (CADisplayLink *)displayLinkWithTarget:(id)target selector:(SEL)sel;
- (void)addToRunLoop:(NSRunLoop *)runloop forMode:(NSString *)mode;

都是封装于CFRunLoopTimerRef
3.4、CFRunLoopObserverRef

CFRunLoopObserver向内部报告RunLoop当前状态的更改
CFRunLoopObserverRef 是观察者,每个 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
};

CFRunLoopObserver的创建

CFRunLoopRef runLoop = CFRunLoopGetCurrent();
CFStringRef runLoopMode = kCFRunLoopDefaultMode;
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler
(kCFAllocatorDefault, kCFRunLoopBeforeWaiting, true, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity _) {
    // TODO here
});
CFRunLoopAddObserver(runLoop, observer, runLoopMode);

四、Runloop本质:mach portmach_msg()

MachXNU的内核,进程、线程和虚拟内存等对象通过端口发消息进行通信,Runloop通过mach_msg()函数发送消息,如果没有port 消息,内核会将线程置于等待状态mach_msg_trap() 。如果有消息,判断消息类型处理事件,并通过modeItemcallback回调

五、OC中Runloop参与的地方

5.1、Autorelease Pool

App启动后,苹果在主线程 RunLoop 里注册了两个 Observer,其回调都是_wrapRunLoopWithAutoreleasePoolHandler()
第一个 Observer 监视的事件是 Entry(即将进入Loop),其回调内会调用 _objc_autoreleasePoolPush()创建自动释放池。其 order 是-2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。
第二个 Observer监视了两个事件:BeforeWaiting(准备进入休眠) 时调用_objc_autoreleasePoolPop()_objc_autoreleasePoolPush()释放旧的池并创建新池;Exit(即将退出Loop) 时调用 _objc_autoreleasePoolPop()来释放自动释放池。这个 Observer 的 order 是 2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。
在主线程执行的代码,通常是写在诸如事件回调、Timer回调内的。这些回调会被 RunLoop创建好的 AutoreleasePool环绕着,所以不会出现内存泄漏,开发者也不必显示创建 Pool 了。

5.2、事件响应

苹果注册了一个Source1 (基于 mach port 的) 用来接收系统事件,其回调函数为__IOHIDEventSystemClientQueueCallback()
当一个硬件事件发生后,首先由IOKit.framework 处理,随后用runloop(mach port) 转发给需要的App进程。随后苹果注册的那个 Source1就会触发回调,并调用 _UIApplicationHandleEventQueue() 进行应用内部的分发。
_UIApplicationHandleEventQueue()会把IOHIDEvent处理并包装成 UIEvent 进行处理或分发,其中包括识别UIGesture/处理屏幕旋转/发送给UIWindow等。

5.3、手势识别

当上面的_UIApplicationHandleEventQueue() 识别了一个手势时,其首先会调用 Cancel将当前的 touchesBegin/Move/End 系列回调打断。随后系统将对应的UIGestureRecognizer标记为待处理。
苹果注册了一个Observer 监测BeforeWaiting(Loop即将进入休眠) 事件,这个Observer的回调函数是 _UIGestureRecognizerUpdateObserver(),其内部会获取所有刚被标记为待处理的 GestureRecognizer,并执行GestureRecognizer的回调。
当有 UIGestureRecognizer 的变化(创建/销毁/状态改变)时,这个回调都会进行相应处理。

5.3、更新UI

当在操作 UI 时,比如改变了Frame、更新了 UIView/CALayer 的层次时,或者手动调用了 UIView/CALayersetNeedsLayout/setNeedsDisplay方法后,这个 UIView/CALayer 就被标记为待处理,并被提交到一个全局的容器去。
苹果注册了一个 Observer监听BeforeWaiting(即将进入休眠) 和Exit(即将退出Loop) 事件,回调去执行一个很长的函数:_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()。这个函数里会遍历所有待处理的 UIView/CAlayer 以执行实际的绘制和调整,并更新 UI 界面。

5.4、定时器

NSTimer 其实就是CFRunLoopTimerRef,他们之间是toll-free bridged的。一个 NSTimer注册到RunLoop后,RunLoop 会为其重复的时间点注册好事件。例如 10:00, 10:10, 10:20 这几个时间点。RunLoop为了节省资源,并不会在非常准确的时间点回调这个Timer。Timer 有个属性叫做 Tolerance (宽容度),标示了当时间点到后,容许有多少最大误差。

5.5、PerformSelecter

当调用 NSObjectperformSelecter:afterDelay:后,实际上其内部会创建一个Timer并添加到当前线程的RunLoop 中。所以如果当前线程没有RunLoop,则这个方法会失效。
当调用 performSelector:onThread:时,实际上其会创建一个 Timer 加到对应的线程去,同样的,如果对应线程没有RunLoop该方法也会失效。

5.6、GCD回到主线程

当调用 dispatch_async(dispatch_get_main_queue(), block) 时,libDispatch 会向主线程的 RunLoop 发送消息,RunLoop会被唤醒,并从消息中取得这个block,并在回调 __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__()里执行这个 block。但这个逻辑仅限于 dispatch到主线程,dispatch 到其他线程仍然是由libDispatch 处理的。

5.7、网络请求

通常使用 NSURLConnection 时,你会传入一个Delegate,当调用了 [connection start] 后,这个 Delegate 就会不停收到事件回调。实际上,start 这个函数的内部会获取 CurrentRunLoop,然后在其中的 DefaultMode 添加了4个 Source0 (即需要手动触发的Source)。CFMultiplexerSource 是负责各种 Delegate 回调的,CFHTTPCookieStorage 是处理各种 Cookie

六、Runloop的实际使用

6.1常驻线程

常驻线程的好处在于 Run Loops 可以让你使用最小的资源来创建长时间运行线程。因为 run loop 在没有任何事件处理的时候会把它的线程置于休眠状态,它消除了消耗 CPU 周期轮询,并防止处理器本身进入休眠状态并节省电源。

+ (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;
}
- (void)start {
    [self.lock lock];
    if ([self isCancelled]) {
        [self performSelector:@selector(cancelConnection) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
    } else if ([self isReady]) {
        self.state = AFOperationExecutingState;
        [self performSelector:@selector(operationDidStart) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
    }
    [self.lock unlock];
}
6.2NSTimer

若不希望Timer被ScrollView影响,需添加到NSRunLoopCommonModes

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

避免滑动过程中加载图片,使界面卡顿

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

本文参考:
深入理解RunLoop

你可能感兴趣的:(Runloop 总结)