背景
某天晚上睡不着在思考一個問題:組件化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的形式引入
- 性能問題,沒有處理增量機制
參考:
Gradle自定義插件+Transform+javassist= JakeWharton/hugo類似的東西
Android動態編譯技術:Plugin Transform Javassist操作Class文件
Javassist動態字節碼生成技術
Javassist進行方法插樁
如何開發一款高性能的gradle transform