Android 字節(jié)碼插樁全流程解析

Android進(jìn)階寶典 -- Handler應(yīng)用于線上卡頓監(jiān)控中省店,我簡(jiǎn)單介紹了一下關(guān)于ASM實(shí)現(xiàn)字節(jié)碼插樁來(lái)實(shí)現(xiàn)方法耗時(shí)的監(jiān)控鲁纠,但是當(dāng)時(shí)只是找了一個(gè)特定的class文件,針對(duì)某個(gè)特定的方法進(jìn)行插樁呻拌,但是真正的開(kāi)發(fā)中不可能這么做的,因?yàn)檎麄€(gè)工程中會(huì)有成百上千的方法包雀,而且存儲(chǔ)的位置也各有不同谢谦,這個(gè)時(shí)候,我們就需要借助gradle插件來(lái)實(shí)現(xiàn)ASM字節(jié)碼插樁愿阐。

1 準(zhǔn)備工作

但凡涉及到gradle開(kāi)發(fā)微服,我一般都是會(huì)在buildSrc文件夾下進(jìn)行,還有沒(méi)有伙伴不太了解buildSrc的缨历,其實(shí)buildSrc是Android中默認(rèn)的插件工程以蕴,在gradle編譯的時(shí)候糙麦,會(huì)編譯這個(gè)項(xiàng)目并配置到classpath下。這樣的話在buildSrc中創(chuàng)建的插件丛肮,每個(gè)項(xiàng)目都可以引入赡磅。

在buildSrc中可以創(chuàng)建groovy目錄(如果對(duì)groovy或者kotlin了解),也可以創(chuàng)建java目錄宝与,對(duì)于插件開(kāi)發(fā)個(gè)人更便向使用groovy焚廊,因?yàn)楦N近gradle。

1.1 創(chuàng)建插件

創(chuàng)建插件习劫,需要實(shí)現(xiàn)Plugin接口咆瘟,在引入這個(gè)插件后,項(xiàng)目編譯的時(shí)候诽里,就會(huì)執(zhí)行apply方法袒餐。

class ASMPlugin implements Plugin<Project>{

    @Override
    void apply(Project project) {
        def ext = project.extensions.getByType(AppExtension)
        if (ext != null){
            ext.registerTransform(new ASMTransform())
        }
    }
}

在apply方法中,可以執(zhí)行自定義的Task谤狡,也可以執(zhí)行自定義的Transform(其實(shí)也可以看做是一種特殊的Task)灸眼,這里我們自定義了插樁相關(guān)的Transform。

1.2 創(chuàng)建Transform

什么是Transform呢墓懂?就是在class文件打包生成dex文件的過(guò)程中幢炸,對(duì)class字節(jié)碼做處理,最終生成新的dex文件拒贱,那么有什么方式能夠?qū)ψ止?jié)碼操作呢宛徊?ASM是一種方式,使用Javassist也可以織入字節(jié)碼逻澳。

class ASMTransform extends Transform {

    @Override
    String getName() {
        return "ASMTransform"
    }

    @Override
    Set<QualifiedContent.ContentType> getInputTypes() {
        return TransformManager.CONTENT_CLASS
    }

    @Override
    Set<QualifiedContent.Scope> getScopes() {
        return TransformManager.SCOPE_FULL_PROJECT
    }

    @Override
    boolean isIncremental() {
        return false
    }

    @Override
    void transform(Context context, Collection<TransformInput> inputs, Collection<TransformInput> referencedInputs, TransformOutputProvider outputProvider, boolean isIncremental) throws IOException, TransformException, InterruptedException {
        inputs.each { input ->
            input.directoryInputs.each { dic ->
                /**這里會(huì)拿到兩個(gè)路徑闸天,分別是java代碼編譯后的javac/debug/classes,以及kotlin代碼編譯后的 tmp/kotlin-classes/debug */
                println("dic path == >${dic.file.path}")
                /**所有的class文件的根路徑斜做,我們已經(jīng)拿到了苞氮,接下來(lái)就是分析這些文件夾下的class文件*/
                findAllClass(dic.file)
                /**這里一定不能忘記寫(xiě)*/
                def dest = outputProvider.getContentLocation(dic.name, dic.contentTypes, dic.scopes, Format.DIRECTORY)
                FileUtils.copyDirectory(dic.file, dest)
            }
            input.jarInputs.each { jar ->
                /**這里也一定不能忘記寫(xiě)*/
                def dest = outputProvider.getContentLocation(jar.name,jar.contentTypes,jar.scopes,Format.JAR)
                FileUtils.copyFile(jar.file,dest)
            }
        }
    }

    /**
     * 查找class文件
     * @param file 可能是文件也可能是文件夾
     */
    private void findAllClass(File file) {
        if (file.isDirectory()) {
            file.listFiles().each {
                findAllClass(it)
            }
        } else {
            modifyClass(file)
        }
    }

