一、開源背景
大家在自己寫
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.gradle
中 apply
一下我的外掛即可!!!
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 task
的 build.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 傳送門.