教你如何輕松搞定 Runloop

bearman12345 9年前發布 | 13K 次閱讀 RunLoop iOS開發 移動開發

認識 Runloop

  • Runloop 就是運行循環,如果沒有 Runloop,程序一運行就會退出,有 Runloop 就相當于在程序內部開了一個死循環
  • 在 iOS 開發中,有兩套 API 可以訪問 Runloop: NSRunloop CFRunloopRef ,它們是等價的,可以相互轉換
  • NSRunloop 是基于 CFRunloopRef 的 OC 包裝
  • 參考資料: 蘋果官方文檔CFRunloopRef 源碼

Runloop 的本質

  • Mach 是 XNU 的內核,進程、線程和虛擬內存等對象通過端口發消息進行通信,Runloop 通過 mach_msg() 函數發送消息
  • 如果沒有 port 消息,內核會將線程置于等待狀態 mach_msg_trap()
  • 如果有消息,判斷消息類型處理事件,并通過 modeItem 的 callback 進行回調

Runloop 的作用

  • 保證程序的持續運行
  • 處理 APP 中的各類事件
  • 節省 CPU 資源,提高程序性能(有事情就做,沒事情就休息)

Runloop 與線程的關系

  • NSRUnloop 與線程一一對應
  • 主線程中的 runloop 在程序運行時已經創建并啟動了
  • 子線程中的 runloop 需要我們手動創建并開啟
  • runloop 在線程結束時,也會銷毀

獲取 Runloop 對象

  • 獲取主線程 runloop 對象

    NSRunLoop *mainRL = [NSRunLoop mainRunLoop];
    CFRunLoopRef mainRLRef = CFRunLoopGetMain();
  • 獲取當前線程 runloop 對象

    NSRunLoop *currentRL = [NSRunLoop currentRunLoop];
    CFRunLoopRef currentRlRef = CFRunLoopGetCurrent();
  • 通過子線程創建 runloop

    NSRunLoop *curRunloop = [NSRunLoop currentRunLoop];

    這個方法本身是懶加載的,如果是第一次調用該方法,那么就創建子線程對應的 runloop。

  • 補充: runloop 對象是利用字典進行存儲的,key 值對應線程對象,value 值對應該線程的 runloop,在子線程中runloop 不會自動創建。

Runloop 的相關類

與 runloop 相關的共有五個類:CFRunLoopRef、CFRunLoopModeRef、CFRunLoopTimerRef、CFRunLoopSourceRef、CFRunLoopObserverRef

Runloop 五個相關類之間的關系

  1. CFRunLoopRef ( Runloop 對象)

    • Runloop 對象就是Runloop 本身

  2. CFRunLoopModeRef ( Runloop 的運行模式)

    • 一個 Runloop 包含若干個 Mode,而每個 Mode 又包含若干個 Source/Timer/Observer,每次 RunLoop 啟動時,只能指定其中一個 Mode,這個 Mode 被稱作 CurrentMode。如果需要切換 Mode,只能退出 Loop,再重新指定一個 Mode 進入,這樣就可以分隔開不同組的 Source/Timer/Observer,讓其互不影響。

    • Mode 的分類,系統默認注冊了 5 個 Mode:

      • KCFRunLoopDefaultMode :App的默認Mode,通常主線程是在這個 Mode 下運行

      • UITrackingRunLoopMode :界面跟蹤 Mode,用于 ScrollView 追蹤觸摸滑動,保證界面滑動時不受其他 Mode 影響

      • UIInitializationRunLoopMode : 在剛啟動 App 時進入的第一個 Mode,啟動完成后就不再使用

      • GSEventReceiveRunLoopMode : 接受系統事件的內部 Mode,通常用不到

      • kCFRunLoopCommonModes : 占位用的 Mode,不是一種真正的 Mode,就相當于 KCFRunLoopDefaultMode 和 UITrackingRunLoopMode的合體

  3. CFRunLoopTimerRef( Timer 事件)

    • 基于時間的觸發器,基本等同于 NSTimer
    • NSTimer 在各種模式下的運行效果

      [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];

      以上 scheduledTimerWithTimeInterval 方法內部默認把創建的定時器對象添加到當前的 Runloop 中,并且指定運行模式為 NSDefaultRunLoopMode

      NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
      [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

      以上 timerWithTimeInterval 方法創建定時器,如果想要定時器工作,還需要添加到 Runloop 中,并指定運行模式

    • 注意: 當 Runloop 切換到非指定模式,定時器就會停止工作

  4. CFRunLoopSourceRef ( Runloop 要處理的事件源)

    • 事件源也就是輸入源,只需要對它的分類有所了解就可以了

    • 以前的分法(根據官方文檔)

      • Port-Based Sources

      • Custom Input Sources

      • Cocoa Perform Selector Sources

    • 現在的分法(基于函數的調用棧)

      • Source0:非基于 Port 的

      • Source1:基于 Port 的

  5. CFRunLoopObserverRef( Runloop 的監聽者)

    • 主要用于監聽 Runloop 的狀態

    • Runloop 的狀態主要有:

      typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
        kCFRunLoopEntry = (1UL << 0),   //即將進入 Runloop
        kCFRunLoopBeforeTimers = (1UL << 1),    //即將處理 NSTimer
        kCFRunLoopBeforeSources = (1UL << 2),   //即將處理 Sources
        kCFRunLoopBeforeWaiting = (1UL << 5),   //即將進入休眠
        kCFRunLoopAfterWaiting = (1UL << 6),    //剛從休眠中喚醒
        kCFRunLoopExit = (1UL << 7),            //即將退出 Runloop
        kCFRunLoopAllActivities = 0x0FFFFFFFU   //所有狀態改變
      };
    • 實現 Runloop 的監聽

      //創建監聽對象,當 Runloop 的狀態改變時就會調用該方法
      CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
        NSLog(@"對 Runloop 的狀態改變進行監聽", activity);
      });
      //設置監聽
      CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);

