升级Android Studio Ladybug Feature Drop
Androider(研发工程师)离不开使用Android Studio,一般情况下我自己私人电脑一般会追Android Studio的最新版本。
升级到Ladybug,同时Gradle升级到了8.3,捣鼓这个还花了点时间。
字节码ASM插桩插件
Android Studio plugins市场中有两个ASM相关的插件,我比较喜欢ASM Bytecode Viewer。安装完ASM Bytecode Viewer插件之后,需要重启Android Studio才能生效。网上有网友经常说ASM Bytecode Viewer不能用了,应该是Android Studio版本升级之后.class文件存放的位置有变化,导致右键点击相应的java文件时候,选择ASM Bytecode Viewer相应的菜单选项的时候出现ASMPlugin内容为空。相应的方法是对.class文件进行相应的操作。
ASM统计插桩统计方法耗时
之前提供的插桩耗时统计的版本中没有打印类名和方法名,今天提供出来。Talk is cheap. Show me the code.今天我们按照倒叙的方式来描述整个字节码的过程。首先,先来分析MethodVisitor子类的代码,代码如下所示:
public class TraceMethodAdapter extends AdviceAdapter {
private final String methodName;
private final String className;
private boolean find = false;
protected TraceMethodAdapter(int api, MethodVisitor mv, int access, String name, String desc, String className) {
super(api, mv, access, name, desc);
this.className = className;
this.methodName = name;
}
@Override
public void visitTypeInsn(int opcode, String s) {
if (opcode == Opcodes.NEW && "java/lang/Thread".equals(s)) {
find = true;
mv.visitTypeInsn(Opcodes.NEW, "com/sample/asm/CustomThread");
return;
}
super.visitTypeInsn(opcode, s);
}
@Override
public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) {
//需要排查CustomThread自己
//
if ("java/lang/Thread".equals(owner) && !className.equals("com/sample/asm/CustomThread") && opcode == Opcodes.INVOKESPECIAL && find) {
find = false;
mv.visitMethodInsn(opcode, "com/sample/asm/CustomThread", name, desc, itf);
// Log.e("asmcode", "className:%s, method:%s, name:%s", className, methodName, name);
return;
}
super.visitMethodInsn(opcode, owner, name, desc, itf);
//
// if (owner.equals("android/telephony/TelephonyManager") && name.equals("getDeviceId") && desc.equals("()Ljava/lang/String;")) {
// Log.e("asmcode", "get imei className:%s, method:%s, name:%s", className, methodName, name);
// }
}
private int timeLocalIndex = 0;
@Override
protected void onMethodEnter() {
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
timeLocalIndex = newLocal(Type.LONG_TYPE); //这个是LocalVariablesSorter 提供的功能,可以尽量复用以前的局部变量
mv.visitVarInsn(LSTORE, timeLocalIndex);
}
@Override
protected void onMethodExit(int opcode) {
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
mv.visitVarInsn(LLOAD, timeLocalIndex);
mv.visitInsn(LSUB);//此处的值在栈顶
mv.visitVarInsn(LSTORE, timeLocalIndex);//因为后面要用到这个值所以先将其保存到本地变量表中
int stringBuilderIndex = newLocal(Type.getType("Ljava/lang/StringBuilder"));
mv.visitTypeInsn(Opcodes.NEW, "java/lang/StringBuilder");
mv.visitInsn(Opcodes.DUP);
mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false);
mv.visitVarInsn(Opcodes.ASTORE, stringBuilderIndex);//需要将栈顶的 stringbuilder 保存起来否则后面找不到了
mv.visitVarInsn(Opcodes.ALOAD, stringBuilderIndex);
mv.visitLdcInsn(className + "." + methodName + " time:");
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
mv.visitInsn(Opcodes.POP);
mv.visitVarInsn(Opcodes.ALOAD, stringBuilderIndex);
mv.visitVarInsn(Opcodes.LLOAD, timeLocalIndex);
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(J)Ljava/lang/StringBuilder;", false);
mv.visitInsn(Opcodes.POP);
mv.visitLdcInsn("Geek");
mv.visitVarInsn(Opcodes.ALOAD, stringBuilderIndex);
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);
mv.visitMethodInsn(Opcodes.INVOKESTATIC, "android/util/Log", "d", "(Ljava/lang/String;Ljava/lang/String;)I", false);//注意: Log.d 方法是有返回值的,需要 pop 出去
mv.visitInsn(Opcodes.POP);//插入字节码后要保证栈的清洁,不影响原来的逻辑,否则就会产生异常,也会对其他框架处理字节码造成影响
}
}
代码中有相应的注释,希望对你有所帮助。这里还需要对以下几点做解释说明:
- 类名className和方法名methodName是创建TraceMethodAdapter时候作为参数传递进来的。
- newLocal(Type.LONG_TYPE)是这个是LocalVariablesSorter提供的功能,可以尽量复用以前的临时变量。
- visitInsn(LSUB)就是将操作数栈中的两个数出栈。栈有先进后出和后进先出的特性。出栈的第一个数作为减数,出栈的第二个数作为被减数,并将结果重新压入操作数栈。代码中有很多都是操作数栈和局部变量之间的交互作用。
- 方法调用的时候,使用斜线作为间隔的类全名,类型限定符的时候前面加L。比如mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false)这句代码java/lang/StringBuilder是类名,()Ljava/lang/String;方法名描述符中String使用Ljava/lang/String;。int stringBuilderIndex = newLocal(Type.getType("Ljava/lang/StringBuilder"))声明临时变量的使用就使用Ljava/lang/StringBuilder。
然后来分析raceClassAdapter类,代码如下:
public class TraceClassAdapter extends ClassVisitor {
private String className;
TraceClassAdapter(int i, ClassVisitor classVisitor) {
super(i, 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;
}
@Override
public MethodVisitor visitMethod(int access, String name, String desc,
String signature, String[] exceptions) {
MethodVisitor methodVisitor = cv.visitMethod(access, name, desc, signature, exceptions);
return new TraceMethodAdapter(api, methodVisitor, access, name, desc, this.className);
}
}
TraceClassAdapter就是把类名className作为参数传递进来。
其次再看AddTimerClassVisitorFactory类,代码如下所示:
public abstract class AddTimerClassVisitorFactory implements AsmClassVisitorFactory<InstrumentationParameters.None> {
@Override
public ClassVisitor createClassVisitor(ClassContext classContext, ClassVisitor nextVisitor) {
return new TraceClassAdapter(Opcodes.ASM9, nextVisitor);
}
@Override
public boolean isInstrumentable(ClassData classData) {
return classData.getClassName().contains("com.zj.android_asm");
}
}
isInstrumentable方法,如果返回true代表此类需要处理。
最后来看MethodRecordPlugin类,代码如下所示:
class MethodRecordPlugin : Plugin<Project> {
override fun apply(target: Project) {
val androidComponents = target.extensions.getByType(AndroidComponentsExtension::class.java)
androidComponents.onVariants { variant ->
variant.instrumentation.transformClassesWith(
AddTimerClassVisitorFactory::class.java,
InstrumentationScope.ALL
) {}
variant.instrumentation.setAsmFramesComputationMode(
FramesComputationMode.COMPUTE_FRAMES_FOR_INSTRUMENTED_METHODS
)
}
}
}
整个流程也就是这样的,我们在build.gradle使用apply语法实际上是调用的Plugin,Plugin代码逻辑中instrumentation会注册AsmClassVisitorFactory,AsmClassVisitorFactory判断哪些类需要处理,createClassVisitor方法返回ClassVisitor, 根据相应的逻辑重写ClassVisitor相应的方法(比如修改相应的方法,增删改相应的数据等)。
运行结果如下图所示:
ASM方法调用替换
如果调用第三方框架的方法有问题,可能是逻辑错误,也可能是隐私相关的问题,同时我们也没有第三方框架的源码,没有办法通过直接修改的方式来实现,就可以用ASM把有问题的方法替换成没有问题的方法。今天就以简单的打印日志替换为例,代码如下面所示:
mport static org.objectweb.asm.Opcodes.ASM9;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
public class ReplaceMethodClassVisitor extends ClassVisitor {
ReplaceMethodClassVisitor(ClassVisitor classVisitor) {
super(ASM9, classVisitor);
}
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);
return new MethodVisitor(ASM9, mv) {
@Override
public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) {
System.out.println("owner is " + owner + " name is " + name);
if (owner.equals("java/io/PrintStream") && name.equals("println")) {
// 替换 System.out.println() 为 Log.d()
super.visitLdcInsn("Hello ASM"); // Log Message
// super.visitLdcInsn("Hello world"); // Log Message
super.visitMethodInsn(Opcodes.INVOKESTATIC, "android/util/Log", "d", "(Ljava/lang/String;Ljava/lang/String;)I", false);
} else {
super.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
}
}
};
}
}
运行结果如下图所示:
ASM异常监控
防止抛出的NullPointerException、ArrayIndexOutOfBoundsException等崩溃,从而防止崩溃的发生,从而保证软件的稳定性。自动插入 try-catch,防止崩溃。 出现崩溃的代码如下所示:
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
setSupportActionBar(binding.toolbar)
sayHello()
test(500)
testMethod()
}
fun sayHello() {
println("Hello, World!")
}
fun testMethod() {
val result: Int = 10 / 0 // 出现异常
}
private fun test(time: Long) {
Thread.sleep(time)
}
private fun sum(i: Int, j: Int): Int {
return i + j
}
}
MainActivity调用了testMethod()方法,已经”Caused by: java.lang.ArithmeticException: divide by zero“异常,导致应用崩溃。 如何把整个方法体trycatch呢,示例代码如下所示:
public class TryCatchMethodVisitor extends MethodVisitor {
private Label startLabel = new Label();
private Label endLabel = new Label();
private Label catchLabel = new Label();
public TryCatchMethodVisitor(MethodVisitor mv) {
super(Opcodes.ASM9, mv);
}
@Override
public void visitCode() {
super.visitLabel(startLabel); // ✅ 标记 try 开始
super.visitCode();
}
@Override
public void visitInsn(int opcode) {
if (opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN) {
super.visitLabel(endLabel); // ✅ 标记 try 结束
}
super.visitInsn(opcode);
}
@Override
public void visitMaxs(int maxStack, int maxLocals) {
super.visitTryCatchBlock(startLabel, endLabel, catchLabel, "java/lang/Exception");
super.visitLabel(catchLabel);
super.visitFrame(Opcodes.F_SAME1, 0, null, 1, new Object[]{"java/lang/Exception"});
super.visitVarInsn(Opcodes.ASTORE, 1);
super.visitLdcInsn("ASM");
super.visitLdcInsn("Exception caught!");
super.visitVarInsn(Opcodes.ALOAD, 1);
super.visitMethodInsn(Opcodes.INVOKESTATIC, "android/util/Log", "e",
"(Ljava/lang/String;Ljava/lang/String;Ljava/lang/Throwable;)I", false);
super.visitInsn(Opcodes.POP);
super.visitInsn(Opcodes.RETURN); // ✅ 确保 return 存在
super.visitMaxs(maxStack + 3, maxLocals);
}
}
总结
ASM可以做的事情很多,统计函数耗时是一个基本功能,方法替换,自动插入 try-catch,防止崩溃。希望文章对您有所帮助。