现在我们有一个需求,统计登录失败、某个activity的页面停留时间,我们怎么办?
1、在登录失败的地方、activity的生命周期处加日志,保存到本地数据看,再找个合适的时机上报给服务器(如页面启动且有网),这个方案可变性,操作性,定制性很高但会让客户端多很多无用代码
2、利用友盟、听云、oneapm等平台进行统计,但定制性不足,有时候无法满足需求
那么我们怎么去自己实现一个无痕埋点的统计呢?
先说说apk的生成过程吧,.java文件经过aapt工具打包生成.class文件,这其中包括资源文件、java接口和aidl文件,再通过gradle命令行将.class文件生成dex文件,dex文件再通过apkbuilder进行混淆,生成apk文件,再进行签名加固生成发布的apk文件,newrelic的方案是在.class文件生成dex的文件过程中,自定义gradle事件,拿到所有的.class字节码,再调用classrewriter找到对应的方法,再调用agent的方法进行上报统计,我们看下工程目录:
这是一个maven工程,这三个包的功能我们倒序的看下:
1、android-gradle_plugin:
主要是自定义gradle事件,用到了groovy语言,关于如何自定义gradle插件,百度下好了,有很多这里不再说了。
我们重点看下代码:
...
@Override
void apply(Project project) {
project.configure(project, {
if (project.hasProperty("android")) {
// for new android gradle build api since 1.4 version
if (android.respondsTo("registerTransform")) {
registerTransform(android, project);
return;
}
project.task(["type" : DemeterInstrumentTask.class], "demeterInstrumentTask");
project.task(["type" : DemeterDeinstrumentTask.class], "demeterDeinstrumentTask");
android.applicationVariants.all { variant ->
variant.dex.dependsOn("demeterInstrumentTask");
variant.dex.dependsOn("demeterDeinstrumentTask");
logger.info('[uyun] Added instrumentation tasks to ' + variant.name + ' variant. [applicationVariants]');
};
android.testVariants.all { variant ->
variant.dex.dependsOn("demeterInstrumentTask");
variant.dex.dependsOn("demeterDeinstrumentTask");
logger.info('[uyun] Added instrumentation tasks to ' + variant.name + ' variant. [testVariants]');
};
}
});
}
...
所有的gradle插件最开始都会走到Plugin的apply方法,我们看到这里其实是注册了两个事件,demeterInstrumentTask和demeterDeinstrumentTask,就是安装和卸载插件的事件,可能这是所有自定义插件通用的套路,这个我也不是很确定,能确定的可以说下,我们看下demeterInstrumentTask类:
...
class DemeterInstrumentTask extends DemeterTask {
@TaskAction
public void demeterInstrumentTask() {
try {
String extraArgs = System.getProperty("Demeter.AgentArgs");
String encodedProjectRoot = project.getProjectDir().getCanonicalPath().bytes.encodeBase64().toString();
String agentArgs = "projectRoot=" + encodedProjectRoot;
if (extraArgs != null) {
agentArgs += ";" + extraArgs;
}
logger.info("[Demeter] Attaching to process " + pid);
injectAgent(agentArgs);
}
catch (Exception e) {
logger.error("[Demeter] Error encountered while loading the Demeter agent");
throw new RuntimeException(e);
}
}
}
...
这里拿到.class文件的主工程目录,调用injectAgent方法:
...
public void injectAgent(String agentArgs) {
def vm = VirtualMachine.attach(getPid());
vm.loadAgent(getJarFilePath(), agentArgs);
vm.detach();
}
...
String jarFilePath = RewriterAgent.class.getProtectionDomain().getCodeSource().getLocation().toURI().getPath();
这里调用了虚拟机的api,加载Agent即会调用ClassRewriter的方法:
RewriterAgent的premain方法会被调用,到这里ClassRewriter的包开始启动了。
我们看下RewriterAgent的premain方法:
...
DemeterClassTransformer classTransformer;
if (agentOptions.containsKey("deinstrument")) {
log.info("Deinstrumenting...");
classTransformer = new NoOpClassTransformer();
} else {
classTransformer = new DexClassTransformer(log);
createInvocationDispatcher(log);
}
instrumentation.addTransformer(classTransformer, true);
List
for (Class> clazz : instrumentation.getAllLoadedClasses()) {
if (classTransformer.modifies(clazz)) {
classes.add(clazz);
}
}
if (!classes.isEmpty()) {
if (instrumentation.isRetransformClassesSupported()) {
log.debug("Retransform classes: " + classes);
instrumentation.retransformClasses(classes.toArray(new Class[classes.size()]));
} else {
log.error("Unable to retransform classes: " + classes);
}
}
if (!agentOptions.containsKey("deinstrument")) {
redefineClass(instrumentation, classTransformer, ProcessBuilder.class);
}
...
上面的逻辑是取到所有的class文件,重新定义到classTransformer中,DexClassTransformer是我们的重点,看下实现:
private static final class DexClassTransformer implements RewriterAgent.DemeterClassTransformer
private static interface DemeterClassTransformer extends ClassFileTransformer {
boolean modifies(Class> paramClass);
}
ClassFileTransformer 内部有个接口方法:transform,字节码加载到虚拟机前会进入这个方法
...
/**
* 进行类转换操作。
*/
public byte[] transform(ClassLoader classLoader, String className, Class> clazz, ProtectionDomain protectionDomain, byte[] bytes)
throws IllegalClassFormatException {
}
...
createInvocationDispatcher方法再通过动态代理实现classVisitor的注册,classReader,ClassWriter都是asm中的类,通过他们的api可以实现对class文件的修改,
```
put(RewriterAgent.getProxyInvocationKey(DEXER_MAIN_CLASS_NAME, PROCESS_CLASS_METHOD_NAME), new InvocationHandler() {
@Override
public Object invoke(Object proxy, java.lang.reflect.Method method, Object[] args) throws Throwable {
ClassReader cr = new ClassReader(bytes);
ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_MAXS);
context.reset();
cr.accept(new PrefilterClassVisitor(context, log), 7);
className = context.getClassName();
if (!context.hasTag(Annotations.INSTRUMENTED)) {
ClassVisitor cv = cw;
if (context.getClassName().startsWith(******/android")) {
cv = new DemeterClassVisitor(cv, context, log);
} else if (context.getClassName().startsWith("android/support/")) { // android 视图相关的类
cv = new ActivityClassVisitor(cv, context, log);
} else if (isExcludedPackage(context.getClassName())) {
return null;
} else {
cv = new AnnotatingClassVisitor(cv, context, log);
cv = new ActivityClassVisitor(cv, context, log);
}
cr.accept(cv, 12);
} else {
log.warning(MessageFormat.format("[{0}] class is already instrumented! skipping ...",
new Object[] { context.getFriendlyClassName() }));
}
return context.newClassData(cw.toByteArray());
}
```
这里去找到Android系统相关的类,构建自己需要替换的类和对应的方法,如ActivityClassVisitor:
看下继承结构:
public class ClassAdapter implements ClassVisitor
最终继承asm的ClassVisitor,看下方法:
```
public void visit( final int version,final int access,final String name,final String signature, final String superName,final String[] interfaces){
cv.visit(version, access, name, signature, superName, interfaces);
}
public MethodVisitor visitMethod(final int access,final String name,final String desc,final String signature,final String[] exceptions) {
return cv.visitMethod(access, name, desc, signature, exceptions);
}
public void visitEnd() {
cv.visitEnd();
}
```
visit在访问头部的时候,如定义的局部变量、静态变量等,会回调这个方法。
visitMethod访问方法的时候调用,构建一个GeneratorAdapter
public class GeneratorAdapter extends LocalVariablesSorter
回调visitCode方法进行拦截,可以对取得的方法进行插入式的操作,
visitEnd在访问到拦截方法后,方法结束前调用
因此通过visitMethod和visitEnd方法,可以实现aop的操作,我们看下visitMethod方法:
```
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
if (this.instrument && traceMethodMap.containsKey(name) && traceMethodMap.get(name).equals(desc)) {
MethodVisitor methodVisitor = super.visitMethod(access, name, desc, signature, exceptions);
ActivityTraceMethodVisitor traceMethodVisitor = new ActivityTraceMethodVisitor(methodVisitor, access, name, desc,
this.context);
traceMethodVisitor.setStartTracing();
return traceMethodVisitor;
}
return super.visitMethod(access, name, desc, signature, exceptions);
}
```
这里构建了一个ActivityTraceMethodVisitor,看下它的继承关系;
AdviceAdapter是asm官方工具类,主要用到visitCode和visitInsn方法,
visitCode中调用onMethodEnter
visitInsn分析字节码中是否含有return、throw等字节码,调用onMethodExit方法,
最终我们在ActivityTraceMethodVisitor中定义onMethodEnter和onMethodExit方法,
在方法中反射调用对应类需要注入的方法
另外重点在EventHookClassVisitor中
```
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);
Method method = new Method(name, desc);
MethodVisitorFactory v = this.methodVisitors.get(method);
if (v != null) {
this.methodVisitors.remove(method); // 如果存在当前类存在需要hook的方法,则hook住该方法,同时从map在移除
return v.createMethodVisitor(access, method, mv, false);
}
return mv;
}
```
visitMethod方法所有方法的调用都会走这里,识别出需要拦截的方法进行hook,所以methodVisitors就是我们所存放的类名和方法名数组,只要定义好这些要拦截的类和方法,就可以了。
核心其实在asm的使用,其中很多方法我也没能完全理解,只能把我目前理解到的分享出来,望大家见谅。
最后是比较简单的agent包:
这个包主要是最上层拦截到的数据,进行封装、存数据库,再定时发送给服务器,如果发送成功,清空本地数据库即可。
要实现无痕埋点,需要的技术栈很深,本文也只是提供了一种思路,自定义groovy事件,其实在很多开源项目都用到了,比如butterKnife,不过他增加了注解更方便的去找所需要的控件。