Gradle-初探代碼注入Transform

簡(jiǎn)介

本文主要介紹gradle打包過(guò)程中transform階段浮禾,這里大概說(shuō)下AOP(Aspect Oriented Programming)份汗,這是一種面向切面的思想,預(yù)支對(duì)應(yīng)的是OOP(Object Oriented Programming)面向?qū)ο缶幊毯兀@里不展開說(shuō)明“敬剩可以看下對(duì)AOP總結(jié)的思維導(dǎo)圖


image

劃重點(diǎn)

本篇文章主要介紹下面的幾點(diǎn):

  • Transform可以做什么
  • 簡(jiǎn)單了解App打包過(guò)程,以及介紹Transform
  • 生成自己的MyConfig類文件互拾,有助我們更好理解
  • 介紹ASM

Transform可以做什么

最主要的目的就是:解耦,開發(fā)人員專注于需求颜矿,其他的邊角料寄猩,交給Transform來(lái)處理骑疆。

  1. 權(quán)限判斷,避免代碼中相關(guān)的地方都是權(quán)限申請(qǐng)和處理的代碼
  2. 無(wú)痕埋點(diǎn)箍铭,簡(jiǎn)單的場(chǎng)景可以使用泊柬,但是場(chǎng)景比較復(fù)雜時(shí)坡疼,就不好處理了衣陶,目前網(wǎng)上沒有很好的解決方案
  3. 性能監(jiān)控柄瑰,trace+字節(jié)碼插樁剪况,完美監(jiān)控
  4. 事件防抖,避免短期內(nèi)多次點(diǎn)擊按鈕
  5. 熱修復(fù)译断,在所有方法前插入一個(gè)預(yù)留的函數(shù),可以將有bug的方法替換成下發(fā)的方法孙咪。
  6. 優(yōu)化代碼堪唐,例如刪除項(xiàng)目中體積很大的R文件中的字段翎蹈、優(yōu)化內(nèi)聯(lián)函數(shù)等等
  7. ....
    還有很多功能,都值得我們?nèi)L試荤堪。

App打包過(guò)程 & Transform

首先我們回顧一下App的打包流程合陵,App打包都需要經(jīng)理哪些流程,每一個(gè)步驟都干了什么拥知?

apk打包過(guò)程

谷歌官網(wǎng)的一幅圖

image

從上面的圖中大概理一下流程

  1. 編譯器將app的源碼編譯成DEX(Dalvik Executable)文件(其中包括運(yùn)行在Android設(shè)備上的字節(jié)碼),將所有其他的內(nèi)容轉(zhuǎn)換為已經(jīng)編譯的資源低剔。
  2. APK打包器將DEX文件和已編譯資源合并成單個(gè)APK。不過(guò)襟齿,必須先簽署APK镀琉,才能將應(yīng)用安裝并部署到Android設(shè)備上
  3. APK打包器使用調(diào)試或者發(fā)布密鑰庫(kù)簽署你的APK:
    • 如果你構(gòu)建的是debug版本應(yīng)用蕊唐,打包器會(huì)使用debug密鑰庫(kù)簽署你的應(yīng)用,Android Studio自動(dòng)使用debug密鑰庫(kù)配置新項(xiàng)目
    • 如果你構(gòu)建的是release版本替梨,打包器會(huì)使用release密鑰庫(kù)簽署你的應(yīng)用
  4. 在生成最終APK之前钓试,打包器會(huì)使用zipalign工具對(duì)應(yīng)用進(jìn)行優(yōu)化副瀑,減少其在設(shè)備上運(yùn)行時(shí)的內(nèi)存占用

然后再看一張谷歌之前的打包流程圖

image

這張圖相比較第一張圖而言就更加詳細(xì)了,從這張圖中糠睡,可以看到打包流程可以分為以下七步:

  1. aapt-打包res資源文件,生成R.java狈孔、resources.arsc和res文件(二進(jìn)制&非二進(jìn)制如res/raw和pic保持原樣)
  2. AIDL-Android借口定義語(yǔ)言信认,Android提供的IPC(Inter Process Communication均抽,進(jìn)程間通信)的一種獨(dú)特實(shí)現(xiàn)。這個(gè)階段處理.aidl文件油挥,生成對(duì)應(yīng)的Java接口文件。
  3. Java Compiler-通過(guò)Java Compiler編譯R.java深寥、Java接口文件攘乒、Java源文件惋鹅,生成.class文件。
  4. dex-通過(guò)dex命令负饲,將.class文件和第三方庫(kù)中的.class文件處理生成class.dex喂链。
  5. apkbuilder-將class.dex、resources.arsc妥泉、res文件夾(res/raw資源被原封不動(dòng)的打包進(jìn)APK之外,其他資源都會(huì)被編譯或者處理)盲链、OtherResouces(assets文件夾)蝇率、AndroidManifest.xml打包進(jìn)apk文件刽沾。
  6. Jarsigner-對(duì)上面的apk進(jìn)行debug或release簽名
  7. aipalign-將簽名后的pak進(jìn)行對(duì)其處理

