iOS之RunLoop

来源:http://www.cnblogs.com/lizheng114/archive/2017/05/09/6821807.html
-Advertisement-
Play Games

RunLoop是iOS線程相關的比較重要的一個概念,無論是主線程還是子線程,都對應一個RunLoop,如果沒有RunLoop,線程會馬上被系統回收。 本文主要CFRunLoop的源碼解析,並簡單闡述一下CFRunLoop的原理。 CFRunLoop是開源的,開源地址在:http://opensour ...


RunLoop是iOS線程相關的比較重要的一個概念,無論是主線程還是子線程,都對應一個RunLoop,如果沒有RunLoop,線程會馬上被系統回收。

本文主要CFRunLoop的源碼解析,並簡單闡述一下CFRunLoop的原理。

CFRunLoop是開源的,開源地址在:http://opensource.apple.com/tarballs/CF/ 

先看一張圖,這是主線程的RunLoop調用函數截圖:

我們找到相應的CFRunLoop源碼:

void CFRunLoopRun(void) {    /* DOES CALLOUT */
    int32_t result;
    do {
        result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
        CHECK_FOR_FORK();
    } while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}

可以看到,系統建立了一個do while迴圈,當狀態在stop或者finished時,就會退出迴圈,RunLoop會結束,線程會被回收。

註:1.0e10,這個表示1.0乘以10的10次方,這個參數主要是規定RunLoop的時間,傳這個時間,表示線程常駐。

SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {     /* DOES CALLOUT */
    CHECK_FOR_FORK();
    if (__CFRunLoopIsDeallocating(rl)) return kCFRunLoopRunFinished;
    __CFRunLoopLock(rl);
    CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false);
    if (NULL == currentMode || __CFRunLoopModeIsEmpty(rl, currentMode, rl->_currentMode)) {
        Boolean did = false;
        if (currentMode) __CFRunLoopModeUnlock(currentMode);
        __CFRunLoopUnlock(rl);
        return did ? kCFRunLoopRunHandledSource : kCFRunLoopRunFinished;
    }
    volatile _per_run_data *previousPerRun = __CFRunLoopPushPerRunData(rl);
    CFRunLoopModeRef previousMode = rl->_currentMode;
    rl->_currentMode = currentMode;
    int32_t result = kCFRunLoopRunFinished;

    if (currentMode->_observerMask & kCFRunLoopEntry ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
    result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
    if (currentMode->_observerMask & kCFRunLoopExit ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);

    __CFRunLoopModeUnlock(currentMode);
    __CFRunLoopPopPerRunData(rl, previousPerRun);
    rl->_currentMode = previousMode;
    __CFRunLoopUnlock(rl);
    return result;
}

先執行:__CFRunLoopFindMode,查找是否有Mode

  1. 如果有則返回找到的。
  2. 如果沒有,且不需要創建,則返回NULL。
  3. 如果沒有,需要創建,則新建一個Mode。

看代碼標註:

