Transform+Javassist实现一个方法耗时打印

背景

某天晚上睡不着在思考一个问题:组件化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

遗留问题

  1. 没有处理子模块,因为没有处理Jar文件,子模块和第三方库都是以Jar的形式引入
    方法1:把插件应用到子模块,让插件处理子模块的注入(处理简单)
    方法2:插件只应用与主模块,处理子模块的jar(处理比较麻烦,还会增加编译时间)
    • 找到具体的子模块jar
    • 解压并处理注入
    • 重新压缩jar,并删除解压文件
  2. 性能问题,没有处理增量编译

参考:
Gradle自定义插件+Transform+javassist= JakeWharton/hugo类似的东西
Android动态编译技术:Plugin Transform Javassist操作Class文件
Javassist动态字节码生成技术
Javassist进行方法插桩
如何开发一款高性能的gradle transform

发布了19 篇原创文章 · 获赞 25 · 访问量 5598

猜你喜欢

转载自blog.csdn.net/pxq10422/article/details/101163797
今日推荐