iOS-NSRunLoop實現原理++

一. 認識NSRunloop

概念

Runloop就像它的名字一樣,就是跑環.我的理解就是一個死循環.是一個可以隨時睡眠,隨時喚醒的死循環

大家可以想一下,手機app爲什麼會一直運行?而且在接收到用戶點擊等等操作時就會有所反映.這個離不開runloop.

iOS app啓動時就會啓動一個runloop,而且這種模式應該Android也有,所以纔會有了app能一直運行

每個線程都有一個runloop,但是隻有主線程的runloop是默認開啓的,其他子線程需要調用NSRunLoop *runloop = [NSRunLoop currentRunLoop];獲取runloop的同時就會創建runloop

一個線程可以創建多個runloop,但是隻能是嵌套模式.也就是一個線程只有一個根runloop


 1.1 NSRunloop與程序運行

   那麼具體什麼是NSRunLoop呢?其實NSRunLoop的本質是一個消息機制的處理模式。讓我們首先來看一下程序的入口——main.m文件,一個ios程序啓動後,只有短短的十行代碼居然能保持整個應用程序一直運行而沒有退出,是不是有點意思?程序之所以沒有直接退出是因爲UIApplicationMain這個函數內部默認啓動了一個跟主線程相關的NSRunloop對象UIApplicationMain這個函數一直執行沒有返回就保存程序一直運行的狀態。

複製代碼
 1 #import <UIKit/UIKit.h>
 2 
 3 #import "AppDelegate.h"
 4 
 5 int main(int argc, char * argv[]) {
 6 
 7     @autoreleasepool {
 8 
 9         return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
10 
11     }
12 
13 }
複製代碼

 

  文章之初我們暫且將NSRunloop理解爲實現這樣功能的一段代碼 , 這可以幫助我們更好的理解NSRunloop處理事件的過程(實際上遠比這複雜的多:))。

複製代碼
 1 int main(int argc, char * argv[]) {
 2 
 3   BOOL runnning =YES;
 4    do{
 5         ...
 6        //處理各種操作 各種事件
 7        ...
 8       }while(running);
 9   
10     return 0;
11 }    
複製代碼

 

 

   下面用官方提供的一幅非常經典的圖片,來認識NSRunloop循環處理時間的流程。

       

   通過所有的“消息”都被添加到了NSRunLoop中去,而在這裏這些消息並分爲“input source”和“Timer source” 並在循環中檢查是不是有事件需要發生,如果需要那麼就調用相應的函數處理。由此形成了運行->檢測->休眠 ->運行 的循環狀態。

    

 

 1.1 NSRunloop與線程之間關係的解析

 

   簡單說,一條線程對應一個NSRunloop對象。主線程NSRunloop對象是默認開啓的,其他線程的NSRunloop對象需要手動獲取。其實NSRunloop對象是懶加載的,所以不需要實例化這個類,而是直接調用獲取線程Runloop的方法即可喚醒。Runloop在第一次獲取時創建,在線程結束時銷燬。保持NSRunloop一直存在的方法稍後介紹。

 

  1.1.1  獲得NSRunloop對象的方法

  iOS其實有兩套Api來訪問和使用Runloop , NSRunloop是對CFRunloopRef的進一步封裝,並且CFRunloopRef是線程安全的,而這一點NSRunloop並不能保證。

  Foundation ->NSRunloop

   獲得當前線程的Runloop的方法  [NSRunloop currentRunloop];

   獲得主線程的Runloop的方法 [NSRunloop  mainRunloop];

 

  Core Foundation ->CFRunloopRef

  CFRunloopGetCurrent();

  CFRunloopGetMain();

 

  由蘋果官方文檔可以看出線程和Runloop對象的對應關係。如果你仔細閱讀可以從代碼中可以看出runloop的存儲方式是字典,而且key是線程。