    /**
     * 進(jìn)行字節(jié)碼插樁
     * @param file 需要插樁的字節(jié)碼文件
     */
    private void modifyClass(File file) {
        println("最終的class文件 ==> ${file.absolutePath}")
        /**如果不是.class文件,拋棄*/
        if (!file.absolutePath.endsWith(".class")) {
            return
        }

        /**BuildConfig.class文件以及R文件都拋棄*/
        if (file.absolutePath.contains("BuildConfig.class") || file.absolutePath.contains("R")) {
            return
        }

        doASM(file)
    }

    /**
     * 進(jìn)行ASM字節(jié)碼插樁
     * @param file 需要插樁的class文件
     */
    private void doASM(File file) {
        def fis = new FileInputStream(file)
        def cr = new ClassReader(fis)
        def cw = new ClassWriter(ClassWriter.COMPUTE_MAXS)
        cr.accept(new ASMClassVisitor(Opcodes.ASM9, cw), ClassReader.SKIP_FRAMES | ClassReader.SKIP_DEBUG)
        /**重新覆蓋*/
        def bytes = cw.toByteArray()
        def fos = new java.io.FileOutputStream(file.absolutePath)
        fos.write(bytes)
        fos.flush()
        fos.close()
    }
}

如果想要使用Transform瓤逼,那么需要引入transform-api笼吟,其實(shí)在transform 1.5之后gradle就支持Transform了。

implementation 'com.android.tools.build:transform-api:1.5.0'

當(dāng)執(zhí)行Transform任務(wù)的時(shí)候霸旗,最終會(huì)執(zhí)行到transform方法贷帮,在這個(gè)方法中可以獲取TransformInput的輸入,主要包括兩種:文件夾和Jar包诱告;對(duì)于Jar包撵枢,我們不需要處理,只需要拷貝到目標(biāo)文件夾下即可。

對(duì)于文件夾我們是需要處理的锄禽,因?yàn)檫@里包含了我們要處理的.class文件潜必,對(duì)于Java編譯后的class文件是存在javac/debug/classes根文件夾下,對(duì)于kotlin編譯后的class文件是存在temp/classes根文件下沃但。

所以在整個(gè)編譯的過(guò)程中磁滚,只要是.class文件都會(huì)執(zhí)行doASM這個(gè)方法,在這個(gè)方法中就是我們?cè)谏瞎?jié)提到的對(duì)于字節(jié)碼的插樁宵晚。

1.3 ASM字節(jié)碼插樁

class ASMClassVisitor extends ClassVisitor {

    ASMClassVisitor(int api) {
        super(api)
    }

    @Override
    MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
        println("visitMethod==>$name")
        /**所有的方法都會(huì)在ASMMethodVisitor中插入字節(jié)碼*/
        def method = super.visitMethod(access, name, descriptor, signature, exceptions)
        return new ASMMethodVisitor(api, method, access, name, descriptor)
    }

    ASMClassVisitor(int api, ClassVisitor classVisitor) {
        super(api, classVisitor)
    }

    @Override
    FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) {
        return super.visitField(access, name, descriptor, signature, value)
    }

    @Override
    AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
        return super.visitAnnotation(descriptor, visible)
    }
}
class ASMMethodVisitor extends AdviceAdapter {

    private def methodName
    /**
     * Constructs a new {@link AdviceAdapter}.
     *
     * @param api the ASM API version implemented by this visitor. Must be one of {@link
     *     Opcodes#ASM4}, {@link Opcodes#ASM5}, {@link Opcodes#ASM6} or {@link Opcodes#ASM7}.
     * @param methodVisitor the method visitor to which this adapter delegates calls.
     * @param access the method's access flags (see {@link Opcodes}).
     * @param name the method's name.
     * @param descriptor the method's descriptor (see {@link Type Type}).
     */
    protected ASMMethodVisitor(int api, MethodVisitor methodVisitor, int access, String name, String descriptor) {
        super(api, methodVisitor, access, name, descriptor)
        this.methodName = name
    }

    @Override
    protected void onMethodEnter() {
        super.onMethodEnter()
        visitFieldInsn(GETSTATIC,
                "com/lay/learn/base_net/LoggUtils",
                "INSTANCE",
                "Lcom/lay/learn/base_net/LoggUtils;")
        visitMethodInsn(INVOKEVIRTUAL, "com/lay/learn/base_net/LoggUtils", "start", "()V", false)
    }

    @Override
    protected void onMethodExit(int opcode) {
        super.onMethodExit(opcode)
        visitFieldInsn(GETSTATIC,
                "com/lay/learn/base_net/LoggUtils",
                "INSTANCE",
                "Lcom/lay/learn/base_net/LoggUtils;")
        visitLdcInsn(methodName)
        visitMethodInsn(INVOKEVIRTUAL, "com/lay/learn/base_net/LoggUtils", "end", "(Ljava/lang/String;)V",false)
    }
}

這里就不再細(xì)說(shuō)了垂攘,貼上源碼大家可以借鑒一下哈。

