iOS開發小記RunLoop篇

NO IMAGE

趁熱打個鐵,迫不及待想記錄新東西了

什麼是RunLoop


一般來講,一個線程只能執行一次任務,執行完線程就會退出。如果我們需要這樣一個機制,讓線程能隨時處理事件而不退出,通常的邏輯代碼如下:

function loop() {
initialize();
do {
var message = get_next_message();
process_message(message);
} while (message != quit);
}

這種模型通常叫做Event Loop。這個模型的關鍵點在於:如何管理事件/消息,如何讓線程在沒有處理消息時休眠以避免佔用資源,在有消息到來時立即被喚醒。
所以,RunLoop實際是一個對象,該對象管理了其需要處理的事件和消息,並提供了入口函數來處理上面的Event Loop的邏輯。線程執行這個函數後,就會一直處在函數內部“接收消息->等待->處理”的循環中,直到接收到退出消息(如quit),函數結束。

OSX/iOS 系統中,提供了兩個這樣的對象:NSRunLoop 和 CFRunLoopRef。
CFRunLoopRef 是在 Core Foundation 框架內的,它提供了純 C 函數的 API,所有這些 API 都是線程安全的。它是基於pthread的。
NSRunLoop 是基於 CFRunLoopRef 的封裝,提供了面向對象的 API,但是這些 API 不是線程安全的。

  • RunLoop與線程的關係

蘋果不允許直接創建 RunLoop,它只提供了兩個自動獲取的函數:CFRunLoopGetMain() 和 CFRunLoopGetCurrent()。 這兩個函數內部的邏輯大概是下面這樣:

/// 全局的Dictionary,key 是 pthread_t, value 是 CFRunLoopRef
static CFMutableDictionaryRef loopsDic;
/// 訪問 loopsDic 時的鎖
static CFSpinLock_t loopsLock;
/// 獲取一個 pthread 對應的 RunLoop。
CFRunLoopRef _CFRunLoopGet(pthread_t thread) {
OSSpinLockLock(&loopsLock);
if (!loopsDic) {
// 第一次進入時,初始化全局Dic,並先為主線程創建一個 RunLoop。
loopsDic = CFDictionaryCreateMutable();
CFRunLoopRef mainLoop = _CFRunLoopCreate();
CFDictionarySetValue(loopsDic, pthread_main_thread_np(), mainLoop);
}
/// 直接從 Dictionary 裡獲取。
CFRunLoopRef loop = CFDictionaryGetValue(loopsDic, thread));
if (!loop) {
/// 取不到時,創建一個
loop = _CFRunLoopCreate();
CFDictionarySetValue(loopsDic, thread, loop);
/// 註冊一個回調,當線程銷燬時,順便也銷燬其對應的 RunLoop。
_CFSetTSD(..., thread, loop, __CFFinalizeRunLoop);
}
OSSpinLockUnLock(&loopsLock);
return loop;
}
CFRunLoopRef CFRunLoopGetMain() {
return _CFRunLoopGet(pthread_main_thread_np());
}
CFRunLoopRef CFRunLoopGetCurrent() {
return _CFRunLoopGet(pthread_self());
}

從上述代碼可以看出,線程和RunLoop是一一對應的,其關係保存在一個全局的Dictionary裡。線程創建時是沒有RunLoop的,只有第一次主動獲取的時候才會創建,否則會一直沒有,直到線程結束時銷燬。你只能在一個線程內部獲取其RunLoop對象(主線程除外)。

  • 主線程的RunLoop

在創建一個iOS程序後,會自動生成main.m文件,如下

int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}

其中UIApplicationMain函數內部為主線程開啟了RunLoop,邏輯代碼如Event Loop模型所示。
下圖為蘋果官方給出的RunLoop模型圖。

iOS開發小記RunLoop篇

從上圖中可以看出,RunLoop就是線程中的一個循環,RunLoop在循環中會不斷檢測,通過Input sources(輸入源)和Timer sources(定時源)兩種來源等待接受事件;然後對接受到的事件通知線程進行處理,並在沒有事件的時候進行休息。

