java 字节码管理单独线程

前言

       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 类中新增一个白名单功能,不对白名单里面的类进行访问修改, 并且还需要推进第三方优化。不确定可不可以被替换的类那么建议放到白名单内。

总结  

       回顾一下整个流程:

  1. 创建 AsmHookThread 类;
  2. 分析字节码的差异,确定如何修改字节码;
  3. 使用 ASM 框架进行编码实现;
  4. 新增白名单功能,以及推进第三方优化 sdk。

        对于 java 字节码管理单独线程如果还有什么问题可以私信笔者,或者有不同的方案也欢迎一起进行交流。

展示评论