版权声明:
本公众号发布的所有文章,均属于原创,版权归本公众号所有。
未经允许,不得转载。
一、前言
回顾一下,之前说的美团的无埋点方案,是重写需要的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一样,有在项目内供本项目使用的,也有可以发布出去,供其他项目使用的。
这两种方式的区别:
在项目内的 build.gradle 文件中编写,或者直接以一个单独的 Module 存在。这种方式的确定就是不方便移植和复用。
另外一种就是一个单独的插件,可以发布出去,供其他项目使用的。有点是方便移植和复用。
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 的相关内容,可以查阅文档:
http://tools.android.com/tech-docs/new-build-system/transform-api
所以我们在上面,使用 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 的文章:
https://www.ibm.com/developerworks/cn/java/j-dyn0916/
三、举个例子
既然关键技术点已经介绍过了,那么就以一个简单的例子,试着编写一个 Gradle Plugin ,在其中修改其内的 class 字节码,最简单的,在构造方法中添加一行代码。
创建一个空项目,只有一个 Activity 页面。开始编写注入逻辑。
这里查找到需要的 class 文件之后,首先判断是否有构造方法,如果没有构造方法就创建一个,然后在其构造方法内注入一行代码。
在 Transform.transform() 中,调用注入的方法。
在主项目中,添加这个 Gradle Plugin 插件。
最终运行之后,我们来看看反编译后的效果。
但是这样实际上并没有办法修改第三方库里的类,这个需要我们特殊处理。前面已经提到,在 Transform 中,需要区分当前项目的目录,而针对第三方库的 Jar 包,我们需要先对其进行解压,然后修改完成之后,再压缩回去。
接下来在 Demo 中,新建一个叫”lib” 的 module ,在其内只有一个类,编译成 Jar 包,在主项目中引用它。
那么我们开始编写解压的逻辑。
然后再重新压缩成 Jar 包的方法也跟上。
最后,我们还需要修改 Transform.transform() 方法。
这里为了方便,指定需要解压的 Jar 包,并且解压在根目录下。最终会重新打包成一个 Jar 包,给主项目引用。
接下来看看反编译后的 apk 。
可以看到,对 Jar 包内的类,用这样的方式也是可以将其修改的。
最后看看整个项目的目录结构。
四、结语
有需要 Demo 源码的朋友,可以在本公众号回复 “GradlePlugin” 获得。
实际上,完整的替换方案,会比这里复杂很多。有需要可以先了解一下这些技术细节再尝试编写接下去的逻辑,有时间的话,之后再继续分享其涉及到的细节。