iOS線程數量監控工具

簡單卻強大的線程監控工具 KKThreadMonitor :當線程過多或瞬間建立大量子線程(線程爆炸),控制檯就打印出全部的線程堆棧。便於分析形成子線程過多或線程爆炸的緣由。git

/******* 線程爆炸,控制檯打印以下: ********/

🔥💥💥💥💥💥一秒鐘開啓 28 條線程!💥💥💥💥💥🔥
👇👇👇👇👇👇👇堆棧👇👇👇👇👇👇👇
2020-04-12 12:36:29.270755+0800 BaiduIphoneVideo[55732:6928996] 
當前線程數量:43
callStack of thread: 9219
libsystem_kernel.dylib         0x18022cc38 semaphore_wait_trap  +  8
libdispatch.dylib              0x00003928 _dispatch_sema4_wait  +  28
BaiduIphoneVideo               0x101285be0 -[TDXXXX脫敏🤣處理(去掉真實類名)XXXXager sendRequestWithBody:withURL:flag:]  +  440
BaiduIphoneVideo               0x10128535c __29-[TDAXXXX脫敏🤣處理(去掉真實類名)XXXXnager sendMessage]_block_invoke  +  804
libdispatch.dylib              0x00001dfc _dispatch_call_block_and_release  +  32

... //省略中間線程堆棧

callStack of thread: 8707
libsystem_kernel.dylib         0x18022cc38 semaphore_wait_trap  +  8
libdispatch.dylib              0x00003928 _dispatch_sema4_wait  +  28
BaiduIphoneVideo               0x1026d2cd4 +[BaiduMobStatDeviceInfo testDeviceId]  +  56
BaiduIphoneVideo               0x1026ca8a4 -[BaiduMobStatLogManager checkHeaderBeforeSendLog:]  +  3384
BaiduIphoneVideo               0x1026cefdc -[BaiduMobStatLogManager _syncSendAllLog]  +  536
BaiduIphoneVideo               0x1026c6398 __33-[BaiduMobStatController sendLog]_block_invoke  +  56
libdispatch.dylib              0x00001dfc _dispatch_call_block_and_release  +  32

... //省略中間線程堆棧

callStack of thread: 80387
libsystem_kernel.dylib         0x18024ea60 __open_nocancel  +  8
libsystem_kernel.dylib         0x1802356e0 open$NOCANCEL  +  20
BaiduIphoneVideo               0x101dcd488 -[XXXX脫敏🤣處理(去掉真實類名)XXXX recursiveCalculateAtPath:isAbnormal:isOutdated:needCheckIgnorePath:]  +  1360
BaiduIphoneVideo               0x101dcd488 -[XXXX脫敏🤣處理(去掉真實類名)XXXX recursiveCalculateAtPath:isAbnormal:isOutdated:needCheckIgnorePath:]  +  1360
BaiduIphoneVideo               0x101dcd488 -[XXXX脫敏🤣處理(去掉真實類名)XXXX recursiveCalculateAtPath:isAbnormal:isOutdated:needCheckIgnorePath:]  +  1360
BaiduIphoneVideo               0x101dccd90 -[XXXX脫敏🤣處理(去掉真實類名)XXXX initWithOutdatedDays:abnormalFolderSize:abnormalFolderFileNumber:ignoreRelativePathes:checkSparseFile:sparseFileLeastDifferPercentage:sparseFileLeastDifferSize:visitors:]  +  576
BaiduIphoneVideo               0x101dcd274 +[XXXX脫敏🤣處理(去掉真實類名)XXXX folderSizeAtPath:]  +  72
BaiduIphoneVideo               0x101de3900 __56-[HMDToBCrashTracker(Environment) environmentInfoUpdate]_block_invoke_2  +  88
libdispatch.dylib              0x00001dfc _dispatch_call_block_and_release  +  32

👆👆👆👆👆👆👆堆棧👆👆👆👆👆👆👆
複製代碼

開發原由

有必定iOS開發基礎的,應該都知道不合理建立線程,是會形成性能問題的。而業務的積累和迭代過程,可能就不可避免形成子線程過多或線程爆炸等問題。因此咱們須要一個工具來監控子線程過多或線程爆炸等問題。網上沒有找到這樣的工具,只好本身動手了~github

不合理建立/使用線程形成的問題

合理使用多線程,將耗時操做放到子線程,能夠提升App的流暢。可是若是不加控制建立子線程,形成線程爆炸,會形成性能問題。不合理使用線程有以下問題:安全

  1. 建立子線程過多,是會形成性能問題的,由於建立線程須要佔用內存空間(默認的狀況下,主線程佔1M,子線程佔用512KB)。
  2. 不合理建立和使用線程,容易引起數據一致性(線程安全)和死鎖問題。