最后看一張更加詳細(xì)的圖片

image

Transform

Transform階段就是在apk打包圖中紅圈的位置,第二張圖更加詳細(xì)的表示了Transform的過(guò)程侧漓,是在.class->.dex的過(guò)程。

image

Gradle Transform是Android官方提供給開發(fā)者在項(xiàng)目構(gòu)建階段由class到dex轉(zhuǎn)換期間修改class文件的一套api布蔗。目前經(jīng)典的應(yīng)用就是字節(jié)碼插樁和代碼注入技術(shù)藤违。有了這個(gè)API纵揍,我們就可以根據(jù)自己的業(yè)務(wù)需求做一些定制。

先看下transform主要有哪些方法

image
  1. getName():Transform的名稱泽谨,但是這里并不是真正的名稱,真正的名稱還需要進(jìn)行拼接
  2. getInputTypes():Transform處理文件的類型
    • CLASSES 表示要處理編譯后的字節(jié)碼吧雹,可能是jar包也可能是目錄
    • RESOURCES表示處理標(biāo)準(zhǔn)的java資源
  3. getScopes():Transform的作用域
    type Des
    PROJECT 只處理當(dāng)前的文件
    SUB_PROJECTS 只處理子項(xiàng)目
    EXTERNAL_LIBRARIES 只處理外部的依賴庫(kù)
    TESTED_CODE 測(cè)試代碼
    PROVIDED_ONLY 只處理本地或遠(yuǎn)程以provided形式引入的依賴庫(kù)
    PROJECT_LOCAL_DEPS (Deprecated,使用EXTERNAL_LIBRARIES) 只處理當(dāng)前項(xiàng)目的本地依賴吮炕,例如jar腊脱、aar
    SUB_PROJECTS_LOCAL_DEPS (Deprecated龙亲,使用EXTERNAL_LIBRARIES) 只處理子項(xiàng)目的本地依賴。
  4. isIncremental():是否支持增量編譯悍抑,增量編譯就是如果第二次編譯相應(yīng)的task沒有改變,那么就直接跳過(guò)搜骡,節(jié)省時(shí)間,更詳細(xì)的解釋可以看這里
  5. transform():這是最主要的方法记靡,這里對(duì)文件或jar進(jìn)行處理谈竿,進(jìn)行代碼的插入。
    • TransformInput:對(duì)輸入的class文件轉(zhuǎn)變成目標(biāo)字節(jié)碼文件空凸,TransformInput就是這些輸入文件的抽象嚎花。目前它包含DirectoryInput集合與JarInput集合呀洲。
    • DirectoryInput:源碼方式參與項(xiàng)目編譯的所有目錄結(jié)構(gòu)及其目錄下的源文件。
    • JarInput:Jar包方式參與項(xiàng)目編譯的所有本地jar或遠(yuǎn)程jar包
    • TransformOutProvider:通過(guò)這個(gè)類來(lái)獲取輸出路徑道逗。

通過(guò)自定義Plugin創(chuàng)建一個(gè)類

我們都知道通過(guò)Gradle編譯后,會(huì)生成一個(gè)BuildConfig的類滓窍,其中有一些項(xiàng)目的信息卖词,例如APPLICATION_ID吏夯、DEBUG等信息,我們依照BuildConfig生成規(guī)則锦亦,也生成一個(gè)自己的MyConfig類舶替,通過(guò)這個(gè)例子我們可以了解一些gradle的語(yǔ)法和api杠园。

在自定義Plugin類的apply()函數(shù)中添加下面的代碼

