埋点是每个App程序员都不得不面对的事情,而因为加埋点导致业务代码臃肿膨胀则是一件让人火大的问题。并且业务代码中的埋点代码还存在在迭代中被误删的风险,因此如何将业务和埋点拆分变成了一个亟需解决的问题。gradle插件+asm代码插桩就可以提供一种解决方案。
预备工作
需求解析/技术方案
好了,现在我们已经掌握了gradle插件和ASM的基本用法了。现在我们来简单分析下我们的现实需求和简单的技术方案。
首先我们需要做到的是两个事情:
1.埋点可以成功上传
2.埋点的处理包括逻辑计算都不在业务代码中进行。
从上面的预备知识中我们可以知道,gradle插件可以再class->aar这个步骤中对class进行修改。而asm就是我们修改class的工具。
实现
从简单的开始,假设我们有一个方法firstMethod用来处理一段很长的业务,其中这个方法里我们需要增加多个埋点。
fun firstMethod(){
业务代码断
埋点代码段1
埋点代码段2
}
对上述代码进行调整,把埋点代码抽离到独立的utils
package com.demo.cn.utils;
public class TraceUtils {
public static void traceForFirst(){
埋点代码段1
埋点代码段2
}
}
业务代码修改为
fun firstMethod(){
业务代码断
TraceUtils.traceForFirst()
}
现在我们的目的改为把TraceUtils.traceForFirst()这行代码从业务中抽离出来。
先看下ASM中的AdviceAdapter抽象类,该类的继承结构如下:
AdviceAdapter->GeneratorAdapter->LocalVariablesSorter->MethodVisitor。该类提供了对class文件中方法的字节码修改。看一下该类中一些我们会用到的一些关键方法:
@Override
protected void onMethodEnter() {
super.onMethodEnter()
}
@Override
AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
return super.visitAnnotation(descriptor, visible)
}
@Override
AnnotationVisitor visitParameterAnnotation(int parameter, String descriptor, boolean visible) {
return super.visitParameterAnnotation(parameter,descriptor,visible);
}
从方法名我们可以大概猜到这些方法的作用,visitAnnotation用来访问方法上的注解,visitParameterAnnotation用来访问参数注解,onMethodEnter负责访问方法的具体字节码。他们的调用顺序为
( visitParameter )*
[ visitAnnotationDefault ]
( visitAnnotation |visitAnnotableParameterCount | visitParameterAnnotation
visitTypeAnnotation | visitAttribute )*
[ visitCode ( visitFrame | visitInsn | visitLabel | visitInsnAnnotation | visitTryCatchBlock | visitTryCatchAnnotation | visitLocalVariable |
visitLocalVariableAnnotation | visitLineNumber* visitMaxs ]
visitEnd
其中onMethodEnter是在visitCode进行了调用。我们可以发现,在每个MethodVisitor里都会先去visit它的Annotation和visitParameterAnnotation,所以我们可以通过注解的方式来标记需要增加埋点的方法。
声明注解
public @interface TraceMethodName {
String value();
}
@Target( ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface TraceParams {
String value();
}
我们用TraceMethodName来标记需要加埋点的方法,用TraceParams来标记具体的参数。这两个注解将配合上面的visitAnnotation和visitParameterAnnotation来定位需要插桩的方法。
现在我们来将之前的的例子加上注解进行标记
@TraceMethodName("traceForFirst")
fun firstMethod(){
业务代码断
TraceUtils.traceForFirst()
}
好了准备工作都做完了,我们现在开始使用asm来处理这段代码
标记声明方法
我们来创建一个类来继承AdviceAdapter,用来解析修改方法的字节码
public class TraceMethodVisitor extends AdviceAdapter {
String eventName = null;
String eventMethonName = null;
String eventProperties = null;
private HashMap<Integer, String> nameMap;
public TraceMethodVisitor(int api, MethodVisitor methodVisitor, int access, String name, String descriptor) {
super(api, methodVisitor, access, name, descriptor);
}
@Override
public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
if (descriptor.equals("Lasm/TraceMethodName;")) {
return new AnnotationVisitor(api) {
@Override
public void visit(String name, Object value) {
super.visit(name, value);
eventMethonName = (String) value;
}
};
}
return super.visitAnnotation(descriptor, visible);
}
@Override
public AnnotationVisitor visitParameterAnnotation(final int parameter, String descriptor, boolean visible) {
if (descriptor.equals("Lasm/TraceParams;")) {
if (nameMap == null) {
nameMap = new HashMap();
}
return new AnnotationVisitor(api) {
@Override
public void visit(String name, Object value) {
super.visit(name, value);
System.out.print(value);
nameMap.put(parameter, (String) value);
}
};
}
return super.visitParameterAnnotation(parameter, descriptor, visible);
}
}
先看visitAnnotation方法,我们只关心descriptor。该参数就是注解的描述符。我们需要做的就是通过字符串匹配来筛选出我们上面声明的注解。
再看下visitParameterAnnotation方法,我们用一个hashmap来保存注解和参数对应关系的键值对。key值是参数的下标,value是注解的value值。(在具体业务中,可能不需要参数注解)
在目标方法上增加插桩内容
现在我们已经通过eventMethonName字段来标记了需要进行插桩的方法。该字段不为null,当前方法需要进行插桩,为null则不需要处理。
我们再来看下插桩的流程:
package cn.tsign.plugin.trace
import org.objectweb.asm.AnnotationVisitor
import org.objectweb.asm.MethodVisitor
import org.objectweb.asm.Type
import org.objectweb.asm.commons.AdviceAdapter
public class TraceMethodVisitor extends AdviceAdapter{
private final int methodAccess;
private final String methodName;
private final String methodDesc;
String eventName = null
String eventProperties = null
String eventMethodName = null
private HashMap<Integer,String> nameMap = new HashMap();
public TraceMethodVisitor(int api, MethodVisitor methodVisitor, int access, String name, String descriptor) {
super(api, methodVisitor, access, name, descriptor)
this.methodAccess = access;
this.methodName = name;
this.methodDesc = descriptor;
}
@Override
protected void onMethodEnter() {
super.onMethodEnter()
if ((nameMap==null||nameMap.isEmpty())&&eventName==null){
return
}
Type methodType = Type.getMethodType(methodDesc);
Type[] parameterTypeList = methodType.getArgumentTypes();
int parameterCount = parameterTypeList.length;
int stackIndex = 1;
//根据方法参数来计算偏移值。
for (int i=0;i<parameterCount;i++){
String type = parameterTypeList[i].toString();
if ("D".equals(type)) {
stackIndex=stackIndex+2;
} else {
stackIndex++;
}
}
if (eventMethodName!=null){
executeWithMethod(parameterCount,parameterTypeList)
}
}
void executeWithMethod(int parameterCount, Type[] parameterTypeList) {
int cursor = 0;
StringBuilder builder = new StringBuilder("(");
//统计参数转移对应的参数
for (int i = 0; i < parameterCount; i++) {
String key = nameMap.get(i);
String type = parameterTypeList[i].toString();
if (key == null || key.length() == 0) {
if ("D".equals(type)) {
cursor += 2;
} else {
cursor++;
}
continue;
}
if ("Z".equals(type)) {
cursor++;
mv.visitVarInsn(ILOAD, cursor); //获取对应的参数
builder.append(type);
} else if ("C".equals(type)) {
cursor++;
mv.visitVarInsn(ILOAD, cursor); //获取对应的参数
builder.append(type);
} else if ("B".equals(type)) {
cursor++;
mv.visitVarInsn(ILOAD, cursor); //获取对应的参数
builder.append(type);
} else if ("S".equals(type)) {
cursor++;
mv.visitVarInsn(ILOAD, cursor); //获取对应的参数
builder.append(type);
} else if ("I".equals(type)) {
cursor++;
mv.visitVarInsn(ILOAD, cursor); //获取对应的参数
builder.append(type);
} else if ("F".equals(type)) {
cursor++;
mv.visitVarInsn(FLOAD, cursor); //获取对应的参数
builder.append(type);
} else if ("J".equals(type)) {
cursor++;
mv.visitVarInsn(LLOAD, cursor); //获取对应的参数
builder.append(type);
} else if ("D".equals(type)) {
cursor++;
mv.visitVarInsn(DLOAD, cursor); //获取对应的参数
cursor++;
builder.append(type);
} else {
cursor++;
mv.visitVarInsn(ALOAD, cursor); //获取对应的参数
builder.append("Ljava/lang/Object;");
}
if (i == parameterCount - 1) {
builder.append(")");
}
}
mv.visitMethodInsn(INVOKESTATIC, "utils/TraceUtils", eventMethodName, builder.toString() + "V", false);
}
}
mv.visitMethodInsn(INVOKESTATIC, "utils/TraceUtils", eventMethodName, builder.toString() + "V", false);
我们已经通过asm工具把TraceUtils中的方法traceForFirst通过asm添加到了firstMethod后面。
网友评论