背景
android app在構建的時候,經常會用到字節碼插樁技術,例如無埋點、方法耗時檢測、插件化、性能優化檢測。它的原理是在將java字節碼轉成dex文件之前,對項目代碼、依賴jar包、android.jar解壓縮,并修改其字節碼文件,最后再對修改后的字節碼文件進行壓縮。如果工程較大的話,還有很多字節碼插樁的gradle插件的話,那么這個工程是很耗IO的,構建速度會非常慢。
gradle插樁會使用到transform,由于公司項目較大,使用的tranform也比較多,通過查看編譯過程中task耗時情況,發現tranform占了大量時間。因為每次tranform都會對字節碼文件解壓,插樁后又進行壓縮。如下圖所示
19年年底開始我就有這樣的想法:能不能對tranform進行合并,多個插樁只有一次IO操作,這樣不僅可以滿足我們的插樁需求,還可以有效減少構建時間。
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<Project> {
@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<IPlugin> plugins = new ArrayList<>();
public void registerPlugin(IPlugin plugin) {
plugins.add(plugin);
}
public List<IPlugin> 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<IPlugin> 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<TransformFlow> 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));
}