class ConfigPlugin : Plugin<Project> {
    override fun apply(project: Project) {
        //只在'applicatoin'中使用,否則拋出異常
        if (!project.plugins.hasPlugin(AppPlugin::class.java)) {
            throw GradleException("this plugin is not application")
        }
        //獲取build.gradle中的"android"閉包
        val android = project.extensions.getByType(AppExtension::class.java)
        //創(chuàng)建自己的閉包
        val config = project.extensions.create("config", ConfigExtension::class.java)
        //遍歷"android"閉包中的"buildTypes"閉包抛蚁,一般有release和debug兩種
        android.applicationVariants.all {
            it as ApplicationVariantImpl
            println("variant name: ${it.name}")

            //創(chuàng)建自己的config task
            val buildConfigTask = project.tasks.create("DemoBuildConfig${it.name.capitalize()}")
            //在task最后去創(chuàng)建java文件
            buildConfigTask.doLast { task ->
                createJavaConfig(it, config)
            }
            // 找到系統(tǒng)的buildConfig Task
            val generateBuildConfigTask = project.tasks.getByName(it.variantData.scope.taskContainer.generateBuildConfigTask?.name)
            //自己的Config Task 依賴于系統(tǒng)的Config Task
            generateBuildConfigTask.let {
                buildConfigTask.dependsOn(it)
                it.finalizedBy(buildConfigTask)
            }
        }
    }

    fun createJavaConfig(variant: ApplicationVariantImpl, config: ConfigExtension) {
        val FileName = "MyConfig"
        val constantStr = StringBuilder()
        constantStr.append("\n").append("package ")
                .append(config.packageName).append("; \n\n")
                .append("public class $FileName {").append("\n")
        config.constantMap.forEach {
            constantStr.append("public static final String ${it.key} = \"${it.value}\";\n")
        }
        constantStr.append("} \n")
        println("content: ${constantStr}")

        val outputDir = variant.variantData.scope.buildConfigSourceOutputDir
        val javaFile = File(outputDir, config.packageName.replace(".", "/") + "/$FileName.java")
        println("javaFilePath: ${javaFile.absolutePath}")
        javaFile.writeText(constantStr.toString(), Charsets.UTF_8)
    }

}

就可以生成MyConfig類,這個(gè)類簡(jiǎn)單定義了一些我們可以在build.gradle中定義的變量瞧甩,可以分為下面的幾個(gè)步驟

  1. 判斷是否為application的module(僅在application中進(jìn)行操作)
  2. 遍歷buildTypes也就是release和debug
  3. 在對(duì)應(yīng)的buildTypes中創(chuàng)建task
  4. 設(shè)置自定義的task依賴于BUildConfig的Task
  5. 新建自定義的MyConfig.java文件

Transform的優(yōu)化:增量與并發(fā)

增量

我們想一個(gè)問(wèn)題钉跷,遍歷一遍項(xiàng)目中所有源文件和jar肚逸,時(shí)間都是很長(zhǎng)的,如果我們每次改一行編譯都需要經(jīng)過(guò)這個(gè)過(guò)程朦促,是很浪費(fèi)時(shí)間膝晾。這個(gè)時(shí)候需要實(shí)現(xiàn)增量編譯务冕,什么意思呢?增量,顧名思義臊旭,就是在已有的基礎(chǔ)上,對(duì)增加的進(jìn)行編譯离熏,這樣在編譯過(guò)一次的基礎(chǔ)上领跛,以后就會(huì)大大的縮短時(shí)間撤奸。

想要開啟增量編譯,我們需要重寫Transform的這個(gè)接口胧瓜,返回true矢棚,上面代碼中的注釋也說(shuō)明了府喳。

 @Override
    boolean isIncremental() {
        return true
    }

這里需要注意一點(diǎn):不是每次的編譯都是可以怎量編譯的,畢竟一次clean build完全沒有增量的基礎(chǔ)钝满,所以兜粘,我們需要檢查當(dāng)前的編譯是否增量編譯弯蚜。
需要做區(qū)分:

  • 不是增量編譯,則清空output目錄碎捺,然后按照前面的方式,逐個(gè)class/jar處理
  • 增量編譯收厨,則要檢查每個(gè)文件的Status晋柱,Status分為四種诵叁,并且對(duì)四種文件的操作不盡相同
    • NOTCHANGED:當(dāng)前文件不需要處理,甚至復(fù)制操作都不用
    • ADDED拧额、CHANGED:正常處理碑诉,輸出給下一個(gè)任務(wù)
    • REMOVED:移除outputProvider獲取路徑對(duì)應(yīng)的文件
