自定義 gradle plugin,教你如何 hook 系統 task 和字節碼

1、開源背景

你們在本身寫 library 的時候估計也遇到過這種困惑:一個 library 中的某個類中有些方法或類只想給該 library 中的類使用,並不想暴露出去,可是因爲項目的包的層級關係,不得不把方法寫爲 public ,致使暴露給了外界!!!java

當時這個問題確實困惑了我一段時間,總不能本身爲了避免對外暴露,把 方法/類 寫爲 非public 吧?那我本身的 library 如何去調用呢?難道本身寫反射?太蠢了吧。android

說時遲那時快,就想着本身搞個什麼騷操做 hook 一下 library 生成的 jar/aar 包吧。腦殼一熱大腿一拍,媽的,寫個插件吧!git

因而,這邊就有了本篇文章的主角 Seeker(Github 傳送門)github

2、自我反思

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

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

3、解決思路

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

  • hook library 最後打包成 aar/jar 的源碼,改變方法的 modifier
  • build 過程直接報錯,告訴用戶這個方法不能夠調用。

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

有了大體方向後,開始準備擼代碼,首先,須要先設計供用戶使用的 Api 層,畢竟大佬們用的好纔是真的好 ;)ide

我定義了一個 @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 的執行過程

4、獲取 @Hide

咱們給方法加上 @Hide 以後,須要找到這些方法,給後面 hook 字節碼的時候用,要作到這一步還有什麼比 APT 更加合適的呢。

APT 的使用較爲簡單,沒什麼須要注意的地方,在此處省略,有興趣的能夠自行了解一下。

總之,咱們須要在這一步獲取到全部含有 @Hide 的方法,而後保存一份到本地,這裏我保存的是 json 文件。

5、hook 過程

這裏咱們須要拆分爲兩步:

  • hook uploadArchives task
  • hook 字節碼文件

由於咱們最終但願打包出來的 jar/aar 發生改變,而打包是經過 uploadArchives task 作的,因此咱們須要對這個 task 進行分析並在某一步。

5.一、尋找須要 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的使用方式也在此再也不敘述了,有興趣能夠自行去看下,下面列出一些我在寫這個插件過程當中遇到的一些問題.


問題1、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)
           }
       })
       // ...
   }
複製代碼

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

問題2、方法變爲非public了,調用該方法的地方怎麼辦?

對於這個問題,沒有很優雅的處理方式,我這邊在 APT 過程當中生成了一個反射代理類,一個 @Hide 對應一個反射的方法,而且會對反射進行緩存,保證了每一個方法的反射只會調用一次,保證性能.

6、效果演示

library 的目錄結構

其中的部分類

經過該插件生成的 .jar 的目錄結構

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

打出的 jar 包中的部分源碼

調用 @Hide 的新舊類對比

從上面的圖片能夠看出,生成的 aar/jar 的字節碼中,方法的 modifier 已經變爲指定的 modifier 了,而且調用的地方也使用反射代理類去進行調用了.

7、總結

對於此次開源來講,整體是失敗的,可是在寫這個開源的過程當中,確實學到了不少東西,知道了如何去 hook 系統的 task,如何去 hook 字節碼等,我以爲更重要的是解決問題的思路,有了問題,如何一步步的去解決它,想自定義一個 gradle 插件,應該從什麼地方入手等.

最後,若是你們在看 Seeker 源碼的過程當中遇到任何問題,能夠直接提交 issue,若是對於文章裏面某些內容感興趣的也能夠直接評論哈,我會看狀況抽時間寫出相應的內容,若是遇到關於 gradle 的一些疑問或者遇到問題,我們也能夠進行探討~互相學習,互相傷害~

再次厚顏無恥的放上本身的 Seeker Github 傳送門.

相關文章
相關標籤/搜索