1. 程式人生 > >自定義 gradle plugin,教你如何 hook 系統 task 和位元組碼

自定義 gradle plugin,教你如何 hook 系統 task 和位元組碼

一、開源背景

大家在自己寫 library 的時候估計也遇到過這種困惑:一個 library 中的某個類中有些方法或類只想給該 library 中的類使用,並不想暴露出去,但是由於專案的包的層級關係,不得不把方法寫為 public ,導致暴露給了外界!!!

當時這個問題確實困惑了我一段時間,總不能自己為了不對外暴露,把 方法/類 寫為 非public 吧?那我自己的 library 如何去呼叫呢?難道自己寫反射?太蠢了吧。

說時遲那時快,就想著自己搞個什麼騷操作 hook 一下 library 生成的 jar/aar 包吧。腦袋一熱大腿一拍,媽的,寫個外掛吧!

於是,這邊就有了本篇文章的主角

Seeker(Github 傳送門)

二、自我反思

在開始之前,先在這裡認個錯,之前腦袋熱的有點快,其實這個問題早就有了解決的方案,@RestrictTo,有興趣的可以點進去了解一下。

在解決問題之前,建議大家多去搜一下有沒有已有的解決方案,我是馬上寫完的時候才發現有 @RestrictTo ,吐血ing,中途有點難受,差點憋出內傷,最後還是自我安慰,就當學習 gradle 了 TAT...

三、解決思路

在我看來要解決這個問題有兩個方向:

  • hook library 最後打包成 aar/jar 的原始碼,改變方法的 modifier
  • build
    過程直接報錯,告訴使用者這個方法不可以呼叫。

由於第二種方案有點暴力,太過不近人情,既然不讓我用,你為啥要暴露出來?暴露出來又報錯是什麼鬼?處於以上考慮,我選擇了一條艱難的道路。

有了大致方向後,開始準備擼程式碼,首先,需要先設計供使用者使用的 Api 層,畢竟大佬們用的好才是真的好 ;)

我定義了一個 @Hide 註解,引數是一個 enum 型別,可以指定 modifier,程式碼如下:

public enum Modifier {
    /** The modifier {@code public} */ PUBLIC,
    /** The modifier {@code
protected} */
PROTECTED, /** The modifier {@code private} */ PRIVATE, /** The modifier with the default value */ DEFAULT; } @Retention(RetentionPolicy.CLASS) @Target(ElementType.METHOD) public @interface Hide { Modifier value() default Modifier.PRIVATE; } 複製程式碼

新增 @Hide 註解到需要 hook 的方法上面,你也可以指定為不同的 modifier ,最後在你的 library build.gradleapply 一下我的外掛即可!!!

Api 設計的很簡潔,對業務也沒有什麼侵入性,因為我們的 library 最後是需要打包成 aar/jar 給其他人呼叫的,所以歸根結底我們需要 hook 一下 uploadArchives task 的執行過程

四、獲取 @Hide

我們給方法加上 @Hide 之後,需要找到這些方法,給後面 hook 位元組碼的時候用,要做到這一步還有什麼比 APT 更加合適的呢。

APT 的使用較為簡單,沒什麼需要注意的地方,在此處省略,有興趣的可以自行了解一下。

總之,我們需要在這一步獲取到所有含有 @Hide 的方法,然後儲存一份到本地,這裡我儲存的是 json 檔案。

五、hook 過程

這裡我們需要拆分為兩步:

  • hook uploadArchives task
  • hook 位元組碼檔案

因為我們最終希望打包出來的 jar/aar 發生改變,而打包是通過 uploadArchives task 做的,所以我們需要對這個 task 進行分析並在某一步。

5.1、尋找需要 hook 的 task

要分析這個 task ,我們需要先知道這個 task 依賴了哪些 task

含有 uploadArchives taskbuild.gradle 中加入以下程式碼,列印下 uploadArchives 的依賴。

void printTaskDependency(Task task) {
    task.getTaskDependencies().getDependencies(task).any() {
        println(">>${it.path}")
        printTaskDependency(it)
    }
}
gradle.getTaskGraph().whenReady {
    printTaskDependency project.tasks.findByName('uploadArchives')
}
複製程式碼

接著,隨便執行一個 gradle 命令,為了方便,直接執行 ./gradlew clean ,檢視列印的日誌。

