编译插桩
编译插桩的应用场景
代码生成 除了 Dagger、ButterKnife 这些常用的注解生成框架,Protocol Buffers、数据库 ORM 框架也都会在编译过程生成代码。
代码监控 除了网络监控和耗电监控,我们可以利用编译插桩技术实现各种各样的性能监控。
代码修改
代码分析
对于代码监控、代码修改以及代码分析这三个场景,一般采用操作字节码的方式。可以操作“.class”的 Java 字节码,也可以操作“.dex”的 Dalvik 字节码,这取决于我们使用的插桩方法。
字节码

编译插桩的量种方法
AspectJ 和 ASM 框架的输入和输出都是 Class 文件,它们是我们最常用的 Java 字节码处理框架。
AspectJ
AspectJ是 Java 中流行的 AOP(aspect-oriented programming)编程扩展框架。
基于AspectJ的扩展工具:
沪江
hugo
AspectJ的优势
成熟稳定: AspectJ 作为从 2001 年发展至今的框架,它已经很成熟,一般不用考虑插入的字节码正确性的问题。
**使用简单:**它可以在方法(包括构造方法)被调用的位置、在方法体(包括构造方法)的内部、在读写变量的位置、在静态代码块内部、在异常处理的位置等前后,插入自定义的代码,或者直接将原位置的代码替换为自定义的代码。
AspectJ的缺点
- 切入点固定:AspectJ 只能在一些固定的切入点来进行操作,如果想要进行更细致的操作则无法完成,它不能针对一些特定规则的字节码序列做操作。
- 正则表达式:AspectJ 的匹配规则是类似正则表达式的规则,比如匹配 Activity 生命周期的 onXXX 方法,如果有自定义的其他以 on 开头的方法也会匹配到。
- 性能较低:AspectJ 在实现时会包装自己的一些类,逻辑比较复杂,不仅生成的字节码比较大,而且对原函数的性能也会有所影响。
使用 AspectJ 有两种方式:
- 完全使用 AspectJ 的语言开发;
- 使用 AspectJ 注解,完全的使用纯 Java 开发。
下面是注解的使用方式:
参考示例:
demo链接
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| private static final String POINTCUT_METHOD = "execution(@org.android10.gintonic.annotation.DebugTrace * *(..))"; private static final String POINTCUT_CONSTRUCTOR = "execution(@org.android10.gintonic.annotation.DebugTrace *.new(..))"; @Pointcut(POINTCUT_METHOD) public void methodAnnotatedWithDebugTrace() {} @Pointcut(POINTCUT_CONSTRUCTOR) public void constructorAnnotatedDebugTrace() {}
@Around("methodAnnotatedWithDebugTrace() || constructorAnnotatedDebugTrace()") public Object weaveJoinPoint(ProceedingJoinPoint joinPoint) throws Throwable { // joint 对象信息 MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature(); String className = methodSignature.getDeclaringType().getSimpleName(); String methodName = methodSignature.getName();
final StopWatch stopWatch = new StopWatch(); stopWatch.start(); Object result = joinPoint.proceed(); stopWatch.stop(); DebugLog.log(className, buildLogMessage(methodName, stopWatch.getTotalTimeMillis())); }
|
ASM
ASM就是一个可以实现 100% 场景的 Java 字节码操作框架。
ASM的特点:
- 操作灵活操作起来很灵活,可以根据需求自定义修改、插入、删除。上手难。
- 上手比较难需要对 Java 字节码有比较深入的了解。
基于 ASM 的字节码处理工具:
Hunter
Hibeaver
Transform
Transform是gradle构建的时候从class文件转换到dex文件期间处理class文件的一套方案,ClassVisitor可以是看做处理单个class文件,Transform可以处理一系列的class文件:从查找到所有class文件,到交给ClassVisitor和MethodVisitor处理后,再到重新覆盖原来的class文件这么一个流程。
ClassVisitor
用于访问 Java 类文件。
ClassReader
用于将 Java 类文件转换成 ClassVisitor 能访问的结构。它有四个构造函数,分别支持 byte[]、InputStream、File Path 三种输入方式。
ClassWriter
用于生成符合 JVM 规范的字节码文件,可以单独使用进行生成字节码文件,也可以配合 ClassReader 或 ClassVisitor 适配器进行现有类文件的修改。ClassWriter 提供了两种创建方式,一种是单独创建,另一种是以 ClassReader 作为参数。
参考示例
demo链接
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
| /** * 处理Jar中的class文件 */ static void handleJarInputs(JarInput jarInput, TransformOutputProvider outputProvider) throws IOException { if (jarInput.getFile().getAbsolutePath().endsWith(".jar")) { String jarName = jarInput.getName(); String md5Name = DigestUtils.md5Hex(jarInput.getFile().getAbsolutePath()); if (jarName.endsWith(".jar")) { jarName = jarName.substring(0, jarName.length() - 4); } JarFile jarFile = new JarFile(jarInput.getFile()); Enumeration enumeration = jarFile.entries(); File tmpFile = new File(jarInput.getFile().getParent() + File.separator + "classes_temp.jar"); if (tmpFile.exists()) { tmpFile.delete(); } JarOutputStream jarOutputStream = new JarOutputStream(new FileOutputStream(tmpFile)); while (enumeration.hasMoreElements()) { JarEntry jarEntry = (JarEntry) enumeration.nextElement(); String entryName = jarEntry.getName(); ZipEntry zipEntry = new ZipEntry(entryName); InputStream inputStream = jarFile.getInputStream(jarEntry); //插桩class if (checkClassFile(entryName)) { //class文件处理 System.out.println("----------- deal with jar class file <" + entryName + ">"); jarOutputStream.putNextEntry(zipEntry); ClassReader classReader = new ClassReader(IOUtils.toByteArray(inputStream)); ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS); ClassVisitor cv = new TestClassAdapter(classWriter); classReader.accept(cv, EXPAND_FRAMES); byte[] code = classWriter.toByteArray(); jarOutputStream.write(code); } else { jarOutputStream.putNextEntry(zipEntry); jarOutputStream.write(IOUtils.toByteArray(inputStream)); } jarOutputStream.closeEntry(); } //结束 jarOutputStream.close(); jarFile.close(); File dest = outputProvider.getContentLocation(jarName + md5Name, jarInput.getContentTypes(), jarInput.getScopes(), Format.JAR); FileUtils.copyFile(tmpFile, dest); tmpFile.delete(); } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public class TestClassAdapter extends ClassVisitor { public TestClassAdapter(ClassVisitor cv) { super(Opcodes.ASM5,cv); } @Override public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { MethodVisitor methodVisitor=super.visitMethod(access, name, desc, signature, exceptions); if (FilterUtil.isMatchingMethod(name,desc)){ return methodVisitor==null?null:new TestMethodAdapter(methodVisitor); }else { return methodVisitor; } } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| public class TestMethodAdapter extends MethodVisitor implements Opcodes { public TestMethodAdapter(MethodVisitor mv) { super(Opcodes.ASM5,mv); }
@Override public void visitCode() { super.visitCode(); mv.visitVarInsn(Opcodes.ALOAD, 0); mv.visitVarInsn(Opcodes.ALOAD, 0); mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "android/support/v4/app/FragmentActivity", "getApplication", "()Landroid/app/Application;", false); mv.visitIntInsn(Opcodes.SIPUSH, 360); mv.visitMethodInsn(Opcodes.INVOKESTATIC, "lqf/com/myutils2/ScreenUtils", "setCustomDebsity", "(Landroid/app/Activity;Landroid/app/Application;I)V", false); mv.visitEnd(); } }
|
字节码工具
ASM Bytecode Outline
ASM Bytecode Viewer Support Kotlin

参考文献
https://blog.csdn.net/innost/article/details/49387395
https://blog.csdn.net/yxhuang2008/article/details/94193201
https://blog.csdn.net/Mr_dsw/article/details/111112109