RunLoop相關類


Core Foundation框架下有關於RunLoop的5個類,如下:

  1. CFRunLoopRef:代表RunLoop的對象。
  2. CFRunLoopModeRef:RunLoop的運行模式。
  3. CFRunLoopSourceRef:就是RunLoop模型圖中提到的輸入源/事件源。
  4. CFRunLoopTimerRef:就是RunLoop模型圖中提到的定時源。
  5. CFRunLoopObserverRef:觀察者,能夠監聽RunLoop的狀態改變。

他們的關係如下圖:

iOS開發小記RunLoop篇

一個RunLoop對象(CFRunLoopRef)包含若干個運行模式(CFRunLoopModeRef)。每個運行模式下又包含若干個輸入源(CFRunLoopSourceRef)、定時源(CFRunLoopTimerRef)、觀察者(CFRunLoopObserverRef),他們有如下特點:
1.每次RunLoop啟動時,只能選擇一個運行模式啟動,這個運行模式被稱為CurrentMode。
2.如果要切換運行模式,只能退出RunLoop,並重新指定一個運行模式啟動。
3.這樣做是為了使不同組的輸入源、定時源、觀察者互不影響。

  • CFRunLoopRef

我們可以通過如下API來獲取Core Fundation中的CFRunLoopRef。

    //獲取主線程的RunLoop
CFRunLoopRef mainRunLoop = CFRunLoopGetMain();
//獲取當前線程的RunLoop
CFRunLoopRef currentRunLoop = CFRunLoopGetCurrent();
  • CFRunLoopModeRef

系統定義了多種運行模式:

  1. kCFRunLoopDefaultMode:App的默認運行模式,通常主線程是在這個運行模式下運行。
  2. UITrackingRunLoopMode:跟蹤用戶交互事件(用於 ScrollView 追蹤觸摸滑動,保證界面滑動時不受其他Mode影響)。
  3. UIInitializationRunLoopMode:在剛啟動App時第進入的第一個 Mode,啟動完成後就不再使用。
  4. GSEventReceiveRunLoopMode:接受系統內部事件,通常用不到。
  5. kCFRunLoopCommonModes:偽模式,不是一種真正的運行模式(後邊會用到)。
    其中kCFRunLoopDefaultMode、UITrackingRunLoopMode、kCFRunLoopCommonModes是我們開發中需要用到的模式。
  • CFRunLoopTimerRef
- (void)viewDidLoad {
[super viewDidLoad];
//將定時器加入到默認運行模式中(一旦用戶交互就不會響應)
NSTimer *timer1 = [NSTimer timerWithTimeInterval:2 target:self selector:@selector(runInDefaultMode) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer1 forMode:NSDefaultRunLoopMode];
//將定時器加入到交互運行模式中(一旦停止交互就不會響應)
NSTimer *timer2 = [NSTimer timerWithTimeInterval:2 target:self selector:@selector(runInTrackingMode) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer2 forMode:UITrackingRunLoopMode];
//將定時器加入到偽模式中(無論是否交互都可以響應)
NSTimer *timer3 = [NSTimer timerWithTimeInterval:2 target:self selector:@selector(runInCommonMode) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer3 forMode:NSRunLoopCommonModes];
}
- (void)runInDefaultMode {
NSLog(@"我只有在默認模式下運行!");
}
- (void)runInTrackingMode {
NSLog(@"我只有在交互模式下運行!");
}
- (void)runInCommonMode {
NSLog(@"我在默認模式和交互模式下都能運行!");
}