最終在編譯的過(guò)程中坝疼,對(duì)所有的方法插入了我們自己的耗時(shí)計(jì)算邏輯搜贤,當(dāng)運(yùn)行之后

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }
}

雖然我們沒(méi)有顯示地在MainActivity的onCreate中插入耗時(shí)檢測(cè)代碼谆沃,但是在控制臺(tái)中我們可以看到钝凶,onCreate方法耗時(shí)180ms

2022-12-28 19:50:19.243 13665-13665/com.lay.learn.asm E/LoggUtils: <init> 耗時(shí)==>0
2022-12-28 19:50:19.458 13665-13665/com.lay.learn.asm E/LoggUtils: onCreate 耗時(shí)==>180

1.4 插件配置

當(dāng)我們完成一個(gè)插件之后,需要在META-INF文件夾下創(chuàng)建一個(gè)gradle-plugins文件夾唁影,并在properties文件中聲明插件全類(lèi)名耕陷。

implementation-class=com.lay.asm.ASMPlugin

要注意插件id就是properties文件的名字。

這樣只要某個(gè)工程中需要字節(jié)碼插樁据沈,只需要引入asm_plugin這個(gè)插件即可在編譯的時(shí)候掃描整個(gè)工程哟沫。

plugins {
    id 'com.android.application'
    id 'org.jetbrains.kotlin.android'
    id 'asm_plugin'
}

附上buildSrc中的gradle配置文件

plugins{
    id 'groovy'
}

repositories {
    google()
    mavenCentral()
}

dependencies {
    implementation gradleApi()
    implementation localGroovy()
    implementation  'org.apache.commons:commons-io:1.3.2'
    implementation "com.android.tools.build:gradle:7.0.3"
    implementation 'com.android.tools.build:transform-api:1.5.0'
    implementation 'org.ow2.asm:asm:9.1'
    implementation 'org.ow2.asm:asm-util:9.1'
    implementation 'org.ow2.asm:asm-commons:9.1'
}

java {
    sourceCompatibility = JavaVersion.VERSION_1_8
    targetCompatibility = JavaVersion.VERSION_1_8
}

最后需要說(shuō)一點(diǎn)就是,在Transform任務(wù)執(zhí)行時(shí)锌介,一定要將文件夾或者jar包傳遞到下一級(jí)的Transform中嗜诀,否則會(huì)導(dǎo)致apk打包時(shí)缺少文件導(dǎo)致apk無(wú)法運(yùn)行

作者:Vector7
鏈接:https://juejin.cn/post/7182178552207376421

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末孔祸,一起剝皮案震驚了整個(gè)濱河市隆敢,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌崔慧,老刑警劉巖拂蝎,帶你破解...
    沈念sama閱讀 217,084評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異惶室,居然都是意外死亡温自,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,623評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門(mén)皇钞,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)悼泌,“玉大人,你說(shuō)我怎么就攤上這事夹界∪辏” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 163,450評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)也拜。 經(jīng)常有香客問(wèn)我以舒,道長(zhǎng),這世上最難降的妖魔是什么慢哈? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,322評(píng)論 1 293
  • 正文 為了忘掉前任蔓钟,我火速辦了婚禮,結(jié)果婚禮上卵贱,老公的妹妹穿的比我還像新娘滥沫。我一直安慰自己,他們只是感情好键俱,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,370評(píng)論 6 390
  • 文/花漫 我一把揭開(kāi)白布兰绣。 她就那樣靜靜地躺著,像睡著了一般编振。 火紅的嫁衣襯著肌膚如雪缀辩。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,274評(píng)論 1 300
  • 那天踪央,我揣著相機(jī)與錄音臀玄,去河邊找鬼。 笑死畅蹂,一個(gè)胖子當(dāng)著我的面吹牛健无,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播液斜,決...
    沈念sama閱讀 40,126評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼累贤,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了少漆?” 一聲冷哼從身側(cè)響起臼膏,我...
    開(kāi)封第一講書(shū)人閱讀 38,980評(píng)論 0 275
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎检疫,沒(méi)想到半個(gè)月后讶请,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,414評(píng)論 1 313
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡屎媳,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,599評(píng)論 3 334
  • 正文 我和宋清朗相戀三年夺溢,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片烛谊。...
    茶點(diǎn)故事閱讀 39,773評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡风响,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出丹禀,到底是詐尸還是另有隱情状勤,我是刑警寧澤鞋怀,帶...
    沈念sama閱讀 35,470評(píng)論 5 344
  • 正文 年R本政府宣布,位于F島的核電站持搜,受9級(jí)特大地震影響密似,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜葫盼,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,080評(píng)論 3 327
  • 文/蒙蒙 一残腌、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧贫导,春花似錦抛猫、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,713評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至峰档,卻和暖如春败匹,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背面哥。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,852評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工哎壳, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留毅待,地道東北人尚卫。 一個(gè)月前我還...
    沈念sama閱讀 47,865評(píng)論 2 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像尸红,于是被迫代替她去往敵國(guó)和親吱涉。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,689評(píng)論 2 354

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