自定义 gradle plugin,教你如何 hook 系统 task 和字节码

·  阅读 3931

一、开源背景

大家在自己写 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.gradleapply 一下我的插件即可!!!

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 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的使用方式也在此不再叙述了,有兴趣可以自行去看下,下面列出一些我在写这个插件过程中遇到的一些问题.


问题一、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 传送门.

收藏成功!
已添加到「」, 点击更改