我們觀察到,沒有做操作時timer1能正常運行,而timer2無響應,用戶操作後timer1停止運行,而timer2正常運行,與此同時timer3始終都能運行,這是為什麼呢?原因如下:

  1. 我們不做任何操作時,RunLoop處於NSDefaultRunLoopMode模式下,所以timer1此時能穩定2秒運行。
  2. 一旦我們進行了操作產生了交互,RunLoop立即結束NSDefaultRunLoopMode模式,並切換到UITrackingRunLoopMode工作,所以timer1不能繼續工作,轉而該模式下的timer2開始工作。
  3. 由於NSRunLoopCommonModes不是一個真正的模式,並非需要中止其他模式再切換,只是使得可以在標記了Common Modes的模式下運行,也就是NSDefaultRunLoopModeNSRunLoopCommonModes,所以timer3能一直工作。

這裡我們可以看下CFRunLoopMode 和 CFRunLoop 的大致結構:

struct __CFRunLoopMode {
CFStringRef _name;            // Mode Name, 例如 @"kCFRunLoopDefaultMode"
CFMutableSetRef _sources0;    // Set
CFMutableSetRef _sources1;    // Set
CFMutableArrayRef _observers; // Array
CFMutableArrayRef _timers;    // Array
...
};
struct __CFRunLoop {
CFMutableSetRef _commonModes;     // Set
CFMutableSetRef _commonModeItems; // Set<Source/Observer/Timer>
CFRunLoopModeRef _currentMode;    // Current Runloop Mode
CFMutableSetRef _modes;           // Set
...
};

一個Mode可以將自己標記為“Common”屬性(將自己的ModelName添加到RunLoop中的_commonModes中)。每當RunLoop發生變化時,RunLoop會將_commonModeItems中的Source/Observer/Timer同步到所有標記了“Common”的Mode中。

另外,說到NSTimer,我們平時使用的以下方法,是自動添加到了RunLoop對象的NSDefaultRunLoopMode模式下,所以一旦交互是無法響應的。

[NSTimer scheduledTimerWithTimeInterval:2 target:self selector:@selector(runInDefaultMode) userInfo:nil repeats:YES];
  • CFRunLoopSourceRef
    CFRunLoopSourceRef分為兩種:
  1. Source0 只包含了一個回調(函數指針),它並不能主動觸發事件。使用時,你需要先調用 CFRunLoopSourceSignal(source),將這個 Source 標記為待處理,然後手動調用 CFRunLoopWakeUp(runloop) 來喚醒 RunLoop,讓其處理這個事件。
  2. Source1 包含了一個 mach_port 和一個回調(函數指針),被用於通過內核和其他線程相互發送消息。這種 Source 能主動喚醒 RunLoop 的線程,其原理在下面會講到。

例如我們點擊一個按鈕,攔截它的響應事件的函數調用棧,可以看到

iOS開發小記RunLoop篇

  1. 首先程序啟動,調用18的main函數,main函數調用17行UIApplicationMain函數,然後一直往上調用函數,最終調用到0行的響應事件。
  2. 同時我們可以看到13行中有Sources0,也就是說我們點擊事件是屬於Sources0函數的,點擊事件就是在Sources0中處理的。
  3. 而至於Sources1,則是用來接收、分發系統事件,然後再分發到Sources0中處理的。
  • 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
kCFRunLoopAllActivities = 0x0FFFFFFFU       // 監聽全部狀態改變  
};

我們可以通過如下代碼來監聽RunLoop狀態的改變

    //創建監聽者
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
NSLog(@"監聽到RunLoop發生改變---%zd", activity);
});
//添加到當前的RunLoop中
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopDefaultMode);
//添加完後釋放
CFRelease(observer);

這裡監聽了所有的狀態,打印日誌可以看到RunLoop的狀態不斷的改變,最終會變成32,也就是馬上會進入休眠狀態。

注:上面的 Source/Timer/Observer 被統稱為 mode item,一個 item 可以被同時加入多個 mode。但一個 item 被重複加入同一個 mode 時是不會重複生效的。如果一個 mode 中一個 item 都沒有,則 RunLoop 會直接退出,不進入循環。

RunLoop原理


根據蘋果在文檔裡的說明,RunLoop 內部的邏輯大致如下:

