1. 程式人生 > >IOS RunLoop詳解以及API使用

IOS RunLoop詳解以及API使用

使用RunLoop的目的:

1) 使用埠或自定義輸入源來 和其他執行緒通訊

2) 使用執行緒的定時器; ( 在子執行緒中新增定時器 )

3) cocoa中使用任何performSelector...的方法

4) 使執行緒長期性工作

否則,開啟一個執行緒的RunLoop沒有意義

一 獲取/建立RunLoop物件

        蘋果不允許直接建立RunLoop,它提供了兩個自動獲取的函式: CFRunLoopGetMain()和CFRunLoopGetCurrent(). 執行緒和RunLoop之間是一一對應的。執行緒建立時並沒有RunLoop,且如果不去獲取,那麼RunLoop一直都不存在. RunLoop的建立發生在第一次獲取的時候( 除了主執行緒的RunLoop,你只能在一個執行緒的內部獲取自己對應的RunLoop ) ;RunLoop的銷燬發生線上程結束後;

     1)  [NSRunLoop currentRunLoop];

      2)  [NSRunLoop mainRunLoop];

      3)  CFRunLoopGetCurrent();

     4)  CFRunLoopGetMain();

     5)  [NSRunLoop currentRunLoop].getCFRunLoop; //NSRunLoop轉CFRunLoopRef

主執行緒的RunLoop是程式開始就建立的;子執行緒的RunLoop在第一次獲取RunLoop物件時建立的;

二. 配置RunLoop ( 輸入源source0,source1, timer, observer )

2.1 RunLoop的組成結構

CFRunLoopRef                 RunLoop物件

CFRunLoopModeRef        RunLoop中的模式,每次只能執行在一種模式下

CFRunLoopSourceRef     輸入源source0,source1

CFRunLoopTimerRef       輸入源timer

CFRunLoopObserverRef  RunLoop的觀察者物件

RunLoop結構體的內部結構:

    struct __CFRunLoop {

CFStringSetRef          _commonModes;       //set

CFMutableSetRef       _commonModeItems; //set

CFMutableSetRef       _modes;    //set

CFRunLoopMideRef   _currentMode ;   //當前執行的mode

    } 

   1. commonModeItems 能增加common表示的mode; modes也能增加多個RunLoopModeRef;

   2. RunLoop支援查詢當前執行緒所執行的Mode, currentMode;

   3. RunLoop中存在一個common集合,用來組合幾種mode,讓其在commonMode時能並存執行;

RunLoopMode結構體的內部結構

   struct __CFRunLoopMode {

CFStringRef                 _name;

CFMutableSetRef        _source0;   //set

CFMutableSetRef        _source1;   //set

CFMutableArrayRef     _observers; //Array

CFMutableArrayRef     _timers;       //Array

   }

   1.每個Mode都有一個名字,用來區分不同的Mode, 以及加入commonMode;

   2.管理事件輸入源集合: source0, source1;

   3. 管理timer輸入源: timer;

   4.該Mode執行時,RunLoop所處狀態的觀察者集合,用來獲取RunLoop所處的不同狀態;

可以列印 NSLog(@"%@",[NSRunLoop currentRunLoop]);來知道

(1) RunLoop的執行狀態(stop or run), 

(2) 在哪種mode(KCFRunLoopDefaultMode)下執行

(3) 以及註冊為CommondMode的源或者timer;

(4) source, timer,observer的數量

關係:

1. CFRunLoopRef代表 RunLoop的執行模式;

2. RunLoop包含若干個Mode, 而每個Mode中又註冊了若干個<Set>Source, <Set>timer,  <Array>observer;  事件輸入源.timer等不直接與RunLoop有關聯,而是註冊在Mode中,         RunLoop每次只能在一種Mode下執行,只有註冊在此Mode下的source,timer,observer才能被執行和反饋;

 3. 如果需要切換Mode,只能退出runLoop,重新指定一個Mode進入。這樣不同組Mode註冊的source,timer,observer相互獨立;

