目標
實現自定義gradle插件,通過ASM實現在MainActivity的onCreate中插入Log打印語句
1. 自定義Gradle插件實現
gradle實現自定義插件一般有三種方式:
1. build.gradle中直接編寫,項目內使用
2. 創建buildSrc module,項目內使用
3. 創建獨立module,可發佈遠程倉庫
考慮到靈活性,選擇第三種方式實現自定義插件
1.1 創建module
創建一個新的module,刪除不必要的文件,只留下build.gradle, src/main這兩個文件和文件夾
1.2 創建目錄和配置文件
1.2.1 在src/main下創建java和groovy目錄以及resources/META-INF/gradle-plugins
目錄,META-INF和gradle-plugins均爲package
1.2.2 在resources/META-INF/gradle-plugins下創建xxxx.properties文件, xxxx爲apply plugin時用到的名字,文件中內容爲implementation-class = 插件的完整路徑,這裏可以先空着,後面創建了插件後再填入
1.3 build.gradle編寫
build.gradle中引入groovy和maven插件,然後引入gradle插件,asm以及gradle api和庫,最後進行編譯
注意:此處的定義的group + 此module名(或者archivesBaseName定義) + version 即是插件的依賴地址,下方倉庫設置中的pom.groupId, pom.artifactId, pom.version是同樣的效果,同時設置會生成兩個不同地址
apply plugin: 'groovy'
apply plugin: 'maven'
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation gradleApi()
implementation localGroovy()
implementation 'com.android.tools.build:gradle:3.5.3'
}
group='danny.lifecycle.plugin'
version='1.0.0'
uploadArchives {
repositories {
mavenDeployer {
// pom.groupId = 'com.xxx.plugin.gradle' //groupId
// pom.artifactId = 'xxx' //artifactId
// pom.version = '1.0.2' //版本號
//本地的Maven地址設置
repository(url: uri('../asm_lifecycle_repo'))
}
}
}
1.4 編寫插件
編譯完成後在groovy中添加package,創建實現Plugi<>接口的類文件,先使用java文件便於包和類的引入,具體實現如下,編寫完成後將.java後綴改成.groovy,這就是自定義插件的入口
public class LifeCyclePlugin implements Plugin<Project> {
@Override
public void apply(Project project) {
}
}
同級目錄下創建繼承Transform的類文件,同樣使用java文件導入包和引用類,具體實現如下,編寫完成後後綴改爲.groovy,Transform的作用是可以在項目構建過程中.class文件轉換成.dex文件期間獲取到.class文件進行讀取修改操作
public class LifeCycleTransform extends Transform {
@Override
public String getName() {
return "LifeCycleTransform";
}
@Override
public Set<QualifiedContent.ContentType> getInputTypes() {
return TransformManager.CONTENT_CLASS;
}
@Override
public Set<? super QualifiedContent.Scope> getScopes() {
return TransformManager.PROJECT_ONLY;
}
@Override
public boolean isIncremental() {
return false;
}
@Override
public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
}
}
getName(): 這裏可以指定此task的名字,不過最終名字需要做一些拼接,transformClassesWith名字ForDebug/Release
getInputTypes(): 處理的文件類型,此處爲class文件
getScopes(): 作用範圍,此處爲只處理當前項目文件
isIncremental(): 是否支持增量編譯
transform(TransformInvocation transformInvocation): 主要處理文件和jar包的方法
編寫完transform後,在plugin中進行註冊,AppExtension就是指的build.gradle中的android{}閉包
public class LifeCyclePlugin implements Plugin<Project> {
@Override
public void apply(Project project) {
def extension = project.extensions.getByType(AppExtension)
LifeCycleTransform transform = new LifeCycleTransform();
extension.registerTransform(transform)
}
}
1.5 生成倉庫
此時在gradle任務中生成了uploadArchives任務,雙擊後即可生成插件倉庫
1.6 自定義gradle插件引入和使用
在項目的根目錄build.gradle中添加倉庫路徑,然後在dependencies中添加classpath,引入插件
buildscript {
repositories {
google()
jcenter()
maven {
url uri('./asm_lifecycle_repo')
}
}
dependencies {
classpath 'com.android.tools.build:gradle:3.5.3'
classpath 'danny.lifecycle.plugin:asm_lifecycle_plugin3:1.0.0'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
接着在要使用的module的build.gradle中通過apply plugin引入插件使用
apply plugin: 'com.android.application'
apply plugin: 'danny.asm.lifecycle'
這樣,就完成了自定義gradle插件的編寫和使用
2. ASM實現字節碼插樁
2.1 引入ASM
完成自定義gradle插件後,在插件的build.gradle中添加ASM依賴
apply plugin: 'groovy'
apply plugin: 'maven'
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation gradleApi()
implementation localGroovy()
implementation 'com.android.tools.build:gradle:3.5.3'
//ASM相關依賴
implementation 'org.ow2.asm:asm:7.1'
implementation 'org.ow2.asm:asm-commons:7.1'
}
group='danny.lifecycle.plugin'
version='1.0.0'
uploadArchives {
repositories {
mavenDeployer {
// pom.groupId = 'com.xxx.plugin.gradle' //groupId
// pom.artifactId = 'xxx' //artifactId
// pom.version = '1.0.2' //版本號
//本地的Maven地址設置
repository(url: uri('../asm_lifecycle_repo'))
}
}
}
2.2 創建Visitor
在自定義插件module中的src/main/java下添加package,然後創建繼承ClassVisitor的類文件,實現visit,visitMethod方法
public class LifecycleClassVisitor extends ClassVisitor {
private String className;
private String superName;
public LifecycleClassVisitor(ClassVisitor classVisitor) {
super(Opcodes.ASM5, classVisitor);
}
@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
super.visit(version, access, name, signature, superName, interfaces);
this.className = name;
this.superName = superName;
}
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
MethodVisitor methodVisitor = cv.visitMethod(access, name, descriptor, signature, exceptions);
if (className.equals("com/example/lifecycledemo/MainActivity") && superName.equals("androidx/appcompat/app/AppCompatActivity")) {
if (name.startsWith("onCreate")) {
return new LifeCycleMethodVisitor(Opcodes.ASM5, methodVisitor, access, name, descriptor, className, superName);
}
}
return methodVisitor;
}
@Override
public void visitEnd() {
super.visitEnd();
System.out.println("ClassVisitor visitEnd()");
}
}
在visit方法中獲取類名,超類名,在visitMethod方法中篩選類名MainActivity,超類AppCompatActivity的文件,接着篩選onCreate方法,最後返回一個繼承 AdviceAdater的類
public class LifeCycleMethodVisitor extends AdviceAdapter {
private String className;
private String methodName;
protected LifeCycleMethodVisitor(int api, MethodVisitor methodVisitor, int access, String name, String descriptor, String className, String superName) {
super(api, methodVisitor, access, name, descriptor);
this.className = className;
this.superName = superName;
System.out.println("MethodVisitor Constructor");
}
@Override
protected void onMethodEnter() {
super.onMethodEnter();
System.out.println("MethodVisitor visitCode========");
mv.visitLdcInsn("TAG");
mv.visitLdcInsn(className + "---->" + methodName);
mv.visitMethodInsn(Opcodes.INVOKESTATIC, "android/util/Log", "i", "(Ljava/lang/String;Ljava/lang/String;)I", false);
mv.visitInsn(Opcodes.POP);
}
@Override
protected void onMethodExit(int opcode) {
mv.visitLdcInsn("TAG");
mv.visitLdcInsn("this is end");
mv.visitMethodInsn(Opcodes.INVOKESTATIC, "android/util/Log", "i", "(Ljava/lang/String;Ljava/lang/String;)I", false);
mv.visitInsn(Opcodes.POP);
super.onMethodExit(opcode);
}
}
在繼承了AdviceAdapter的類中,實現onMethodEnter和onMethodExit方法,對應onCreate方法的開始和結束節點,在這兩個節點通過MethodVisitor的一系列api調用插入Log打印語句
visitLdcInsn(final Object value)對應LDC指令
visitMethodInsn(final int opcode, final String owner, final String name, final String descriptor, final boolean isInterface)是訪問方法指令,此處用到的五個參數
opcode: 對應字節碼指令操作碼,此處傳入了 調用類方法的指令
owner: 方法所在包
name: 方法名
descriptor: 方法描述符,前一個Ljava/lang/String:Ljava/lang/String指明方法有兩個String類型參數,最後的 I 表示方法返回int類型
isInterface: 是否是接口類的實現方法
visitInsn()對應空操作數指令,比如POP, DUP
注:此處也可直接繼承MethodVisitor,實現visitCode方法插入代碼,但要實現在方法結束前插入代碼需要另外實現visitInsn(int opcode)方法,根據opcode == RETURN來判斷指令執行到方法末尾了,插入代碼後再調用super方法即可
2.3 讀取class文件數據
在自定義繼承Transform類的transform方法中進行操作,通過getInputs()獲取輸入的class文件和jar包的路徑,outputProvider管理輸出路徑,接着遍歷inputs,directoryInputs獲取到class文件的路徑集合,再次遍歷,篩選出class文件,通過ClassReader進行讀取,ClassWriter進行寫入,將classWiter傳入自定義的ClassVisitor中,接着調用classReader的accept方法正式對class文件進行讀取並調用classVisitor中的方法,比如visit(),visitMethod()等,我們在裏面對MainActivity的onCreate方法中加入了一行Log打印語句,然後通過classWriter的toByteArray()方法輸出修改後的class文件btye數組,覆蓋掉原來的class文件,最後將修改後的class文件目錄整個copy新的目錄下,這個新目錄是根據輸入的內容,作用範圍等信息生成的,供下一個Task使用
@Override
public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
Collection<TransformInput> inputs = transformInvocation.getInputs()
TransformOutputProvider outputProvider = transformInvocation.outputProvider
inputs.each {TransformInput input ->
input.directoryInputs.each {DirectoryInput directoryInput ->
File dir = directoryInput.file
if (dir) {
dir.traverse (type: FileType.FILES, nameFilter: ~/.*\.class/) { File file ->
println("find class: " + file.name)
//對class文件進行讀取
ClassReader classReader = new ClassReader(file.bytes)
//對class文件的寫入
ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)
//訪問class文件相應的內容,解析到某一個結構就會通知到classVisitor相應的方法
println("before visit")
ClassVisitor visitor = new LifecycleClassVisitor(classWriter)
println("after visit")
//依次調用ClassVisitor接口的各個方法
classReader.accept(visitor, ClassReader.EXPAND_FRAMES)
println("after accept")
//toByteArray方法會將最終修改的字節碼以byte數組形式返回
byte[] bytes = classWriter.toByteArray()
//通過文件流寫入方式覆蓋掉原先的內容,實現class文件的改寫
FileOutputStream fileOutputStream = new FileOutputStream(file.path)
fileOutputStream.write(bytes)
fileOutputStream.close()
}
}
def dest = outputProvider.getContentLocation(directoryInput.name, directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY)
FileUtils.copyDirectory(directoryInput.file, dest)
}
}
}
2.4 運行
代碼都全部編寫之後,再次點擊uploadArchives任務生成本地倉庫,然後就可以運行項目檢測插樁是否成功了,這是項目中的MainActivity文件,可以看到只在onCreate中第一行打印了一個log
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
Log.i("TAG", "is this the first log?");
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
項目運行後,在LogCat中篩選TAG,可以看到打印語句的輸出,onMethodEnter中插入的語句最先輸出,接着是在onCreate方法開頭的語句,最後是在onMethodExit中插入的語句
3. 總結
自定義Gradle插件遵循一定的規則,手動實現幾次就能掌握,插件和ASM的銜接在Transform中完成,插件負責輸入數據,ASM接收數據後進行字節碼修改,最後再重新輸出,ASM的使用主要還是流程和api的掌握,比較不好編寫的是最終插入和修改字節碼的api,如果對字節碼指令不太熟悉的話可以安裝一個ASM Bytecode Viewer插件,將相關操作在java文件中完成,運用插件編譯成字節碼和ASM指令格式,照搬過來就行
參考: Android工程師進階