在iOS开发或者面试中,我们多多少少会听到或者用到RunLoop的相关知识,彻底吃透RunLoop这一块内容,对我们技术能力的提升大有裨益。最近看了一些RunLoop的文章和代码,作为总结写了这篇文章。本篇文章将会从RunLoop的原理和应用两个方面剖析RunLoop,帮助大家更好地理解、应用RunLoop。本篇文章主要内容:
- RunLoop的概念
- RunLoop存在的意义
- RunLoop的构成
- RunLoop的应用
RunLoop的概念
通常情况下,线程执行完成某个任务后,线程就会退出;但某些情况下,我们需要线程常驻并且随时被唤醒以执行其他任务。线程执行一次就退出和循环多次执行可以用图形表示为:
runloop0.png线程循环多次执行用伪代码可以简单表示为:
do {
//获取消息
//处理消息
} while (消息 != 退出)
上面的这种循环模型被称作 Event Loop,事件循环模型在众多系统里都有实现,具体到iOS和OSX中就是我们要介绍的RunLoop。当然RunLoop的实现绝不会是一个简单的while循环所能搞定的,其内部实现会复杂很多,在后面的内容中会详细介绍。
RunLoop存在的意义
RunLoop的官方定义为:
Run loops are part of the fundamental infrastructure associated with threads. A run loop is an event processing loop that you use to schedule work and coordinate the receipt of incoming events. The purpose of a run loop is to keep your thread busy when there is work to do and put your thread to sleep when there is none.
从中我们可以得知RunLoop的主要目的是:当有任务到来时,可以立马唤醒线程让其处理任务,没有任务时保持线程休眠。RunLoop的机制既能有效减少系统资源的浪费,又能使线程随时待命,不至于执行完成后就退出。
RunLoop的构成
在iOS和OSX中,与RunLoop对应的两种对象是NSRunLoop和CFRunLoopRef。两者的关系如下图所示:
runloop1.pngCFRunLoopRef是开源的,所以我们研究其源码是最合适不过的。CFRunloopRef整体结构组成如下:
runloop2.png先通过源码看下RunLoop和线程的关系:
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
if (pthread_equal(t, kNilPthreadT)) {
t = pthread_main_thread_np();//如果线程t为空,将其赋值为主线程
}
__CFSpinLock(&loopsLock);//加锁,保证多线程访问的安全性
if (!__CFRunLoops) {
__CFSpinUnlock(&loopsLock);
CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);//如果维护线程和runloop关系的字典不存在,就新建一个
CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());//主线程的runloop总会被默认创建,且创建一次
CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);//将主线程和主runloop存放到dict中维护
if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {//比较、交换dict和__CFRunLoops,也就是说dict是临时字典,实际上是由__CFRunLoops全局字典维护线程和runloop的关系
CFRelease(dict);
}
CFRelease(mainLoop);
__CFSpinLock(&loopsLock);
}
CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));//在全局字典__CFRunLoops查找线程t对应的runloop
__CFSpinUnlock(&loopsLock);
if (!loop) {
CFRunLoopRef newLoop = __CFRunLoopCreate(t);//如果没找到就新建一个
__CFSpinLock(&loopsLock);
loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
if (!loop) {
CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);//把新建的runloop和线程t存放到__CFRunLoops字典中
loop = newLoop;
}
// don't release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it
__CFSpinUnlock(&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;//返回线程t对应的runloop
}
从代码可以看出:线程和RunLoop是一一对应的关系,这种对应关系维护在一个全局的字典中,主线程的RunLoop会被系统自动创建,其他线程的RunLoop在第一次获取的时候才会创建,不获取的话其RunLoop是不存在的。
一个CFRunLoop包含多个CFRunLoopMode,一个CFRunLoopMode包含多个CFRunLoopSource、CFRunLoopTimer和CFRunLoopObserver。RunLoop在任何时刻只能以一种Mode运行(叫做currentMode),切换Mode时要先退出当前RunLoop。下面我们从源码具体解析:
CFRunLoop结构体:
struct __CFRunLoop {
CFRuntimeBase _base;
pthread_mutex_t _lock; /* locked for accessing mode list */
__CFPort _wakeUpPort; // used for CFRunLoopWakeUp
Boolean _unused;
volatile _per_run_data *_perRunData; // reset for runs of the run loop
pthread_t _pthread;
uint32_t _winthread;
CFMutableSetRef _commonModes;
CFMutableSetRef _commonModeItems;
CFRunLoopModeRef _currentMode; //RunLoop当前运行的Mode
CFMutableSetRef _modes; //RunLoop包含的所有Mode
struct _block_item *_blocks_head;
struct _block_item *_blocks_tail;
CFTypeRef _counterpart;
};RunLoopModeRef _currentMode;
CFMutableSetRef _modes;
struct _block_item *_blocks_head;
struct _block_item *_blocks_tail;
CFTypeRef _counterpart;
};
代码中可以看出:
(1)所有的Mode是被RunLoop的一个集合维护的
(2)当前正在运行的Mode就是currentMode
(3)一个普通的Mode可以被标记为commonMode,所有的commonMode也是被RunLoop的一个集合维护的
(4)commonMode里面的Source、Timer、Observer都叫做commonModeItem,所有的commonModeItem也是被RunLoop的一个集合维护的,不同的commonMode之间是可以同步、共享commonModeItem信息的。
(5)苹果为我们提供了两种Mode:kCFRunLoopDefaultMode (NSDefaultRunLoopMode) 和 UITrackingRunLoopMode
CFRunLoopMode结构体:
struct __CFRunLoopMode {
CFRuntimeBase _base;
pthread_mutex_t _lock; /* must have the run loop locked before locking this */
CFStringRef _name; //Mode名称
Boolean _stopped;
char _padding[3];
CFMutableSetRef _sources0;//事件源source0的集合
CFMutableSetRef _sources1;//事件源source1的集合
CFMutableArrayRef _observers;//观察者observer的数组
CFMutableArrayRef _timers;//计时器timer的数组
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 */
};
代码中可以看出:
(1)CFRunLoopMode中维护一个管理多个事件源的集合,事件源又可以分为Soure0和 Source1。
Source0: 基于非mach_port的,例如Custom Input Sources 、CFSocket等。
Source1: 基于mach_port的,能够主动触发RunLoop的线程处理消息,并且执行相应的回调。
(2)CFRunLoopMode中维护一个管理多个CFRunLoopTimerRef的数组
CFRunLoopTimerRef:包含一个时间长度和一个回调,在Timer被注册到RunLoop的时刻,再经过这个时间长度后,就会执行这个回调,相当于定时器的概念。
(3)CFRunLoopMode中维护一个管理多个CFRunLoopObserverRef的数组。
CFRunLoopObserverRef:可以理解为实时观察RunLoop的执行状态,在重要的时间点向外报告也就是执行相应的回调,这些重要的时间点包括:
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0),//将要进入RunLoop
kCFRunLoopBeforeTimers = (1UL << 1),//将要处理Timer
kCFRunLoopBeforeSources = (1UL << 2),//将要处理Source
kCFRunLoopBeforeWaiting = (1UL << 5),//线程即将进入休眠状态
kCFRunLoopAfterWaiting = (1UL << 6),//线程刚从休眠中唤醒
kCFRunLoopExit = (1UL << 7),//将要退出RunLoop
kCFRunLoopAllActivities = 0x0FFFFFFFU
};
关于RunLoop的具体执行流程,可以参照ibireme博客上的流程图帮助理解:
runloop3.pngRunLoop的应用
AutoreleasePool的实现
runloop4.png主线程上执行的代码被AutoreleasePool包围着,不会出现内存泄漏的情况,所以也不用我们手动创建释放池了。
NSTimer
NSTimer可以看做是CFRunLoopTimerRef的封装,当一个NSTimer被添加到RunLoop后,经过一段时间后会执行相应的回调,具体执行一次还是重复多次执行,由repeats参数决定,具体使用方法简单,这里就不贴代码了
UI刷新
当界面元素的位置、大小发生变化,或者做连续几个动画时,这些变化不会立马体现在界面上,而是暂时保存这些变化,等到RunLoop休眠或者即将退出时,再去执行相应的回调,回调中的代码用于更新界面。
GCD用到RunLoop的情况
GCD中,子线程通过dispatch_async提交任务到主线程时,主线程的RunLoop会收到消息,激活后通过回调处理任务,但这仅限于提交到主线程的任务,如果提交任务到子线程,则还是由GCD本身处理的,就与RunLoop无关了。
参考链接
网友评论