APM之实现篇

在前文中已经详细介绍了APM的android端的原理,接下来会通过代码实现记录某类异常日志这个小功能来深入理解APM的实现原理。场景如下,记录所有捕获的IndexOutOfBoundsException。前文中提到,APM一般分为3个部分,plugin、agent和具体的业务代码。本文也将会按这三个分类来介绍。
注:由于篇幅有限,本文所展示的只有部分关键代码,有兴趣的可自行阅读github上的源码。

  • 业务代码
    我们的业务场景很简单,只需提供一个处理异常的方法就足够了。
public static void pushException(Throwable th){
    //在这里处理异常,如打印或上传日志
}
  • agent
    agent是最复杂的一个部分。它最终要达到的目的就是改写dexer.Main,在它执行processClass方法内的代码之前通过ASM工具修改第二个参数,即源class文件的byte数组。如果这个class的某个方法中包含了捕获IndexOutOfBoundsException的try-catch代码块,我们将在catch内调用上面的pushException方法。然后将这个修改过后的类对应的byte数组再替换回去。
    首先说明,字节码是通过异常表来处理异常的,有兴趣的可以通过字节码查看工具来查看异常表长什么样子。大概就是表里面每行记录都定义了如果代码在start行到end行之间抛出了异常,那么将转到handle行进行处理。这里的start到end就相当于try到catch之间的代码,而handle就是catch内开始的代码。查看ASM文档,AdviceAdapter中的visitTryCatchBlock和visitLabel这两个方法正好能满足我们的需求。只需要在visitTryCatchBlock方法中记录目标exception处理的handle,然后,如果在visitLabel中传入的正是我们刚才记录的handle,则加上调用pushException方法的代码。
public class ExceptionLogMethodAdapter extends AdviceAdapter {
    private TransformContext context;
    //记录所有目标exception的handle
    //key为handle,value是此handle对应的exception。
    //注:一个catch可能包含了多个exception,
    //如catch(IndexOutOfBoundsException | Exception e)
    private HashMap> matchedHandle = new HashMap<>();

    protected ExceptionLogMethodAdapter(TransformContext context
            , MethodVisitor methodVisitor, int access, String name, String desc) {
        super(Opcodes.ASM5, methodVisitor, access, name, desc);
        this.context = context;
    }

    @Override
    public void visitTryCatchBlock(Label start, Label end, Label handle,
                                   String exception) {
        //目标exception,在本文中为java/lang/IndexOutOfBoundsException
        HashSet targetException = context.getExceptions();
        if (exception != null && targetException.contains(exception)) {
            context.getLog().d("find exception " + exception);
            ArrayList handles = matchedHandle.get(handle);
            if(handles == null) handles = new ArrayList<>();
            handles.add(exception);
            matchedHandle.put(handle, handles);
        }
        super.visitTryCatchBlock(start, end, handle, exception);
    }

    @Override
    public void visitLabel(Label label) {
        super.visitLabel(label);
        ArrayList exceptions;
        if(label != null && (exceptions = matchedHandle.get(label)) != null){
            context.getLog().d("instrument exception");
            Label matched = new Label();
            Label end = new Label();
            //捕获的是目标exception的实例才进行处理
            final int N = exceptions.size() - 1;
            if (N >= 1) {
                for (int i = 0; i < N; i++) {
                    compareInstance(IFNE, exceptions.get(i), matched);
                }
            }
            compareInstance(IFEQ, exceptions.get(N), end);
            visitLabel(matched);
            dup();
            //调用pushException方法
            invokeStatic(Type.getObjectType("com/github/sgwhp/openapm/monitor/Monitor")
                    , new Method("pushException", "(Ljava/lang/Throwable;)V"));
            visitLabel(end);
            //将此类标记为已修改
            context.markModified();
        }
    }

    private void compareInstance(int mode, String type, Label to){
        dup();
        instanceOf(Type.getObjectType(type));
        visitJumpInsn(mode, to);
    }
}

前文提到dexer.Main与plugin不在同一个进程,所以要达到改写dexer.Main的目的还必须先改写ProcessBuilder的command成员变量,往其中插入-javaagent参数。同样还是通过ASM工具,当访问到ProcessBuilder的start方法时,如果start的目标是java或者dx,则加入-javaagent或-Jjavaagent参数。

//由于ClassLoader的关系,此类实现InvocationHandler接口
//具体原因请见前文解释
public class ProcessBuilderInvocationHandler implements InvocationHandler {
    private InvocationDispatcher dispatcher;
    private Log log;

    public ProcessBuilderInvocationHandler(InvocationDispatcher dispatcher, Log log) {
        this.dispatcher = dispatcher;
        this.log = log;
    }

