newrelic 数据无痕埋点的实现思路

现在我们有一个需求,统计登录失败、某个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的方法进行上报统计,我们看下工程目录:
newrelic 数据无痕埋点的实现思路_第1张图片
这是一个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> classes = new ArrayList>();
            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,看下它的继承关系;
newrelic 数据无痕埋点的实现思路_第2张图片
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,不过他增加了注解更方便的去找所需要的控件。
 

你可能感兴趣的:(Android)