阅读 1909

美团无埋点方案 - Gradle Plugin 的方式,在编译期间修改 class | 掘金技术征文

版权声明:

本账号发布文章均来自公众号,承香墨影(cxmyDev),版权归承香墨影所有。

未经允许,不得转载。

一、前言

回顾一下,之前说的美团的无埋点方案,是重写需要的UI 控件,然后在其中监听对应的事件,事件触发的时候,上报统计点即可。之前也讲解了,如何使用 AppCompatDelegate 来替换我们项目内的系统控件。

本文就,如何使用一个 Gradle Plugin(以下简称 Plugin),来实现在编译期间,修改 class 字节码的功能,做一个简单的讲解。

二、技术要点

因为涉及的点比较多,所以有一些地方只是点一下,不会详细深入说明,有兴趣的话,可以看看文内推荐的文章,或者自行搜索相关资料。

1、什么是 Gradle 插件

Gradle 是一个自动化构建工具,可以使用一种基于 Groovy 的特性领域语言(DSL)来声明项目设置。Gradle 也提供了一些默认的 Plugin 帮助我们构建项目,如果想要在构建期间定制的操作,就需要单独开发一款和自己功能相关的 Gradle Plugin。

而 Gradle Plugin,是可以使用 Groovy、Java、Scala 进行开发的。本身对 Scala 不熟悉,一般我都是使用 Groovy 来开发 Gradle 插件,而 Groovy 又是可以和 Java 代码混编的,上手还算容易。

Gradle 的插件,可以理解为编写的一个库,所以它和我们编写的 Library一样,有在项目内供本项目使用的,也有可以发布出去,供其他项目使用的。

这两种方式的区别:

  1. 在项目内的 build.gradle 文件中编写,或者直接以一个单独的 Module 存在。这种方式的确定就是不方便移植和复用。
  2. 另外一种就是一个单独的插件,可以发布出去,供其他项目使用的。有点是方便移植和复用。

Gradle 的构建过程,是一个链式的过程,A → B → C,这的一个过程。也就是说,我们依赖 Gradle Plugin,来完成我们指定的任务,就需要了解到,我们的操作是需要插入到 Gradle 构建过程中的那一步。

2、Gradle 的 Project 接口

Project 接口是你写的插件代码和 Gradle 交互的主要接口,通过 Project 可以在插件内,使用 Gradle 特性,而 Project 与 build.gradle 文件是一对一的关系。

在 Gradle 中,通过 Extension 在不同的 Gradle Plugin 之间传递处理后的结果。

例如上面,就是一个 Plugin 的入口,用到了 Project 来操作 Gradle 的构建过程,在其中注册了一个 Transform,这个 Transform 才是在编译期间修改 class 字节码的关键。

3、Gradle 的 Transform

前面提到,开发 Gradle Plugin 的时候,一定要明确需要在什么地方做什么操作。

而从 Gradle 1.5.0 版本开始,Android 的 Gradle 插件中就引入了 Transform API 。和上面链式调用的思想一样,Transform 每次都会得到一个输入,然后做对应的处理,再将输出的结果,输出出去,作为下一个 Transform 的输入。

Transform 的相关内容,可以查阅文档:

tools.android.com/tech-docs/n…

所以我们在上面,使用 registerTransform() 注册了一个我们自己的 Transform ,供我们在编译期间做对应的修改。

Transform 是一个虚类,需要对其进行实现。而最重要的方法就是 transform()。

这差不多算是一个标准实现,可以看到它需要区分出是当前项目内的包,还有第三方库的 Jar 包,进行单独处理。

4、使用 Javassist 修改 class

已经明确,可以在 Transform 中修改 class 字节码,而做这个修改就需要用到:Javassit。

当然这里不是一定需要使用 Javassit ,其他的字节码操作库应该也可以,例如:ASM。

Javassist 可以完全替换一个方法或者构造函数的字节码正文,这里就不展开讨论了。具体 Javassist 的使用,可以自行查阅资料。

使用 Javassist 还需要在 build.gradle 中添加依赖关系:

compile 'org.javassist:javassist:3.20.0-GA'复制代码

推荐一篇讲解 Javassist 的文章:

www.ibm.com/developerwo…

三、举个例子

既然关键技术点已经介绍过了,那么就以一个简单的例子,试着编写一个 Gradle Plugin ,在其中修改其内的 class 字节码,最简单的,在构造方法中添加一行代码。

创建一个空项目,只有一个 Activity 页面。开始编写注入逻辑。

这里查找到需要的 class 文件之后,首先判断是否有构造方法,如果没有构造方法就创建一个,然后在其构造方法内注入一行代码。

在 Transform.transform() 中,调用注入的方法。

在主项目中,添加这个 Gradle Plugin 插件。

最终运行之后,我们来看看反编译后的效果。

但是这样实际上并没有办法修改第三方库里的类,这个需要我们特殊处理。前面已经提到,在 Transform 中,需要区分当前项目的目录,而针对第三方库的 Jar 包,我们需要先对其进行解压,然后修改完成之后,再压缩回去。

接下来在 Demo 中,新建一个叫"lib" 的 module ,在其内只有一个类,编译成 Jar 包,在主项目中引用它。

那么我们开始编写解压的逻辑。

然后再重新压缩成 Jar 包的方法也跟上。

最后,我们还需要修改 Transform.transform() 方法。

这里为了方便,指定需要解压的 Jar 包,并且解压在根目录下。最终会重新打包成一个 Jar 包,给主项目引用。

接下来看看反编译后的 apk 。

可以看到,对 Jar 包内的类,用这样的方式也是可以将其修改的。

最后看看整个项目的目录结构。

四、结语

有需要 Demo 源码的朋友,可以在本公众号回复 "GradlePlugin" 获得。

实际上,完整的替换方案,会比这里复杂很多。有需要可以先了解一下这些技术细节再尝试编写接下去的逻辑,有时间的话,之后再继续分享其涉及到的细节。

本文参加掘金技术征文:juejin.im/post/58d8e9…

公众号二维码.jpg

关注下面的标签,发现更多相似文章
评论