个人博客
http://www.milovetingting.cn
ASM字节码插桩
前言
热修复的多Dex加载方案中,对于5.0以下的系统存在CLASS_ISPREVERIFIED的问题,而解决这个问题的一个方案是:通过ASM插桩,在类的构造方法里引入一个其它dex里的类,从而避免被打上CLASS_ISPREVERIFIED标签。热修复可以参考其它资料或者前面写的一篇文章。本文主要介绍ASM插桩,主要参考 https://juejin.im/post/5c6eaa066fb9a049fc042048
ASM框架
ASM是一个可以分析和操作字节码的框架,通过它可以动态地修改字节码内容。使用ASM可以实现无埋点统计、性能监控等。
什么是字节码插桩
Android编译过程中,往字节码插入自定义的字节码。
插桩时机
Android打包要经过:java文件–class文件–dex文件,通过Gradle提供的Transform API,可以在编译成dex文件前,得到class文件,然后通过ASM修改字节码,即字节码插桩。
实现
下面通过自定义Gradle插件来处理class文件来实现插桩。
自定义Gradle插件
具体自定义Gradle插件的步骤,这里不再详细介绍,可以参考之前的一篇文章或者自行查阅其它资料。
处理Class
插件分为插件部分(src/main/groovy)、ASM部分(src/main/java)
ASMPlugin类继承自Transform并实现Plugin接口,在apply的方法里注册,transform里回调并处理class。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| class ASMPlugin extends Transform implements Plugin<Project> {
@Override void apply(Project project) { def android = project.extensions.getByType(AppExtension) android.registerTransform(this) }
@Override String getName() { return "ASMPlugin" }
@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 { } }
|
主要的逻辑处理都在transform方法里
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| @Override void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException { println('--------------------ASMPlugin transform start--------------------') def startTime = System.currentTimeMillis() Collection<TransformInput> inputs = transformInvocation.inputs TransformOutputProvider outputProvider = transformInvocation.outputProvider if (outputProvider != null) { outputProvider.deleteAll() } inputs.each { input -> input.directoryInputs.each { directoryInput -> handleDirectoryInput(directoryInput, outputProvider) } input.jarInputs.each { jarInput -> handleJarInput(jarInput, outputProvider) } } def time = (System.currentTimeMillis() - startTime) / 1000 println('-------------------- ASMPlugin transform end --------------------') println("ASMPlugin cost $time s") }
|
在transform里处理class文件和jar文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88
|
static void handleDirectoryInput(DirectoryInput directoryInput, TransformOutputProvider outputProvider) { if (directoryInput.file.isDirectory()) { directoryInput.file.eachFileRecurse { file -> def name = file.name if (isClassFile(name)) { println("-------------------- handle class file:<$name> --------------------") ClassReader classReader = new ClassReader(file.bytes) ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS) ClassVisitor classVisitor = new ActivityClassVisitor(classWriter) classReader.accept(classVisitor, org.objectweb.asm.ClassReader.EXPAND_FRAMES) byte[] bytes = classWriter.toByteArray() FileOutputStream fileOutputStream = new FileOutputStream(file.parentFile.absolutePath + File.separator + name) fileOutputStream.write(bytes) fileOutputStream.close() } } } def dest = outputProvider.getContentLocation(directoryInput.name, directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY) FileUtils.copyDirectory(directoryInput.file, dest) }
static void handleJarInput(JarInput jarInput, TransformOutputProvider outputProvider) { if (jarInput.file.getAbsolutePath().endsWith(".jar")) { def jarName = jarInput.name def md5Name = DigestUtils.md5Hex(jarInput.file.absolutePath) if (jarName.endsWith(".jar")) { jarName = jarName.substring(0, jarName.length() - 4) } JarFile jarFile = new JarFile(jarInput.file) Enumeration enumeration = jarFile.entries() File tempFile = new File(jarInput.file.parent + File.separator + "temp.jar") if (tempFile.exists()) { tempFile.delete() } JarOutputStream jarOutputStream = new JarOutputStream(new FileOutputStream(tempFile)) while (enumeration.hasMoreElements()) { JarEntry jarEntry = enumeration.nextElement() String entryName = jarEntry.name ZipEntry zipEntry = new ZipEntry(entryName) InputStream inputStream = jarFile.getInputStream(zipEntry) if (isClassFile(entryName)) { println("-------------------- handle jar file:<$entryName> --------------------") jarOutputStream.putNextEntry(zipEntry) ClassReader classReader = new ClassReader(IOUtils.toByteArray(inputStream)) ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS) ClassVisitor classVisitor = new ActivityClassVisitor(classWriter) classReader.accept(classVisitor, org.objectweb.asm.ClassReader.EXPAND_FRAMES) byte[] bytes = classWriter.toByteArray() jarOutputStream.write(bytes) } else { jarOutputStream.putNextEntry(zipEntry) jarOutputStream.write(IOUtils.toByteArray(inputStream)) } jarOutputStream.closeEntry() } jarOutputStream.close() jarFile.close() def dest = outputProvider.getContentLocation(jarName + "_" + md5Name, jarInput.contentTypes, jarInput.scopes, Format.JAR) FileUtils.copyFile(tempFile, dest) tempFile.delete() } }
static boolean isClassFile(String name) { return (name.endsWith(".class") && !name.startsWith("R\$") && "R.class" != name && "BuildConfig.class" != name && name.contains("Activity")) }
|
在handleDirectoryInput和handleJarInput调用了我们自己定义在src/main/java里的ClassVisitor,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| class ActivityClassVisitor extends ClassVisitor implements Opcodes {
private String mClassName;
private static final String CLASS_NAME_ACTIVITY = "androidx/appcompat/app/AppCompatActivity";
private static final String METHOD_NAME_ONCREATE = "onCreate";
private static final String METHOD_NAME_ONDESTROY = "onDestroy";
public ActivityClassVisitor(ClassVisitor cv) { super(Opcodes.ASM5, cv); }
@Override public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { mClassName = name; super.visit(version, access, name, signature, superName, interfaces); }
@Override public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { MethodVisitor methodVisitor = cv.visitMethod(access, name, desc, signature, exceptions); if (CLASS_NAME_ACTIVITY.equals(mClassName)) { if (METHOD_NAME_ONCREATE.equals(name)) { System.out.println("-------------------- ActivityClassVisitor,visit method:" + name + " --------------------"); return new ActivityOnCreateMethodVisitor(Opcodes.ASM5, methodVisitor); } else if (METHOD_NAME_ONDESTROY.equals(name)) { System.out.println("-------------------- ActivityClassVisitor,visit method:" + name + " --------------------"); return new ActivityOnDestroyMethodVisitor(Opcodes.ASM5, methodVisitor); } } return methodVisitor; } }
|
这里为简化操作,只处理了Activity的onCreate和onDestroy方法。在visitMethod方法里又调用了具体的MethodVisitor。如果对字节码不是特别了解的,可以通过在Android Studio中安装ASM Bytecode Outline插件来辅助。
具体使用:
安装完成ASM Bytecode Outline后,重启Android Studio,然后在相应的Java文件中右键,选择Show Bytecode outline
稍待一会后,会生成相应的字节码,在打开的面板中选择ASMified标签
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| public class ActivityOnCreateMethodVisitor extends MethodVisitor {
public ActivityOnCreateMethodVisitor(int api, MethodVisitor mv) { super(api, mv); }
@Override public void visitCode() { mv.visitLdcInsn("ASMPlugin"); mv.visitLdcInsn("-------------------- MainActivity onCreate --------------------"); mv.visitMethodInsn(INVOKESTATIC, "android/util/Log", "i", "(Ljava/lang/String;" + "Ljava/lang/String;)I", false); mv.visitInsn(POP);
super.visitCode(); }
@Override public void visitInsn(int opcode) { super.visitInsn(opcode); } }
public class ActivityOnDestroyMethodVisitor extends MethodVisitor {
public ActivityOnDestroyMethodVisitor(int api, MethodVisitor mv) { super(api, mv); }
@Override public void visitCode() { super.visitCode();
mv.visitLdcInsn("ASMPlugin"); mv.visitLdcInsn("-------------------- MainActivity onDestroy --------------------"); mv.visitMethodInsn(INVOKESTATIC, "android/util/Log", "i", "(Ljava/lang/String;" + "Ljava/lang/String;)I", false); mv.visitInsn(POP); }
@Override public void visitInsn(int opcode) { super.visitInsn(opcode); } }
|
在visitCode和visitInsn方法里执行具体的操作。
在处理Class过程中,可能会出现各种问题,可以通过调试插件来定位问题。可以参考上一篇文章来调试插件。
引用插件
在app模块引用插件,这里不再详细介绍,可以参考前面的文章
将应用运行在手机上,打开后,可以看到日志输出:
1 2
| 02-25 17:29:45.885 31237 31237 I ASMPlugin: -------------------- MainActivity onCreate -------------------- 02-25 17:29:50.646 31237 31237 I ASMPlugin: -------------------- MainActivity onDestroy --------------------
|
结语
这篇文章只是实现了简单的ASM插桩。可以查阅其它资料,了解更多关于字节码、ASM相关的内容。
源码地址:https://github.com/milovetingting/Samples/tree/master/ASM