美文网首页gradleAndroid开发Android开发
如何简单方便地Hook Gradle插件?

如何简单方便地Hook Gradle插件?

作者: 丘卡皮 | 来源:发表于2022-05-11 21:41 被阅读0次

    作者:程序员江同学
    转载地址:https://juejin.cn/post/7095511659925471240

    前言

    很多时候系统处于安全考虑,将很多东西对外隐藏,而有时我们偏偏又不得不去使用这些隐藏的东西。甚至,我们希望向系统中注入一些自己的代码,修改原有代码的逻辑,以提高程序的灵活性,这时候就需要用到代码Hook。

    在Java或者Kotlin代码中,代码Hook有多种方案,比如反射,动态代理,或者通过修改字节码来实现HOOK,那么如果我们想要修改Gradle插件的代码,该怎么实现呢?

    简单使用

    我们首先来看一个简单的例子,大家肯定都用过com.android.application插件,如果我们想要在这个插件中添加一些代码,可以怎么操作呢?修改方式非常简单

    1. 项目中添加buildSrc模块
    2. buildSrc中添加com.android.tools.build:gradle:7.0.2依赖
    3. 在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});
        }
        ......
    }
    

    可以看出

    1. gradlew其实就是调用到了GradlewWrapperMain的main方法
    2. 然后再通过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中存在一个与插件同名且包名也相同的类,就可以覆盖插件中的代码,从而达到修改原有代码逻辑的目的

    相关文章

      网友评论

        本文标题:如何简单方便地Hook Gradle插件?

        本文链接:https://www.haomeiwen.com/subject/kryjurtx.html