作者:哈雷哈雷_Wong

今天要介紹的RunLoop使用場景很有意思,在做長期項目,需要跟蹤解決用戶問題非常有用。使用RunLoop 監測主線程的卡頓,並將卡頓時的線程堆棧信息保存下來,下次上傳到服務器。

原理

官方文檔說明了RunLoop的執行順序:

1. Notify observers that the run loop has been entered.

2. Notify observers that any ready timers are about to fire.

3. Notify observers that any input sources that are not port based are about to fire.

4. Fire any non-port-based input sources that are ready to fire.

5. If a port-based input source is ready and waiting to fire, process the event immediately. Go to step 9.

6. Notify observers that the thread is about to sleep.

7. Put the thread to sleep until one of the following events occurs:

* An event arrives for a port-based input source.

* A timer fires.

* The timeout value set for the run loop expires.

* The run loop is explicitly woken up.

8. Notify observers that the thread just woke up.

9. Process the pending event.

* If a user-defined timer fired, process the timer event and restart the loop. Go to step 2.

* If an input source fired, deliver the event.

* If the run loop was explicitly woken up but has not yet timed out, restart the loop. Go to step 2.

10. Notify observers that the run loop has exited.


用僞代碼來實現就是這樣的:

{

/// 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);

}


主線程的RunLoop是在應用啓動時自動開啓的,也沒有超時時間,所以正常情況下,主線程的RunLoop 只會在 2---9 之間無限循環下去。

那麼,我們只需要在主線程的RunLoop中添加一個observer,檢測從 kCFRunLoopBeforeSources 到  kCFRunLoopBeforeWaiting 花費的時間 是否過長。

如果花費的時間大於某一個闕值,我們就認爲有卡頓,並把當前的線程堆棧轉儲到文件中,並在以後某個合適的時間,將卡頓信息文件上傳到服務器。

實現步驟

在看了上面的兩個監測卡頓的示例Demo後,我按照上面講述的思路寫了一個Demo,應該更容易理解吧。第一步,創建一個子線程,在線程啓動時,啓動其RunLoop。

+ (instancetype)shareMonitor

{

static dispatch_once_t onceToken;

dispatch_once(&onceToken, ^{

instance = [[[self class] alloc] init];

instance.monitorThread = [[NSThread alloc] initWithTarget:self selector:@selector(monitorThreadEntryPoint) object:nil];

[instance.monitorThread start];

});

return instance;

}


+ (void)monitorThreadEntryPoint

{

@autoreleasepool {

[[NSThread currentThread] setName:@"FluencyMonitor"];

NSRunLoop *runLoop = [NSRunLoop currentRunLoop];

[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];

[runLoop run];

}

}


第二步,在開始監測時,往主線程的RunLoop中添加一個observer,並往子線程中添加一個定時器,每0.5秒檢測一次耗時的時長。

- (void)start

{

if (_observer) {

return;

}

// 1.創建observer

CFRunLoopObserverContext context = {0,(__bridge void*)self, NULL, NULL, NULL};

_observer = CFRunLoopObserverCreate(kCFAllocatorDefault,

kCFRunLoopAllActivities,

YES,

0,

&runLoopObserverCallBack,

&context);

// 2.將observer添加到主線程的RunLoop中

CFRunLoopAddObserver(CFRunLoopGetMain(), _observer, kCFRunLoopCommonModes);

// 3.創建一個timer,並添加到子線程的RunLoop中

[self performSelector:@selector(addTimerToMonitorThread) onThread:self.monitorThread withObject:nil waitUntilDone:NO modes:@[NSRunLoopCommonModes]];

}


- (void)addTimerToMonitorThread

{

if (_timer) {

return;

}

// 創建一個timer

CFRunLoopRef currentRunLoop = CFRunLoopGetCurrent();

CFRunLoopTimerContext context = {0, (__bridge void*)self, NULL, NULL, NULL};

_timer = CFRunLoopTimerCreate(kCFAllocatorDefault, 0.1, 0.01, 0, 0,

&runLoopTimerCallBack, &context);

// 添加到子線程的RunLoop中

CFRunLoopAddTimer(currentRunLoop, _timer, kCFRunLoopCommonModes);

}



第三步,補充觀察者回調處理