因此咱們要合理使用線程,將整個App的子線程數量控制在合理的範圍內。如何合理使用,不是本文的討論範圍;本文介紹如何發現整個App線程數量過多和線程爆炸問題。bash

核心邏輯

具體實現邏輯不到100行,很是簡單。 你們能夠下載代碼 KKThreadMonitor 研究。我講下核心邏輯:多線程

如何獲取線程數量

task_threads能夠獲取到整個App的線程數量。ide

kern_return_t task_threads
(
	task_inspect_t target_task,
	thread_act_array_t *act_list,
	mach_msg_type_number_t *act_listCnt
);
複製代碼

最開始,我是想經過計時器定時調用task_threads函數,來獲取線程數量和增加速度。確實能夠經過這個方式來作,可是間隔多久調用(涉及精度),大量調用這個函數的性能問題等,都代表這不是一個好的方案。函數

很明顯,若是咱們知道了線程的建立函數跟銷燬函數,經過hook這兩個函數,是否是就能夠實時獲取到線程數量了。幸運的是,系統提供了這個函數:工具

//在#include <pthread/introspection.h>文件裏

/**
定義函數指針:pthread_introspection_hook_t
event  : 線程處於的生命週期(下面枚舉了線程的4個生命週期)
thread :線程
addr   :線程棧內存基址
size   :線程棧內存可用大小
*/
typedef void (*pthread_introspection_hook_t)(unsigned int event,
		pthread_t thread, void *addr, size_t size);
		
enum {
	PTHREAD_INTROSPECTION_THREAD_CREATE = 1, //建立線程
	PTHREAD_INTROSPECTION_THREAD_START, // 線程開始運行
	PTHREAD_INTROSPECTION_THREAD_TERMINATE,  //線程運行終止
	PTHREAD_INTROSPECTION_THREAD_DESTROY, //銷燬線程
};

/**
看這個函數名,很像咱們平時hook函數同樣的。
返回值是上面聲明的pthread_introspection_hook_t函數指針:返回原線程生命週期函數。
參數也是函數指針:傳入的是咱們自定義的線程生命週期函數
*/
__attribute__((__nonnull__, __warn_unused_result__))
extern pthread_introspection_hook_t
pthread_introspection_hook_install(pthread_introspection_hook_t hook);
複製代碼

因此咱們能夠經過"hook"線程生命週期函數,來得到線程的新建跟銷燬。post

用鎖保證線程安全

調用startMonitor函數,開始監控線程數量。在這個函數裏用global_semaphore來保證,task_threads獲取的線程數量,到hook完成,線程數量不會變化(加解鎖之間,沒有線程新建跟銷燬)。性能

+ (void)startMonitor
{
    global_semaphore = dispatch_semaphore_create(1);
    dispatch_semaphore_wait(global_semaphore, DISPATCH_TIME_FOREVER);
    mach_msg_type_number_t count;
    thread_act_array_t threads;
    task_threads(mach_task_self(), &threads, &count);
    threadCount = count; //加解鎖之間,保證線程的數量不變
    old_pthread_introspection_hook_t = pthread_introspection_hook_install(kk_pthread_introspection_hook_t);
    dispatch_semaphore_signal(global_semaphore);
    ...
}
複製代碼

經過線程狀態改變,來改變線程數量

//定時器每一秒都將線程增加數置0
+ (void)clearThreadCountIncrease
{
    threadCountIncrease = 0;
}


void kk_pthread_introspection_hook_t(unsigned int event,
pthread_t thread, void *addr, size_t size)
{
    ...
    //建立線程,則線程數量和線程增加數都加1
    if (event == PTHREAD_INTROSPECTION_THREAD_CREATE) {
        threadCount = threadCount + 1;
        ...
        threadCountIncrease = threadCountIncrease + 1;
        ...
    }
    //銷燬線程,則線程數量和線程增加數都加1
    else if (event == PTHREAD_INTROSPECTION_THREAD_DESTROY){
        threadCount = threadCount - 1;
        ...
        if (threadCountIncrease > 0) {
            threadCountIncrease = threadCountIncrease - 1;
        }
    }
}
複製代碼

打印全部線程堆棧

這裏我是用本身另一個庫KKCallStack

如何打印線程堆棧,保留調用現場的博客不少,原理都是:回溯棧幀,得到函數調用地址,而後經過解析MachO文件得到函數名。具體過程能夠參考其它博客~

參考

  1. github.com/kstenerud/K…
  2. wereadteam.github.io/2016/05/03/…
  3. juejin.im/post/5cce3a…
相關文章
相關標籤/搜索