CFRunLoopModeRef 沒有對外暴露,只有系統註冊的5中Mode;

CFRunLoopSourceRef

是事件產生的地方. source有兩個版本:source0和source1.

(1) source0, 只包含一個回撥(函式指標),它並不能主動觸發事件。使用時,需要先呼叫CFRunLoopSourceSignal(source),將這個Source標記為待處理,然後手動呼叫CFRunLoopWajeUp(runLoop)來喚醒RunLoop,讓其處理這個事件;

  (2)  source1, 包含一個mach_port和一個回撥,被用於通過核心和其他執行緒相互發送訊息.這種source能主動喚醒RunLoop的執行緒;

CFRunLoopTimerRef

是基於時間的觸發器,和NSTimer可以混用.其包含一個時間長度和回撥;當其加入到RunLoop時,RunLoop會註冊對應的時間點,當時間點到時,RunLoop會被喚醒以執行那個回撥.

CFRunLoopObserverRef 

是觀察者,每個Observer都包含一個回撥, 當RunLoop的狀態發生變化時,觀察者就能通過回撥接受這個變化;

typedef   CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {

kCFRunLoopEntry         = (1UL << 0), // 即將進入Loop

kCFRunLoopBeforeTimers  = (1UL << 1), // 即將處理 Timer

kCFRunLoopBeforeSources = (1UL << 2), // 即將處理 Source

kCFRunLoopBeforeWaiting = (1UL << 5), // 即將進入休眠

kCFRunLoopAfterWaiting  = (1UL << 6), // 剛從休眠中喚醒

kCFRunLoopExit          = (1UL << 7), // 即將退出Loop

};

上面的source,timer,obsercer被統稱為modeItem, 一個item可以被同時加入多個mode,一個item被重複加入同一個mode時不會有效果.如果一個mode中一個item都沒有,則RunLoop會直接退出,不進入迴圈;

 2.1RunLoop是以指定的Mode執行的,指定的Mode必須存在一個輸入源或者Timer,否則在進入Loop之前RunLoop就退出了;

1)  輸入源 source0, source1

2) 定時器 timer

 3) 觀察者observer

   2.2 生成觀察者observer

  observer只能通過CFRunLoopRef物件的API去新增; 生成並向RunLoop中加入observer:

     CFRunLoopObserverContext context = {0, (__bridge void *)(self),NULL,NULL,NULL}; //不註冊任何回撥函式

                CFRunLoopObserverRef     observer1 = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, test, &context);

            CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer1, kCFRunLoopDefaultMode);

                 void test( CFRunLoopObserverRef observer, CFRunLoopActivity activity, void * info  ){  }

   2.3 生成自定義事件source (source0)

   2.3.1  CFRunLoopSourceContext

typedef struct {

     CFIndexversion;

     void *info;

      const void *(*retain)(const void *info);

          void(*release)(const void *info);

     CFStringRef(*copyDescription)(const void *info);

    Boolean(*equal)(const void *info1, const void *info2);

           CFHashCode(*hash)(const void *info);

          void(*schedule)(void *info, CFRunLoopRef rl, CFRunLoopMode mode);

        void(*cancel)(void *info, CFRunLoopRef rl,CFRunLoopMode mode);

       void(*perform)(void *info);

} CFRunLoopSourceContext;

//輸入源的上下文物件,用來當該輸入源狀態發生變化時,進行回撥

    CFRunLoopSourceContext context = {

       0,

       (__bridge void *)(self),

             NULL,

                NULL,

                NULL,

                NULL,

               NULL,

              /* source註冊到Mode時候的回撥  void schedule ( void * info, CFRunLoopRef r1, CFRunLoopMode mode ){}*/

                schedule,

               /* source從Mode中刪除時候的回撥  void schedule ( void * info, CFRunLoopRef r1, CFRunLoopMode mode ){}*/

               cancel,

             /* source在RunLoop中執行時的回撥 void perform( void * info ){}*/

              perform

    };

