本篇是探索底層Runloop,目的是能夠深入理解Runloop是幹什麼用的?什麼時候用?怎麼用?
1、什麼是runloop?
runloop是一個迴圈,它在持續不斷的跑圈,iOS應用程式剛開啟時,就建立了一個主執行緒,並預設建立了Runloop保持主執行緒的持續執行。
我們到官方文件搜尋一下Runloop
,如圖所示
如此可見,Runloop和執行緒之間有著不清不楚的關係。
再來看一下CFRunloop原始碼中的CFRunLoopRun
函式
void CFRunLoopRun(void) { /* DOES CALLOUT */
int32_t result;
do {
result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
CHECK_FOR_FORK();
} while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}
複製程式碼
我們可以看到,Runloop本質是一個do...while迴圈。 結合官方文件提供的執行迴圈結構看一下,Runloop是如何執行的。
從上圖中我們可以看出,Runloop就是依附線上程上的迴圈,通過輸入源(Input sources)和定時源(Timer sources)接收事件,然後交給執行緒去處理事件。
所以什麼是Runloop? Runloop就是一個迴圈,為了執行緒而生,它的本質是一個do...while迴圈
2、Runloop的作用
- 1、保持程式持續的執行
一般情況下,執行緒在執行完任務就會退出,如果我們不希望執行緒退出,還想讓它執行更多的任務,就需要用到
Runloop
了。 - 2、接收並處理App中的各種事件 Runloop在迴圈時,通過輸入源(input source)和定時源(timer source)接收App事件(觸控事件、UI重新整理時間、定時器、performSelector)。
- 3、提升效能 線上程不工作時休眠,節省CPU資源。
以上就是Runloop的作用,這裡只是概括一下,後面會有具體到用法的Runloop應用。
3、Runloop和執行緒的關係
接下來我們結合Runloop原始碼看看它和執行緒之間的關係,找到_CFRunLoopGet0
這個函式,它的作用是獲取runloop物件
//CFRunloop.c
//這個類是獲取Runloop物件的
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
if (!__CFRunLoops) {//CFRunloops是存放runloop和執行緒對應關係的字典
//建立存放runloop和執行緒的字典
CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
//獲取主執行緒runloop
CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
//將主執行緒和主執行緒對應的Runloop物件mainLoop新增到字典中
CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
CFRelease(dict);
}
CFRelease(mainLoop);
__CFSpinLock(&loopsLock);
}
//獲取子執行緒對應的runloop
CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
__CFSpinUnlock(&loopsLock);
if (!loop) {
//依據執行緒t建立Runloop物件
CFRunLoopRef newLoop = __CFRunLoopCreate(t);
__CFSpinLock(&loopsLock);
loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
if (!loop) {
//繫結執行緒和runloop到字典中
CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
loop = newLoop;
}
}
}
複製程式碼
上述程式碼中有一段CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
,意思是以執行緒為key,runloop為value,將runloop儲存到一個全域性的字典中。
至此我們得出了第一個結論:執行緒和Runloop是一一對應的關係。
再看這段獲取主執行緒runloop的程式碼CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
,意思是建立一個主執行緒的Runloop。
至此得出第二個結論:Runloop是以執行緒為引數建立的,並儲存在全域性的字典裡。
再看後半段程式碼
//獲取子執行緒對應的runloop
CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
__CFSpinUnlock(&loopsLock);
if (!loop) {
//依據執行緒t建立Runloop物件
CFRunLoopRef newLoop = __CFRunLoopCreate(t);
__CFSpinLock(&loopsLock);
loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
if (!loop) {
//繫結執行緒和runloop到字典中
CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
loop = newLoop;
}
}
複製程式碼
這段程式碼的意思是先從__CFRunLoops
字典中獲取Runloop物件,若沒有,則以執行緒為引數建立一個,並儲存到__CFRunLoops
字典裡,至此我們知道了第三個結論:主執行緒的Runloop由系統自動建立,子執行緒的Runloop需要在子執行緒裡手動獲取Runloop時建立。
綜上我們知道了Runloop和執行緒的關係:
1、執行緒和Runloop是一一對應的關係。
2、Runloop是以執行緒為引數建立的,並儲存到全域性的字典裡。
3、主執行緒的Runloop由系統自動建立,子執行緒的Runloop需要在子執行緒裡手動獲取Runloop時建立。
4、Runloop在第一次獲取時建立,線上程銷燬時隨之銷燬。
4、Runloop的五個物件
- __CFRunLoop * CFRunLoopRef;
- __CFRunLoopSource * CFRunLoopSourceRef;
- __CFRunLoopObserver * CFRunLoopObserverRef;
- __CFRunLoopTimer * CFRunLoopTimerRef;
- CFRunloopModeRef(為什麼這個這麼寫呢,因為Runloop並沒有暴露RunloopMode這個物件)
下面逐一講一下Runloop這幾個物件的含義和它們之間的關係,如圖
上圖就是Runloop物件、Mode、Source、Observer、Timer之間的關係。 一個Runloop包含若干個CFRunloopModeRef(執行模式)
,一個CFRunloopModeRef
又包含若干個CFRunLoopSourceRef(輸入源)
/CFRunLoopTimerRef(定時源)
/CFRunLoopObserverRef(觀察源)
,但是Runloop同一時間只能指定一個CFRunloopModeRef(執行模式)
,如果要切換CFRunloopModeRef
,需要先退出Runloop,再指定一個CFRunloopModeRef(執行模式)
進入。
為什麼是這樣的結構? 答:這樣做主要是為了分離Source/Timer/Observer,讓其互不影響。
4.1、CFRunLoopRef(Runloop物件)
CFRunLoopRef
是 Core Foundation 框架下 RunLoop 物件類,可通過如下方式獲取
// 獲得當前執行緒的 RunLoop 物件
CFRunLoopGetCurrent();
// 獲得主執行緒的 RunLoop 物件
CFRunLoopGetMain();
複製程式碼
也可以使用Foundation框架中的NSRunloop
獲取封裝過的Runloop,NSRunloop是對CFRunLoopRef
的封裝
// 獲得當前執行緒的 RunLoop 物件
[NSRunLoop currentRunLoop];
// 獲得主執行緒的 RunLoop 物件
[NSRunLoop mainRunLoop];
複製程式碼
4.2、CFRunLoopSourceRef(輸入源)
先看一下原始碼
struct __CFRunLoopSource {
CFRuntimeBase _base;
uint32_t _bits;
pthread_mutex_t _lock;
CFIndex _order; /* immutable */
CFMutableBagRef _runLoops;
union {
//對應Source0
CFRunLoopSourceContext version0; /* immutable, except invalidation */
//對應Source1
CFRunLoopSourceContext1 version1; /* immutable, except invalidation */
} _context;
};
複製程式碼
從原始碼看出Source分為2個版本
- 1、Source0:這種版本的Source不能主動觸發事件,得是使用者或開發者手動觸發,比如觸控事件和performSelector等App內部事件(UIEvent),需要先進行
CFRunLoopSourceSignal
標記,再通過CFRunLoopWakeUp
喚醒Runloop處理事件。 - 2、Source1:基於Port,用於通過核心和執行緒之間通訊的,這種Source可以主動喚醒Runloop執行緒,一會兒用例子看一下。
先看一個觸控事件觸發Source0的例子,建立個工程,上面放一個按鈕,在點選事件的回撥中打斷點,如圖
在左側欄目中是觸發的方法,如圖 我們可以看到,使用者的觸控事件,果然觸發的是Source0。下面是Source0的使用例子
要建立一個Source0輸入源,需要執行六步走, 1、建立Context上下文 2、建立Source0輸入源物件 3、獲取Runloop 4、繫結Runloop、Source0和mode 5、標記執行訊號CFRunloopSourceSignal 6、喚醒CFRunLoopWakeUp
- (void)source0Demo{
//1、建立Context上下文
CFRunLoopSourceContext context = {
0,
NULL,
NULL,
NULL,
NULL,
NULL,
NULL,
schedule,
cancel,
perform,
};
/**
2、建立CFRunLoopSourceRef
引數一:傳遞NULL或kCFAllocatorDefault以使用當前預設分配器。
引數二:優先順序索引,指示處理執行迴圈源的順序。這裡我傳0為了的就是自主回撥
引數三:為執行輸入源儲存上下文資訊的結構
*/
CFRunLoopSourceRef source0 = CFRunLoopSourceCreate(CFAllocatorGetDefault(), 0, &context);
//3、獲取Runloop
CFRunLoopRef rlp = CFRunLoopGetCurrent();
//4、繫結Source、Runloop、Mode,此時我們的source就進入待緒狀態
CFRunLoopAddSource(rlp, source0, kCFRunLoopDefaultMode);
//5、 一個執行訊號
CFRunLoopSourceSignal(source0);
//6、 喚醒 run loop 防止沉睡狀態
CFRunLoopWakeUp(rlp);
// 取消 移除
// CFRunLoopRemoveSource(rlp, source0, kCFRunLoopDefaultMode);
CFRelease(rlp);
}
void schedule(void *info, CFRunLoopRef rl, CFRunLoopMode mode){
NSLog(@"準備處理事件");
}
void perform(void *info){
NSLog(@"少年,執行事件吧");
}
void cancel(void *info, CFRunLoopRef rl, CFRunLoopMode mode){
NSLog(@"移除輸入源,事件停止執行");
}
複製程式碼
從程式碼中可以看出,若要讓Runloop執行Source0的事件,需要先發出一個執行訊號CFRunLoopSourceSignal
,再呼叫CFRunLoopWakeUp
喚醒Runloop執行任務。
控制檯列印效果如下
下面是Source1使用Port進行執行緒間通訊的例子
建立
@interface ViewController ()<NSPortDelegate>
@property (nonatomic, strong) NSPort* subThreadPort;//主執行緒Port
@property (nonatomic, strong) NSPort* mainThreadPort;//子執行緒Port
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
[self portCommunicateTest];
}
- (void)portCommunicateTest{
self.mainThreadPort = [NSPort port];
self.mainThreadPort.delegate = self;
// port - source1 -- runloop
//port是操作Source1的,所以同樣依賴於runloop
[[NSRunLoop currentRunLoop] addPort:self.mainThreadPort forMode:NSDefaultRunLoopMode];
//建立子執行緒
dispatch_async(dispatch_get_global_queue(0, 0), ^{
//例項化子執行緒對應的Port
self.subThreadPort = [NSPort port];
self.subThreadPort.delegate = self;
[[NSRunLoop currentRunLoop] addPort:self.subThreadPort forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] run];
});
}
//NSPort的代理方法,執行緒間通訊的回撥
- (void)handlePortMessage:(id)message {
NSLog(@"當前執行緒是 == %@", [NSThread currentThread]); // 3 1
NSLog(@"傳來的訊息內容 = %@", [[NSString alloc] initWithData:[message valueForKey:@"components"][0] encoding:NSUTF8StringEncoding]);
sleep(1);
if (![[NSThread currentThread] isMainThread]) {
//像子執行緒的Port傳送訊息
NSMutableArray* components = [NSMutableArray array];
NSData* data = [@"world" dataUsingEncoding:NSUTF8StringEncoding];
[components addObject:data];
[self.mainThreadPort sendBeforeDate:[NSDate date] components:components from:self.subThreadPort reserved:0];
}
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
//注意,component必須以NSData的形式傳遞
NSMutableArray* components = [NSMutableArray array];
NSData* data = [@"Hello" dataUsingEncoding:NSUTF8StringEncoding];
[components addObject:data];
[self.subThreadPort sendBeforeDate:[NSDate date] components:components from:self.mainThreadPort reserved:0];
}
複製程式碼
點選螢幕後,列印結果如下
在回撥訊息處打斷點,發現如圖所示 看到左側顯示的輸入源是Source1
,至此使用NSPort進行執行緒間通訊的例子執行完畢,真可愛,執行緒還能這麼玩。
4.3、CFRunloopModeRef
Runloop有五種執行模式
-
UIInitializationRunLoopMode
:在App剛啟動進入的執行模式,啟動完成後會切換到kCFRunLoopDefaultMode,從此不再使用。
-
kCFRunLoopDefaultMode
: 預設執行模式,在主執行緒執行在這個模式下。
-
UITrackingRunLoopMode
:介面跟蹤模式,當見面滾動時,會切換到這個執行模式下,保證不收其他模式的影響。
-
GSEventReceiveRunLoopMode
: 接受系統事件的內部執行模式。
-
kCFRunLoopCommonModes
: 佔位模式,通常用來標記kCFRunLoopDefaultMode
和UITrackingRunLoopMode
,如果NSTimer加入這個模式,將不受執行模式切換的影響。
4.4、CFRunloopTimerRef
CFRunloopTimerRef
是一個時間觸發器,它包含一個時間長度和一個回撥(函式回撥),在加入Runloop時,Runloop會註冊一個時間點,經過時間長度後,Runloop會被喚醒執行回撥。Timer的底層就是一個CFRunloopTimerRef,它受Mode
切換的影響。如果把Timer加入到kCFRunLoopCommonModes就不會受切換影響了,像下面這樣
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1.0f repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"log NSTimer runloop");
}];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
複製程式碼
4.5、CFRunloopObserverRef
CFRunloopObserverRef
是一個觀察者,用來監控Runloop的狀態的,它分為以下狀態
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 即將進入Loop 1
kCFRunLoopBeforeTimers = (1UL << 1), // 即將處理 Timer 2
kCFRunLoopBeforeSources = (1UL << 2), // 即將處理 Source 4
kCFRunLoopBeforeWaiting = (1UL << 5), // 即將進入休眠 32
kCFRunLoopAfterWaiting = (1UL << 6), // 剛從休眠中喚醒 64
kCFRunLoopExit = (1UL << 7), // 即將退出Loop 128
};
複製程式碼
舉個例子
- (void)obseverDemo{
//1、建立觀察者上下文
CFRunLoopObserverContext context = {
0,
((__bridge void *)self),
NULL,
NULL,
NULL
};
//2、獲取當前Runloop物件
CFRunLoopRef rlp = CFRunLoopGetCurrent();
//3、建立觀察者CFRunLoopObserverRef
CFRunLoopObserverRef observerRef = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, runLoopObserverCallBack, &context);
//4、將觀察者observer和runloop物件、Mode關聯起來
CFRunLoopAddObserver(rlp, observerRef, kCFRunLoopDefaultMode);
}
void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info){
switch (activity) {
case kCFRunLoopEntry:
NSLog(@"RunLoop進入");
break;
case kCFRunLoopBeforeTimers:
NSLog(@"RunLoop要處理Timers了");
break;
case kCFRunLoopBeforeSources:
NSLog(@"RunLoop要處理Sources了");
break;
case kCFRunLoopBeforeWaiting:
NSLog(@"RunLoop要休息了");
break;
case kCFRunLoopAfterWaiting:
NSLog(@"RunLoop醒來了");
break;
case kCFRunLoopExit:
NSLog(@"RunLoop退出了");
break;
default:
break;
}
}
複製程式碼
在我們滾動檢視時,控制檯列印如下
由此可見,官方說的對☺,確實可以通過CFRunloopObserverRef監聽Runloop的狀態。
Runloop的應用有很多,之前在專案中應用的比較深的是將對CPU壓力比較大的UI任務拆分成多個小任務,通過監聽Runloop的Observer空閒時機,在空閒時強制其執行小任務,高效利用系統資源提升效能。 RunLoopWorkDistribution瞭解一下(?)