    //当ASM访问到start时会调用此方法,传入的args参数就是ProcessBuilder的command成员
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        List list = (List) args[0];
        String str1 = list.get(0);
        File file = new File(str1);
        String param = null;
        if (TransformAgent.dx.contains(file.getName().toLowerCase()))
            //getAgentPath获取agent路径,具体实现见下文
            param = "-Jjavaagent:" + TransformAgent.getAgentPath();
        else if (TransformAgent.java.contains(file.getName().toLowerCase()))
            param = "-javaagent:" + TransformAgent.getAgentPath();
        if (param != null) {
            if (TransformAgent.attachParams != null)
                param = param + "=" + TransformAgent.attachParams;
            list.add(1, toParam(param));
        }
        log.d("Execute: " + list.toString());
        return null;
    }

    private String toParam(String param) {
        if (System.getProperty("os.name").toLowerCase().contains("win"))
            return "\"" + param + "\"";
        return param;
    }
}

最后要实现的就是agent的入口了。我们要提供一个public static void agentmain(String args, Instrumentation inst)方法,给inst参数设置一个ClassFileTransformer,在这个transformer内分别调用我们上面给出的代码来实现对dexer.Main和ProcessBuilder进行改造。

public class TransformAgent {
    public static final Class LOGGER = Logger.class;
    public static final Set dx = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(new String[] { "dx", "dx.bat" })));
    public static final Set java = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(new String[] { "java", "java.exe" })));

    //入口
    public static void agentmain(String args, Instrumentation inst){
        premain(args, inst);
    }

    public static void premain(String args, Instrumentation inst) {
        try {
            //设置ClassFileTransformer,
            //内部将对ProcessBuilder和dexer.Main进行改造
            IClassTransformer modifier = new ClassTransformer(log);
            createInvocationDispatcher(log);
            inst.addTransformer(modifier, true);
            Class[] classes = inst.getAllLoadedClasses();
            ArrayList classesToBeTransform = new ArrayList<>();
            for (Class cls : classes) {
                if(modifier.transforms(cls)){
                    classesToBeTransform.add(cls);
                }
            }
            if(!classesToBeTransform.isEmpty()){
                if(inst.isRetransformClassesSupported()){
                    inst.retransformClasses(classesToBeTransform.toArray(new Class[classesToBeTransform.size()]));
                } 
            }
            redefineClass(inst, modifier, ProcessBuilder.class);
        } catch (Exception e) {
            throw new RuntimeException("agent startup error");
        }
    }

    //改造ProcessBuilder的类是ProcessBuilderInvocationHandler,
    //改造dexer.Main的类本文没列出来,可以将这两个类的派发都放到一个类去做,
    //然后将这个类的实例设置到Logger里去,这样ProcessBuilder和dexer.Main就能获取到了
    private static void createInvocationDispatcher(Log log) throws Exception {
        Field treeLock = LOGGER.getDeclaredField("treeLock");
        treeLock.setAccessible(true);
        Field modifiers = Field.class.getDeclaredField("modifiers");
        modifiers.setAccessible(true);
        modifiers.setInt(treeLock, treeLock.getModifiers() & 0xFFFFFFEF);//去掉final
        if (!(treeLock.get(null) instanceof InvocationDispatcher)) {
            treeLock.set(null, new InvocationDispatcher(log));
        }
    }

    public static String getAgentPath() throws URISyntaxException {
        return new File(TransformAgent.class.getProtectionDomain()
                .getCodeSource().getLocation().toURI().getPath()).getAbsolutePath();
    }
}

别忘了把MANIFEST文件加上,在src/META-INF目录下新建MANIFEST.MF文件,里面加一行Agent-Class: agentmain所属类全限定名。

  • plugin
    我们以android studio的gradle插件为例。这个插件要实现什么功能?没错,就是要把agent加载进来。用IntelliJ新建一个gradle工程,然后把tools.jar(在jdk的lib目录下)和前面创建的agent.jar加入到Libraries中。创建一个实现Plugin< Project >的类,通过它来启动agent。
public class OpenAPMPlugin implements Plugin<Project> {

    @Override
    public void apply(Project project) {
        String nameOfRunningVM = ManagementFactory.getRuntimeMXBean().getName();
        int p = nameOfRunningVM.indexOf('@');
        String pid = nameOfRunningVM.substring(0, p);
        try {
            String jarFilePath = TransformAgent.class.getProtectionDomain().getCodeSource().getLocation().toURI().getPath();
            jarFilePath = new File(jarFilePath).getCanonicalPath();
            VirtualMachine vm = VirtualMachine.attach(pid);
            vm.loadAgent(jarFilePath, System.getProperty("openapm.agentArgs"));
            vm.detach();
        } catch (URISyntaxException | IOException | AgentInitializationException | AttachNotSupportedException | AgentLoadException e) {
            throw new RuntimeException(e);
        }
    }
}

plugin同样也需要配置文件,目录为resources/META-INF/gradle-plugins,具体名称可自行定义,加入implementation-class=插件类的全限定名。
打包插件的时候需要注意,不要把tools.jar和agent.jar给打包进去了。

  • 使用
    将plugin.jar和agent.jar放到android项目根目录的plugin文件夹中,并在build.gradle中添加dependencies
classpath fileTree(dir: 'plugin', include: '*.jar')

最后在app目录的build.gradle中添加apply plugin: ‘plugin配置文件的名称’,done。

你可能感兴趣的:(Android,Java)