void schedule ( void * info, CFRunLoopRef r1, CFRunLoopMode mode ){ }

         void cancel( void * info, CFRunLoopRef r1, CFRunLoopMode mode ){ }

void perform( void * info ){ }

      2.3.2  CFRunLoopSourceRef 

  NSTimer,CFRunLoopTimerRef 或 CFRunLoopObserverRef註冊在RunLoop中的某種Mode下之後,會根據時間,或者runLoop的狀態自動執行回撥,但是source0卻需要程式設計師自己在其他執行緒中去傳送訊號;, 下面介紹CFRunLoopSourceRef(source0)的註冊,回撥使用,以及移除;

  (1)建立source的環境,設定註冊,執行,刪除三個對應的回撥;

    CFRunLoopSourceContext context = {

       0,

                    (__bridge void *)(self),

               NULL,

             NULL,

                          NULL,

                          NULL,

                          NULL,

                          schedule,

                           cancel,

                           perform

               };

(2)  建立source (source0)

      self.source = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &context);

(3)  註冊到指定Mode中

        self.runLoop = CFRunLoopGetCurrent();

        CFRunLoopAddSource( self.runLoop, self.source, kCFRunLoopDefaultMode );

 (4) Source所處狀態回撥:( 3中狀態:註冊,執行,刪除 )      

       因為註冊,刪除都會自動觸發對應的回撥方法;

      關於"執行"回撥的呼叫: 因為我們註冊的事件都是source0,所以沒有對應的訊號能夠喚醒你註冊的事件,而且CFRunLoopSouorceSignal沒有喚醒RunLoop對應執行緒的能力 ,那麼只有你自己去喚醒了; 使用如下方法去喚醒該source;

CFRunLoopSouorceSignal( self.source );

CFRunLoopWakeup( self.runLoop );

(5) Source從Mode中移除:

    CFRunLoopSourceInvalidate(self.source);  此時會有刪除回撥;

  2.3.3 生成基於時間的Timer

       NSTimer在RunLoop中註冊:

    NSTimer * timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(test) userInfo:nil repeats:YES];

        [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];

   CFRunLoopTimerRef在RunLoop中註冊

    CFRunLoopTimerContext context = { 0, NULL,NULL,NULL,NULL };

      CFRunLoopTimerRef   timer = CFRunLoopTimerCreate(kCFAllocatorDefault, 1, 0.1, 0, 0, callback, &context);

    CFRunLoopAddTimer(CFRunLoopGetCurrent(), timer, kCFRunLoopDefaultMode);

2.3.4 生成基於埠的源:

     NSPort, NSMachPort,CFMessagePortRef

     現在ios系統不允許生成帶有名字的port,否則直接報錯並且crash; 只能使用匿名port,匿名port是不會被回撥的;現在的唯一作用就是如AFNetworking中使用一樣,註冊一個匿名port,讓RunLoop能進入Loop不退出;

      [[NSRunLoop currentRunLoop] addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];

  2.2  執行緒安全與RunLoop物件

CFRunLoopRef執行緒安全

NSRunLoop執行緒不安全,所以操作應該在runloop自身對應的執行緒的中完成;

三.啟動RunLoop