@Override
public void transform(TransformInvocation transformInvocation){
    Collection<TransformInput> inputs = transformInvocation.getInputs();
    TransformOutputProvider outputProvider = transformInvocation.getOutputProvider();
    boolean isIncremental = transformInvocation.isIncremental();
    //如果非增量势腮,則清空舊的輸出內(nèi)容
    if(!isIncremental) {
        outputProvider.deleteAll();
    }   
    for(TransformInput input : inputs) {
        for(JarInput jarInput : input.getJarInputs()) {
            Status status = jarInput.getStatus();
            File dest = outputProvider.getContentLocation(
                    jarInput.getName(),
                    jarInput.getContentTypes(),
                    jarInput.getScopes(),
                    Format.JAR);
            if(isIncremental && !emptyRun) {
                switch(status) {
                    case NOTCHANGED:
                        break;
                    case ADDED:
                    case CHANGED:
                        transformJar(jarInput.getFile(), dest, status);
                        break;
                    case REMOVED:
                        if (dest.exists()) {
                            FileUtils.forceDelete(dest);
                        }
                        break;
                }
            } else {
                transformJar(jarInput.getFile(), dest, status);
            }
        }
        for(DirectoryInput directoryInput : input.getDirectoryInputs()) {
            File dest = outputProvider.getContentLocation(directoryInput.getName(),
                    directoryInput.getContentTypes(), directoryInput.getScopes(),
                    Format.DIRECTORY);
            FileUtils.forceMkdir(dest);
            if(isIncremental && !emptyRun) {
                String srcDirPath = directoryInput.getFile().getAbsolutePath();
                String destDirPath = dest.getAbsolutePath();
                Map<File, Status> fileStatusMap = directoryInput.getChangedFiles();
                for (Map.Entry<File, Status> changedFile : fileStatusMap.entrySet()) {
                    Status status = changedFile.getValue();
                    File inputFile = changedFile.getKey();
                    String destFilePath = inputFile.getAbsolutePath().replace(srcDirPath, destDirPath);
                    File destFile = new File(destFilePath);
                    switch (status) {
                        case NOTCHANGED:
                            break;
                        case REMOVED:
                            if(destFile.exists()) {
                                FileUtils.forceDelete(destFile);
                            }
                            break;
                        case ADDED:
                        case CHANGED:
                            FileUtils.touch(destFile);
                            transformSingleFile(inputFile, destFile, srcDirPath);
                            break;
                    }
                }
            } else {
                transformDir(directoryInput.getFile(), dest);
            }
        }
    }
}

這樣做真的有用嗎?讓我們用數(shù)據(jù)說(shuō)話捎拯,首先準(zhǔn)備好測(cè)試數(shù)據(jù),一個(gè)demo署照,對(duì)所有的源文件和第三方依賴庫(kù)進(jìn)行掃描,使用增量和非增量的模式進(jìn)行三次編譯建芙,然后取平均值没隘。
兩種方式計(jì)算自定義transform:

  1. ./gradlew assembleDebug --profile命令禁荸,在根目錄的build/reports目錄下會(huì)生成一個(gè)文件,可以查找每一個(gè)transform所花費(fèi)的時(shí)間赶熟。
  2. 在自定義的transformTask之前記錄時(shí)間瑰妄,在執(zhí)行完后記錄所花費(fèi)的時(shí)間映砖,貼一下簡(jiǎn)單的代碼
 Task doubleCheckTask = project.tasks["transformClassesWithDoubleCheckTransformFor${variant.name.capitalize()}"]
        doubleCheckTask.configure {
            def startTime
            doFirst {
                startTime = System.nanoTime()
            }
            doLast {
                println()
                println " --> COST: ${TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime)} ms"
                println()
            }
        }

這里我們使用的是第二種方式,我們?cè)诿看沃桓膭?dòng)一行代碼的情況下邑退,然后讓我們看結(jié)論

試驗(yàn)次數(shù) 非增量 增量
1 1309ms 151ms
2 1093ms 183ms
3 1153ms 130ms

可以發(fā)現(xiàn),增量的速度比全量的速度快了將近10倍多地技,這是增量的文件比較少的情況蜈七,但是總的來(lái)說(shuō)增量編譯還是會(huì)大幅度增加編譯的速度莫矗。

并發(fā)編譯

并發(fā)編譯并不復(fù)雜,只需要將上面處理單個(gè)jar/class的邏輯趣苏,并發(fā)處理狡相,最后阻塞等待所有任務(wù)結(jié)束即可食磕。看下偽代碼:

 WaitableExecutor waitableExecutor = WaitableExecutor.useGlobalSharedThreadPool();