/* call with rl locked, returns mode locked */
static CFRunLoopModeRef __CFRunLoopFindMode(CFRunLoopRef rl, CFStringRef modeName, Boolean create) {
    CHECK_FOR_FORK();
    CFRunLoopModeRef rlm;
    struct __CFRunLoopMode srlm;
    memset(&srlm, 0, sizeof(srlm));
    _CFRuntimeSetInstanceTypeIDAndIsa(&srlm, __kCFRunLoopModeTypeID);
    srlm._name = modeName;
    rlm = (CFRunLoopModeRef)CFSetGetValue(rl->_modes, &srlm);
    if (NULL != rlm) {  //如果有則返回
        __CFRunLoopModeLock(rlm);
        return rlm;
    }
    if (!create) {    //情況2
        return NULL;
    }
    //情況3
    rlm = (CFRunLoopModeRef)_CFRuntimeCreateInstance(kCFAllocatorSystemDefault, __kCFRunLoopModeTypeID, sizeof(struct __CFRunLoopMode) - sizeof(CFRuntimeBase), NULL);
    if (NULL == rlm) {
        return NULL;
    }
....//後面為創建一個Mode並賦初始值

__CFRunLoopMode是什麼

__CFRunLoopMode我自己理解為一種運行類型,它表示了當前線程運行在哪種類型下,會被哪種類型的事件喚醒。就好比你在程式中設置一個定時器Timer,運行在DefaultMode下,但是如果你滑動UIScrollview,系統會將當前線程的Mode改為UITrackingRunLoopMode,這時你的Timer就不會得到調用,因為當前線程的Mode和你Timer的Mode不同。當然,如果你想無論在哪種Mode下,Timer都想得到調用的話,你需要將Mode設置為CommonMode。

那麼,為什麼要這樣設計呢?我理解為這樣設計更為靈活。你可以指定事件需要在當前RunLoop是什麼Mode的時候被調用,跟上面舉的例子一樣。

系統提供了對應於__CFRunLoopMode的五種類型到NSRunloopMode中:

  1. NSDefaultRunLoopMode,預設模式,在Run Loop沒有指定Mode的時候,預設就跑在Default Mode下
  2. NSConnectionReplyMode,用來監聽處理網路請求NSConnection的事件
  3. NSModalPanelRunLoopMode,OS X的Modal面板事件
  4. UITrackingRunLoopMode,拖動事件
  5. NSRunLoopCommonModes,是一個模式集合,當綁定一個事件源到這個模式集合的時候就相當於綁定到了集合內的每一個模式

我們經常在代碼中使用的是NSDefaultRunLoopMode和NSRunLoopCommonModes。你也可以使用自定義的Mode。

__CFRunLoopMode包含了什麼及其作用

__CFRunLoopMode的結構體中包含了:Source0,Source1,Observers,Timers,Ports等。

Source0:處理App內部事件,如UIEvent、CFSocket,對應(CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION)這個函數。

Source1:由RunLoop和內核管理,Mach port驅動,如CFMachPort、CFMessagePort。對應(CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION)這個函數。

Observers:主要負責修改RunLoop的狀態。狀態包括:

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0),
    kCFRunLoopBeforeTimers = (1UL << 1),
    kCFRunLoopBeforeSources = (1UL << 2),
    kCFRunLoopBeforeWaiting = (1UL << 5),
    kCFRunLoopAfterWaiting = (1UL << 6),
    kCFRunLoopExit = (1UL << 7),
    kCFRunLoopAllActivities = 0x0FFFFFFFU
};

Timers:負責讓App響應NSTimers,延遲的perform事件,對應(CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION)這個函數。我們可以在viewDidLoad裡面加入如下代碼:

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

- (void)test{
    NSLog(@"abc");
}

可以得到如下圖:

這裡簡單介紹下DoTimers和DoTimer兩個函數。DoTimers是一個for迴圈,在for迴圈裡面調用DoTimer。然後調用CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION,這個函數代碼很簡單,就是調用NSTimer或者Perform的回調。

Ports:port是用來做進程或者線程間通信的,分為CFMachPort, CFMessagePort, CFSocketPort,詳細介紹可以看 這篇文章

再接著代碼往下看:

volatile _per_run_data *previousPerRun = __CFRunLoopPushPerRunData(rl);

_per_run_data是用來描述當前CFRunLoop的狀態的,有三種狀態:初始狀態,wake,stop三種。註意到 volatile 這個關鍵字,它的意思是告訴編譯器不要優化這個變數,要每次都從記憶體中讀取該變數。

接著往下:

if (currentMode->_observerMask & kCFRunLoopEntry ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);

通知觀察者開始進入RunLoop。主要是通過(CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION)這個函數來完成。

接下來就要進入__CFRunLoopRun這個核心函數了。這個函數比較複雜,跟port相關的就忽略不講了。

dispatch_source_t timeout_timer = NULL;
    struct __timeout_context *timeout_context = (struct __timeout_context *)malloc(sizeof(*timeout_context));
    if (seconds <= 0.0) { // instant timeout
        seconds = 0.0;
        timeout_context->termTSR = 0ULL;
    } else if (seconds <= TIMER_INTERVAL_LIMIT) {
        dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, DISPATCH_QUEUE_OVERCOMMIT);
        timeout_timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
        dispatch_retain(timeout_timer);
        timeout_context->ds = timeout_timer;
        timeout_context->rl = (CFRunLoopRef)CFRetain(rl);
        timeout_context->termTSR = startTSR + __CFTimeIntervalToTSR(seconds);
        dispatch_set_context(timeout_timer, timeout_context); // source gets ownership of context
        dispatch_source_set_event_handler_f(timeout_timer, __CFRunLoopTimeout);
        dispatch_source_set_cancel_handler_f(timeout_timer, __CFRunLoopTimeoutCancel);
        uint64_t ns_at = (uint64_t)((__CFTSRToTimeInterval(startTSR) + seconds) * 1000000000ULL);
        dispatch_source_set_timer(timeout_timer, dispatch_time(1, ns_at), DISPATCH_TIME_FOREVER, 1000ULL);
        dispatch_resume(timeout_timer);
    } else { // infinite timeout
        seconds = 9999999999.0;
        timeout_context->termTSR = UINT64_MAX;
    }