3.1 啟動的API:

   1) [[NSRunLoop currentRunLoop] run]; //無條件且以預設的NSDefaultRunLoopMode啟動

   2) [[NSRunLoop currentRunLoop] runUntilDate:[NSDate new]]; //指定過期時間且以預設的NSDefaultRunLoopMode啟動

   3) [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate new]];//指定過期時間,指定啟動方式

   4)  CFRunLoopRun(); //子執行緒的runLoop需要啟動

   5) CFRunLoopRunInMode(<#CFRunLoopMode mode#>, <#CFTimeInterval seconds#>, <#Boolean returnAfterSourceHandled#>)

3.2   Int32 result = CFRunLoopRunInMode( kCFRunLoopDefaultMode. 10, YES );

   if( result==kcFRunLoopRunStopped || result==kCFRunLoopFinished ) 

   {… }

    注意: RunLoop啟動的Mode,必須存在至少一個輸入源或者Timer,否則無法進入Loop,其中並不包括Observer;

四.退出RunLoop

     讓RunLoop退出的方法:

    (1)給RunLoop設定超時時間;

      (2)   通知RunLoop停止

CFRunLoopStop([NSRunLoop currentRunLoop].getCFRunLoop);

      (3)   刪除Mode中的所有輸入源;  (這種方法不太好用)

五.雜談

1. 因為Mode是以stringModeName去對應相應的CFRunLoopModeRef,且CFRunLoopModeRef並沒有建立方式,所以我們能使用的只有兩種, KCFRunLoopDefaultMode 和   UITrackingRunLoopMode; 一般就是用的KcFRunLoopDefaultMode;

2. 關於kCFRunLoopCommonModes,

為什麼這個不算能使用中的一種,這種只是timer,source新增進Mode中的一種方式,被新增進來的timer,source會被具有common標記     的Mode所共有;即RunLoop 並不會有kCFRunLoopCommonModes這種執行狀態,只是將該Mode下注冊的timer,source0讓其他mode共有;

 至於如何讓其他Mode具有common標誌,不用擔心,你知道的KCFRunLoopDefaultMode 和 UITrackingRunLoopMode都已經加上了common標誌;

3.關於定時器與頁面滑動:

     很多人在不懂之前,寫的定時器與頁面滑動的事件相沖突,即頁面滑動時,定時器不工作: 主要原因是頁面滑動時,主執行緒的RunLoop會停止,並且以  UITrackingRunLoopMode形式啟動,這個時候schedule方式生成的NSTimer處在KCFRunLoopDefaultMode下,所以不會被執行;解決辦法時,將NSTimer註冊到kCFRunLoopCommonModes下,則NSTimer在Mode切換時仍然可以執行;

4.runLoop的執行流程:

(1) 以指定Mode啟動之後,根據ModeNameString生成對應Mode,檢查當前Mode中是否存在Item(source,timer),(observer不算),如果沒有,RunLoop直接返回;如果存在Item,則(2);

(2) 通知Observer,RunLoop即將進入Loop;  在 CFRunLoopRun()之後的程式表示式在RunLoop執行時不會指定到的,因為進入了loop;

(3) 通知Observer,即將觸發timer回撥;

(4) 通知observer,即將出發source0(非port)回撥;

(5) 執行source0的回撥;

(6) 檢查是否有source1(基於port)處於ready狀態,如果有就直接處理source1,然後跳轉去訊息處理;

         (7) 如果runLoop沒有任何任務,就通知observer,RunLoop即將進入休眠(sleep),否則跳過休眠;

(8)在休眠的RunLoop被喚醒 , 比如CFRunLoopWakeup( self.runLoop );喚醒之後,處理該事件,並且開始新的一輪loop;

5. 主執行緒中的RunLoop狀態函式:

{

/// 1. 通知Observers,即將進入RunLoop

/// 此處有Observer會建立AutoreleasePool: _objc_autoreleasePoolPush();

__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopEntry);

do{

/// 2. 通知 Observers: 即將觸發 Timer 回撥。

__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeTimers);

/// 3. 通知 Observers: 即將觸發 Source (非基於port的,Source0) 回撥。

__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeSources);

__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);

/// 4. 觸發 Source0 (非基於port的) 回撥。

__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(source0);

__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);

/// 6. 通知Observers,即將進入休眠

/// 此處有Observer釋放並新建AutoreleasePool: _objc_autoreleasePoolPop(); _objc_autoreleasePoolPush();

