1. 配置階段
在工程下創建Module,命名buildSrc,注意S大寫,不是這個名字本項目識別不到插件。
刪除所有的目錄僅留下java目錄,在java目錄下創建插件:
public class AutoTracePlugin implements Plugin<Project> {
@Override
public void apply(Project project) {
System.out.println("插件引用了");
}
}
繼續在java同級目錄創建resources,暴露給主項目調用的插件位置,在resources下創建目錄/META-INF/gradle-plugins,繼續創建文件xxx.properties,xxx就是主項目會引用的插件名字。
xxx.properties配置:
implementation-class=com.beiins.dolly.AutoTracePlugin
指定插件的相對路徑。
在主app的build.gradle下引用:
apply plugin: 'com.beiins.dolly.plugin'
插件的定義和引用就完成了,點擊build,就能看到輸出“插件引用了”。
2. 插件Transform定義
插件只是讀取字節碼的入口,真正操作字節碼需要交給Transform,Transform的定義比較固定化:
public class AutoTraceTransform extends Transform {
@Override
public String getName() {
return "auto-trace-transform";
}
@Override
public Set<QualifiedContent.ContentType> getInputTypes() {
return TransformManager.CONTENT_CLASS;
}
@Override
public Set<? super QualifiedContent.Scope> getScopes() {
return TransformManager.SCOPE_FULL_PROJECT;
}
@Override
public boolean isIncremental() {
return false;
}
@Override
public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
Collection<TransformInput> inputs = transformInvocation.getInputs();
TransformOutputProvider outputProvider = transformInvocation.getOutputProvider();
inputs.forEach(transformInput -> {
transformInput.getJarInputs().forEach(jarInput -> {
File dest = outputProvider.getContentLocation(jarInput.getName(), jarInput.getContentTypes(), jarInput.getScopes(), Format.JAR);
try {
System.out.println("拷貝jar文件" + jarInput.getName());
FileUtils.copyFile(jarInput.getFile(), dest);
} catch (Exception e) {
e.printStackTrace();
}
});
transformInput.getDirectoryInputs().forEach(directoryInput -> {
File dest = outputProvider.getContentLocation(directoryInput.getName(), directoryInput.getContentTypes(), directoryInput.getScopes(), Format.DIRECTORY);
try {
System.out.println("拷貝directory文件" + directoryInput.getName());
FileUtils.copyDirectory(directoryInput.getFile(), dest);
} catch (Exception e) {
e.printStackTrace();
}
});
});
}
}
必須重寫4個方法,指定Transform的name,指定Transform接受的輸入,指定Transform作用的范圍,指定Transform是否支持增量更新。最關鍵的是transform,對字節碼的操作就在這個方法內,上面是簡單的拷貝工程的class文件,從主app的build/intermediates/javac下拷貝到build/intermediates/transforms下。
自動埋點,需要對以上遍歷的jarInputs和directoryInputs進行修改:
public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
Collection<TransformInput> inputs = transformInvocation.getInputs();
TransformOutputProvider outputProvider = transformInvocation.getOutputProvider();
inputs.forEach(transformInput -> {
transformInput.getJarInputs().forEach(jarInput -> {
File dest = outputProvider.getContentLocation(jarInput.getName(), jarInput.getContentTypes(), jarInput.getScopes(), Format.JAR);
try {
System.out.println("拷貝jar文件" + jarInput.getName());
FileUtils.copyFile(jarInput.getFile(), dest);
} catch (Exception e) {
e.printStackTrace();
}
});
transformInput.getDirectoryInputs().forEach(directoryInput -> {
File dir = directoryInput.getFile();
try {
traverse(dir);
} catch (Exception e) {
e.printStackTrace();
} finally {
File dest = outputProvider.getContentLocation(directoryInput.getName(), directoryInput.getContentTypes(), directoryInput.getScopes(), Format.DIRECTORY);
try {
System.out.println("拷貝directory文件" + directoryInput.getName());
FileUtils.copyDirectory(directoryInput.getFile(), dest);
} catch (Exception e) {
e.printStackTrace();
}
}
});
});
}
/**
* 循環遍歷java文件修改
*
* @param file
*/
private void traverse(File file) throws Exception {
if (file == null || !file.exists()) {
return;
}
if (file.isDirectory()) {
File[] files = file.listFiles();
for (File f : files) {
traverse(f);
}
} else {
System.out.println("find class========== " + file.getName());
if (!file.getName().endsWith(".class")) {
return;
}
ClassReader classReader = new ClassReader(new FileInputStream(file.getPath()));
ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS);
ClassVisitor classVisitor = new AutoTraceClassVisitor(classWriter);
classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES);
byte[] bytes = classWriter.toByteArray();
FileOutputStream outputStream = new FileOutputStream(file.getPath());
outputStream.write(bytes);
outputStream.flush();
outputStream.close();
}
}
主要增加了traverse循環遍歷class文件進行字節碼修改,關鍵類AutoTraceClassVisitor,對class文件進行訪問,如果滿足條件,就插入字節碼。
自定義ClassVisitor訪問類:
public class AutoTraceClassVisitor extends ClassVisitor {
private String mClassName;
private String mSuperName;
private String[] mInterfaces;
public AutoTraceClassVisitor(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);
System.out.println("visit name = " + name + " superName = " + superName);
mClassName = name;
mSuperName = superName;
mInterfaces = interfaces;
}
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
System.out.println("visit name = " + name + " signature = " + signature + " access = " + access);
MethodVisitor methodVisitor = cv.visitMethod(access, name, descriptor, signature, exceptions);
if ("onClick(Landroid/view/View;)V".equals(name + descriptor)) {
System.out.println("遍歷到點擊事件了");
return new AutoTraceMethodVisitor(methodVisitor, mClassName, name);
}
return methodVisitor;
}
@Override
public void visitEnd() {
super.visitEnd();
}
}
在visitMethod方法類進行訪問時,對類中遍歷的類進行判斷,如果是onClick方法,并且入參是View類型,也就是點擊事件的回調方法,就觸發方法的訪問類AutoTraceMethodVisitor。
自定義方法訪問類AutoTraceMethodVisitor:
public class AutoTraceMethodVisitor extends MethodVisitor {
private String mMethodName;
private String mClassName;
public AutoTraceMethodVisitor(int api) {
super(api);
}
public AutoTraceMethodVisitor(MethodVisitor methodVisitor, String className, String methodName) {
super(Opcodes.ASM5, methodVisitor);
mClassName = className;
mMethodName = methodName;
}
@Override
public void visitCode() {
super.visitCode();
mv.visitVarInsn(ALOAD, 1);
mv.visitMethodInsn(INVOKESTATIC, "com/beiins/point/PointUtil", "record", "(Landroid/view/View;)V", false);
}
}
在訪問方法的code之前,插入字節碼。這個字節碼不用看懂,看懂也沒啥用,直接使用Android Studio插件ASM Bytecode Viewer,對想要插入的代碼進行編譯,獲取對應的字節碼。
3. 字節碼生成
按照插入的代碼越精簡風險越小的原理,最好是提前寫一個工具類,將插入的代碼封裝成工具類調用,類似:
PointUtil.record(v);
這樣一行代碼,風險極小。新建一個類,寫一個方法,方法定義一個參數View,方法內只有上述工具類調用,將干擾降到最低:
public void method(View view){
PointUtil.record(view);
}
在安裝ASM Bytecode Viewer插件的條件下,右鍵,選擇ASM Bytecode Viewer,在右邊就可以看到字節碼了。
注意辨別行號。