//異步并發(fā)處理jar/class
waitableExecutor.execute(() -> {
   //jar織入字節(jié)碼
    return null;
});
waitableExecutor.execute(() -> {
   //file織入字節(jié)碼
    return null;
});  
//等待所有任務(wù)結(jié)束
waitableExecutor.waitForTasksWithQuickFail(true);

與增量編譯一樣彬伦,做一些實(shí)驗(yàn)對(duì)比滔悉,用數(shù)據(jù)說(shuō)話单绑。

試驗(yàn)次數(shù) 正常編譯 并發(fā)編譯
1 1309ms 856ms
2 1093ms 702ms
3 1153ms 790ms

使用ASM織入代碼

從前面幾個(gè)章節(jié)中,了解了自定義Plugin搂橙、Transform歉提、Transform的優(yōu)化,最后一步苔巨,就是對(duì)目標(biāo)類進(jìn)行改造,也就是對(duì)class文件進(jìn)行代碼織入

ASM簡(jiǎn)介

ASM官網(wǎng)中這樣介紹ASM

ASM is an all purpose Java bytecode manipulation and analysis framework. It can be used to modify existing classes or to dynamically generate classes, directly in binary form. ASM provides some common bytecode transformations and analysis algorithms from which custom complex transformations and code analysis tools can be built. ASM offers similar functionality as other Java bytecode frameworks, but is focused on performance. Because it was designed and implemented to be as small and as fast as possible, it is well suited for use in dynamic systems (but can of course be used in a static way too, e.g. in compilers).

ASM是用來(lái)對(duì)Java字節(jié)碼進(jìn)行修改和分析的框架侄泽。ASM可以用來(lái)修改已經(jīng)存在的類或者動(dòng)態(tài)生成類礁芦,它是直接對(duì)二進(jìn)制文件進(jìn)行操作的悼尾。ASM提供了一些常見的字節(jié)碼轉(zhuǎn)換和分析算法,從這些轉(zhuǎn)換和分析算法中構(gòu)建定制復(fù)雜的轉(zhuǎn)換和代碼分析工具闺魏。因?yàn)樗辉O(shè)計(jì)的和實(shí)現(xiàn)的非常的小和盡可能得快未状,所以它非常和用來(lái)動(dòng)態(tài)系統(tǒng)(但是也可以用在靜態(tài)的方式舷胜,例如在編譯時(shí))

常用字節(jié)碼框架

常用的字節(jié)碼框架就三個(gè)Aspectj、Javassist烹骨、ASM翻伺,三個(gè)都有什么區(qū)別呢沮焕?

織入代碼的時(shí)期

一圖勝千言,直接看圖


image

效率和學(xué)習(xí)成本

  • AspectJ:是一個(gè)代碼生成工具峦树,使用它定義的語(yǔ)法生成規(guī)則來(lái)編寫辣辫,基本上要掃描所有的文件魁巩,當(dāng)然AspectJx已經(jīng)實(shí)現(xiàn)的非常的好。最主要的是有個(gè)坑谷遂,在抖音目前的多module的工程上葬馋,是有很多坑的肾扰,例如,和后面的Transform過(guò)程有一些沖突集晚,導(dǎo)致代碼一直織入失敗窗悯,而且編譯的時(shí)長(zhǎng)也大大增加偷拔。
  • Javasist:直接操作修改編譯后的字節(jié)碼蒋院,而且可以自定義Transform,編譯時(shí)長(zhǎng)可以做很大空間的優(yōu)化悦污,就是織入代碼的效率不如ASM铸屉。
    有關(guān)javassits的使用可以看這篇文章

根據(jù)網(wǎng)上的信息切端,大神得出的數(shù)據(jù)結(jié)果,這里盜用一下踏枣,基本上有3倍的差別,文件越多钙蒙,ASM和Javasist的效率相差就越大。

ASM的用法

ASM框架中的核心類有以下幾個(gè):

  • ClassReader:用來(lái)解析編譯過(guò)的class字節(jié)碼文件
  • ClassWriter:用來(lái)重新構(gòu)建編譯后的類躬厌,比如修改類名马昨、屬性以及方法扛施,甚至可以生成新的類的字節(jié)碼文件
  • ClassVisitor:主要負(fù)責(zé)“拜訪”類成員信息。其中包括標(biāo)記在類上的注解疙渣、類的構(gòu)造方法匙奴、類的字段妄荔、類的方法泼菌、靜態(tài)代碼塊啦租。
  • AdviceAdapter:實(shí)現(xiàn)了MethodVisitor接口,主要負(fù)責(zé)“拜訪”方法的信息篷角,用來(lái)具體的方法字節(jié)碼操作焊刹。

