APM之实现篇
来源:互联网 发布:淘宝买的office2016 编辑:程序博客网 时间:2024/04/19 15:37
在前文中已经详细介绍了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<Label, ArrayList<String>> 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<String> targetException = context.getExceptions(); if (exception != null && targetException.contains(exception)) { context.getLog().d("find exception " + exception); ArrayList<String> 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<String> 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<String> list = (List<String>) 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<String> dx = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(new String[] { "dx", "dx.bat" }))); public static final Set<String> 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<Class> 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。
- APM之实现篇
- APM之实现篇
- APM之原理篇
- APM之原理篇
- android APM 实现手记
- APM
- APM
- C#5.0 以Task方式实现APM
- C#多线程值之APM 一
- APM源码分析之 主流程
- APM源码分析之 姿态控制
- APM源码分析之 油门跟踪
- 移植APM代码之PID调节
- APM 数据传输之远程调用(RPC)
- APM Agent 之 动态注入 agent
- 每天读一点儿APM(PIX)代码之外传:apm固件尺寸问题
- APM飞控学习之路:3 APM系统介绍与开发环境搭建
- 基于Power Threading在Compact Framework中实现APM模式
- 【面试必备】iOS-Swift 面试题及其答案
- 艺龙十万级服务器监控系统开发的架构和心得
- Java基础知识总结(四)——异常与异常处理表
- 新的征程
- Java基础知识总结(五)——安全
- APM之实现篇
- 一个非常漂亮的简约大气的table
- OllyDbg使用学习 笔记
- Java基础知识总结(七)——泛型
- Android Studio点9图问题
- Linux下PPTP的VPN拨号设置(客户端)
- 《算法竞赛入门经典2ndEdition 》例题5-6 团体队列(Team Queue, Uva540)
- Java基础知识总结(八)——反射
- 数据结构的理解和应用——红黑树