theme: smartblue
前言
前文提要 Thread也会OOM吗?
之前和大家聊过一次pthread oom
问题。基于当时的场景以及对Rxjava的分析,只能说解决了一小部分问题。但是实际上只要我们滥用了线程,特别是华为设备,还是有可能发生对应的问题的。
所以这次打算再展开下,顺便把自己最近做的一些这方面相关的给大家做一次简单的分享。
这一次我们从两方面入手,看看能不能有效的解决这部分问题。
- 通过
debug
工具hook所有DefaultThreadFactory
创建的无名线程 - 通过
plugin asm
进行线程池替换,把违法乱纪人员逮捕起来
正文
如果你的线上已经开始出现了这部分问题,从哪里开始下手其实是非常头疼的,因为光从线上的堆栈上来看,你很难分析出问题,同时因为是偶发线上,所以也没办法稳定复现这部分问题。
插句嘴,这篇文章没法帮你解决native端的线程溢出问题
这种对对开发来说,就是一个非常棘手的问题了。
我的看法是先能把当前的未命名的线程池都抓出来,然后将每个线程池都进行命名,这样当我们再次碰到类似的问题的时候就可以通过线程名来计数,看看谁是开启线程最多的人。之后看看这群大佬能不能优化下自己的代码。
Epic Hook
我在线上通过bugly排查过线程oom问题,这种问题并不能孤立起来看,最后一个堆栈只是压死骆驼的最后一根稻草而已。我看了下其他相邻线程的情况,并罗列了下发现其中有很多pool-x-thread-x
这种相关的,这些就是默认的线程池构造中的ThreadFactory
导致的创建的线程。
之前在iocanary
文章内和大家介绍过一部分过于动态hook的能力,我们这次的调试工具也是基于Epic,当然和xhook
还是有点差别的。
Android IO监控 | 性能监控系列
Epic提供了hook构造函数和方法的能力,这里我们主要要用的就是hook函数构造。DexposedBridge.hookAllConstructors
也就是这个方法了。
DexposedBridge.hookAllConstructors(Executors.defaultThreadFactory().javaClass, object : XC_MethodHook() {
@Throws(Throwable::class)
override fun beforeHookedMethod(param: MethodHookParam) {
super.beforeHookedMethod(param)
}
})
我们hook
的目标是Executors.defaultThreadFactory().javaClass
的构造函数。通过DexposedBridge.hookAllConstructors
方法,我们就可以获取到所有需要hook的class的构造函数调用。
因为
DefaultThreadFactory
的构造函数是私有的,所以比较麻烦
然后我们需要做的是什么呢?如果能获取到构造函数调用前的堆栈,是不是就很完美了。但是如何获取到堆栈呢,我第一时间想到的就是抛出一个异常打印。那么堆栈是在哪里被持有的呢????
代码语言:javascript复制private static List<StackTraceElement> stackTraceInCurrentThread() {
return newArrayList(Thread.currentThread().getStackTrace());
}
这部分我们只要跟踪下异常的打印函数其实就能知道个大概了。这个是在Throwables
里面获取到的。从这里我们其实可以看出来,堆栈信息是保存在线程上的。
代码语言:javascript复制这么说起来线程被作为gcroot就可以理解了。因为虚拟机持有了所有存活的线程实例和堆栈。
DexposedBridge.hookAllConstructors(Executors.defaultThreadFactory().javaClass, object : XC_MethodHook() {
@Throws(Throwable::class)
override fun beforeHookedMethod(param: MethodHookParam) {
super.beforeHookedMethod(param)
val thread = Thread.currentThread()
val stackTraceElements = thread.stackTrace
if (checkLegalStack(stackTraceElements)) {
instance.addStack(stackTraceElements)
Log.i(TAG, "stack: ${stackTraceElements.toString()}")
}
}
})
这里我们先hook了ThreadFactory
,然后将这部分没有命名的线程池的调用堆栈记录下来,之后将堆栈信息写入文件。
然后让测试同学配合执行monkey,之后我们只要导出这份文件,就可以把当前项目内违规使用的线程池给罗列出来。
之后我们只需要将这部分无命名的线程池更换有命名规则的线程池,那么之后线上就能把这部分干扰我们排查问题的帮凶给搞定了。
魔改三方sdk
github demo 链接 ,虽然我已经看透了大家白嫖的本质了
当我们执行完上述方法之后,我们基本可以确保我们自己可控范围内所有ThreadFactory
都已经被我们修改完了。但是这个时候如果这部分线程池构造是第三方sdk内的呢?如何将这部分不讲武德的三方sdk的线程池构造也调整了呢?
这次也还是使用asm吧,之前我们在使用asm的时候大部分场景都是采取新增一个函数调用的方式。这次我们将采取类替换的规则。
简单的说,我们会进行类扫描,当发现当前行执行的是线程池构造的init函数的时候,将其替换成我们安全合法的线程池构造。这样我们就能对第三方sdk的代码进行修正了。
这部分代码我用ClassVisitor
完全写不出来,还是要依靠ClassNode
了。
class ThreadAsmHelper : AsmHelper {
@Throws(IOException::class)
override fun modifyClass(srcClass: ByteArray?): ByteArray {
val classNode = ClassNode(Opcodes.ASM5)
val classReader = ClassReader(srcClass)
//1 将读入的字节转为classNode
classReader.accept(classNode, 0)
//2 对classNode的处理逻辑
val iterator: Iterator<MethodNode> = classNode.methods.iterator()
while (iterator.hasNext()) {
val method = iterator.next()
method.instructions?.iterator()?.forEach {
if (it.opcode == Opcodes.INVOKESTATIC) {
if (it is MethodInsnNode) {
it.hookExecutors()
}
}
}
}
val classWriter = ClassWriter(0)
//3 将classNode转为字节数组
classNode.accept(classWriter)
return classWriter.toByteArray()
}
private fun MethodInsnNode.hookExecutors() {
when (this.owner) {
EXECUTORS_OWNER -> {
info("owner:${this.owner} name:${this.name} ")
ThreadPoolCreator.poolList.forEach {
if (it.name == this.name && this.name == it.name && this.owner == it.owner) {
this.owner = Owner
this.name = it.methodName
this.desc = it.replaceDesc()
info("owner:${this.owner} name:${this.name} desc:${this.desc} ")
}
}
}
}
}
}
首先我们简单的分析下,线程池的默认构造都是基于Executors
的静态方法。那么从bytecode上来说,我们能确定第一个操作符必然是INVOKESTATIC
。
这部分就是所有基于asm扫描插入的代码了。其实逻辑非常的简单哦,首先获取到ClassNode
,然后遍历所有的方法,然后开始逐行读取,之后判断操作符是否符合INVOKESTATIC
,之后我们只要判断Desc,methodName,name,owner这几个是否符合我们所需要魔改的规则,如果是的话则对其进行替换。这样就完成了这部分功能了。
具体的代码大家可以去看下我写的AndroidAutoTrack
,里面有这部分代码的操作。
接入Apm系统
这部分内容现在停留在我的设想中,并没有投入实际的开发中,有时间我应该会尝试下这部分能力。
Apm作为一个app性能分析工具,主要的目的是辅助开发童靴快速定位问题,同时能帮助大家优化代码。
我个人感觉这部分也完全能作为APM的一小部分能力。我们可以基于Activity
维度收集页面当前的线程总数。
另外我们需要做的就是设置几个阈值,当线程数到达低危,中危,高危之后进行线程名上报的操作。
同时因为收集了Activity
维度的线程数据,我们就可以根据页面的状况进行评估,看看是不是哪个特定页面的操作问题,导致线程数量直线上升。
说句题外话,之前面阿里的时候被问过这样一道APM的阈值设计题,当时的告警设计我就完全没考虑到啊,菜狗虾正式在下。
总结
我觉得开发每过一段时间就要对之前你做的事情进行一次总结。比方说之前我们做的是不是够好了,还有没有优化的空间,同时有没有可能有技术手段进行监控,防范于未然。
之前听一位大佬说回头看看你三个月之前写的代码,如果你觉得之前写的很完美,证明你划了三个月的水。