ClassVisitor的全部方法如下内地,按一定的次序來(lái)遍歷類中的成員

image

讓我們簡(jiǎn)單寫個(gè)demo,這段代碼很簡(jiǎn)單阱缓,通過(guò)Visitor API讀取一個(gè)class的內(nèi)容非凌,保存到另一個(gè)文件中去

static void copy(File inputFile, File outputFile) {
        def weavedBytes = inputFile.bytes
        ClassReader classReader = new ClassReader(bytes)
        ClassWriter classWriter = new ClassWriter(classReader,
                ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS)

        DoubleClickCheckModifyClassAdapter classAdapter = new DoubleClickCheckModifyClassAdapter(classWriter)
        try {
            classReader.accept(classAdapter, ClassReader.EXPAND_FRAMES)
            weavedBytes = classWriter.toByteArray()
        } catch (Exception e) {
            println "Exception occurred when visit code \n " + e.printStackTrace()
        }

        outputFile.withOutputStream{
            it.write(weavedBytes)
        }
    }

首先荆针,我們通過(guò)ClassReader讀取某個(gè)class文件颁糟,然后定義一個(gè)ClassWriter,這個(gè)ClassWriter其實(shí)就是一個(gè)ClassVisitor的實(shí)現(xiàn)喉悴,負(fù)責(zé)將ClassReader傳遞過(guò)來(lái)的數(shù)據(jù)寫到一個(gè)字節(jié)流中,而真正觸發(fā)這個(gè)邏輯就是通過(guò)ClassWriter的accept方式箕肃。

上面代碼DoubleClickCheckModifyClassAdapter類婚脱,也是一個(gè)Visitor勺像,也就是我們自定義需要實(shí)現(xiàn)的功能。

最后吟宦,我們通過(guò)ClassWriter的toByteArray()篮洁,將從ClassReader傳遞到ClassWriter的字節(jié)碼導(dǎo)出殃姓,寫入新的文件即可。這樣我們就完成了字節(jié)碼的操作蜗侈,是不是感覺也不難炊邦。

ASM code

從上面的例子中焊切,可以看出來(lái),只有DoubleClickCheckModifyClassAdapter需要自己定義艇抠,其他的都由上面的模板來(lái)寫就行枣抱,來(lái)看下這個(gè)類是怎么實(shí)現(xiàn)的佩厚。


public class DoubleClickCheckModifyClassAdapter extends ClassVisitor implements Opcodes {

    public DoubleClickCheckModifyClassAdapter(ClassVisitor cv) {
        super(Opcodes.ASM5, cv);
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
        MethodVisitor methodVisitor = cv.visitMethod(access, name, desc, signature, exceptions);
        if ((ASMUtils.isPublic(access) && !ASMUtils.isStatic(access)) && 
                name.equals("onClick") && 
                desc.equals("(Landroid/view/View;)V")) {
            methodVisitor = new View$OnClickMethodVisitor(methodVisitor);
        }

        return methodVisitor;
    }
}

可以從上面的visitMethod方法中看到吆倦,我們只對(duì)View的onClick函數(shù)進(jìn)行代碼織入,然后再看下View$OnClickMethodVisitor的實(shí)現(xiàn)

public class View$OnClickMethodVisitor extends MethodVisitor {
    private boolean weaved;

    public View$OnClickMethodVisitor(MethodVisitor mv) {
        super(Opcodes.ASM5, mv);
    }

    @Override
    public void visitCode() {
        super.visitCode();
        if (weaved) return;

        AnnotationVisitor annotationVisitor =
                mv.visitAnnotation("L" + DoubleCheckConfig.checkClassAnnotation + ";", false);
        annotationVisitor.visitEnd();

        mv.visitMethodInsn(Opcodes.INVOKESTATIC, DoubleCheckConfig.checkClassPath, "isClickable", "()Z", false);
        Label l1 = new Label();
        mv.visitJumpInsn(Opcodes.IFNE, l1);
        mv.visitInsn(Opcodes.RETURN);
        mv.visitLabel(l1);
    }

