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().
<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>
- kCFRunLoopEntry -- 进入runloop循环
- kCFRunLoopBeforeTimers -- 处理定时调用前回调
- kCFRunLoopBeforeSources -- 处理input sources的事件
- kCFRunLoopBeforeWaiting -- runloop睡眠前调用
- kCFRunLoopAfterWaiting -- runloop唤醒后调用
- kCFRunLoopExit -- 退出runloop
CoreFoundation中关于RunLoop的5个重要的类:
- CFRunLoopRef:运行循环对象,也就是它自身.
- CFRunLoopModeRef:指定runloop的运行模式.给事件源分组,避免互相影响.一个runLoop可以有很多个Mode,1个Mode可以有很多个Source,Observer,Timer,但是在同一时刻只能同时执行一种Mode.
- CFRunLoopSourceRef:输入源.
- CFRunLoopTimerRef:定时源,定时器,必须加入到runloop.
- CFRunLoopObserverRef(观察者,观察是否有事件).
关于五个主要的类可以描述为一个RunLoop对象中包含若干个运行模式(CFRunLoopModeRef),而每一个运行模式下又包含若干个输入源(CFRunLoopSourceRef)、定时源(CFRunLoopTimerRef)、观察者(CFRunLoopObserverRef).
单次Runloop可以处理Source1(触摸/锁屏/摇晃),Source0事件(需要手动触发),Timer事件和观察者事件.
ibireme.pngRunloop 模式
系统默认定义了多种运行模式(CFRunLoopModeRef),常见的有五种:
-
kCFRunLoopDefaultMode:App的默认Mode,通常主线程是在这个 Mode 下运行的.
-
UITrackingRunLoopMode:界面跟踪 Mode,用于ScrollView 追踪触摸滑动,保证界面滑动时不受其他Mode 影响.
-
UIInitializationRunLoopMode:在刚启动 App 时第进入的第一个Mode,启动完成之后就不再使用.
-
GSEventReceiveRunLoopMode:接收系统事件的内部 Mode,通常用不到.
-
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];
}
}
-
(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;
}`</code></pre>
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