static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info){

FluencyMonitor *monitor = (__bridge FluencyMonitor*)info;

NSLog(@"MainRunLoop---%@",[NSThread currentThread]);

switch (activity) {

case kCFRunLoopEntry:

NSLog(@"kCFRunLoopEntry");

break;

case kCFRunLoopBeforeTimers:

NSLog(@"kCFRunLoopBeforeTimers");

break;

case kCFRunLoopBeforeSources:

NSLog(@"kCFRunLoopBeforeSources");

monitor.startDate = [NSDate date];

monitor.excuting = YES;

break;

case kCFRunLoopBeforeWaiting:

NSLog(@"kCFRunLoopBeforeWaiting");

monitor.excuting = NO;

break;

case kCFRunLoopAfterWaiting:

NSLog(@"kCFRunLoopAfterWaiting");

break;

case kCFRunLoopExit:

NSLog(@"kCFRunLoopExit");

break;

default:

break;

}

}


從打印信息來看,RunLoop進入睡眠狀態的時間可能會非常短,有時候只有1毫秒,有時候甚至1毫秒都不到,靜止不動時,則會長時間進入睡覺狀態。

因爲主線程中的block、交互事件、以及其他任務都是在 kCFRunLoopBeforeSources kCFRunLoopBeforeWaiting 之前執行,所以我在即將開始執行Sources 時,記錄一下時間,並把正在執行任務的標記置爲YES,將要進入睡眠狀態時,將正在執行任務的標記置爲NO。

第四步,補充timer 的回調處理

static void runLoopTimerCallBack(CFRunLoopTimerRef timer, void *info)

{

FluencyMonitor *monitor = (__bridge FluencyMonitor*)info;

if (!monitor.excuting) {

return;

}

// 如果主線程正在執行任務,並且這一次loop 執行到 現在還沒執行完,那就需要計算時間差

NSTimeInterval excuteTime = [[NSDate date] timeIntervalSinceDate:monitor.startDate];

NSLog(@"定時器---%@",[NSThread currentThread]);

NSLog(@"主線程執行了---%f秒",excuteTime);

if (excuteTime >= 0.01) {

NSLog(@"線程卡頓了%f秒",excuteTime);

[monitor handleStackInfo];

}

}


timer 每 0.01秒執行一次,如果當前正在執行任務的狀態爲YES,並且從開始執行到現在的時間大於闕值,則把堆棧信息保存下來,便於後面處理。

爲了能夠捕獲到堆棧信息,我把timer的間隔調的很小(0.01),而評定爲卡頓的闕值也調的很小(0.01)。實際使用時這兩個值應該是比較大,timer間隔爲1s,卡頓闕值爲2s即可。

2016-12-15 08:56:39.921 RunLoopDemo03[957:16300] lag happen, detail below:

Incident Identifier: 68BAB24C-3224-46C8-89BF-F9AABA2E3530

CrashReporter Key: TODO

Hardware Model: x86_64

Process: RunLoopDemo03 [957]

Path: /Users/harvey/Library/Developer/CoreSimulator/Devices/6ED39DBB-9F69-4ACB-9CE3-E6EB56BBFECE/data/Containers/Bundle/Application/5A94DEFE-4E2E-4D23-9F69-7B1954B2C960/RunLoopDemo03.app/RunLoopDemo03

Identifier: com.Haley.RunLoopDemo03

Version: 1.0 (1)

Code Type: X86-64

Parent Process: debugserver [958]


Date/Time: 2016-12-15 00:56:38 +0000

OS Version: Mac OS X 10.1 (16A323)

Report Version: 104


Exception Type: SIGTRAP

Exception Codes: TRAP_TRACE at 0x1063da728

Crashed Thread: 4


Thread 0:

0 libsystem_kernel.dylib 0x000000010a14341a mach_msg_trap + 10

1 CoreFoundation 0x0000000106f1e7b4 __CFRunLoopServiceMachPort + 212

2 CoreFoundation 0x0000000106f1dc31 __CFRunLoopRun + 1345

3 CoreFoundation 0x0000000106f1d494 CFRunLoopRunSpecific + 420

4 GraphicsServices 0x000000010ad8aa6f GSEventRunModal + 161

5 UIKit 0x00000001073b7964 UIApplicationMain + 159

6 RunLoopDemo03 0x00000001063dbf8f main + 111

7 libdyld.dylib 0x0000000109d7468d start + 1


Thread 1:

0 libsystem_kernel.dylib 0x000000010a14be5e kevent_qos + 10

