如何簡單方便地Hook Gradle插件?
前言
很多時(shí)候系統(tǒng)處于安全考慮,將很多東西對(duì)外隱藏,而有時(shí)我們偏偏又不得不去使用這些隱藏的東西。甚至,我們希望向系統(tǒng)中注入一些自己的代碼,修改原有代碼的邏輯,以提高程序的靈活性,這時(shí)候就需要用到代碼Hook。
在Java或者Kotlin代碼中,代碼Hook有多種方案,比如反射,動(dòng)態(tài)代理,或者通過修改字節(jié)碼來實(shí)現(xiàn)HOOK,那么如果我們想要修改Gradle插件的代碼,該怎么實(shí)現(xiàn)呢?
簡單使用
我們首先來看一個(gè)簡單的例子,大家肯定都用過com.android.application插件,如果我們想要在這個(gè)插件中添加一些代碼,可以怎么操作呢?修改方式非常簡單
項(xiàng)目中添加 buildSrc模塊buildSrc中添加com.android.tools.build:gradle:7.0.2依賴在 buildSrc中添加與插件中同名的AppPlugin即可,如下所示
package com.android.build.gradle
import org.gradle.api.Project
class AppPlugin: BasePlugin() {
override fun apply(project: Project) {
super.apply(project)
println("hook AppPlugin demo")
project.apply(INTERNAL_PLUGIN_ID)
}
}
private val INTERNAL_PLUGIN_ID = mapOf("plugin" to "com.android.internal.application")
然后我們?cè)偻揭幌马?xiàng)目,就可以發(fā)現(xiàn)hook AppPlugin demo的日志可以打印出來了,就這樣在AppPlugin中添加了我們想要的邏輯
在了解怎么使用了之后,我們?cè)賮矸治鱿聻槭裁催@樣做就可以覆蓋插件中的AppPlugin,我們首先需要了解下Gradle插件到底是怎么運(yùn)行起來的
Gradle運(yùn)行的入口是什么?
我們都知道,Java運(yùn)行需要一個(gè)main函數(shù),Groovy作為一個(gè)JVM語言,相信也是一樣的,那么我們是怎么調(diào)用到Groovy的main函數(shù)的呢?
在我們運(yùn)行Gradle的時(shí)候,都是通過gradlew來運(yùn)行的,gradlew其實(shí)是對(duì)gradle的一個(gè)包裝,本質(zhì)上就是一個(gè)shell腳本
exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
可以看出,其實(shí)就是調(diào)用了GradleWrapperMain并傳遞給它一系列參數(shù),那我們?cè)賮砜聪?code style="font-size: 14px;overflow-wrap: break-word;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;background-color: rgba(27, 31, 35, 0.05);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(239, 112, 96);">GradleWrapperMain
public class GradleWrapperMain {
......
//執(zhí)行 gradlew 腳本命令時(shí)觸發(fā)調(diào)用的入口。
public static void main(String[] args) throws Exception {
......
//調(diào)用BootstrapMainStarter
wrapperExecutor.execute(
args,
new Install(logger, new Download(logger, "gradlew", wrapperVersion()), new PathAssembler(gradleUserHome)),
new BootstrapMainStarter());
}
}
public class BootstrapMainStarter {
public void start(String[] args, File gradleHome) throws Exception {
//調(diào)用GradleMain的main方法
Class<?> mainClass = contextClassLoader.loadClass("org.gradle.launcher.GradleMain");
Method mainMethod = mainClass.getMethod("main", String[].class);
mainMethod.invoke(null, new Object[]{args});
}
......
}
可以看出
gradlew其實(shí)就是調(diào)用到了GradlewWrapperMain的main方法然后再通過 BootstrapMainStarter方法調(diào)用到GradleMain,這里才是Gradle執(zhí)行真正的入口
當(dāng)前插件是怎樣調(diào)用的?
上面介紹了Gradle運(yùn)行了的入口,但是要從入口跟代碼跟到我們插件加載的入口是非常麻煩的,我們換個(gè)思路,看下AppPlugin是怎么被加載的
class AppPlugin: BasePlugin() {
override fun apply(project: Project) {
//...
RuntimeException().printStackTrace()
}
}
我們?cè)诩虞dAppPlugin時(shí)通過以下方式直接打印出堆棧即可,堆棧如下所示:
java.lang.RuntimeException
at com.android.build.gradle.AppPlugin.apply(AppPlugin.kt:9)
at com.android.build.gradle.AppPlugin.apply(AppPlugin.kt:5)
at org.gradle.api.internal.plugins.ImperativeOnlyPluginTarget.applyImperative(ImperativeOnlyPluginTarget.java:43)
...
at org.gradle.configuration.internal.DefaultUserCodeApplicationContext.apply(DefaultUserCodeApplicationContext.java:43)
at org.gradle.api.internal.plugins.DefaultPluginManager.doApply(DefaultPluginManager.java:156)
at org.gradle.api.internal.plugins.DefaultPluginManager.apply(DefaultPluginManager.java:127)
...
at org.gradle.configuration.BuildTreePreparingProjectsPreparer.prepareProjects(BuildTreePreparingProjectsPreparer.java:64)
at org.gradle.configuration.BuildOperationFiringProjectsPreparer$ConfigureBuild.run(BuildOperationFiringProjectsPreparer.java:52)
...
通過這些堆棧,我們就可以看出AppPlugin是怎么一步一步被加載的,其中要注意到BuildTreePreparingProjectsPreparer和DefaultPluginManager兩個(gè)步驟,分別承擔(dān)構(gòu)建classloader父子關(guān)系與設(shè)置當(dāng)前線程上下文classloader,感興趣的同學(xué)可以直接查看源碼
Gradle類加載機(jī)制
我們通過在buildSrc中添加同名類的方式就可以實(shí)現(xiàn)覆蓋插件中代碼的效果,猜想應(yīng)該是通過類似Java的類加載機(jī)制實(shí)現(xiàn),我們首先打印下app模塊的classLoader
fun printClassloader(){
println("classloader:"+this.javaClass.classLoader)
println("classloader parent:"+this.javaClass.classLoader.parent)
println("classloader grantparent:"+this.javaClass.classLoader.parent.parent)
}
如上,分別打印classloader與父祖classloader,輸出結(jié)果如下
classloader:VisitableURLClassLoader(ClassLoaderScopeIdentifier.Id{coreAndPlugins:settings[:]:settings[:buildSrc]:buildSrc[:buildSrc]:root-project[:buildSrc]:Project/TopLevel/stage2(local)})
classloader parent:VisitableURLClassLoader(ClassLoaderScopeIdentifier.Id{coreAndPlugins:settings[:]:settings[:buildSrc]:buildSrc[:buildSrc]:root-project[:buildSrc](export)})
classloader grantparent:CachingClassLoader(FilteringClassLoader(VisitableURLClassLoader(legacy-mixin-loader)))
可以看出,其實(shí)buildSrc模塊的classloader其實(shí)是當(dāng)前模塊的父classLoader,在雙親委托機(jī)制下,會(huì)首先委托給父classloader來查找,那么在buildSrc模塊中已經(jīng)加載了的類自然會(huì)覆蓋插件中的類了,也就可以輕松實(shí)現(xiàn)對(duì)插件代碼邏輯的修改
總結(jié)
由于在Gradle代碼運(yùn)行過程中,buildSrc模塊的classloader是項(xiàng)目中module的父classloader,因此在加載類的過程中,會(huì)首先委托給父classloader來查找,如果我們?cè)?code style="font-size: 14px;overflow-wrap: break-word;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;background-color: rgba(27, 31, 35, 0.05);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(239, 112, 96);">buildSrc中存在一個(gè)與插件同名且包名也相同的類,就可以覆蓋插件中的代碼,從而達(dá)到修改原有代碼邏輯的目的
最后歡迎大家加入 音視頻開發(fā)進(jìn)階 知識(shí)星球 ,這里有知識(shí)干貨、編程答疑、開發(fā)教程,還有很多精彩分享。
更多內(nèi)容可以在星球菜單中找到,隨著時(shí)間推移,干貨也會(huì)越來越多!??!

給出 10元 優(yōu)惠券,漲價(jià)在即,目前還是白菜價(jià),基本上提幾個(gè)問題就回本,投資自己就是最好的投資?。?!

加我微信 ezglumes ,拉你進(jìn)技術(shù)交流群
推薦閱讀:
音視頻開發(fā)工作經(jīng)驗(yàn)分享 || 視頻版
開通專輯 | 細(xì)數(shù)那些年寫過的技術(shù)文章專輯
Android NDK 免費(fèi)視頻在線學(xué)習(xí)!?。?/a>
推薦幾個(gè)堪稱教科書級(jí)別的 Android 音視頻入門項(xiàng)目
覺得不錯(cuò),點(diǎn)個(gè)在看唄~

