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

JingYeoh發表於2018-11-27

一、開源背景

大家在自己寫 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 的執行過程

自定義 gradle plugin,教你如何 hook 系統 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 的提示。

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

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

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

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

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

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 的目錄結構

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

其中的部分類

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

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

自定義 gradle plugin,教你如何 hook 系統 task 和位元組碼
可以看到,這邊多了兩個 _*RefDelegate 類,這就是生成的反射代理類.

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

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

呼叫 @Hide 的新舊類對比

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

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

七、總結

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

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

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

相關文章