前言
TT语音每次启动app到进入首页就会创建240个左右的线程,其中有接近一半是获取SharePreferences对象所创建的线程,这些线程很多都是一次性的,用完后没有立即销毁,这样会占用内存,而且,频繁的创建线程,可能会导致线程数过多造成OOM,因此,需要找到可优化的空间,然后尽可能优化这些线程,但是,如果要找到优化的空间,就需要了解整个app的线程使用情况,监听到哪个页面,哪行代码,创建了哪个线程,才能给我们的优化带来准确的方向
监控方案
通过AOP运行时插桩的方式,这里借助了epic,监听APP中所有线程的创建,可以监听到哪个页面,哪行代码,创建了那个线程,然后通过日志方式打印创建的线程信息,代码如下
class ThreadCheck : XC_MethodHook() {
var isCanAppendLog = false
override fun afterHookedMethod(param: MethodHookParam?) {
super.afterHookedMethod(param)
if (param?.thisObject is Thread) {
val thread = param.thisObject as Thread
val es = Thread.currentThread().stackTrace
val normalInfo = StringBuilder(" \nThreadTrace:")
.append("\nthreadName:${thread.name}")
.append("\n====================================threadTraceStart=======================================")
for (e in es) {
if (e.className == "dalvik.system.VMStack" && e.methodName == "getThreadStackTrace") {
isCanAppendLog = false
}
if (e.className == "me.weishu.epic.art.entry.Entry" && e.methodName == "referenceBridge") {
isCanAppendLog = true
} else {
if (isCanAppendLog) {
normalInfo.append("\n${e.className}(methodName:${e.methodName},lineNumber:${e.lineNumber})")
}
}
}
normalInfo.append("\n=====================================threadTraceEnd=======================================")
Log.i(tag, normalInfo.toString())
}
}
companion object {
const val tag = "====>ThreadCheck"
fun hook() {
if (!BuildConfig.DEBUG) {
return
}
DexposedBridge.hookAllConstructors(Thread::class.java, ThreadCheck())
}
}
}
日志打印格式
从上图结合下图可以看出在下面的类创建了AsyncTask,而且使用完后没有立即销毁,造成了内存的浪费,而且,频繁的创建线程可能会导致APP因为线程数量过多造成OOM
分析
如上图所示,TT语音在android8.0以上的机型上出现的大量这种类似的OOM,这是因为线程数量过多或者虚拟内存不足造成的,创建线程是需要消耗虚拟内存的,不同机型允许的最大线程数量是不一样的,例如在华为的部分机型上,这个上限被修改的很低(大约500),需要注意的是,APP的线程总数=工作中的线程数+sleep状态的线程数,所以这些手机容易出现线程数溢出的问题,而TT语音一进入首页就会大概创建240个左右的线程,虽然这里面有很多线程都是很快就工作结束的,然后变成无用线程的,也就是说这些工作结束后的线程并不会计入APP的线程总数中,但是,频繁的创建线程依然可能会导致上图中的OOM,因为尽管他们工作时间很短,但是依然可能是压垮骆驼的最后一根稻草,而且,频繁的创建线程,会造成虚拟内存的消耗,加大OOM的可能性,因此,尽量减少线程的创建是线程优化的关键一步,然后,开始统计线程使用情况,情况如下:
线程使用情况
1. 代码中使用 new Thread或者new AsyncTask或者new HandlerThread创建的线程,例如上图中创建了AsyncTask
2. 获取SharePreferences对象的时候创建的线程,每次获取SharePreferences对象的时候都会重新创建线程,原因是我们获取SharePreferences对象的模式是多进程模式,这个情况下每次获取SharePreferences对象的时候都会创建新线程
3. 代码中协程创建的线程
4. 第三方sdk创建的线程以及获取SharePreference对象创建的线程
5. 代码中使用线程池创建的线程
6. Maven依赖库创建的线程
优化方式
- 获取SharePreferences对象的时候考虑数据是否在多个进程中使用,如果不是,将模式改成MODE_PRIVATE,这样就不会每次获取SharePreferences对象的时候都重新创建线程
- 对于代码中使用new Thread或者new AsyncTask或者new HandlerThread创建的线程,建议改成使用线程池或者协程来复用线程
QQ交流群
770892444