背景
android app在構(gòu)建的時(shí)候菱涤,經(jīng)常會(huì)用到字節(jié)碼插樁技術(shù),例如無(wú)埋點(diǎn)洛勉、方法耗時(shí)檢測(cè)粘秆、插件化、性能優(yōu)化檢測(cè)收毫。它的原理是在將java字節(jié)碼轉(zhuǎn)成dex文件之前攻走,對(duì)項(xiàng)目代碼、依賴jar包牛哺、android.jar解壓縮陋气,并修改其字節(jié)碼文件,最后再對(duì)修改后的字節(jié)碼文件進(jìn)行壓縮引润。如果工程較大的話,還有很多字節(jié)碼插樁的gradle插件的話痒玩,那么這個(gè)工程是很耗IO的淳附,構(gòu)建速度會(huì)非常慢。
gradle插樁會(huì)使用到transform蠢古,由于公司項(xiàng)目較大奴曙,使用的tranform也比較多,通過(guò)查看編譯過(guò)程中task耗時(shí)情況草讶,發(fā)現(xiàn)tranform占了大量時(shí)間洽糟。因?yàn)槊看蝨ranform都會(huì)對(duì)字節(jié)碼文件解壓,插樁后又進(jìn)行壓縮堕战。如下圖所示
19年年底開始我就有這樣的想法:能不能對(duì)tranform進(jìn)行合并坤溃,多個(gè)插樁只有一次IO操作,這樣不僅可以滿足我們的插樁需求嘱丢,還可以有效減少構(gòu)建時(shí)間薪介。
ByteX tranform合并原理探究
2020年年初的時(shí)候看到字節(jié)跳動(dòng)團(tuán)隊(duì)開源了ByteX地址:https://github.com/bytedance/ByteX,驚奇地發(fā)現(xiàn)他們已經(jīng)實(shí)現(xiàn)tranform的合并功能越驻,于是我便花了點(diǎn)時(shí)間研究其中原理汁政。
根據(jù)byteX文檔介紹道偷,必須實(shí)現(xiàn)在gradle文件中apply宿主插件,然后再apply 各個(gè)子btyex插件记劈,才會(huì)進(jìn)行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目木、宿主插件通過(guò)ByteXExtension持有各個(gè)插件的引用换途。
那我們就先看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)));
}
}
其實(shí)就是創(chuàng)建了一個(gè)ByteXExtension擴(kuò)展類嘶窄,對(duì)應(yīng)宿主插件的gradle配置怀跛。然后注冊(cè)了BtyeXTransform。
ByteXExtension:這個(gè)類是實(shí)現(xiàn)tranfrom 合并的關(guān)鍵柄冲,它會(huì)保留每個(gè)子BtyeX插件吻谋,每個(gè)子BtyeX插件都會(huì)實(shí)現(xiàn)IPlugin接口,并通過(guò)執(zhí)行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());
}
這里就非常巧妙了漓拾,因?yàn)樗拗鞑寮鞘紫葧?huì)apply的的,那么在子ByteX子插件apply方法中是可以拿到宿主插件創(chuàng)建的ByteXExtension對(duì)象戒祠,那只要調(diào)用registerPlugin方法便可以在宿主插件中持有子插件的引用骇两,有了子插件的引用,那么就好辦了姜盈,只要在宿主中注冊(cè)的tranfrom中循環(huán)執(zhí)行每個(gè)插件的插樁邏輯低千,就可以完成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馏颂、通過(guò)在宿主中注冊(cè)的tranform示血,循環(huán)迭代每個(gè)子插件的tranform插樁邏輯實(shí)現(xiàn)tranfrom的合并。
我們來(lái)看看ByteXTransform對(duì)象救拉,這個(gè)類沒什么可看到难审,我們來(lái)看看其分類CommonTransform,由于插樁邏輯都是在tranfrom方法中實(shí)現(xiàn)的亿絮,我們直接看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();
}
}
}
這個(gè)方法主要實(shí)現(xiàn)了什么邏輯:拿到每個(gè)子插件的引用畏鼓,并放到一個(gè)隊(duì)列中回铛,然后依次給每個(gè)子插件生成一個(gè)TransformFlow插樁流氛悬,然后調(diào)用其run方法執(zhí)行剃浇,最終通過(guò)MainProcessHandler鏈?zhǔn)秸{(diào)用來(lái)實(shí)現(xiàn)字節(jié)碼插樁邏輯。
TransformFlow: 對(duì)插樁過(guò)程的抽象斗锭, 處理全部的構(gòu)建產(chǎn)物(一般為class文件)的過(guò)程定義為一次TransformFlow.一個(gè)插件可以獨(dú)立使用單獨(dú)的TransformFlow,也可以搭車到全局的MainTransformFlow(traverse,traverseAndroidJar,transform形成一個(gè)MainTransformFlow地淀。
TransformEngine:開啟線程池去處理插樁邏輯,充分利用打包資源岖是,加快構(gòu)建速度帮毁。
MainProcessHandler:處理插樁邏輯实苞,內(nèi)部是通過(guò)鏈?zhǔn)秸{(diào)用來(lái)實(shí)現(xiàn)的。每個(gè)btyex子插件都會(huì)實(shí)現(xiàn)該接口烈疚。
@Override
public void traverse(@NotNull String relativePath, @NotNull ClassVisitorChain chain) {
super.traverse(relativePath, chain);
chain.connect(new PreProcessClassVisitor(this.context));
}