如何简单方便地Hook Gradle插件?
共 6241字,需浏览 13分钟
·
2022-05-13 22:55
前言
很多时候系统处于安全考虑,将很多东西对外隐藏,而有时我们偏偏又不得不去使用这些隐藏的东西。甚至,我们希望向系统中注入一些自己的代码,修改原有代码的逻辑,以提高程序的灵活性,这时候就需要用到代码Hook
。
在Java
或者Kotlin
代码中,代码Hook
有多种方案,比如反射,动态代理,或者通过修改字节码来实现HOOK
,那么如果我们想要修改Gradle
插件的代码,该怎么实现呢?
简单使用
我们首先来看一个简单的例子,大家肯定都用过com.android.application
插件,如果我们想要在这个插件中添加一些代码,可以怎么操作呢?修改方式非常简单
项目中添加 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")
然后我们再同步一下项目,就可以发现hook AppPlugin demo
的日志可以打印出来了,就这样在AppPlugin
中添加了我们想要的逻辑
在了解怎么使用了之后,我们再来分析下为什么这样做就可以覆盖插件中的AppPlugin
,我们首先需要了解下Gradle
插件到底是怎么运行起来的
Gradle
运行的入口是什么?
我们都知道,Java
运行需要一个main
函数,Groovy
作为一个JVM
语言,相信也是一样的,那么我们是怎么调用到Groovy
的main函数的
呢?
在我们运行Gradle
的时候,都是通过gradlew
来运行的,gradlew
其实是对gradle
的一个包装,本质上就是一个shell
脚本
exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
可以看出,其实就是调用了GradleWrapperMain
并传递给它一系列参数,那我们再来看下GradleWrapperMain
public class GradleWrapperMain {
......
//执行 gradlew 脚本命令时触发调用的入口。
public static void main(String[] args) throws Exception {
......
//调用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 {
//调用GradleMain的main方法
Class<?> mainClass = contextClassLoader.loadClass("org.gradle.launcher.GradleMain");
Method mainMethod = mainClass.getMethod("main", String[].class);
mainMethod.invoke(null, new Object[]{args});
}
......
}
可以看出
gradlew
其实就是调用到了GradlewWrapperMain
的main
方法然后再通过 BootstrapMainStarter
方法调用到GradleMain
,这里才是Gradle
执行真正的入口
当前插件是怎样调用的?
上面介绍了Gradle
运行了的入口,但是要从入口跟代码跟到我们插件加载的入口是非常麻烦的,我们换个思路,看下AppPlugin
是怎么被加载的
class AppPlugin: BasePlugin() {
override fun apply(project: Project) {
//...
RuntimeException().printStackTrace()
}
}
我们在加载AppPlugin
时通过以下方式直接打印出堆栈即可,堆栈如下所示:
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
两个步骤,分别承担构建classloader
父子关系与设置当前线程上下文classloader
,感兴趣的同学可以直接查看源码
Gradle
类加载机制
我们通过在buildSrc
中添加同名类的方式就可以实现覆盖插件中代码的效果,猜想应该是通过类似Java
的类加载机制实现,我们首先打印下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
,输出结果如下
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)))
可以看出,其实buildSrc
模块的classloader
其实是当前模块的父classLoader
,在双亲委托机制下,会首先委托给父classloader
来查找,那么在buildSrc
模块中已经加载了的类自然会覆盖插件中的类了,也就可以轻松实现对插件代码逻辑的修改
总结
由于在Gradle
代码运行过程中,buildSrc
模块的classloader
是项目中module
的父classloader
,因此在加载类的过程中,会首先委托给父classloader
来查找,如果我们在buildSrc
中存在一个与插件同名且包名也相同的类,就可以覆盖插件中的代码,从而达到修改原有代码逻辑的目的
最后欢迎大家加入 音视频开发进阶 知识星球 ,这里有知识干货、编程答疑、开发教程,还有很多精彩分享。
更多内容可以在星球菜单中找到,随着时间推移,干货也会越来越多!!!
给出 10元 优惠券,涨价在即,目前还是白菜价,基本上提几个问题就回本,投资自己就是最好的投资!!!
加我微信 ezglumes ,拉你进技术交流群
推荐阅读:
觉得不错,点个在看呗~