iOS開發小記RunLoop篇

在每次運行開啟RunLoop時,所在線程的RunLoop會自動處理之前未處理的事件,並且通知相關觀察者。

  1. 通知觀察者:即將進入RunLoop。
  2. 通知觀察者:即將觸發Timer回調。
  3. 通知觀察者:即將觸發Source0回調。
  4. 觸發Source0(非port)回調。
  5. 如果有 Source1 (基於port) 處於 ready 狀態,直接處理這個 Source1 然後跳轉8去處理消息。
  6. 通知觀察者:即將進入休眠。如果一個事件到達了基於port的源、定時器啟動、自身超時或者被手動顯示都會講RunLoop從休眠中喚醒。
  7. 通知觀察者:即將被喚醒。
  8. 處理事件:Timer時間到了、dispatch到主線程的block、Source1發出事件了。
  9. 通知觀察者:即將結束RunLoop。

注:RunLoop 的核心就是一個 mach_msg() ,RunLoop 調用這個函數去接收消息,如果沒有別人發送 port 消息過來,內核會將線程置於等待狀態。例如你在模擬器裡跑起一個 iOS 的 App,然後在 App 靜止時點擊暫停,你會看到主線程調用棧是停留在 mach_msg_trap() 這個地方。

  • 函數調用棧
{
/// 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);
}

系統中的應用


  • AutoreleasePool

AutoreleasePool用於在代碼塊結束時釋放所有在代碼塊中創建的對象,最重要的使用場景就是臨時創建了大量的對象,例如在循環中創建對象,可以在循環體內使用AutoreleasePool,及時清理內存。

App啟動後,蘋果在主線程 RunLoop 裡註冊了兩個 Observer,其回調都是 _wrapRunLoopWithAutoreleasePoolHandler()。

第一個 Observer 監視的事件是 Entry(即將進入Loop),其回調內會調用 _objc_autoreleasePoolPush() 創建自動釋放池。其 order 是-2147483647,優先級最高,保證創建釋放池發生在其他所有回調之前。

第二個 Observer 監視了兩個事件: BeforeWaiting(準備進入休眠) 時調用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 釋放舊的池並創建新池;Exit(即將退出Loop) 時調用 _objc_autoreleasePoolPop() 來釋放自動釋放池。這個 Observer 的 order 是 2147483647,優先級最低,保證其釋放池只發生在其他所有回調之後。

在主線程執行的代碼,通常是寫在諸如事件回調、Timer回調內的。這些回調會被 RunLoop 創建好的 AutoreleasePool 環繞著,所以不會出現內存洩漏,開發者也不必顯式創建 Pool 了。

  • 事件響應

蘋果註冊了一個Source1(基於mach port)來接收系統事件,如果發生硬件事件(觸摸/鎖屏/搖晃等),Source1會觸發回調__IOHIDEventSystemClientQueueCallback() ,函數內然後觸發Source0,Source0再通過_UIApplicationHandleEventQueue() 分發到應用內部。
_UIApplicationHandleEventQueue() 會把 IOHIDEvent 處理幷包裝成 UIEvent 進行處理或分發,其中包括識別 UIGesture/處理屏幕旋轉/發送給 UIWindow 等。通常事件比如 UIButton 點擊、touchesBegin/Move/End/Cancel 事件都是在這個回調中完成的。
注:網上對於點擊事件是Source0還是Source1觸發有爭議,在 __IOHIDEventSystemClientQueueCallback 處下一個 Symbolic Breakpoint可以看到,確實是如上述邏輯。

  • 手勢識別

當上面的_UIApplicationHandleEventQueue() 接收到一個手勢時,其首先會調用 Cancel 將當前的 touchesBegin/Move/End 系列回調打斷。隨後系統將對應的 UIGestureRecognizer 標記為待處理。
蘋果註冊了一個Observer來監聽BeforeWaiting(即將進入休眠),其回調函數內部會獲取所有剛才標記了未處理的手勢,並觸發它們的回調。
當有 UIGestureRecognizer 的變化(創建/銷燬/狀態改變)時,這個回調都會進行相應處理。

  • 界面更新

