btyeX字節(jié)碼插樁合并實(shí)現(xiàn)原理探究

背景

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ì)非常慢。

字節(jié)碼插樁.png

gradle插樁會(huì)使用到transform蠢古,由于公司項(xiàng)目較大奴曙,使用的tranform也比較多,通過(guò)查看編譯過(guò)程中task耗時(shí)情況草讶,發(fā)現(xiàn)tranform占了大量時(shí)間洽糟。因?yàn)槊看蝨ranform都會(huì)對(duì)字節(jié)碼文件解壓,插樁后又進(jìn)行壓縮堕战。如下圖所示

tranform優(yōu)化前.png

19年年底開始我就有這樣的想法:能不能對(duì)tranform進(jìn)行合并坤溃,多個(gè)插樁只有一次IO操作,這樣不僅可以滿足我們的插樁需求嘱丢,還可以有效減少構(gòu)建時(shí)間薪介。

tranform優(yōu)化后.png

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));
    }

Bytex tranfrom合并原理流程圖

Bytex tranfrom合并原理流程圖.png
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末黔牵,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子爷肝,更是在濱河造成了極大的恐慌猾浦,老刑警劉巖,帶你破解...
    沈念sama閱讀 207,248評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件灯抛,死亡現(xiàn)場(chǎng)離奇詭異金赦,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)对嚼,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,681評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門夹抗,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人纵竖,你說(shuō)我怎么就攤上這事漠烧。” “怎么了靡砌?”我有些...
    開封第一講書人閱讀 153,443評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵已脓,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我通殃,道長(zhǎng)度液,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,475評(píng)論 1 279
  • 正文 為了忘掉前任画舌,我火速辦了婚禮恨诱,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘骗炉。我一直安慰自己,他們只是感情好蛇受,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,458評(píng)論 5 374
  • 文/花漫 我一把揭開白布句葵。 她就那樣靜靜地躺著,像睡著了一般兢仰。 火紅的嫁衣襯著肌膚如雪乍丈。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,185評(píng)論 1 284
  • 那天把将,我揣著相機(jī)與錄音轻专,去河邊找鬼。 笑死察蹲,一個(gè)胖子當(dāng)著我的面吹牛请垛,可吹牛的內(nèi)容都是我干的催训。 我是一名探鬼主播,決...
    沈念sama閱讀 38,451評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼宗收,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼漫拭!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起混稽,我...
    開封第一講書人閱讀 37,112評(píng)論 0 261
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤采驻,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后匈勋,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體礼旅,經(jīng)...
    沈念sama閱讀 43,609評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,083評(píng)論 2 325
  • 正文 我和宋清朗相戀三年洽洁,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了痘系。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,163評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡诡挂,死狀恐怖碎浇,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情璃俗,我是刑警寧澤奴璃,帶...
    沈念sama閱讀 33,803評(píng)論 4 323
  • 正文 年R本政府宣布,位于F島的核電站城豁,受9級(jí)特大地震影響苟穆,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜唱星,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,357評(píng)論 3 307
  • 文/蒙蒙 一雳旅、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧间聊,春花似錦攒盈、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,357評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至尚蝌,卻和暖如春迎变,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背飘言。 一陣腳步聲響...
    開封第一講書人閱讀 31,590評(píng)論 1 261
  • 我被黑心中介騙來(lái)泰國(guó)打工衣形, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人姿鸿。 一個(gè)月前我還...
    沈念sama閱讀 45,636評(píng)論 2 355
  • 正文 我出身青樓谆吴,卻偏偏與公主長(zhǎng)得像倒源,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子纪铺,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,925評(píng)論 2 344

推薦閱讀更多精彩內(nèi)容