iOS学习笔记iOS DeveloperIOS

iOS-RunLoop浅析

2017-05-19  本文已影响113人  FlyElephant

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

}

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

上一篇 下一篇

猜你喜欢

热点阅读