Runloop 的啟動

  1. 選擇一個運行模式,且只能選擇一個

  2. 判斷當前選擇的運行模式是否為空

  3. 檢查當前運行模式里面是否有 Source 或 Timer,如果都沒有,則 Runloop 立即退出

  4. 至少有 Source 或 Timer 中的任意一個,則 Runloop 開啟

  5. 在檢查的時候不會檢查 Observer

Runloop 的運行處理邏輯

Runloop 的運行處理邏輯

  • 在運行 Runloop 時,RUnloop 會自動處理之前未處理的消息,并通知相關的監聽者,具體的處理邏輯如下:

    1. 通知觀察者 Runloop 已經啟動

    2. 通知觀察者即將要開始的定時器

    3. 通知觀察者即將啟動的非基于端口的源

    4. 啟動已經準備好的非基于端口的源

    5. 如果有基于端口的源并處于等待狀態,立即啟動,跳到第九步

    6. 通知觀察者 Runloop 進入休眠

    7. Runloop 進入休眠,等待發生以下事件時喚醒

      • 有事件到達基于端口的源

      • 定時器啟動

      • Runloop 超時

      • Runloop 被外界手動喚醒

    8. 通知觀察者,線程剛被喚醒

    9. 處理喚醒時收到的消息,之后跳到第二步

      • 如果有用戶定義的定時器啟動,處理定時器事件并重啟 Runloop

      • 如果輸入源啟動,傳遞相應的信息

      • 如果 Runloop 被外界手動喚醒且未超時,重啟 Runloop

    10. 通知觀察者,Runloop 即將結束

Runloop 的應用

  • 常駐線程

    • 在子線程中創建一個 Runloop

      NSRunLoop *currentRunloop= [NSRunLoop currentRunLoop];
    • 需要至少指定 Runloop 的 Source 或者 Timer 中的任意一個(一般情況下指定 Source,比較簡單)
    • 需要指定 Runloop 的運行模式(保證 Runloop 不退出)

      [currentRunloop addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
    • 需要手動開啟 Runloop

      [currentRunloop run];
  • imageView 的顯示

    • 控制方法在特定模式下可用

      [self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"Snip2016"] afterDelay:3.0];

      以上方法默認添加到當前的 Runloop 中,并且指定運行模式為默認 KCFRunLoopDefaultMode,如果 Runloop 切換運行模式,則圖片不會加載到 imageView 上。

      [self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"Snip20161009_32"] afterDelay:3.0 inModes:@[NSRunLoopCommonModes]];

      以上方法添加到當前 Runloop 中,且指定運行模式為 NSRunLoopCommonModes

  • 自動釋放池

    • 進入 Runloop 的時候第一次創建
    • 退出 Runloop 的時候最后一次釋放(超時或線程銷毀)
    • 其他時候的創建與釋放

      • 當 Runloop 即將休眠的時候會把之前的自動釋放池釋放,再重新創建一個新的自動釋放池
      void msg(int n)
      {
        NSLog(@"runloop被喚醒");
        NSLog(@"runloop處理事件---%zd",n);
      }
      int main(int argc, const char * argv[]) {
      @autoreleasepool {
         NSLog(@"runloop啟動了");
      
            do {
                NSLog(@"runloop詢問,還有事情需要我處理嗎?");
                NSLog(@"沒有事情的話,我就睡覺了");
                NSLog(@"runloop進入到休眠");
      
                int number = 0;
                scanf("%zd",&number);
                msg(number);
      
            } while (1);
        }
        return 0;
      }

 

來自:http://www.jianshu.com/p/4bad817df9ae

 

 本文由用戶 bearman12345 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!