在前文中已经详细介绍了APM的android端的原理,接下来会通过代码实现记录某类异常日志这个小功能来深入理解APM的实现原理。场景如下,记录所有捕获的IndexOutOfBoundsException。前文中提到,APM一般分为3个部分,plugin、agent和具体的业务代码。本文也将会按这三个分类来介绍。
注:由于篇幅有限,本文所展示的只有部分关键代码,有兴趣的可自行阅读github上的源码。
public static void pushException(Throwable th){
//在这里处理异常,如打印或上传日志
}
public class ExceptionLogMethodAdapter extends AdviceAdapter {
private TransformContext context;
//记录所有目标exception的handle
//key为handle,value是此handle对应的exception。
//注:一个catch可能包含了多个exception,
//如catch(IndexOutOfBoundsException | Exception e)
private HashMap
前文提到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所属类全限定名。
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给打包进去了。
classpath fileTree(dir: 'plugin', include: '*.jar')
最后在app目录的build.gradle中添加apply plugin: ‘plugin配置文件的名称’,done。