uploadArchives 依賴的 tasks:點選檢視詳細內容
>>:mock-lib:sourcesJar
>>:mock-lib:bundleRelease
>>:mock-lib:mergeReleaseConsumerProguardFiles
>>:mock-lib:processReleaseManifest
>>:mock-lib:checkReleaseManifest
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:generateReleaseRFile
>>:mock-lib:packageReleaseResources
>>:mock-lib:generateReleaseResources
>>:mock-lib:compileReleaseRenderscript
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:generateReleaseResValues
>>:mock-lib:processReleaseManifest
>>:mock-lib:checkReleaseManifest
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:platformAttrExtractor
>>:mock-lib:packageReleaseResources
>>:mock-lib:generateReleaseResources
>>:mock-lib:compileReleaseRenderscript
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:generateReleaseResValues
>>:mock-lib:prepareLintJar
>>:mock-lib:extractReleaseAnnotations
>>:mock-lib:compileReleaseJavaWithJavac
>>:mock-lib:javaPreCompileRelease
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:generateReleaseRFile
>>:mock-lib:packageReleaseResources
>>:mock-lib:generateReleaseResources
>>:mock-lib:compileReleaseRenderscript
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:generateReleaseResValues
>>:mock-lib:processReleaseManifest
>>:mock-lib:checkReleaseManifest
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:platformAttrExtractor
>>:mock-lib:generateReleaseBuildConfig
>>:mock-lib:checkReleaseManifest
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:compileReleaseAidl
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:compileReleaseRenderscript
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:generateReleaseSources
>>:mock-lib:generateReleaseRFile
>>:mock-lib:packageReleaseResources
>>:mock-lib:generateReleaseResources
>>:mock-lib:compileReleaseRenderscript
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:generateReleaseResValues
>>:mock-lib:processReleaseManifest
>>:mock-lib:checkReleaseManifest
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:platformAttrExtractor
>>:mock-lib:prepareLintJar
>>:mock-lib:compileReleaseRenderscript
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:generateReleaseBuildConfig
>>:mock-lib:checkReleaseManifest
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:compileReleaseAidl
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:generateReleaseRFile
>>:mock-lib:packageReleaseResources
>>:mock-lib:generateReleaseResources
>>:mock-lib:compileReleaseRenderscript
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:generateReleaseResValues
>>:mock-lib:processReleaseManifest
>>:mock-lib:checkReleaseManifest
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:platformAttrExtractor
>>:mock-lib:generateReleaseBuildConfig
>>:mock-lib:checkReleaseManifest
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:compileReleaseAidl
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:compileReleaseRenderscript
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:transformClassesAndResourcesWithSyncLibJarsForRelease
>>:mock-lib:extractReleaseAnnotations
>>:mock-lib:compileReleaseJavaWithJavac
>>:mock-lib:javaPreCompileRelease
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:generateReleaseRFile
>>:mock-lib:packageReleaseResources
>>:mock-lib:generateReleaseResources
>>:mock-lib:compileReleaseRenderscript
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:generateReleaseResValues
>>:mock-lib:processReleaseManifest
>>:mock-lib:checkReleaseManifest
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:platformAttrExtractor
>>:mock-lib:generateReleaseBuildConfig
>>:mock-lib:checkReleaseManifest
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:compileReleaseAidl
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:compileReleaseRenderscript
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:generateReleaseSources
>>:mock-lib:generateReleaseRFile
>>:mock-lib:packageReleaseResources
>>:mock-lib:generateReleaseResources
>>:mock-lib:compileReleaseRenderscript
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:generateReleaseResValues
>>:mock-lib:processReleaseManifest
>>:mock-lib:checkReleaseManifest
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:platformAttrExtractor
>>:mock-lib:prepareLintJar
>>:mock-lib:compileReleaseRenderscript
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:generateReleaseBuildConfig
>>:mock-lib:checkReleaseManifest
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:compileReleaseAidl
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:generateReleaseRFile
>>:mock-lib:packageReleaseResources
>>:mock-lib:generateReleaseResources
>>:mock-lib:compileReleaseRenderscript
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:generateReleaseResValues
>>:mock-lib:processReleaseManifest
>>:mock-lib:checkReleaseManifest
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:platformAttrExtractor
>>:mock-lib:generateReleaseBuildConfig
>>:mock-lib:checkReleaseManifest
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:compileReleaseAidl
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:compileReleaseRenderscript
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:compileReleaseJavaWithJavac
>>:mock-lib:javaPreCompileRelease
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:generateReleaseRFile
>>:mock-lib:packageReleaseResources
>>:mock-lib:generateReleaseResources
>>:mock-lib:compileReleaseRenderscript
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:generateReleaseResValues
>>:mock-lib:processReleaseManifest
>>:mock-lib:checkReleaseManifest
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:platformAttrExtractor
>>:mock-lib:generateReleaseBuildConfig
>>:mock-lib:checkReleaseManifest
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:compileReleaseAidl
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:compileReleaseRenderscript
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:generateReleaseSources
>>:mock-lib:generateReleaseRFile
>>:mock-lib:packageReleaseResources
>>:mock-lib:generateReleaseResources
>>:mock-lib:compileReleaseRenderscript
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:generateReleaseResValues
>>:mock-lib:processReleaseManifest
>>:mock-lib:checkReleaseManifest
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:platformAttrExtractor
>>:mock-lib:prepareLintJar
>>:mock-lib:compileReleaseRenderscript
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:generateReleaseBuildConfig
>>:mock-lib:checkReleaseManifest
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:compileReleaseAidl
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:transformResourcesWithMergeJavaResForRelease
>>:mock-lib:processReleaseJavaRes
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:compileReleaseJavaWithJavac
>>:mock-lib:javaPreCompileRelease
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:generateReleaseRFile
>>:mock-lib:packageReleaseResources
>>:mock-lib:generateReleaseResources
>>:mock-lib:compileReleaseRenderscript
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:generateReleaseResValues
>>:mock-lib:processReleaseManifest
>>:mock-lib:checkReleaseManifest
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:platformAttrExtractor
>>:mock-lib:generateReleaseBuildConfig
>>:mock-lib:checkReleaseManifest
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:compileReleaseAidl
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:compileReleaseRenderscript
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:generateReleaseSources
>>:mock-lib:generateReleaseRFile
>>:mock-lib:packageReleaseResources
>>:mock-lib:generateReleaseResources
>>:mock-lib:compileReleaseRenderscript
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:generateReleaseResValues
>>:mock-lib:processReleaseManifest
>>:mock-lib:checkReleaseManifest
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:platformAttrExtractor
>>:mock-lib:prepareLintJar
>>:mock-lib:compileReleaseRenderscript
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:generateReleaseBuildConfig
>>:mock-lib:checkReleaseManifest
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:compileReleaseAidl
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:compileReleaseAidl
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:packageReleaseRenderscript
>>:mock-lib:transformNativeLibsWithSyncJniLibsForRelease
>>:mock-lib:transformNativeLibsWithMergeJniLibsForRelease
>>:mock-lib:mergeReleaseJniLibFolders
>>:mock-lib:generateReleaseAssets
>>:mock-lib:compileReleaseShaders
>>:mock-lib:mergeReleaseShaders
>>:mock-lib:compileReleaseNdk
>>:mock-lib:preReleaseBuild
>>:mock-lib:preBuild
>>:mock-lib:packageReleaseAssets
>>:mock-lib:generateReleaseAssets
>>:mock-lib:compileReleaseShaders
>>:mock-lib:mergeReleaseShaders
>>:mock-lib:compileReleaseShaders
>>:mock-lib:mergeReleaseShaders
複製程式碼