當在操作 UI 時,比如改變了 Frame、更新了 UIView/CALayer 的層次時,或者手動調用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法後,這個 UIView/CALayer 就被標記為待處理,並被提交到一個全局的容器去。
蘋果註冊了一個Observer來監聽 BeforeWaiting(即將進入休眠) 和 Exit (即將退出Loop) 事件,並在回調函數裡遍歷所有待處理的 UIView/CAlayer 以執行實際的繪製和調整,並更新 UI 界面。

  • 定時器

NSTimer實際也就相當於CFRunLoopTimerRef,他們之間是 toll-free bridged 的。一個 NSTimer 註冊到 RunLoop 後,RunLoop 會為其重複的時間點註冊好事件。例如 10:00, 10:10, 10:20 這幾個時間點。RunLoop為了節省資源,並不會在非常準確的時間點回調這個Timer。Timer 有個屬性叫做 Tolerance (寬容度),標示了當時間點到後,容許有多少最大誤差。
如果某個時間點被錯過了,例如執行了一個很長的任務,則那個時間點的回調也會跳過去,不會延後執行。就比如等公交,如果 10:10 時我忙著玩手機錯過了那個點的公交,那我只能等 10:20 這一趟了。
CADisplayLink 是一個和屏幕刷新率一致的定時器(但實際實現原理更復雜,和 NSTimer 並不一樣,其內部實際是操作了一個 Source)。如果在兩次屏幕刷新之間執行了一個長任務,那其中就會有一幀被跳過去(和 NSTimer 相似),造成界面卡頓的感覺。在快速滑動TableView時,即使一幀的卡頓也會讓用戶有所察覺。

  • PerformSelector

當調用 NSObject 的 performSelecter:afterDelay: 後,實際上其內部會創建一個 Timer 並添加到當前線程的 RunLoop 中。所以如果當前線程沒有 RunLoop,則這個方法會失效。
當調用 performSelector:onThread: 時,實際上其會創建一個Source0加到對應的線程去,同樣的,如果對應線程沒有 RunLoop 該方法也會失效。()

  • 關於GCD

RunLoop底層會用到GCD的東西,GCD的實現也用到了RunLoop,比如dispatch_async()函數。
當調用 dispatch_async(dispatch_get_main_queue(), block) 時,libDispatch 會向主線程的 RunLoop 發送消息,RunLoop會被喚醒,並從消息中取得這個 block,並在回調 CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE() 裡執行這個 block。但這個邏輯僅限於 dispatch 到主線程,dispatch 到其他線程仍然是由 libDispatch 處理的。

實際應用


  • UITableView內容延遲加載

例如我們需要在cell上展示分時圖,那麼在滾動的時候如果有一堆的分時圖需要重複的清空再計算繪製,就有可能造成卡頓。
首先cell複用分時圖需要使用到兩個方法clearTimeLinerefreshTimeLine,我們可以利用PerformSelector調用refreshTimeLine將其放在主線程的NSDefaultRunLoopMode下,這樣避免滾動時還會觸發繪圖操作,減少計算和繪製,提高性能,同時也減少了內存佔用。

  • 後臺常駐線程

如果在實際開發中有大量的耗時操作需要在後臺完成,頻繁的新建子線程並不是好的方案,我們可以選擇讓這條線程常駐內存。

- (void)viewDidLoad {
[super viewDidLoad];
//強引用子線程,初始化該線程並啟動
self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
[self.thread start];
//通過performSelector來在子線程中處理耗時操作,避免重複創建
[self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:NO];
}
- (void)run {
//開啟當前線程的RunLoop,此處添加port是避免RunLoop退出
[[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] run];
}
  • 監測卡頓(後續詳細補一篇)

