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…
相关文章
相关标签/搜索