btyeX字节码插桩合并实现原理探究

背景

android app在构建的时候,经常会用到字节码插桩技术,例如无埋点、方法耗时检测、插件化、性能优化检测。它的原理是在将java字节码转成dex文件之前,对项目代码、依赖jar包、android.jar解压缩,并修改其字节码文件,最后再对修改后的字节码文件进行压缩。如果工程较大的话,还有很多字节码插桩的gradle插件的话,那么这个工程是很耗IO的,构建速度会非常慢。

字节码插桩.png

gradle插桩会使用到transform,由于公司项目较大,使用的tranform也比较多,通过查看编译过程中task耗时情况,发现tranform占了大量时间。因为每次tranform都会对字节码文件解压,插桩后又进行压缩。如下图所示

tranform优化前.png

19年年底开始我就有这样的想法:能不能对tranform进行合并,多个插桩只有一次IO操作,这样不仅可以满足我们的插桩需求,还可以有效减少构建时间。

tranform优化后.png

ByteX tranform合并原理探究

2020年年初的时候看到字节跳动团队开源了ByteX地址:https://github.com/bytedance/ByteX,惊奇地发现他们已经实现tranform的合并功能,于是我便花了点时间研究其中原理。

根据byteX文档介绍,必须实现在gradle文件中apply宿主插件,然后再apply 各个子btyex插件,才会进行tranform合并,否则跟普通插桩插件是一样的。

buildscript {
    ext.plugin_version="0.1.4"
    repositories {
        google()
        jcenter()
    }
  
    dependencies {
        classpath "com.bytedance.android.byteX:base-plugin:${plugin_version}"
        // Add bytex plugins' dependencies on demand. 按需添加插件依赖
        classpath "com.bytedance.android.byteX:refer-check-plugin:${plugin_version}"
        // ...
    }
}

apply plugin: 'com.android.application'
// apply ByteX宿主
apply plugin: 'bytex'
ByteX {
    enable true
    enableInDebug false
    logLevel "DEBUG"
}

// 按需apply bytex 插件
apply plugin: 'bytex.refer_check'
// ...

1、宿主插件通过ByteXExtension持有各个插件的引用。

那我们就先看base_plugin的代码,如下

public class ByteXPlugin implements Plugin {
    @Override
    public void apply(@NotNull Project project) {
        AppExtension android = project.getExtensions().getByType(AppExtension.class);
        ByteXExtension extension = project.getExtensions().create("ByteX", ByteXExtension.class);
        android.registerTransform(new ByteXTransform(new Context(project, android, extension)));
    }
}

其实就是创建了一个ByteXExtension扩展类,对应宿主插件的gradle配置。然后注册了BtyeXTransform。

ByteXExtension:这个类是实现tranfrom 合并的关键,它会保留每个子BtyeX插件,每个子BtyeX插件都会实现IPlugin接口,并通过执行ByteXExtension的registerPlugin方法完成添加子插件。

byteX子插件的父类AbsPlugin的apply方法中有这样一段代码:

 if (!alone()) {
                try {
                    ByteXExtension byteX = project.getExtensions().getByType(ByteXExtension.class);
                    byteX.registerPlugin(this);
                } catch (UnknownDomainObjectException e) {
                    android.registerTransform(getTransform());
                }
            } else {
                android.registerTransform(getTransform());
            }

这里就非常巧妙了,因为宿主插件是首先会apply的的,那么在子ByteX子插件apply方法中是可以拿到宿主插件创建的ByteXExtension对象,那只要调用registerPlugin方法便可以在宿主插件中持有子插件的引用,有了子插件的引用,那么就好办了,只要在宿主中注册的tranfrom中循环执行每个插件的插桩逻辑,就可以完成tranform的合并了。

public class ByteXExtension extends BaseExtension {

    private final List plugins = new ArrayList<>();

    public void registerPlugin(IPlugin plugin) {
        plugins.add(plugin);
    }