複製代碼
 1 // should only be called by Foundation
 2 // t==0 is a synonym for "main thread" that always works
 3 
 4 //函數返回值爲CFRunLoopRef  形參類型爲pthread_t 根據線程創建runloop對象
 5 
 6 CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
 7 
 8     if (pthread_equal(t, kNilPthreadT)) {
 9 
10 t = pthread_main_thread_np();
11 
12     }
13 
14     __CFLock(&loopsLock);
15 
16     if (!__CFRunLoops) {
17 
18         __CFUnlock(&loopsLock);
19 
20 CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
21 
22 //由此句可得出  調用其他線程NSRunloop對象也會首先創建主線程NSRunloop對象
23 CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
24 
25 CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
26 
27 if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
28 
29     CFRelease(dict);
30 
31 }
32 
33 CFRelease(mainLoop);
34 
35         __CFLock(&loopsLock);
36 
37     }
38 
39     CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
40 
41     __CFUnlock(&loopsLock);
42 
43     if (!loop) {
44 
45 CFRunLoopRef newLoop = __CFRunLoopCreate(t);
46 
47         __CFLock(&loopsLock);
48 
49 loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
50 
51 if (!loop) {
52 
53     CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
54 
55     loop = newLoop;
56 
57 }
58
59         // don't release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it
60 
61         __CFUnlock(&loopsLock);
62 
63 CFRelease(newLoop);
64 
65     }
66 
67     if (pthread_equal(t, pthread_self())) {
68 
69         _CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL);
70 
71         if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) {
72 
73             _CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop);
74 
75         }
76     }
77     return loop;
78 }
複製代碼

 

   1.1.2  Mode

  

  Mode中有三個非常重要的組成部分,Timer(定時器)、 Source(事件源) 以及Observor(觀察者)。一個 RunLoop 包含若干個 Mode,每個 Mode 又包含若干個 Source/Timer/Observer。首先要指出的是一個runloop啓動時必須指定一個Mode  , 並且這個Mode被稱爲currentMode 。如果要切換Mode,只能退出runloop重新進入。這樣做主要是爲了分隔開不同組的 Source/Timer/Observer,讓其互不影響隨後我們會分別介紹每一類的具體作用與應用場景。

  

 

  系統默認註冊的Mode有五種

    kCFRunloopDefaultMode    // App默認Mode   通常主線程是在這個mode下運行

    UITrackingRunloopMode    // 界面跟蹤Mode  用於scrollView追蹤觸摸  界面滑動時不受其他Mode影響

    UIinitializationRunloopMode    //app一啓動進入的第一個Mode,啓動完成後就不再使用

    GSEventRecieveRunloopMode   //蘋果使用繪圖相關

    NSRunLoopCommonModes   //佔位模式

  

 

  1.1.2.1 CFRunloopTimerRef 基於時間的觸發器

   NSTimer

  首先說一下NSTimer,一個NSRunloop可以創建多個Timer。因爲定時器只會運行在指定Mode ,一旦Runloop進入其他模式, 定時器就不會工作了。

  NSTimer的創建方法 

 [NSTimer scheduledTimerWithTimeInterval:<#(NSTimeInterval)#> target:<#(nonnull id)#> selector:<#(nonnull SEL)#> userInfo:<#(nullable id)#> repeats:<#(BOOL)#>]

  該方法默認添加到當前runloop,並Mode爲kCFRunloopDefaultMode

 

1  NSTimer * timer =[NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(test) userInfo:nil repeats:YES];
2 [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes]; //手動添加到runloop 可以指定Mode

  這樣聲明的NSTimer可以解決在滑動scrollView時NSTimer不工作的問題。forMode:NSRunLoopCommonModes的意思爲,定時器可以運行在標記爲common modes模式下。具體包括兩種: kCFRunloopDefaultMode     UITrackingRunloopMode。

 

  GCD定時器

  GCD定時器的優點有很多,首先不受Mode的影響,NSTimer受Mode影響時常不能正常工作,除此之外GCD的精確度明顯高於NSTimer,這些優點讓我們有必要了解GCD定時器這種方法。

 

   1.1.2.2  CFRunloopSourceRef  事件源(輸入源)

 

  按照蘋果官方文檔,Source分類

  Port-Based  Sources  基於端口的  和其他線程 或者內核

  Custom Input  Sources  

  Cocoa   Perform   Selector  Sources  

 

  按照函數調用棧來分類   

  Source0 :   非基於Port

  Source1:   基於port的,通過內核 和其他線程通信,接收、分發事件。

 

 

 

   1.1.2.3  CFRunloopObservorRef  觀察者監聽runloop狀態改變

 

複製代碼
 1 /* Run Loop Observer Activities */
 2 
 3 typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
 4 
 5     kCFRunLoopEntry = (1UL << 0),
 6 
 7     kCFRunLoopBeforeTimers = (1UL << 1),
 8 
 9     kCFRunLoopBeforeSources = (1UL << 2),
10 
11     kCFRunLoopBeforeWaiting = (1UL << 5),
12 
13     kCFRunLoopAfterWaiting = (1UL << 6),
14 
15     kCFRunLoopExit = (1UL << 7),
16 
17     kCFRunLoopAllActivities = 0x0FFFFFFFU
18 
19 };

複製代碼

   // 創建observer

    CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivitiesYES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {

        NSLog(@"----監聽到RunLoop狀態發生改變---%zd", activity);

    });

 

    // 添加觀察者:監聽RunLoop的狀態

    CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);

 

二 、實際應用

  1. 只在NSRUnloopDefaultModes 下顯示圖片

  上面再舉例NSTimer中已經闡述其中原理了,在此不再重複舉例了。

  2.  常駐線程

  NSThread * thread  = [NSThread   alloc ]initWithTarget  selector

  [thread start];

  通常執行完方法後線程就銷燬了,那麼現在有這樣的需求,需要一條子線程一直存在,等待處理任務,與主線程之間互不干擾  (可以類比主線程存在原理,即添加消息循環Runloop)

1 //   通過添加port 或者timer
2 
3  [[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
4 
5 [[NSRunLoop currentRunLoop] run];

 

Ps:Runloop運行首先判斷Mode是否爲空,如果爲空則退出循環,還可以通過removePort來移除端口。本例用添加port來實現,其他方法請讀者自己多嘗試。:)

  3. 關於自動釋放池 

  關於自動釋放池,子線程開啓runloop要開啓針對當前線程的autoreleasepool,在每次NSRunloop休眠前清理自動釋放池。

  關於自動釋放池的具體用法本文暫時不進行描述,待日後在整理修改本帖。

發佈了43 篇原創文章 · 獲贊 13 · 訪問量 7萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章