這一段邏輯比較清晰,使用GCD建立一個定時任務,在指定的時間內,使用__CFRunLoopTimeOut喚醒RunLoop。因為使用了DISPATCH_TIME_FOREVER,所以__CFRunLoopTimeOut只會調用一次。

__CFRunLoopDoBlocks(rl, rlm); //執行RunLoop中的block
Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
if (sourceHandledThisLoop) {
   __CFRunLoopDoBlocks(rl, rlm);
}
//執行Mode裡面的Source0的事件,如perform,uievent等//不知道為什麼__CFRunLoopDoBlocks要執行兩次

後續會有兩次狀態的改變:kCFRunLoopBeforeWaiting和kCFRunLoopAfterWaiting,接著就是跟port相關的了,就不寫了。

最後一段:

if (sourceHandledThisLoop && stopAfterHandle) {
    // 進入loop時參數說處理完事件就返回。
    retVal = kCFRunLoopRunHandledSource;
} else if (timeout_context->termTSR < mach_absolute_time()) {
    // 超出傳入參數標記的超時時間了
    retVal = kCFRunLoopRunTimedOut;
} else if (__CFRunLoopIsStopped(rl)) {
    // 被外部調用者強制停止了
    __CFRunLoopUnsetStopped(rl);
    retVal = kCFRunLoopRunStopped;
} else if (rlm->_stopped) {
    rlm->_stopped = false;
    retVal = kCFRunLoopRunStopped;
} else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {
    // source/timer/observer一個都沒有了
    retVal = kCFRunLoopRunFinished;
}

 總體來講,RunLoop比較基礎但是也是比較複雜,在閱讀源碼的過程中也遇到不少疑惑,有些疑惑可能需要在後續的研究中才能慢慢發現答案。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • var val = $("select[name='type_irb'] option:selected").val(); ...
  • 轉載請註明——博客園igoslly:http://www.cnblogs.com/igoslly/p/6833544.html 轉載請註明——博客園igoslly:http://www.cnblogs.com/igoslly/p/6833544.html 在實際方法調用中,程式按順序逐句執行,直到“ ...
  • 要點:就是緩存輸入的內容到 本地 下麵就是實現保存 搜索內容到本地 和 清空本地歷史的 方法 activity 下拉彈出layout佈局 ...
  • 我們開發中常用到圖片上傳,比如頭像上傳之類的,還有類似發佈空間說說時發佈多張圖片等等,我在這裡通過借鑒網路資源,並依賴於七牛雲存儲做了一個圖片上傳的小結。 我首先封裝了一個圖片上傳的工具類,繼承自nsobject,命名為QiniuTool;再多圖上傳時,還需要單張圖片的上傳結果處理,因此,我在這裡有 ...
  • 該View轉自 http://blog.csdn.net/Kalwang/article/details/4708721 ,感謝這位大神。 ...
  • 此次掃碼功能以iOS系統原生的AVFoundation框架為基礎。 廢話不多說,直接上代碼 #import <UIKit/UIKit.h> @interface ScanViewController : UIViewController @end 在.m文件中創建對象 #import "ScanVi ...
  • 1.原代碼提示快捷鍵為:Ctrl+空格,與Windows輸入法衝突,所以將代碼提示快捷鍵設置為:Ctrl+反斜杠。 ...
  • 入行快10年,有點積蓄,三年前買了代步車。於是乎,汽車油耗開銷就成了每個月都必須關註的問題。三年來,用過了無數油耗記錄軟體,比如最知名的“小熊油耗”,從第一次用,一直到最新一版,感覺越來越“臃腫”,功能實在是太豐富了,甚至都做到“大數據”這一層面,作為一個小白車主,其實,我只關心:我的車油耗如何。 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...