改不完的 Bug,写不完的矫情。公众号 小木箱成长营 现在专注移动基础开发 ,涵盖音视频和 APM,信息安全等各个知识领域;只做全网最 Geek 的公众号,欢迎您的关注!
APM简要介绍
APM 全称 Application Performance Management & Monitoring (应用性能管理/监控)
性能问题是导致 App 用户流失的罪魁祸首之一,如果用户在使用我们 App 的时候遇到诸如页面卡顿、响应速度慢、发热严重、流量电量消耗大等问题的时候,很可能就会卸载掉我们的 App。这也是我们在目前工作中面临的巨大挑战之一,尤其是低端机型。
商业化的APM平台:著名的 NewRelic,还有国内的听云、OneAPM 、阿里百川-码力APM的SDK、百度的APM收费产品等等。
APM工作方式:
- 首先在客户端(Android、iOS、Web等)采集数据;
- 接着将采集到的数据整理上报到服务器(多种方式 json、xml,上传策略等等);
- 服务器接收到数据后建模、存储、挖掘分析,让后将数据进行可视化展示(spark+flink),供用户使用。
- 那么移动端需要做的事情就是:
- 双端统一原则(技术选型 NDK c++)
- 数据采集 (采集指标、细化等等)
- 数据存储(写文件?mmap、fileIO流)
- 数据上报(上报策略、上报方式)
那我们到底应该怎么做?一定要学会看开源的东西。让我们先看看大厂的开源怎么做的?我们在自己造轮子,完成自己的APM采集框架。
目前核心开源APM框架产品
你会发现自定义Gradle插件技术、ASM技术、打包流程Hook、Android打包流程等。那思考一下,为什么大家做的主要的流程都是一样的,不一样的是具体的实现细节,比如如何插桩采集到页面帧率、流量、耗电量、GC log等等。
ArgusAPM性能监控平台介绍&SDK开源-卜云涛.pdf
我们先简单来看下在matrix中,如何利用Java Hook和 Native Hook完成IO 磁盘性能的监控?
Java Hook的hook点是系统类CloseGuard
,hook的方式是使用动态代理。
private boolean tryHook() { try { Class<?> closeGuardCls = Class.forName("dalvik.system.CloseGuard"); Class<?> closeGuardReporterCls = Class.forName("dalvik.system.CloseGuard$Reporter"); Method methodGetReporter = closeGuardCls.getDeclaredMethod("getReporter"); Method methodSetReporter = closeGuardCls.getDeclaredMethod("setReporter", closeGuardReporterCls); Method methodSetEnabled = closeGuardCls.getDeclaredMethod("setEnabled", boolean.class); sOriginalReporter = methodGetReporter.invoke(null); methodSetEnabled.invoke(null, true); // open matrix close guard also MatrixCloseGuard.setEnabled(true); ClassLoader classLoader = closeGuardReporterCls.getClassLoader(); if (classLoader == null) { return false; } methodSetReporter.invoke(null, Proxy.newProxyInstance(classLoader, new Class<?>[]{closeGuardReporterCls}, new IOCloseLeakDetector(issueListener, sOriginalReporter))); return true; } catch (Throwable e) { MatrixLog.e(TAG, "tryHook exp=%s", e); } return false; }
这里的CloseGuard有啥用?为什么腾讯的人要hook这个。这个在后续的分线中我们在来详细的说。如果要解决这个疑问,做好的办法就是看源码。(==系统埋点方式,监控系统资源的异常回收==)
关于native hook:
Native Hook是采用PLT(GOT) Hook的方式hook了系统so中的IO相关的open
、read
、write
、close
方法。在代理了这些系统方法后,Matrix做了一些逻辑上的细分,从而检测出不同的IO Issue。
JNIEXPORT jboolean JNICALL Java_com_tencent_matrix_iocanary_core_IOCanaryJniBridge_doHook(JNIEnv *env, jclass type) { __android_log_print(ANDROID_LOG_INFO, kTag, "doHook"); for (int i = 0; i < TARGET_MODULE_COUNT; ++i) { const char* so_name = TARGET_MODULES[i]; __android_log_print(ANDROID_LOG_INFO, kTag, "try to hook function in %s.", so_name); //打开so文件,并在内存中映射成ELF文件格式 loaded_soinfo* soinfo = elfhook_open(so_name); if (!soinfo) { __android_log_print(ANDROID_LOG_WARN, kTag, "Failure to open %s, try next.", so_name); continue; } //替换open函数 elfhook_replace(soinfo, "open", (void*)ProxyOpen, (void**)&original_open); elfhook_replace(soinfo, "open64", (void*)ProxyOpen64, (void**)&original_open64); bool is_libjavacore = (strstr(so_name, "libjavacore.so") != nullptr); if (is_libjavacore) { if (!elfhook_replace(soinfo, "read", (void*)ProxyRead, (void**)&original_read)) { __android_log_print(ANDROID_LOG_WARN, kTag, "doHook hook read failed, try __read_chk"); //http://refspecs.linux-foundation.org/LSB_4.1.0/LSB-Core-generic/LSB-Core-generic/libc---read-chk-1.html 类似于read() if (!elfhook_replace(soinfo, "__read_chk", (void*)ProxyRead, (void**)&original_read)) { __android_log_print(ANDROID_LOG_WARN, kTag, "doHook hook failed: __read_chk"); elfhook_close(soinfo); return false; } } if (!elfhook_replace(soinfo, "write", (void*)ProxyWrite, (void**)&original_write)) { __android_log_print(ANDROID_LOG_WARN, kTag, "doHook hook write failed, try __write_chk"); if (!elfhook_replace(soinfo, "__write_chk", (void*)ProxyWrite, (void**)&original_write)) { __android_log_print(ANDROID_LOG_WARN, kTag, "doHook hook failed: __write_chk"); elfhook_close(soinfo); return false; } } } elfhook_replace(soinfo, "close", (void*)ProxyClose, (void**)&original_close); elfhook_close(soinfo); } return true; }
关于transform api:
我们编译Android项目时,如果我们想拿到编译时产生的Class文件,并在生成Dex之前做一些处理,我们可以通过编写一个Transform
来接收这些输入(编译产生的Class文件),并向已经产生的输入中添加一些东西。
如何使用的?
- 编写一个自定义的
Transform
- 注册一个Plugin完成或者在gradle文件中直接注册。
//MyCustomPlgin.groovy public class MyCustomPlgin implements Plugin<Project> { @Override public void apply(Project project) { project.getExtensions().findByType(BaseExtension.class) .registerTransform(new MyCustomTransform()); } }
project.extensions.findByType(BaseExtension.class).registerTransform(new MyCustomTransform()); //在build.gradle中直接写
MatrixTraceTransform 利用编译期字节码插桩技术,优化了移动端的FPS、卡顿、启动的检测手段。在打包过程中,hook生成Dex的Task任务,添加方法插桩的逻辑。我们的hook点是在Proguard之后,Class已经被混淆了,所以需要考虑类混淆的问题。
MatrixTraceTransform
主要逻辑在transform
方法中:
@Override public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException { long start = System.currentTimeMillis() //是否增量编译 final boolean isIncremental = transformInvocation.isIncremental() && this.isIncremental() //transform的结果,重定向输出到这个目录 final File rootOutput = new File(project.matrix.output, "classes/${getName()}/") if (!rootOutput.exists()) { rootOutput.mkdirs() } final TraceBuildConfig traceConfig = initConfig() Log.i("Matrix." + getName(), "[transform] isIncremental:%s rootOutput:%s", isIncremental, rootOutput.getAbsolutePath()) //获取Class混淆的mapping信息,存储到mappingCollector中 final MappingCollector mappingCollector = new MappingCollector() File mappingFile = new File(traceConfig.getMappingPath()); if (mappingFile.exists() && mappingFile.isFile()) { MappingReader mappingReader = new MappingReader(mappingFile); mappingReader.read(mappingCollector) } Map<File, File> jarInputMap = new HashMap<>() Map<File, File> scrInputMap = new HashMap<>() transformInvocation.inputs.each { TransformInput input -> input.directoryInputs.each { DirectoryInput dirInput -> //收集、重定向目录中的class collectAndIdentifyDir(scrInputMap, dirInput, rootOutput, isIncremental) } input.jarInputs.each { JarInput jarInput -> if (jarInput.getStatus() != Status.REMOVED) { //收集、重定向jar包中的class collectAndIdentifyJar(jarInputMap, scrInputMap, jarInput, rootOutput, isIncremental) } } } //收集需要插桩的方法信息,每个插桩信息封装成TraceMethod对象 MethodCollector methodCollector = new MethodCollector(traceConfig, mappingCollector) HashMap<String, TraceMethod> collectedMethodMap = methodCollector.collect(scrInputMap.keySet().toList(), jarInputMap.keySet().toList()) //执行插桩逻辑,在需要插桩方法的入口、出口添加MethodBeat的i/o逻辑 MethodTracer methodTracer = new MethodTracer(traceConfig, collectedMethodMap, methodCollector.getCollectedClassExtendMap()) methodTracer.trace(scrInputMap, jarInputMap) //执行原transform的逻辑;默认transformClassesWithDexBuilderForDebug这个task会将Class转换成Dex origTransform.transform(transformInvocation) Log.i("Matrix." + getName(), "[transform] cost time: %dms", System.currentTimeMillis() - start) }
看到这里了,我们是不是应该总结下APM的核心技术是什么?
大厂面试之一:APM的核心技术是什么?做过自研的APM吗?
APM核心原理一句话总结:
依据打包原理,在class转换为dex的过程中,调用gradletransformapi遍历class文件,借助Javassist、ASM等框架修改字节码,插入我们自己的代码实现性能数据的统计。这个过程是在编译器见完成
你掌握了APM的核心原理,也可以做Android的无痕埋点了,本质是一样的,不一样的是Hook的地方不一样。
APM监控维度和指标
App基础性能指标集中为8类:网络性能、崩溃、启动加载、内存、图片、页面渲染、IM和VoIP(业务相关性和你的APP相关)、用户行为监控,基础维度包括App、系统平台、App版本和时间维度。
网络性能
网络服务成功率,平均耗时和访问量、上下行的速率监控。访问的链接、耗时等等。思考怎么结合okhttp来做?
网络监控业务背景
在不确定哪个网络url耗时比较慢的情况下,做一个全链路网络监控体系,这个是我们值得深思的问题,这边使用AspectJ和利用OKhttp自身的EventListener对网络信息进行二次封装上报
网络监控实现步骤
第一步: 将我们需要监控的数据源转换成 Bundle 对象方便传输
public interface BundleMapping { /** * 转换数据结构为Bundle */ Bundle asBundle(); }
第二步: 确定我们需要监控的字段
字段 | 字段含义 | 备注 |
total | 总网络时间 | 调用结束时间 减去 请求开始时间 |
pathname | 请求地址 | / |
dns | dnsEndTime - dnsStartTime | DNS查询结束时间 减去 DNS查询开始时间 |
protocol | 请求协议 | / |
tcp | 连接结束时间 | 调用结束时间 减去 连接开始时间 |
no_dns_tcp_tls | 总网络时间 | 调用结束时间 减去 请求开始时间 |
tls | 总网络时间 | 调用结束时间 减去 请求开始时间 |
no_response | 是否无响应内容(304或返回body为空) | / |
ttfb | ttfb首字节时间 | 响应结束时间 减去 请求开始时间 |
download | 总网络时间 | 响应结束时间 减去 响应开始时间 |
pure_network | 总网络时间 | 响应结束时间 减去 响应开始时间 |
transfer_size | 传输大小 | / |
failed | 请求失败信息 | / |
public class NetWorkData implements BundleMapping { //请求地址 String url; //请求协议 String protocol; //请求开始时间 long callStartTime; //响应开始时间 long responseStartTime; //响应结束时间 long responseEndTime; //DNS查询开始时间 long dnsStartTime; //DNS查询结束时间 long dnsEndTime; //连接开始时间 long connectStartTime; //连接结束时间 long connectEndTime; //是否同时重用DNS、TCP、TLS boolean isDnsTcpTls; //SSL 连接开始时间 long secureConnectStartTime; //SSL 连接结束时间 long secureConnectEndTime; //是否无响应内容(304或返回body为空) boolean isNoResponse; //调用结束时间 long callEndTime; //请求失败信息 String failMessage; //传输大小 long byteCount; @Override public Bundle asBundle() { Bundle bundle = new Bundle(); //总网络时间 bundle.putLong("total", callEndTime - callStartTime); bundle.putString("pathname", url); //dns bundle.putLong("dns", dnsEndTime - dnsStartTime); bundle.putString("protocol", protocol); //tcp bundle.putLong("tcp", connectEndTime - connectStartTime); bundle.putBoolean("no_dns_tcp_tls", isDnsTcpTls); //tls bundle.putLong("tls", secureConnectEndTime - secureConnectStartTime); bundle.putBoolean("no_response", isNoResponse); //ttfb首字节时间 bundle.putLong("ttfb", responseEndTime - callStartTime); //download bundle.putLong("download", responseEndTime - responseStartTime); //pure_network bundle.putLong("pure_network", responseEndTime - callStartTime); //transfer_size bundle.putLong("transfer_size", byteCount); //连接失败 bundle.putString("failed", failMessage); return bundle; } }
第三步: 添加我们需要上报的标记,根据不同的域名去区分
/** * 获取上报Tag */ String getDataTag() { String tag; if (url == null) { tag = "biz"; } else { if (url.contains("microkibaco_report")) { tag = "data"; } else if (url.contains("blog")) { tag = "blog"; } else { tag = "github"; } } return "network_api_" + tag; }
第五步: 添加沪江aspectj插件
classpath 'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:2.0.6'
implementation 'org.aspectj:aspectjrt:1.8.9' implementation "com.squareup.okhttp3:okhttp:3.12.1"
第六步: 给OkHttp 添加事件监听
@Keep public class MkOkHttpEventListener extends EventListener { private NetWorkData mNetWorkData; public static final Factory FACTORY = new Factory() { @Override public EventListener create(Call call) { return new MkOkHttpEventListener(); } }; /** * 每个请求都会构建 */ private MkOkHttpEventListener() { mNetWorkData = new NetWorkData(); } @Override public void callStart(Call call) { mNetWorkData.url = call.request().url().toString(); mNetWorkData.callStartTime = SystemClock.elapsedRealtime(); } @Override public void connectStart(Call call, InetSocketAddress inetSocketAddress, Proxy proxy) { mNetWorkData.connectStartTime = SystemClock.elapsedRealtime(); } @Override public void connectEnd(Call call, InetSocketAddress inetSocketAddress, Proxy proxy, Protocol protocol) { mNetWorkData.connectEndTime = SystemClock.elapsedRealtime(); } @Override public void connectFailed(Call call, InetSocketAddress inetSocketAddress, Proxy proxy, Protocol protocol, IOException ioe) { } @Override public void dnsStart(Call call, String domainName) { mNetWorkData.dnsStartTime = SystemClock.elapsedRealtime(); } @Override public void dnsEnd(Call call, String domainName, List<InetAddress> inetAddressList) { mNetWorkData.dnsEndTime = SystemClock.elapsedRealtime(); } @Override public void secureConnectStart(Call call) { mNetWorkData.secureConnectStartTime = SystemClock.elapsedRealtime(); } @Override public void secureConnectEnd(Call call, Handshake handshake) { mNetWorkData.secureConnectEndTime = SystemClock.elapsedRealtime(); } @Override public void responseHeadersStart(Call call) { mNetWorkData.responseStartTime = SystemClock.elapsedRealtime(); } @Override public void responseHeadersEnd(Call call, Response response) { if (response.code() == 304) { mNetWorkData.isNoResponse = true; } mNetWorkData.protocol = response.protocol().toString(); mNetWorkData.responseEndTime = SystemClock.elapsedRealtime(); } @Override public void responseBodyEnd(Call call, long byteCount) { //响应为空 if (byteCount == 0) { mNetWorkData.isNoResponse = true; } mNetWorkData.byteCount = byteCount; } @Override public void callEnd(Call call) { mNetWorkData.callEndTime = SystemClock.elapsedRealtime(); //上报 APM.getReportStrategy().report(mNetWorkData.getDataTag(), mNetWorkData.asBundle()); } @Override public void callFailed(Call call, IOException ex) { mNetWorkData.failMessage = ex.getMessage(); //上报 APM.getReportStrategy().report(mNetWorkData.getDataTag(), mNetWorkData.asBundle()); } }
Firebase 有一定的时效性,所以我们把日志全部采集到Json里面交给Bundle统一上报
崩溃
崩溃数据的采集和分析,类似于Bugly平台的功能
启动加载
App的启动我们做了大的力气进行了优化,多线程等等, Spark的有向无环图(DAG)来处理业务的依赖性。对App的冷启动时长、Android安装后首次启动时长和Android Bundle(atlas框架)启动加载时长进行监控。
内存
四大监测目标:内存峰值、内存均值、内存抖动、内存泄露。
IM和VoIP等业务指标
这两项都属于业务型技术指标的监控,例如对各类IM消息到达率和VoIP通话的成功率、平均耗时和请求量进行监控。这里需要根据自己的APP的业务进行针对性的梳理。
用户行为监控
用于App统计用户行为,实际上就是监控所有事件并把事件发送到服务上去。这在以前是埋点做的事情,现在也规整成APM需要做的事情,比如用户的访问路径,类似于PC时代的PV,UV等概念。
图片
资源文件的监测,比如Bitmap冗余处理。haha库处理,索引值。
页面渲染
界面流畅性监测、FPS的监测、慢函数监测、卡顿监测、文件IO开销监测等导致页面渲染的各种问题。