0%

Android编译插桩

编译插桩

编译插桩的应用场景

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

字节码

avatar

编译插桩的量种方法

AspectJ 和 ASM 框架的输入和输出都是 Class 文件,它们是我们最常用的 Java 字节码处理框架。

AspectJ

AspectJ是 Java 中流行的 AOP(aspect-oriented programming)编程扩展框架。
基于AspectJ的扩展工具:
沪江
hugo

AspectJ的优势

成熟稳定: AspectJ 作为从 2001 年发展至今的框架,它已经很成熟,一般不用考虑插入的字节码正确性的问题。
**使用简单:**它可以在方法(包括构造方法)被调用的位置、在方法体(包括构造方法)的内部、在读写变量的位置、在静态代码块内部、在异常处理的位置等前后,插入自定义的代码,或者直接将原位置的代码替换为自定义的代码。

AspectJ的缺点
  1. 切入点固定:AspectJ 只能在一些固定的切入点来进行操作,如果想要进行更细致的操作则无法完成,它不能针对一些特定规则的字节码序列做操作。
  2. 正则表达式:AspectJ 的匹配规则是类似正则表达式的规则,比如匹配 Activity 生命周期的 onXXX 方法,如果有自定义的其他以 on 开头的方法也会匹配到。
  3. 性能较低:AspectJ 在实现时会包装自己的一些类,逻辑比较复杂,不仅生成的字节码比较大,而且对原函数的性能也会有所影响。

使用 AspectJ 有两种方式:

  1. 完全使用 AspectJ 的语言开发;
  2. 使用 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的特点:

  1. 操作灵活操作起来很灵活,可以根据需求自定义修改、插入、删除。上手难。
  2. 上手比较难需要对 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

avatar

参考文献

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