前言
我們會通過ASM+Transform 代碼插樁來實現(xiàn)方法耗時監(jiān)控 另外 用到了一個比較好用的插件
ASM Bytecode Outline 這樣在不會寫操作碼的時候 也可以去實現(xiàn)
我們主要分為三個部分來實現(xiàn)代碼插樁
- 注解 (標記需要插樁的方法)
- Plugin+Transform實現(xiàn)代碼掃描 尋找插樁點
- ASM實現(xiàn)代碼生成注入
國際慣例 先貼個源碼 show me the code,no BB
注解
很簡單,直接上代碼
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface CostTime {
}
Plugin+Transform
首先看一眼結構目錄 有一個比較坑的點 坑了我半天的時間
19207E7A-87EC-4EF0-B6EE-C8B7E7152855.png
之前項目自動生成 是java 結果打出來的jar包 只包含了java文件 好氣啊
1.聲明Plugin
public class CostTimePlugin implements Plugin<Project> {
@Override
public void apply(Project project) {
project.android.registerTransform(new CostTimeTransform())
}
}
2.實現(xiàn)Transform
已經將注釋都寫在代碼中 直接看代碼就可以 很簡單易懂
package com.dsg.CostTImePlugin
import com.android.build.api.transform.Context
import com.android.build.api.transform.DirectoryInput
import com.android.build.api.transform.Format
import com.android.build.api.transform.JarInput
import com.android.build.api.transform.QualifiedContent
import com.android.build.api.transform.Transform
import com.android.build.api.transform.TransformException
import com.android.build.api.transform.TransformInput
import com.android.build.api.transform.TransformOutputProvider
import com.android.build.gradle.internal.pipeline.TransformManager
import org.apache.commons.codec.digest.DigestUtils
import org.apache.commons.io.FileUtils
import org.objectweb.asm.ClassReader
import org.objectweb.asm.ClassWriter
class CostTimeTransform extends Transform {
@Override
String getName() {
//Transform名稱
return "CostTime"
}
@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(Context context, Collection<TransformInput> inputs, Collection<TransformInput> referencedInputs, TransformOutputProvider outputProvider, boolean isIncremental) throws IOException, TransformException, InterruptedException {
//遍歷輸入
for (TransformInput input in inputs) {
//遍歷Directioy
for (DirectoryInput dirInput in input.directoryInputs) {
//處理需要插樁的文件
modifyClassWithPath(dirInput.file)
//Copy修改之后的文件
File dest = outputProvider.getContentLocation(dirInput.name, dirInput.contentTypes,
dirInput.scopes, Format.DIRECTORY)
FileUtils.copyDirectory(dirInput.file, dest)
}
//遍歷JarInput 因為我們這里只對自己的方法插樁 所以不對JarInput做處理
for (JarInput jarInput : input.jarInputs) {//jar(第三方庫,module)
if (jarInput.scopes.contains(QualifiedContent.Scope.SUB_PROJECTS)) {//module library
//從module中獲取注解信息
// readClassWithJar(jarInput)
}
//雖然不做處理 但是還是要記得重新拷貝回去 不然會有問題
copyFile(jarInput, outputProvider)
}
}
}
void modifyClassWithPath(File dir) {
def root = dir.absolutePath
dir.eachFileRecurse { File file ->
def filePath = file.absolutePath
//過濾非class文件
if (!filePath.endsWith(".class")) return
def className = getClassName(root, filePath)
//過濾系統(tǒng)文件
if (isSystemClass(className)) return
//hook關鍵代碼
hookClass(filePath, className)
}
}
void hookClass(String filePath, String className) {
//1.聲明ClassReader
ClassReader reader = new ClassReader(new FileInputStream(new File(filePath)))
//2聲明 ClassWriter
ClassWriter writer = new ClassWriter(reader, ClassWriter.COMPUTE_MAXS)
//3聲明ClassVisitor
CostTimeMethodAdapter adapter = new CostTimeMethodAdapter(writer)
//4調用accept方法 傳入classVisitor
reader.accept(adapter, ClassReader.EXPAND_FRAMES)
if (adapter.changed) {
println className + "is changed:" + adapter.changed
byte[] bytes = writer.toByteArray()
FileOutputStream fos = new FileOutputStream(new File(filePath))
fos.write(bytes)
}
}
//默認排除
static final DEFAULT_EXCLUDE = [
'^android\\..*',
'^androidx\\..*',
'.*\\.R$',
'.*\\.R\\$.*$',
'.*\\.BuildConfig$',
]
//獲取類名
String getClassName(String root, String classPath) {
return classPath.substring(root.length() + 1, classPath.length() - 6)
.replaceAll("/", ".") // unix/linux
.replaceAll("\\\\", ".") //windows
}
boolean isSystemClass(String fileName) {
for (def exclude : DEFAULT_EXCLUDE) {
if (fileName.matches(exclude)) return true
}
return false
}
void copyFile(JarInput jarInput, TransformOutputProvider outputProvider) {
def dest = getDestFile(jarInput, outputProvider)
FileUtils.copyFile(jarInput.file, dest)
}
static File getDestFile(JarInput jarInput, TransformOutputProvider outputProvider) {
def destName = jarInput.name
// 重名名輸出文件,因為可能同名,會覆蓋
def hexName = DigestUtils.md5Hex(jarInput.file.absolutePath)
if (destName.endsWith(".jar")) {
destName = destName.substring(0, destName.length() - 4)
}
// 獲得輸出文件
File dest = outputProvider.getContentLocation(destName + "_" + hexName, jarInput.contentTypes, jarInput.scopes, Format.JAR)
return dest
}
}
關鍵代碼就在hookClass
方法
ASM插樁
簡單講一下ASM各類的作用
ClassReader會讀取java字節(jié)碼
ClassWriter通過toByteArray可以生成修改之后的字節(jié)碼
ClassWriter是ClassVisitor的子類 我們一般會代理ClassWriter的實現(xiàn)
MethodVisitor 讀取Method方法
MethodVisitor 的子類很多 具體可以參考 ASM官網 有很多不同功能的MethodVisitor
我們會通過ClassReader讀取字節(jié)碼 然后通過ClassVisitor進行字節(jié)碼的修改
然后再通過ClassWriter生成我們修改之后的字節(jié)碼
大致的思路就是這樣 接下來看一下源碼
package com.dsg.CostTImePlugin;
import org.objectweb.asm.*;
import org.objectweb.asm.commons.LocalVariablesSorter;
import static org.objectweb.asm.Opcodes.*;
/**
* @author DSG
* @Project ASMCostTime
* @date 2020/6/22
* @describe
*/
public class CostTimeClassAdapter extends ClassVisitor {
public boolean changed; //是否修改過
private String owner;
private boolean isInterface;
public CostTimeClassAdapter(ClassVisitor visitor) {
super(ASM4, visitor);
}
public CostTimeClassAdapter(int api, ClassVisitor classVisitor) {
super(api, classVisitor);
}
@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
cv.visit(version, access, name, signature, superName, interfaces);
owner = name;
isInterface = (access & ACC_INTERFACE) != 0;
}
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);
if (!isInterface && mv != null && !name.equals("<init>")) {
//將MethodVisitor交由CostTimeMethodAdapter代理
mv = new CostTimeMethodAdapter(access, name, descriptor, mv);
}
return mv;
}
//繼承自LocalVariablesSorter 有序遍歷素有方法
class CostTimeMethodAdapter extends LocalVariablesSorter {
private String name;
private boolean isAnnotationed;
private int time;
public CostTimeMethodAdapter(int access, String name, String descriptor, MethodVisitor methodVisitor) {
super(ASM4, access, descriptor, methodVisitor);
this.name = name;
}
/**
* 遍歷代碼的開始
*/
@Override
public void visitCode() {
super.visitCode();
if (isAnnotationed) {
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
time = newLocal(Type.LONG_TYPE);
mv.visitVarInsn(LSTORE, time);
}
}
@Override
public void visitFieldInsn(int opcode, String owner, String name, String descriptor) {
super.visitFieldInsn(opcode, owner, name, descriptor);
}
@Override
public void visitIntInsn(int opcode, int operand) {
super.visitIntInsn(opcode, operand);
}
/**
* 遍歷操作碼 判斷是否是return語句 如果是return 就插入我們的代碼
*
* @param opcode 操作碼
*/
@Override
public void visitInsn(int opcode) {
if ((opcode >= IRETURN && opcode <= RETURN) || opcode == ATHROW) {
if (isAnnotationed) {
//這里的代碼都可以由ASM插件生成
//Label可以生成局部變量
Label l1 = new Label();
mv.visitLabel(l1);
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
mv.visitVarInsn(LLOAD, time);
mv.visitInsn(LSUB);
mv.visitVarInsn(LSTORE, 3);
Label l2 = new Label();
mv.visitLabel(l2);
mv.visitLdcInsn(owner);
mv.visitTypeInsn(NEW, "java/lang/StringBuilder");
mv.visitInsn(DUP);
mv.visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false);
mv.visitLdcInsn("func " + name + " cost Time:");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
mv.visitVarInsn(LLOAD, 3);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(J)Ljava/lang/StringBuilder;", false);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);
mv.visitMethodInsn(INVOKESTATIC, "android/util/Log", "e", "(Ljava/lang/String;Ljava/lang/String;)I", false);
}
}
super.visitInsn(opcode);
}
/**
* @param descriptor 最先執(zhí)行 判斷是否存在注解 如果存在 就進行插樁
* @param visible
* @return
*/
@Override
public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
isAnnotationed = ("Lcom/dsg/annotations/CostTime;".equals(descriptor));
if (!changed && isAnnotationed) {
changed = true;
}
return super.visitAnnotation(descriptor, visible);
}
@Override
public void visitMaxs(int maxStack, int maxLocals) {
super.visitMaxs(maxStack, maxLocals);
}
@Override
public void visitEnd() {
super.visitEnd();
}
}
}
代碼也寫的很詳細了 主要思路就是
- 判斷是否存在注解 是否需要插樁
- 在頭部插入當前時間
- 在return之前計算方法耗時
總結
其實ASM相對感覺還是比較簡潔明了的 只要我們找好注入點 生成我們需要的代碼就可以 還可以通過ASM插件來生成字節(jié)碼
感覺ASM+Transform的方式還是比較常見的 比如之前分析的Robust原理一樣 而且ASM基本沒有性能上的損耗 所以我們還是有必要深度學習一下