通過上面列印的資訊可以看到依賴的 task 還是蠻多的,我們從前往後一步步排查。注:每個人打印出來的內容可能不太一樣,定義的 task 可能不同。

sourceJar : 先看第一個 task sourceJar,這個 task 是,我這邊自己定義的,用於打包 java 原始碼的 task,因為是自定義的,所以可以忽略,直接看下一個 task 。

bundleRelease: 這個 task 是做什麼的呢?大概從字面意思可以猜出和打包有關,我們在 build.gradle 中輸入 bundle 看看 IDE 的提示。

幸運!果然有相應的提示,直接看到了這個對應的是 AndroidZip 類,毋庸置疑,這個肯定和打包有關。

再往前看看其他的 task: 放眼望去,基本上都是 package*/compile*/generate*/ 之類開頭的,看名字就可以才出來這些是做什麼的,(手動滑稽臉),我們應該是找到了需要 hook 的 task 了!!!

結果上面的分析和大膽的猜測,我們需要 hook 一下 bundle* 這個task,這個 task 既然是打包用的,那麼我們需要在這個打包之前找到位元組碼存放的位置,然後去 hook 它!!!

5.2 hook task

自定義 gradle plugin 的過程和 gradle 的生命週期等等在此處不進行敘述了,有興趣可以去網上自行了解。

我們在自定義的外掛的 afterEvaluate 中尋找 bundle* task:

mProject.afterEvaluate {
    processVariant()
}
void processVariant() throws NotFoundException {
    // variant 一般有 debug 和 release
    mProject.android.libraryVariants.all { variant ->
        process(variant)
    }
}
void process(variant){
    String taskPath = 'bundle' + mVariant.name.capitalize()
    Task bundleTask = mProject.tasks.findByPath(taskPath)
    if (bundleTask == null) {
        throw new RuntimeException("Can not find task ${taskPath}!")
    }
    bundleTask.doFirst {
        // do hook
    }    
}
複製程式碼

我們在打包之前執行位元組碼的 hook 即可。

5.3 hook 位元組碼檔案deng

要 hook 位元組碼檔案,我們這邊需要考慮以下幾個事情。

  • 位元組碼檔案的儲存路徑在哪?json file
  • 如何改變位元組碼檔案?
  • 要如何改變?