1 libdispatch.dylib 0x0000000109d13074 _dispatch_mgr_invoke + 248

2 libdispatch.dylib 0x0000000109d12e76 _dispatch_mgr_init + 0


Thread 2:

0 libsystem_kernel.dylib 0x000000010a14b4e6 __workq_kernreturn + 10

1 libsystem_pthread.dylib 0x000000010a16e221 start_wqthread + 13


Thread 3:

0 libsystem_kernel.dylib 0x000000010a14341a mach_msg_trap + 10

1 CoreFoundation 0x0000000106f1e7b4 __CFRunLoopServiceMachPort + 212

2 CoreFoundation 0x0000000106f1dc31 __CFRunLoopRun + 1345

3 CoreFoundation 0x0000000106f1d494 CFRunLoopRunSpecific + 420

4 Foundation 0x00000001064d7ff0 -[NSRunLoop runMode:beforeDate:] + 274

5 Foundation 0x000000010655f991 -[NSRunLoop runUntilDate:] + 78

6 UIKit 0x0000000107e3d539 -[UIEventFetcher threadMain] + 118

7 Foundation 0x00000001064e7ee4 __NSThread__start__ + 1243

8 libsystem_pthread.dylib 0x000000010a16eabb _pthread_body + 180

9 libsystem_pthread.dylib 0x000000010a16ea07 _pthread_body + 0

10 libsystem_pthread.dylib 0x000000010a16e231 thread_start + 13


Thread 4 Crashed:

0 RunLoopDemo03 0x00000001063dfae5 -[PLCrashReporter generateLiveReportWithThread:error:] + 632

1 RunLoopDemo03 0x00000001063da728 -[FluencyMonitor handleStackInfo] + 152

2 RunLoopDemo03 0x00000001063da2cf runLoopTimerCallBack + 351

3 CoreFoundation 0x0000000106f26964 __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__ + 20

4 CoreFoundation 0x0000000106f265f3 __CFRunLoopDoTimer + 1075

5 CoreFoundation 0x0000000106f2617a __CFRunLoopDoTimers + 250

6 CoreFoundation 0x0000000106f1df01 __CFRunLoopRun + 2065

7 CoreFoundation 0x0000000106f1d494 CFRunLoopRunSpecific + 420

8 Foundation 0x00000001064d7ff0 -[NSRunLoop runMode:beforeDate:] + 274

9 Foundation 0x00000001064d7ecb -[NSRunLoop run] + 76

10 RunLoopDemo03 0x00000001063d9cbd +[FluencyMonitor monitorThreadEntryPoint] + 253

11 Foundation 0x00000001064e7ee4 __NSThread__start__ + 1243

12 libsystem_pthread.dylib 0x000000010a16eabb _pthread_body + 180

13 libsystem_pthread.dylib 0x000000010a16ea07 _pthread_body + 0

14 libsystem_pthread.dylib 0x000000010a16e231 thread_start + 13


Thread 4 crashed with X86-64 Thread State:

rip: 0x00000001063dfae5 rbp: 0x000070000f53fc50 rsp: 0x000070000f53f9c0 rax: 0x000070000f53fa20

rbx: 0x000070000f53fb60 rcx: 0x0000000000005e0b rdx: 0x0000000000000000 rdi: 0x00000001063dfc6a

rsi: 0x000070000f53f9f0 r8: 0x0000000000000014 r9: 0xffffffffffffffec r10: 0x000000010a1433f6

r11: 0x0000000000000246 r12: 0x000060800016b580 r13: 0x0000000000000000 r14: 0x0000000000000006

r15: 0x000070000f53fa40 rflags: 0x0000000000000206 cs: 0x000000000000002b fs: 0x0000000000000000

gs: 0x0000000000000000



剩下的工作就是將字符串保存進文件,以及上傳到服務器了。

我們不能將卡頓的闕值定的太小,也不能將所有的卡頓信息都上傳,原因有兩點,一,太浪費用戶流量;二、文件太多,App內存儲和上傳後服務器端保存都會佔用空間。

可以參考微信的做法,7天以上的文件刪除,隨機抽取上傳,並且上傳前對文件進行壓縮處理等。

Demo 鏈接

https://github.com/Haley-Wong/RunLoopDemos

如果感覺這篇文章不錯可以點擊在看:point_down:

相關文章