背景
某天晚上睡不着在思考一个问题:组件化app module的Application的生命周期如何让lib module感知到,即lib module在应用启动时在自己的Application里做初始化操作而不用写到app module的Application里,实现完全解耦。 查阅资料后发现好像可以用Transform+class代码注入(Javassist)的方式实现,因为以前没接触过,方法耗时打印又是一个比较简单常见的项目,适合练手,故记录一下Transform和class字节码操作的基本使用
Transform和Javassist
- Transform
Gradle Transform是Android官方提供给开发者在项目构建阶段即由class到dex转换期间修改class文件的一套api。目前比较经典的应用是字节码插桩、代码注入技术。
参考:Gradle Transform - Javassist
Javassist(Java Programming Assistant) 使得操作Java字节码变得简单。它是一个用于在Java中编辑字节码的类库;它使Java程序能够在运行时定义新类,并在JVM加载时修改类文件。与其他类似的字节码编辑器不同,Javassist提供两个级别的API:源级别和字节码级别。如果用户使用源级别API,他们可以编辑类文件而不需要了解Java字节码的规范。整个API仅使用Java语言的风格进行设计。您甚至可以以源文本的形式指定插入的字节码; Javassist将即时编译它。另一方面,字节码级别API允许用户像其他编辑器一样直接编辑类文件(class file)。
参考:Javassist官方文档翻译
具体实现
1. 自定义gradle插件
插件的目的值把Transform注册到具体项目工程中,来发挥Transform的作用。
创建工程cost-plugin并添加Transform api依赖
apply plugin: 'groovy'
apply plugin: 'maven-publish'
dependencies {
//gradle sdk
implementation gradleApi()
//groovy sdk
implementation localGroovy()
//transform api 这里的版本要跟工程的版本一致或者更低即( classpath 'com.android.tools.build:gradle:3.5.0')
implementation 'com.android.tools.build:gradle:3.5.0'
//处理io操作
implementation 'commons-io:commons-io:2.5'
}
publishing {
publications {
mavenJava(MavenPublication) {
groupId 'com.pxq.myplugin'
artifactId 'cost'
version '1.0.0'
from components.java
}
}
}
publishing {
repositories {
maven {
// 这里用本地目录
url uri('../repos')
}
}
}
2. 创建Transform并注册
2.1 创建Transform
/**
* 编译过程中处理class文件
* author : pxq
* date : 19-9-22 下午4:11
*/
class ClassTransform extends Transform{
@Override
String getName() {
return ClassTransform.simpleName
}
//输入类型,这里只处理class文件
@Override
Set<QualifiedContent.ContentType> getInputTypes() {
return TransformManager.CONTENT_CLASS
}
@Override
Set<? super QualifiedContent.Scope> getScopes() {
return TransformManager.SCOPE_FULL_PROJECT
}
@Override
boolean isIncremental() {
return false
}
@Override
void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
super.transform(transformInvocation)
println '---- transform start ----'
transformInvocation.inputs.each {input ->
input.directoryInputs.each {dirInput ->
//TODO 对class类进行处理
println dirInput.file.path
// 将input的目录复制到output指定目录 否则运行时会报ClassNotFound异常
def dest = transformInvocation.outputProvider.getContentLocation(dirInput.name,
dirInput.contentTypes, dirInput.scopes, Format.DIRECTORY)
FileUtils.copyDirectory(dirInput.file, dest)
}
input.jarInputs.each { jarInput ->
// 重命名输出文件(同目录copyFile会冲突)
def jarName = jarInput.name
def md5Name = DigestUtils.md5Hex(jarInput.file.getAbsolutePath())
if (jarName.endsWith(".jar")) {
jarName = jarName.substring(0, jarName.length() - 4)
}
def dest = transformInvocation.outputProvider.getContentLocation(jarName + md5Name, jarInput.contentTypes, jarInput.scopes, Format.JAR)
FileUtils.copyFile(jarInput.file, dest)
}
}
println '---- transform end ----'
}
}
2.2 在插件中注册Transform
/**
* 方法耗时插件,用来注册Transform
* author : pxq
* date : 19-9-22 下午3:43
*/
class CostPlugin implements Plugin<Project>{
@Override
void apply(Project project) {
//AppExtension即android{...}
def android = project.extensions.getByType(AppExtension)
//注册transform
android.registerTransform(new ClassTransform())
}
}
把插件应用到app module中,gradle执行效果如下
3. 利用Javassist实现代码注入
3.1 获取类文件
写一个类去处理Transform的输入,过滤出我们想要的类
class InjectUtil {
static void injectCost(File classPath) {
println "injectUtil ${classPath.path}"
if (classPath.isDirectory()){
//遍历所有文件
classPath.eachFileRecurse { classFile ->
//过滤掉一些生成的类
if (check(classFile)) {
println "find class : ${classFile.path}"
}
}
}
}
//过滤掉一些生成的类
private static boolean check(File file) {
if (file.isDirectory()) {
return false
}
def filePath = file.path
return !filePath.contains('R$') &&
!filePath.contains('R.class') &&
!filePath.contains('BuildConfig.class')
}
}
在Transform类中调用
@Override
void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
super.transform(transformInvocation)
println '---- transform start ----'
transformInvocation.inputs.each {input ->
input.directoryInputs.each {dirInput ->
//注入cost统计代码
InjectUtil.injectCost(dirInput.file)
....
}
3.2 注入代码
3.2.1 定义约束
建立cost-api工程,定义注解用来标记要处理的方法
/**
* 一种约束,用来标记要统计耗时的方法
* author : pxq
* date : 19-9-22 下午3:36
*/
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.METHOD)
public @interface MethodCost {
}
发布到本地maven作为共用模块
apply plugin: 'java-library'
apply plugin: 'maven-publish'
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
}
publishing {
publications {
mavenJava(MavenPublication) {
groupId 'com.pxq.cost'
artifactId 'cost-api'
version '1.0.0'
from components.java
}
}
}
publishing {
repositories {
maven {
// 这里用本地目录
url uri('../repos')
}
}
}
3.2.2 根据约束注入代码
思路是把原方法改名,然后生成一个与原方法同名的代理方法,代理方法中调用原方法并计算耗时,即把原方法“包裹”起来。
/**
* 向目标类注入耗时计算代码,生成同名的代理方法,在代理方法中调用原方法计算耗时
* @param baseClassPath 写回原路径
* @param clazz
*/
private static void inject(String baseClassPath, String clazz) {
def ctClass = sClassPool.get(clazz)
//解冻
if (ctClass.isFrozen()) {
ctClass.defrost()
}
ctClass.getDeclaredMethods().each { ctMethod ->
//判断是否要处理
if (ctMethod.hasAnnotation(MethodCost.class)) {
println "before ${ctMethod.name}"
//把原方法改名,生成一个同名的代理方法,添加耗时计算
def name = ctMethod.name
def newName = name + COST_SUFFIX
println "after ${newName}"
def body = generateBody(ctClass, ctMethod, newName)
println "generateBody : ${body}"
//原方法改名
ctMethod.setName(newName)
//生成代理方法
def proxyMethod = CtNewMethod.make(ctMethod.modifiers, ctMethod.returnType, name, ctMethod.parameterTypes, ctMethod.exceptionTypes, body, ctClass)
//把代理方法添加进来
ctClass.addMethod(proxyMethod)
}
}
ctClass.writeFile(baseClassPath)
ctClass.detach()//释放
}
/**
* 生成代理方法体,包含原方法的调用和耗时打印
* @param ctClass
* @param ctMethod
* @param newName
* @return
*/
private static String generateBody(CtClass ctClass, CtMethod ctMethod, String newName){
//方法返回类型
def returnType = ctMethod.returnType.name
println returnType
//生产的方法返回值
def methodResult = "${newName}(\$\$);"
if (!"void".equals(returnType)){
//处理返回值
methodResult = "${returnType} result = "+ methodResult
}
println methodResult
return "{long costStartTime = System.currentTimeMillis();" +
//调用原方法 xxx$$Impl() $$表示方法接收的所有参数
methodResult +
"android.util.Log.e(\"METHOD_COST\", \"${ctClass.name}.${ctMethod.name}() 耗时:\" + (System.currentTimeMillis() - costStartTime) + \"ms\");" +
//处理一下返回值 void 类型不处理
("void".equals(returnType) ? "}" : "return result;}")
}
3.2.3 测试及效果
在方法上使用注解
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
new Thread(new Runnable() {
@Override
public void run() {
try {
testCost(1000);
JavaBean javaBean = testCostWithReturn(2000);
Log.d(TAG, "run: " + javaBean.toString());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
@MethodCost
public void testCost(int x) throws InterruptedException {
Thread.sleep(x);
}
@MethodCost
public JavaBean testCostWithReturn(int x) throws InterruptedException {
Thread.sleep(x);
return new JavaBean("testCostReturn", 1);
}
找到app/build/intermediates/transforms/ClassTransform/路径下生成的方法,可见原来的方法已经被改名,被调用的方法是代理方法:
效果:
4 额外的处理
我们可以为插件添加extension来控制是否需要注入代码,例如
import org.gradle.api.Project
/**
* 接收额外的输入,如是否需要注入代码
* author : pxq
* date : 19-9-25 下午10:24
*/
class CostExtension{
static final String EXTENSION_NAME = 'cost'
//默认注入耗时计算
boolean injectCost = true
/**
* 创建extension
* @param project
*/
static void create(Project project){
project.extensions.create(CostExtension.EXTENSION_NAME, CostExtension)
}
/**
* 判断是否需要注入
* @param project
* @return
*/
static boolean checkInject(Project project){
return project.extensions.getByName(CostExtension.EXTENSION_NAME).injectCost
}
}
在ClassTransform中读取
@Override
void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
super.transform(transformInvocation)
println '---- transform start ----'
inject = CostExtension.checkInject(mProject)
println "injectCost = ${inject}"
transformInvocation.inputs.each { input ->
input.directoryInputs.each { dirInput ->
if (inject) {
//注入cost统计代码
InjectUtil.injectCost(dirInput.file, mProject)
}
...
在app的build.gradle中添加
apply plugin: 'com.android.application'
apply plugin: 'com.pxq.cost'
cost{
injectCost = false
}
...
当injectCost = false时不再处理
Github传送门 https://github.com/drkingwater/MethodCost
完
遗留问题
- 没有处理子模块,因为没有处理Jar文件,子模块和第三方库都是以Jar的形式引入
方法1:把插件应用到子模块,让插件处理子模块的注入(处理简单)
方法2:插件只应用与主模块,处理子模块的jar(处理比较麻烦,还会增加编译时间)- 找到具体的子模块jar
- 解压并处理注入
- 重新压缩jar,并删除解压文件
- 性能问题,没有处理增量编译
参考:
Gradle自定义插件+Transform+javassist= JakeWharton/hugo类似的东西
Android动态编译技术:Plugin Transform Javassist操作Class文件
Javassist动态字节码生成技术
Javassist进行方法插桩
如何开发一款高性能的gradle transform