位元組碼檔案的儲存路徑在哪?

通過一系列查詢(我沒有找到如何在 gradle 中獲取該路徑的方法,有大佬知道麻煩告知),最終找到了相對路徑:/intermediates/packaged-classes/(release/debug)

如何改變位元組碼檔案?

這邊引入了一個第三方庫 javassist 去改變位元組碼檔案。

要如何改變?

通過之前 APT 期間生成的 json 檔案,遍歷位元組碼檔案,找到相應的方法後,改變 modifier@Hide 對應的 modifier,然後刪除 @Hide .

以上問題我們都知道解決的方案了,剩下的就是實施過程了,javassist的使用方式也在此不再敘述了,有興趣可以自行去看下,下面列出一些我在寫這個外掛過程中遇到的一些問題.


問題一、javassist 尋找類的問題

javassist 中,我們去尋找某一個類需要通過一個類 ClassPool 來進行,再次之前我們需要把需要用到的類的 位元組碼路徑 匯入到 ClassPool 中,在這裡,遇到了第一個問題,在 gradle 專案中有的類是直接快取在 ~/.gradle/ 資料夾下的,有的類引用的是專案 libs 目錄下的,並且有的是 .jar 包,有的是 .aar 包,我們如何去把這些類一一匯入?

回答: 獲取 gradle 的 dependencies 依賴,然後獲取依賴的路徑,然後加上本地的位元組碼檔案,如果是 .jar 檔案,則直接解壓到某一個特定的臨時資料夾中(task執行完畢後需要刪除這些臨時檔案),如果是 .aar 檔案,則先解壓 .aar 後再解壓其中的 classes.jar 檔案.

   // 獲取 gradle dependencies 的過程
   private List<Configuration> mCopyDependencies
   private void copyDependencies(Configuration configuration) {
       if (configuration == null) {
           return
       }
       Configuration copyConf = null
       try {
           copyConf = mProject.configurations.getByName("${configuration.name}Copy")
       } catch (Exception ignore) {
       }
       if (copyConf == null) {
           copyConf = mProject.configurations.create("${configuration.name}Copy")
       }
       copyConf.visible = false
       copyConf.extendsFrom configuration
       mCopyDependencies.add(copyConf)
   }
   private void configureDependencies() {
       mCopyDependencies = new ArrayList<>()
       copyDependencies(mProject.configurations.getByName("implementation"))
       copyDependencies(mProject.configurations.getByName("api"))
       copyDependencies(mProject.configurations.getByName("compile"))
       copyDependencies(mProject.configurations.getByName("compileOnly"))
       copyDependencies(mProject.configurations.getByName("provided"))
   }
複製程式碼
    // 獲取 dependencies 的本地路徑
    // 該方法執行在 afterEvaluate 中
    private void resolveArtifacts() {
       def set = new HashSet<>()
       mCopyDependencies.forEach({
           it.each {
               set.add(it.path)
           }
       })
       // ...
   }
複製程式碼

在此期間,你可以獲取/更改/刪除你依賴的第三方庫,根據需求不同,可以做任何操作.

問題二、方法變為非public了,呼叫該方法的地方怎麼辦?

對於這個問題,沒有很優雅的處理方式,我這邊在 APT 過程中生成了一個反射代理類,一個 @Hide 對應一個反射的方法,並且會對反射進行快取,保證了每個方法的反射只會呼叫一次,保證效能.

六、效果演示

library 的目錄結構

其中的部分類

通過該外掛生成的 .jar 的目錄結構

可以看到,這邊多了兩個 _*RefDelegate 類,這就是生成的反射代理類.

打出的 jar 包中的部分原始碼

呼叫 @Hide 的新舊類對比

從上面的圖片可以看出,生成的 aar/jar 的位元組碼中,方法的 modifier 已經變為指定的 modifier 了,並且呼叫的地方也使用反射代理類去進行呼叫了.

七、總結

對於這次開源來說,總體是失敗的,但是在寫這個開源的過程中,確實學到了很多東西,知道了如何去 hook 系統的 task,如何去 hook 位元組碼等,我覺得更重要的是解決問題的思路,有了問題,如何一步步的去解決它,想自定義一個 gradle 外掛,應該從什麼地方入手等.

最後,如果大家在看 Seeker 原始碼的過程中遇到任何問題,可以直接提交 issue,如果對於文章裡面某些內容感興趣的也可以直接評論哈,我會看情況抽時間寫出相應的內容,如果遇到關於 gradle 的一些疑問或者遇到問題,咱們也可以進行探討~互相學習,互相傷害~

再次厚顏無恥的放上自己的 Seeker Github 傳送門.