前言
2022年,七猫免费小说客户端团队为了减少线上内存溢出情况,内存的管理与优化成为了客户端部门 2022 年的重点工作之一,我们为此投入很多的精力去收集、排查、优化,不断地在内存管理、代码管理上进行一些尝试,并且取得了不错的成果。
本片文章讲述了如何使用 java 字节码技术优化线程创建。管理单独线程也是内存优化手段之一,使用线程池替代 new Thread 能起到复用线程、减少线程创建数量的作用,从而有效地降低因线程数量超限导致内存溢出发生的概率。
继上一篇《安卓 JVM 线程监控》中,我们通过在应用运行时 hook native 代码去捕获线程创建,但此项技术只能捕获到已执行代码片段创建的线程,未被执行代码创建的线程是无法捕获的,且线下自测几乎不可能覆盖所有的代码场景,所以仍然有一部分线程没有被我们监控到。使用字节码技术管理单独线程能弥补这一缺点,两种技术结合使用可以覆盖除 native 层外的所有场景。
原理
扫描应用字节码获取到所有 new Thread 代码之后,将 new Thread 的字节码替换为线程池的字节码,就达到了管理线程创建的目的。
那么怎样将 new Thread 替换为线程池呢?我们可以创建一个 AsmHookThread 类,AsmHookThread 继承 Thread 类,在 start() 函数中使用线程池执行 run()。此时只需要再将 new Thread 替换为 new AsmHookThread 就可以了,代码如下:
import android.util.Log;
import java.util.concurrent.atomic.AtomicInteger;
public class AsmHookThread extends Thread {
public static volatile AtomicInteger id = new AtomicInteger(0);
public AsmHookThread() {
super();
}
public AsmHookThread(Runnable target) {
super(target);
}
public AsmHookThread(String name) {
super(name);
}
public AsmHookThread(ThreadGroup group, Runnable target) {
super(group, target);
}
public AsmHookThread(ThreadGroup group, String name) {
super(group, name);
}
public AsmHookThread(ThreadGroup group, Runnable target, String name) {
super(group, target, name);
}
public AsmHookThread(ThreadGroup group, Runnable target, String name, long stackSize) {
super(group, target, name, stackSize);
}
public static String generateThreadName(String name) {
return String.format("AsmHookThread-%1s-%2s", id.incrementAndGet(), name);
}
@Override
public synchronized void start() {
WorkExecutor.getInstance().execute(new AsmHookRunnable(generateThreadName(getName())));
}
class AsmHookRunnable implements Runnable {
String name;
public AsmHookRunnable(String name) {
this.name = name;
}
@Override
public void run() {
try {
AsmHookThread.this.run();
Log.d("AsmHookThread", String.format("ThreadName=%1s,runnable=%2s, run over", Thread.currentThread().getName(), name));
} catch (Exception e) {
e.printStackTrace();
Log.d("AsmHookThread", String.format("ThreadName=%1s,runnable=%2s, run exception", Thread.currentThread().getName(), name));
}
}
}
}
AsmHookThread.start() 重写了 Thread.start(),函数内使用了项目内统一的线程池 WorkExecutor 来执行 AsmHookThread.run() 。
字节码对比
完成了 AsmHookThread 的创建之后,字节码怎么替换?替换成什么样子呢?我们先对比一下 new Thread 与 new AsmHookThread 在使用不同构造函数时候的字节码:
无参创建 Thread:
有参创建 Thread:
经过观察其实我们可以发现,即使用不同的构造函数创建线程,字节码差异点也都一样,所以只要将上面图中绿线标记的字节码替换成对应的字节码即可。
NEW java/lang/Thread 替换为 NEW com/qimao/thread/hook/AsmHookThread
INVOKESPECIAL java/lang/Thread.<init> (Ljava/lang/Runnable;)V 替换为 INVOKESPECIAL com/qimao/thread/hook/AsmHookThread.<init> (Ljava/lang/Runnable;)V
INVOKEVIRTUAL java/lang/Thread.start ()V 替换为 INVOKEVIRTUAL com/qimao/thread/hook/AsmHookThread.start ()V
ASM 字节码技术
介绍完了原理,下面就介绍如何使用 ASM 字节码框架去修改字节码。
简单介绍一下ASM 的工作流程:
1. ClassReader 读取源文件的二进制流,获取到字节码;
2. ClassVisitor 访问、修改 class 字节码,MethodVisitor 访问、修改函数字节码,FieldVisitor 访问、修改变量字节码;
3. ClassWriter 将最后的结果输出为 .class 文件。
我们需要自定义一个 ClassVisitor 用来筛选需求要访问和修改的 class 类,还需要自定义一个 MethodVisitor 来修改方法内部的字节码。
自定义 ClassVisitor 类:
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
public class ThreadTrackerClassVisitor extends ClassVisitor implements Opcodes {
//是否需要访问方法, 如果是 ThreadFactory 的子类,那么不访问此类
boolean needVisitMethod;
public ThreadTrackerClassVisitor(ClassVisitor cv) {
super(Opcodes.ASM5, cv);
}
@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
super.visit(version, access, name, signature, superName, interfaces);
if (interfaces != null && interfaces.length > 0) {
// 如果是某接口的子类,那么先看看是否实现了 ThreadFactory 接口
boolean isChildOfThreadFactory = false;
for (String anInterface : interfaces) {
if (anInterface.equalsIgnoreCase("java/util/concurrent/ThreadFactory")) {
isChildOfThreadFactory = true;
}
}
if (!isChildOfThreadFactory) {
// 不是 ThreadFactory 的子类,需要 track
needVisitMethod = true;
} else {
// 是 ThreadFactory 子类,不处理
needVisitMethod = false;
}
} else {
needVisitMethod = true;
}
}
@Override
public MethodVisitor visitMethod(int access0, String name0, String desc0, String signature0, String[] exceptions) {
MethodVisitor mv = super.visitMethod(access0, name0, desc0, signature0, exceptions);
if (needVisitMethod) {
return new ThreadTrackerMethodVisitor(Opcodes.ASM5, mv, access0, name0, desc0);
} else {
return mv;
}
}
}
ThreadTrackerClassVisitor 类在入口函数 visit() 里面判断当前 class 是否是 ThreadFactory 的实现类,因为 ThreadFactory 是线程池用来创建线程的,是不能被修改的。
ThreadTrackerClassVisitor.visitMethod() 会访问 class 的每一个函数,包括构造函数、成员函数、静态函数、静态代码块等等,对于需要访问的类,此函数返回 ThreadTrackerMethodVisitor 类,不需要访问的返回 super.visitMethod() 。
自定义 MethodVisitor:
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.commons.AdviceAdapter;
public class ThreadTrackerMethodVisitor extends AdviceAdapter {
private final String NAME_THREAD = "java/lang/Thread";
private final String NAME_ASMHOOKTHREAD = "com/qimao/thread/hook/AsmHookThread";
protected ThreadTrackerMethodVisitor(int api, MethodVisitor methodVisitor, int access, String name, String descriptor) {
super(api, methodVisitor, access, name, descriptor);
}
@Override
public void visitTypeInsn(int opcode, String type) {
if (NAME_THREAD.equals(type) && opcode == NEW) {
// new Thread() 替换为 new AsmHookThread()
mv.visitTypeInsn(opcode, NAME_ASMHOOKTHREAD);
} else {
super.visitTypeInsn(opcode, type);
}
}
@Override
public void visitMethodInsn(int opcodeAndSource, String owner, String name, String descriptor, boolean isInterface) {
if (NAME_THREAD.equals(owner)) {
// 调用线程的函数
if (name.equalsIgnoreCase("<init>")) {
// thread 的构造函数替换为 AsmHookThread 的构造函数
mv.visitMethodInsn(Opcodes.INVOKESPECIAL, NAME_ASMHOOKTHREAD, name, descriptor, false);
} else if (name.equalsIgnoreCase("start")) {
// Thread.start() 的构造函数替换为 AsmHookThread.start()
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, NAME_ASMHOOKTHREAD, name, descriptor, false);
} else {
super.visitMethodInsn(opcodeAndSource, owner, name, descriptor, isInterface);
}
} else {
super.visitMethodInsn(opcodeAndSource, owner, name, descriptor, isInterface);
}
}
}
ThreadTrackerMethodVisitor 是 MethodVisitor 的子类,ThreadTrackerMethodVisitor 的内部逻辑都是用来处理 class 函数字节码的。
字节码中不同操作码对应不同的 MethodVisitor 函数,比如:
NEW java/lang/Thread 对应 MethodVisitor.visitTypeInsn() 函数;
INVOKEVIRTUAL java/lang/Thread.<init> (Ljava/lang/Runnable;)V 对应 MethodVisitor.visitMethodInsn() 函数。
所以我们在不同函数里面处理不同操作码的字节码即可,具体实现请看代码。
注意点
完成了上面的步骤,基本上就可以在项目中运行了,但需要注意的是,有些第三方 sdk 会特意使用 new thread 来进行某些业务,因为使用线程池中可能会存在等待的情况,但作者认为不能等待,所以并非所有的 new thread 都可以替换为线程池。
针对上述情况,我们需要把所有被改动了字节码的类全部打印出来,然后一个个地查看源码,确定可以被替换的就替换,确定不可以被替换的需要在 ThreadTrackerClassVisitor 类中新增一个白名单功能,不对白名单里面的类进行访问修改, 并且还需要推进第三方优化。不确定可不可以被替换的类那么建议放到白名单内。
总结
回顾一下整个流程:
- 创建 AsmHookThread 类;
- 分析字节码的差异,确定如何修改字节码;
- 使用 ASM 框架进行编码实现;
- 新增白名单功能,以及推进第三方优化 sdk。
对于 java 字节码管理单独线程如果还有什么问题可以私信笔者,或者有不同的方案也欢迎一起进行交流。