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的形式引入
  2. 性能問題,沒有處理增量機制

參考:
Gradle自定義插件+Transform+javassist= JakeWharton/hugo類似的東西
Android動態編譯技術:Plugin Transform Javassist操作Class文件
Javassist動態字節碼生成技術
Javassist進行方法插樁
如何開發一款高性能的gradle transform

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章