__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeWaiting);

/// 7. sleep to wait msg.

mach_msg() -> mach_msg_trap();

/// 8. 通知Observers,執行緒被喚醒

__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopAfterWaiting);

/// 9. 如果是被Timer喚醒的,回撥Timer

__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(timer);

/// 9. 如果是被dispatch喚醒的,執行所有呼叫 dispatch_async 等方法放入main queue 的 block

__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(dispatched_block);

/// 9. 如果如果Runloop是被 Source1 (基於port的) 的事件喚醒了,處理這個事件

__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__(source1);

while(...);

/// 10. 通知Observers,即將退出RunLoop

/// 此處有Observer釋放AutoreleasePool: _objc_autoreleasePoolPop();

__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopExit);

尤其注意 mach_msg() -> mach_msg_trap() 睡眠狀態;

6  關於定時器NSTimer, CFRunLoopTimerRef

 NSTimer有個屬性叫做tolerance(寬容度),標記了當時間到點之後容許的最大誤差;如果這個時間點錯過了,那麼就需要等待下一個時間點到來了;

重點: 

(1)關於UI介面重新整理:

當在操作UI時,比如改變frame,更新了UIView/CALayer的層次時,或手動呼叫了UIView/CALayer的setNeedsLayout/setNeedsDisplay方法後,這個UIView/CALayer就被標    記為待處理,並被提交到一個全域性的容器中. 蘋果註冊了一個Observer監聽BeforeWaiting(即將進入休眠)和Exit(即將退出Loop)事件,回撥去執行一個很長的函式。

   ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()。

      這個函式裡會便利所有待處理的UIView/CALayer以執行實際的繪製和調整,並且更新UI介面。( UIView/CALayer的實際繪製和調整必須在主執行緒中,但是待處理標記可以在其他執行緒中操作 );

(2)網路中的執行 NSRULConnection

          source1    RunLoop                                                    CFMultiplexer  CFHTTPCooklie storage

CFSocket —-—--------------------------------———-> NSRULconnectionLoader  ——————————------------------------——>  delegate

在使用NSURLConnection時,你會傳入一個Delegate,當呼叫[connection start]後,這個Delegate就會不停地收到事件回撥。 實際上,start這個函式的內部會獲取CurrentRunLoop,然後在其中的KCFDefaultMode中新增4個source0(需要手動觸發的source). CFMultiplexerSource是負責各種Delegate回撥的,CFHTTPCookieStorage是處理各種Cookie的.

當開始網路傳輸時,我們可以看到NSURLConnection建立了兩個新的執行緒Lcom.apple.NSURLConnectionLoader和com,apple,CFSocked.private.CFSocket負責最底層的socket連線,NSURLConnectionLoader這個執行緒內部會使用RunLoop來接受底層socket的事件,並通過之前新增的Source0通知上層的Delegate; NSURLConnectionLoader中的RunLoop通過一些基於mach port的source接受來自底層CFSocket的通知,當收到通知後,其會在適合的時機向CFMutliplexSource等source0傳送通知,同時喚醒Delegate執行緒的RunLoop來讓其處理這些通知.CFMultiplexerSource會在Delegate執行緒的RunLoop對Delegate執行實際回撥;

7.  當子執行緒進入runLoop,如何註冊事件或拋給該執行緒執行某些任務:

由於IOS禁止了基於埠port的通訊,所以只能用系統自帶的方法:

   [self performSelectorOnMainThread:<#(nonnull SEL)#> withObject:<#(nullable id)#> waitUntilDone:<#(BOOL)#>];

       [self performSelector:<#(nonnull SEL)#> onThread:<#(nonnull NSThread *)#> withObject:<#(nullable id)#> waitUntilDone:<#(BOOL)#>];

將任務拋給指定的執行緒,且會註冊到對應的RunLoop,這種方法會被立刻執行;