    @Override
    public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
        /*Lcom/smartdengg/clickdebounce/Debounced;*/
        weaved = desc.equals("L" + DoubleCheckConfig.checkClassAnnotation + ";");
        return super.visitAnnotation(desc, visible);
    }

}

最主要的實(shí)現(xiàn)就是visitCode函數(shù)蚕泽,這里面,我們實(shí)現(xiàn)了代碼的織入须妻。

我們?cè)O(shè)想一下仔蝌,如果要對(duì)某個(gè)class進(jìn)行修改,那需要對(duì)字節(jié)碼具體做什么修改呢敛惊?最直觀的方法就是,先編譯生成目標(biāo)class绰更,然后看它的字節(jié)碼和原來(lái)class的字節(jié)碼有什么區(qū)別锡宋,但是這樣還不夠,其實(shí)我們最終并不是讀取字節(jié)碼特恬,而是使用ASM來(lái)修改。

問(wèn)題來(lái)了癌刽,我不懂ASM役首,對(duì)字節(jié)碼更是很陌生妒穴,怎么辦摊崭?難道我要從新學(xué)習(xí)一下字節(jié)碼讼油,才能進(jìn)行開發(fā)嗎?答案當(dāng)然不是呢簸,如果我們只是對(duì)字節(jié)碼做一些簡(jiǎn)單的操作,完全可以使用工具來(lái)幫我們完成

這里安利一個(gè)非常好用的工具根时,Intellij IDEA有個(gè)插件Asm Bytecode Outline,可以查看一個(gè)class文件的bytecode和ASM code蛤迎,同樣确虱,Android Studio同樣也有一個(gè)類似的插件ASM Bytecode Viewer實(shí)現(xiàn)了同樣的功能。

如何編寫ASM代碼

這是我們?cè)创a

public class A {
    static void toast(Context context) {
        Toast.makeText(context,"test",Toast.LENGTH_LONG).show();
    }
}

我們想通過(guò)字節(jié)碼插裝后變?yōu)?/p>

public class A {
    static void toast(Context context) {
        Log.i("tag","test");
        Toast.makeText(context,"test",Toast.LENGTH_LONG).show();
    }
}

也就是在toast函數(shù)的第一行插入Log代碼校辩。

  1. 我們將要插入的代碼先寫入源代碼中,在文件中右擊鼠標(biāo).
  2. 點(diǎn)擊ASM Bytecode Viewer
  3. 打開右側(cè)的ASM預(yù)覽界面辆童,就能看到對(duì)應(yīng)的ASM代碼
image

image

到此為止,貌似使用對(duì)比ASM code的方式把鉴,來(lái)實(shí)現(xiàn)字節(jié)碼修改也不難,但是庭砍,這種方式只是可以實(shí)現(xiàn)一些修改字節(jié)碼的基礎(chǔ)場(chǎng)景场晶,還有很多場(chǎng)景是需要對(duì)字節(jié)碼有一些基礎(chǔ)只是才能做到,而且诗轻,要閱讀懂ASM code,也是需要一定字節(jié)碼的知識(shí)凯旭。所以使套,如果要開發(fā)字節(jié)碼工程,還是需要學(xué)習(xí)一番字節(jié)碼的鞠柄。

實(shí)際應(yīng)用

Theory without practice is empty,practice without theory is blind

我們既然已經(jīng)了解了ASM的原理,那么我們應(yīng)用于實(shí)踐厌杜,我們不能為了學(xué)技術(shù)而學(xué)技術(shù)奉呛,技術(shù)最終是要服務(wù)于業(yè)務(wù)的夯尽,我們更加應(yīng)該從業(yè)務(wù)的角度出發(fā)瞧壮,來(lái)思考問(wèn)題和提升自己(題外話)匙握。