    public List getPlugins() {
        return ImmutableList.copyOf(plugins);
    }

    public void clearPlugins() {
        plugins.clear();
    }

    @Override
    public String getName() {
        return "byteX";
    }
}

2、通过在宿主中注册的tranform,循环迭代每个子插件的tranform插桩逻辑实现tranfrom的合并。
我们来看看ByteXTransform对象,这个类没什么可看到,我们来看看其分类CommonTransform,由于插桩逻辑都是在tranfrom方法中实现的,我们直接看CommonTranfrom的tranform方法:

    @Override
    public final void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
        super.transform(transformInvocation);
        init(transformInvocation);
        TransformContext transformContext = getTransformContext(transformInvocation);
        List plugins = getPlugins().stream().filter(p -> p.enable(transformContext)).collect(Collectors.toList());

        Timer timer = new Timer();
        TransformEngine transformEngine = new TransformEngine(transformContext);

        try {
            if (!plugins.isEmpty()) {
                Queue flowSet = new PriorityQueue<>((o1, o2) -> o2.getPriority() - o1.getPriority());
                MainTransformFlow commonFlow = new MainTransformFlow(transformEngine);
//                flowSet.add(commonFlow);
                for (int i = 0; i < plugins.size(); i++) {
                    IPlugin plugin = plugins.get(i);
                    TransformFlow flow = plugin.registerTransformFlow(commonFlow, transformContext);
                    if (!flowSet.contains(flow)) {
                        flowSet.add(flow);
                    }
                }
                while (!flowSet.isEmpty()) {
                    TransformFlow flow = flowSet.poll();
                    if (flow != null) {
                        if (flowSet.size() == 0) {
                            flow.asTail();
                        }
                        flow.run();
                        Graph graph = flow.getClassGraph();
                        if (graph != null) {
                            //clear the class diagram.we won’t use it anymore
                            graph.clear();
                        }
                    }
                }
            } else {
                transformEngine.skip();
            }
            afterTransform(transformInvocation);
        } catch (Throwable throwable) {
            LevelLog.sDefaultLogger.e(throwable.getClass().getName(), throwable);
            throw throwable;
        } finally {
            for (IPlugin plugin : plugins) {
                try {
                    plugin.afterExecute();
                } catch (Throwable throwable) {
                    LevelLog.sDefaultLogger.e("do afterExecute", throwable);
                }
            }
            transformContext.release();
            release();
            timer.record("Total cost time = [%s ms]");
            if (BooleanProperty.ENABLE_HTML_LOG.value()) {
                HtmlReporter.getInstance().createHtmlReporter(getName());
                HtmlReporter.getInstance().reset();
            }
        }
    }

这个方法主要实现了什么逻辑:拿到每个子插件的引用,并放到一个队列中,然后依次给每个子插件生成一个TransformFlow插桩流,然后调用其run方法执行,最终通过MainProcessHandler链式调用来实现字节码插桩逻辑。

TransformFlow: 对插桩过程的抽象, 处理全部的构建产物(一般为class文件)的过程定义为一次TransformFlow.一个插件可以独立使用单独的TransformFlow,也可以搭车到全局的MainTransformFlow(traverse,traverseAndroidJar,transform形成一个MainTransformFlow。

TransformEngine:开启线程池去处理插桩逻辑,充分利用打包资源,加快构建速度。

MainProcessHandler:处理插桩逻辑,内部是通过链式调用来实现的。每个btyex子插件都会实现该接口。

 @Override
    public void traverse(@NotNull String relativePath, @NotNull ClassVisitorChain chain) {
        super.traverse(relativePath, chain);
        chain.connect(new PreProcessClassVisitor(this.context));
    }

Bytex tranfrom合并原理流程图

Bytex tranfrom合并原理流程图.png

你可能感兴趣的:(btyeX字节码插桩合并实现原理探究)