由於我們絕大多數操作都是基於非port通信的,也就是source0,所以我們可以通過使用子線程來檢測RunLoop中kCFRunLoopBeforeSourceskCFRunLoopBeforeWaiting兩個狀態之間的時間來判斷這一輪操作是否卡頓,並把當前線程的堆棧信息存儲到文件中,在某個合適的時機上傳到服務器。

大致步驟如下:

  1. 創建一個子線程,打開其RunLoop並註冊一個定時器用來監聽主線程RunLoop的狀態變化。
  2. 在主線程的RunLoop中註冊一個觀察者,然後監聽kCFRunLoopBeforeSourceskCFRunLoopBeforeWaiting兩個狀態。
  3. 在觀察者回調方法中,在kCFRunLoopBeforeSources記錄更新時間,並且記錄狀態為NO,用於定時器區分狀態;在kCFRunLoopBeforeWaiting時將狀態置為YES。
  4. 在定時器的執行函數裡,判斷如果當前狀態為NO,那麼表示主線程的RunLoop還在運行,計算當前時間與記錄的更新時間的差值,如果大於閾值,那麼將堆棧信息保存在文件中,擇時上傳。

優化點

  1. 卡頓時間:按照微信團隊的標準,這個卡頓時間為2秒。
  2. 函數調用堆棧:可以使用三方開源庫獲取。
  3. 寫入策略:首先與內存中上次的卡頓函數調用堆棧作比較,如果不同,才需要寫入文件。
  4. 上傳策略:抽樣用戶上傳,上傳前20個堆棧文件。
  5. 本地保存策略:僅保存7天。

相關代碼

#import "ViewController.h"
static CGFloat lagTimeInterval = 0.5;
@interface ViewController ()
//監聽子線程
@property (nonatomic, strong) NSThread *monitorThread;
//是否進入休眠
@property (nonatomic, assign) BOOL isBeforeWaiting;
//即將處理source0的時間
@property (nonatomic, strong) NSDate *beforeSource0Time;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
//創建監聽子線程,打開其RunLoop
_monitorThread = [[NSThread alloc] initWithTarget:self selector:@selector(openRunLoop) object:nil];
[_monitorThread start];
//添加定時器
[self performSelector:@selector(startMonitorTimer) onThread:_monitorThread withObject:nil waitUntilDone:NO];
//主線程RunLoop添加觀察者
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
switch (activity) {
case kCFRunLoopBeforeSources:
{
_beforeSource0Time = [NSDate date];
_isBeforeWaiting = NO;
}
break;
case kCFRunLoopBeforeWaiting:
{
_isBeforeWaiting = YES;
}
break;
default:
break;
}
});
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
CFRelease(observer);
}
//打開子線程的RunLoop對象
- (void)openRunLoop {
@autoreleasepool {
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSPort port] forMode:NSRunLoopCommonModes];
[runLoop run];
}
}
//添加定時器到子線程的RunLoop中
- (void)startMonitorTimer {
NSTimer *timer = [NSTimer timerWithTimeInterval:0.5*lagTimeInterval repeats:YES block:^(NSTimer * _Nonnull timer) {
//如果_isBeforeWaiting狀態為YES,表示主線程RunLoop即將進入休眠
if(!_isBeforeWaiting) {
//獲取當前時間與記錄時間的差值
NSTimeInterval timeInterval = [[NSDate date] timeIntervalSinceDate:_beforeSource0Time];
//如果大於卡頓時間,則打印出來
if(timeInterval >= lagTimeInterval) {
NSLog(@"##############卡了");
[self logStack];
} else {
NSLog(@"##############沒卡");
}
}
}];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
}
- (void)logStack {
NSLog(@"%@", [NSThread callStackSymbols]);
}
@end

相關文章

WebRTC點對點通訊架構設計

angular髒檢查原理及偽代碼實現

區塊鏈不談技術的都是韭菜——區塊鏈技術組成及架構

iOS開發小記網絡篇(持續更新)