有一個(gè)場(chǎng)景,在可點(diǎn)擊的地方圈纺,經(jīng)常出現(xiàn)連擊秦忿,但是結(jié)果并不是我們想要的蛾娶,例如灯谣,跳轉(zhuǎn)到個(gè)人頁(yè)面蛔琅,我們快速點(diǎn)擊兩次,就會(huì)發(fā)現(xiàn)出現(xiàn)了兩個(gè)個(gè)人頁(yè)面罗售,需要back兩次才能回到之前的頁(yè)面辜窑,這個(gè)肯定是不符合預(yù)期的莽囤,幾乎在所有的點(diǎn)擊地方,都應(yīng)該做防抖動(dòng)的操作(抖動(dòng)朽缎,就是快速或者不小心在短時(shí)間內(nèi)多次點(diǎn)擊惨远,我也不知道為什么叫抖動(dòng),網(wǎng)上都這樣寫)北秽,現(xiàn)在有幾個(gè)思路:

  • Kotlin中實(shí)現(xiàn)view的擴(kuò)展函數(shù),這樣就可以統(tǒng)一的地方做防抖動(dòng)操作最筒,但是如果大家不是理解這個(gè)點(diǎn),有可能還會(huì)調(diào)用之前的點(diǎn)擊事件床蜘,那么還有可能出現(xiàn)這個(gè)問(wèn)題辙培。
  • Java中封裝一個(gè)工具類,要求每個(gè)開發(fā)人員在在OnClick中添加這個(gè)函數(shù)扬蕊,但是這個(gè)很容易被遺忘搀别,可操作性不大尾抑。
  • 使用AOP(AspectJ和ASM)歇父,在編譯期間再愈,將所有的Onclick函數(shù)中做判斷,而且ASM兼容Java和Kotlin翎冲。這里選擇ASM垂睬,上一章節(jié)已經(jīng)說(shuō)明原因

從以上幾點(diǎn)中府适,可以得出使用ASM是目前最好的方案羔飞。

參考文章

ASM 操作字節(jié)碼初探
一起玩轉(zhuǎn)Android項(xiàng)目中的字節(jié)碼
一文讀懂 AOP | 你想要的最全面 AOP 方法探討

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末檐春,一起剝皮案震驚了整個(gè)濱河市么伯,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌田柔,老刑警劉巖俐巴,帶你破解...
    沈念sama閱讀 206,839評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件硬爆,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡缀磕,警方通過(guò)查閱死者的電腦和手機(jī)缘圈,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門袜蚕,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)糟把,“玉大人牲剃,你說(shuō)我怎么就攤上這事≡涓担” “怎么了缠犀?”我有些...
    開封第一講書人閱讀 153,116評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)辨液。 經(jīng)常有香客問(wèn)我文判,道長(zhǎng)室梅,這世上最難降的妖魔是什么戏仓? 我笑而不...
    開封第一講書人閱讀 55,371評(píng)論 1 279
  • 正文 為了忘掉前任亡鼠,我火速辦了婚禮,結(jié)果婚禮上间涵,老公的妹妹穿的比我還像新娘仁热。我一直安慰自己勾哩,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,384評(píng)論 5 374
  • 文/花漫 我一把揭開白布思劳。 她就那樣靜靜地躺著迅矛,像睡著了一般潜叛。 火紅的嫁衣襯著肌膚如雪秽褒。 梳的紋絲不亂的頭發(fā)上威兜,一...
    開封第一講書人閱讀 49,111評(píng)論 1 285
  • 那天,我揣著相機(jī)與錄音椒舵,去河邊找鬼蚂踊。 笑死笔宿,一個(gè)胖子當(dāng)著我的面吹牛犁钟,可吹牛的內(nèi)容都是我干的措伐。 我是一名探鬼主播,決...
    沈念sama閱讀 38,416評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼侥加,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼捧存!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起昔穴,我...
    開封第一講書人閱讀 37,053評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎吗货,沒想到半個(gè)月后泳唠,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體宙搬,經(jīng)...
    沈念sama閱讀 43,558評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,007評(píng)論 2 325
  • 正文 我和宋清朗相戀三年勇垛,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了脖母。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,117評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡谆级,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出讼积,到底是詐尸還是另有隱情,我是刑警寧澤勤众,帶...
    沈念sama閱讀 33,756評(píng)論 4 324
  • 正文 年R本政府宣布舆绎,位于F島的核電站决摧,受9級(jí)特大地震影響亿蒸,放射性物質(zhì)發(fā)生泄漏掌桩。R本人自食惡果不足惜姑食,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,324評(píng)論 3 307
  • 文/蒙蒙 一波岛、第九天 我趴在偏房一處隱蔽的房頂上張望音半。 院中可真熱鬧,春花似錦曹鸠、人聲如沸煌茬。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,315評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春眠屎,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背改衩。 一陣腳步聲響...
    開封第一講書人閱讀 31,539評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工岖常, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留葫督,地道東北人竭鞍。 一個(gè)月前我還...
    沈念sama閱讀 45,578評(píng)論 2 355
  • 正文 我出身青樓橄镜,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親蛉鹿。 傳聞我的和親對(duì)象是個(gè)殘疾皇子滨砍,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,877評(píng)論 2 345

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