Android 性能优化技术月报 | 2024 年 5 月
每个月都会有一些 Android 性能优化相关的优质内容发布,然而,碎片化阅读使得这些知识难以形成完整体系,且容易被遗忘。为解决这些问题,我决定尝试使用技术月报的形式,总结我在最近一个月内查阅的 Android 性能优化相关的优质内容。
月报的主要内容包括:整理展示我在最近一个月所查阅的 Android 性能优化领域的最新技术动态、精选博客,精选视频等内容。
精选博客
支付宝是如何监测、治理耗时类问题的?
支付宝介绍端内性能监控与治理的文章,比较有意思的是支付宝启动耗时的优化手段:支付宝首页贴图技术。
首页贴图的原理,就是应用冷启动后,给用户展示上一次首展示的内容,其实就是图片,这些图片记录着各个区域的点击事件。用户可以直接点击页面任何区域。这样就减少了页面刷新等待的时间。极大提升了冷启动速度。
贴图技术的原理就是把首页拆成几个部分,比如四大金刚、九宫格、消息提醒、腰封广告、首页推荐 feed 流。在用户使用完支付宝后,压后台的时机,把对应区域的图片截图,并保存。下一次冷启动的时候会判断是否截图成功,如果成功就直接贴图。
Android崩在so里面,怎么定位Native堆栈呢?
在Android系统中,我们有时需要获取Native层的堆栈信息,例如在进行性能分析、问题定位和调试等场景。
Android NDK提供了unwind.h头文件,其中定义了unwind函数,可以用于获取任意线程的堆栈信息,示例代码如下:
#include <unwind.h>
#include <dlfcn.h>
#include <stdio.h>
struct BacktraceState {
void** current;
void** end;
};
_Unwind_Reason_Code unwind_callback(struct _Unwind_Context* context, void* arg) {
BacktraceState* state = static_cast<BacktraceState*>(arg);
uintptr_t pc = _Unwind_GetIP(context);
if (pc) {
if (state->current == state->end) {
return _URC_END_OF_STACK;
} else {
*state->current++ = reinterpret_cast<void*>(pc);
}
}
return _URC_NO_REASON;
}
void capture_backtrace(void** buffer, int max) {
BacktraceState state = {buffer, buffer + max};
_Unwind_Backtrace(unwind_callback, &state);
}
void print_backtrace(void** buffer, int count) {
for (int idx = 0; idx < count; ++idx) {
const void* addr = buffer[idx];
const char* symbol = "";
Dl_info info;
if (dladdr(addr, &info) && info.dli_sname) {
symbol = info.dli_sname;
}
// 计算相对地址
void* relative_addr = reinterpret_cast<void*>(reinterpret_cast<uintptr_t>(addr) - reinterpret_cast<uintptr_t>(info.dli_fbase));
printf("%-3d %p %s (relative addr: %p)\n", idx, addr, symbol, relative_addr);
}
}
int main() {
const int max_frames = 128;
void* buffer[max_frames];
capture_backtrace(buffer, max_frames);
print_backtrace(buffer, max_frames);
return 0;
}
_Unwind_Backtrace和_Unwind_GetIP函数是在libunwind库中定义的,这个库是GNU C Library(glibc)的一部分。然而,Android系统并不使用glibc,而是使用一个更轻量级的C库,叫做Bionic libc。因此,_Unwind_Backtrace和_Unwind_GetIP函数在Android系统中的可用性取决于Bionic libc的版本和Android系统的版本。
在早期的Android版本中(例如Android 4.x),Bionic libc并未完全实现libunwind库的功能,因此_Unwind_Backtrace和_Unwind_GetIP函数可能无法正常工作。在这些版本中,我们通常需要使用其他方法来获取堆栈信息,例如手动遍历栈帧或者使用第三方的库。
从Android 5.0(Lollipop)开始,Bionic libc开始提供更完整的libunwind库的支持,包括_Unwind_Backtrace和_Unwind_GetIP函数。因此,在Android 5.0及更高版本中,我们可以直接使用这两个函数来获取堆栈信息。
【思考】学习源码的三重境界
本文讲述了作者在学习源码过程中的三个阶段。
- 首先是“看山是山”,指的是初学者专注于源码的细节,了解其表面知识。
- 其次是“看山不是山”,在这一阶段,学习者开始从宏观角度理解源码,探索设计背后的意图,并考虑版本变化。
- 最后是“看山不如上山”,强调通过实践来深入理解源码,例如通过编写代码或参与开源项目。
作者强调了源码学习的重要性,建议读者不仅要通过书籍和博客学习,还要深入源码以获得更深层次的理解。
稳定性优化:ANR监控方案
在程序发生 ANR 时,系统会弹出 ANR 的弹窗,并将 ANR 日志信息写入到 /data/anr/ 目录下的文件中,但是我们并没有直接的接口去感知到 ANR 发生了,也没有权限去读取 /data/anr/ 目录下的文件。本文介绍了如何实现一套 ANR 监控方案。
- 信号捕获检测方案: ANR 发生时,系统会给对应的进程发送一个 SIGQUIT 信号,通常我们可以在 sigaction 函数中处理信号的捕获,但是系统屏蔽了 SIGQUIT 信号,不允许 sigaction 接收 SIGQUIT 信号,而是会启动一个 SignalCatcher 线程,该线程会通过 sigwait 函数阻塞监听 SIGQUIT 信号。解决方案是使用 pthread_sigmask 函数将 SIGQUIT 信号从当前线程的信号屏蔽集中移除,然后遍历 /proc/{pid}/task 目录下所记录的该进程下所有的线程数据,拿到名称为 “SignalCatcher” 线程对应的线程 id,往 SignalCatcher 线程发生一个 SIGQUIT 信号。
- ActivityManagerService(AMS)接口进行二次确认:当进程收到了 SIGQUIT 信号,只能说明当前进程有可能发生了 ANR ,但是并不能够百分百确定发生了 ANR,比如其他应用发生 ANR 时, CPU 使用率占用比较高的进程也会收到 SIGQUIT 信号,因此需要进行二次确认。通过 ActivityManager.getProcessesInErrorState 可以获取当前进程的错误状态,如果进程是 NOT_RESPONDING 状态,则说明进程发生了 ANR。
- 抓取 Trace 文件:在明确了 ANR 发生后,接下来就是 /data/anr/traces.txt 文件的读取。SignalCatcher 线程在收到 SIGQUIT 信号后会获取各个线程的 Trace 信息,并且通过系统的 write 函数来把 Trace 的数据写入到 /data/anr/traces.txt 文件中。我们通过 PLT Hook 这个 write 方法,就可以获取写入到 traces.txt 文件中的 ANR Trace 数据了。
性能无损线上代码覆盖率采集,实现应用动态监控!
前面介绍过两种线上代码覆盖率采集的方案,一种是基于插桩方案,对性能会有一定损耗,另一种是 Hack classTable,使用起来比较麻烦。本文主要介绍如何通过 JVMTI 来实现线上代码覆盖率采集,
JVMTI(JVM Tool Interface)是 Java 虚拟机所提供的 Native 编程接口。通过这些接口,开发人员不仅可以调试在虚拟机上运行的 Java 程序,还能查看它们运行的状态、控制环境变量,甚至修改代码逻辑,从而帮助开发人员监控和优化程序性能。但在Android(ART)上被Google官方在Release模式中禁用掉了,如果需要使用则需要hack,同时对于系统版本有一定要求(>=Android8)。
JVMTI 可以实现以下功能:
- 重定义 Class
- 跟踪对象分配和垃圾回收过程
- 遵循对象的引用树,遍历堆中的所有对象
- 检查 Java 调用堆栈
- 暂停和恢复所有线程
- 其他功能
代码覆盖率采集功能使用 jvmti 提供的一个函数 GetLoadedClasses 方法,我们通过 hack 方法开启线上 JVMTI 检测后,在合适的时机调用这个方法即可。