本文作者
作者:Pika
链接:
https://juejin.cn/post/7143944351016550437
本文由作者授权发布。
在我们日常开发中,多线程管理一直是非常头疼的问题之一,尤其在历史性长,结构复杂的app中,线程数会达到好几百个甚至更多,然而过多的线程不仅仅带来了内存上的消耗同时也降低了cpu调度的效率,过多的cpu调度带来的消耗的坏处甚至超过了多线程带来的好处。
某个场景会创造过多的线程,最终导致oom。 线程池过多问题,比如三方库有一套线程池,自己项目也有一套线程池,随着三方/二方业务接入,导致了不相兼容的线程池数越多,降低了全体线程池数的调度效率,比如多个okhttp的调用。 历史原因导致,new Thread横行,又或者是各种线程使用不规范,导致工程混乱。 即使是空闲时候,依旧有线程在不断Waiting。 各种线程死锁问题。
最终种种原因导致,我们的项目在上线过程中,会遇到各种线程不明的情况,对排查问题或者解决问题带来极大的考验。
线程监控
当前线程统计
Thread.getAllStackTraces()
Map<Thread, StackTraceElement[]>
[Thread[Binder:30506_2,5,main], Thread[FinalizerWatchdogDaemon,5,system], Thread[Binder:30506_3,5,main], Thread[Jit thread pool worker thread 0,5,system], Thread[ReferenceQueueDaemon,5,system], Thread[Profile Saver,5,system], Thread[main,5,main], Thread[Binder:30506_1,5,main], Thread[RenderThread,7,main], Thread[pika_thread,5,main], Thread[vivo.PerfThread,5,main], Thread[Signal Catcher,10,system], Thread[FinalizerDaemon,5,system], Thread[HeapTaskDaemon,5,system]]
Thread.getAllStackTraces().keys.map {
it.name
}
线程信息具体化
method.instructions.insertBefore(
node,
new LdcInsnNode(klass.name)
)
def r = node.desc.lastIndexOf(')')
把构造函数描述变成了带有string name的构造函数描述
def desc =
"${node.desc.substring(0, r)}Ljava/lang/String;${node.desc.substring(r)}"
println(" * ${node.owner}.${node.name}${node.desc} => ${node.owner}.${node.name}$desc: ${klass.name}.${method.name}${method.desc}")
node.desc = desc
当然,Thread还有很多构造函数,我们就不一一举例子去适配,相关的操作也是类似的,涉及到Executors等其他创建线程的方式,我们也可以通过这种指令替换的方式去进行Thread的命名操作。这里就不再赘述,可以参考booster 的做法。
https://github.com/didi/booster/tree/master/booster-transform-thread
线程统一
Thread创建
var thread = Thread{
Log.i("hello","this is my thread ${Thread.currentThread().name}")
}
NEW java/lang/Thread
DUP
INVOKEDYNAMIC run()Ljava/lang/Runnable; [
// handle kind 0x6 : INVOKESTATIC
java/lang/invoke/LambdaMetafactory.metafactory(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
// arguments:
()V,
// handle kind 0x6 : INVOKESTATIC
com/example/spider/MainActivity.onCreate$lambda-0()V,
()V
]
INVOKESPECIAL java/lang/Thread.<init> (Ljava/lang/Runnable;)V
ASTORE 2
NEW 创建一个java/lang/Thread对象,此时只是引用被创建,所引用的对象还没有创建,并加入操作数栈顶部。
class MyThread(private val runnable: Runnable) : Thread(runnable) {
// 调用到自己的start
override fun start() {
Log.i("hello", "MyThread")
// runnable 在定义的统一线程池执行
ThreadHelper.runInCustomPool(runnable)
}
}
class MyThreadHookUtils {
static THREAD = "java/lang/Thread"
static void transform(ClassNode klass) {
// 我们自定义的MyThread类不需要参加转化
if (klass.name.equals("com/example/spider/MyThread")) {
return
}
klass.methods?.forEach { methodNode ->
methodNode.instructions.each {
if (it.opcode == Opcodes.INVOKESPECIAL) {
transformInvokeSpecial((MethodInsnNode) it, klass, methodNode)
}
}
}
}
private static void transformInvokeSpecial(MethodInsnNode node, ClassNode klass, MethodNode method) {
// 如果不是构造函数,就直接退出
if (node.owner != THREAD) {
return
}
println("transformInvokeSpecial")
transformThreadInvokeSpecial(node, klass, method)
}
private static void transformThreadInvokeSpecial(
MethodInsnNode node,
ClassNode klass,
MethodNode method
) {
println("init ===> " + node.desc + " " + node.owner)
if (node.desc.equals("(Ljava/lang/Runnable;)V")) {
int index = method.instructions.indexOf(node)
def dyc = method.instructions[index - 1]
InsnList insertNodes1 = new InsnList()
TypeInsnNode newInsnNode = new TypeInsnNode(Opcodes.NEW, "com/example/spider/MyThread")
InsnNode dupNode = new InsnNode(Opcodes.DUP)
insertNodes1.add(newInsnNode)
insertNodes1.add(dupNode)
method.instructions.insertBefore(dyc, insertNodes1)
MethodInsnNode methodHookNode = new MethodInsnNode(Opcodes.INVOKESPECIAL,
"com/example/spider/MyThread",
"<init>",
"(Ljava/lang/Runnable;)V",
false)
TypeInsnNode typeInsnNode = new TypeInsnNode(Opcodes.CHECKCAST, "java/lang/Thread")
InsnList insertNodes = new InsnList()
insertNodes.add(methodHookNode)
insertNodes.add(typeInsnNode)
method.instructions.insertBefore(node, insertNodes)
method.instructions.remove(node)
println("hook ===> " + node.name + " " + node.owner + " " + method.instructions.indexOf(node))
}
}
}
注意
注意的是,这种全局Thread插桩是有风险的,在实际项目中,我们会通过白名单的方式,选择性的去统一部分Thread,因为全局统一容易导致不可预期的问题。同时还有一个非常注意的点,我们可以看到上面关于指令的代码全部是基于index的去定位各种指令集的,NEW -> DUP ->INVOKEDYNAMIC ->INVOKESPECIAL 然而在真实项目中,这个指令集顺序不一定可靠,因为可能会被插入其他指令或者无关指令,所以我们还有一步就是指令顺序的校验,必须是满足NEW -> DUP ->INVOKEDYNAMIC ->INVOKESPECIAL这几个顺序的函数指令集才进行插桩,这部分内容比较简单,就不列举了,比较INSN指令的OpCode即可,校验规则按照项目实际需要。
CoordinatorLayout驾轻就熟,不怕UI任意需求