本文微信公众号「安卓小煜」首发
1. 背景
最近重读了 Android 开发高手课,记录一下阅读的一些笔记。
包括有些之前课后作业完成不了的,这次也都一并完成了。
如果想看课后作业的,可以直接定位到最后一节,前面的都是原文的一个汇总。
2. 崩溃现场
你要分析一个东西,肯定需要一些相关信息,因此首先就需要采集崩溃的现场信息。
2.1 崩溃信息
- 进程名、线程名:判断是前台进程还是后台进程,是否 UI 线程
- 崩溃堆栈和类型:Java 崩溃?Native 崩溃?ANR?特别要看崩溃堆栈的栈顶
2.2 系统信息
- Logcat :应用运行日志
系统运行日志,系统 event logcat 在 /system/etc/event-log-tags 记录 App 运行的一些基本情况 - 机型、系统、厂商、CPU、ABI、Linux 版本等
- 设备状态:是否 root、是否是模拟器
2.3 内存信息
-
系统剩余内存:系统内存状态,通过 /proc/meminfo 文件可以得到。当 可用内存 < MemTotal * 10%,各种问题容易出现
-
应用使用内存 :Java 内存、RSS、PSS,可以得到应用本身内存的占用大小和分布。
RSS(Resident Set Size)------ 实际占用的物理内存,包括共享库,但不包含在交换分区的空间
PSS(Proportional Set Size)------ 与 RSS 的区别是,以平分方式计算共享库大小(共享库/进程个数)
RSS、PSS 可以通过 /proc/self/smap 得到
-
虚拟内存 :通过 /proc/self/status 得到,通过 /proc/self/maps 文件可以得到具体分布情况。
对于 32 位进程,如果是 32 位 CPU,虚拟内存达到 3GB 就可能内存申请失败;64 位 CPU,虚拟内存一般在 3~4 GB 之间
对于 64 位进程,虚拟内存不会成为问题
2.4 资源信息
根据资源信息判断是否存在资源泄漏
- 文件句柄 fd :通过 /proc/self/limits 获得。
一般单个进程允许打开的最大文件句柄个数为 1024,如果超过 800 个需要输出到日志排查。 - 线程数 :通过 /proc/self/status 获得。
超过 400 个需要输出到日志排查。 - JNI:通过 DumpReferenceTables 统计 JNI 引用表
2.5 应用信息
- 崩溃场景
- 关键操作路径
- 其他自定义信息
3. 崩溃分析
第一步:确定重点
- 确定严重程度:主要是关注性价比
- 崩溃基本信息
- Java 崩溃
- Native 崩溃:观察 signal、code、fault addr 等内容,以及崩溃时 Java 堆栈
关于 signal 的介绍,可以查看 siginfo_t
比较常见的:
SIGSEGV:空指针、非法指针
SIGABRT:ANR、调用 abort() 退出 - ANR
- 先看主线程的堆栈,是否因为锁等待导致
- 看 ANR 日志中 iowait、CPU、GC、system server 等信息,进一步确定是 I/O 问题、CPU 竞争问题,还是由于大量 GC 导致卡死
- Logcat
注意日志级别是 Warning、Error,还有一些关键 tag(比如 am_anr、am_kill) - 各个资源情况
在 Logcat 和各个资源情况的分析时,尤其要关注内存与线程相关信息
第二步:查找共性
- 系统信息:机型、系统、ROM、厂商、ABI
- 应用信息
第三步:尝试复现
疑难问题:系统崩溃
举一个系统崩溃的例子来说明上面崩溃分析三步曲。
- 查找可能的原因。通过共性归类,找到一些怀疑的点。
- 尝试规避。查看可疑的代码调用,是否可以更换其他实现方式规避。
- Hook 解决。使用 Java Hook 或者 Native Hook 解决。
4. 课后作业
仓库地址:Chapter02
- 注意点一:模拟器版本号
我使用 API 30 Android 11.0 测试是不通过的,但是使用 API 22 Android 5.1 是可以的
代码里面也有对应注释:Android P API 28 以后不能反射 FinalizerWatchdogDaemon - 注意点二:用到了反射,建议搭配源码食用更佳
我这里参考的源码是 Android 系统 Lollipop - 5.1.1_r6 ,具体位置为:FinalizerWatchdogDaemon
这边把关键代码的地方添加注释如下:
java
// 获取 FinalizerWatchdogDaemon 类
final Class clazz = Class.forName("java.lang.Daemons$FinalizerWatchdogDaemon");
// 获取 FinalizerWatchdogDaemon 的 INSTANCE 字段,通过源码发现该字段赋值为 FinalizerWatchdogDaemon 实例对象
final Field field = clazz.getDeclaredField("INSTANCE");
field.setAccessible(true);
// 获得 FinalizerWatchdogDaemon 实例对象,这里可以传 null 是因为实例对象是 static 修饰的
final Object watchdog = field.get(null);
// 获得父类 Daemon 的 thread 字段
final Field thread = clazz.getSuperclass().getDeclaredField("thread");
thread.setAccessible(true);
// 将 FinalizerWatchdogDaemon 的 thread 变量设置为 null
thread.set(watchdog, null);
将 thread 置为 null 之后, finalizerTimedOut(finalizedObject) 就不会调用到,也就不会出现 java.util.concurrent.TimeoutException: com.dodola.watchdogkiller.GhostObject.finalize() timed out after 10 seconds
scss
private static void finalizerTimedOut(Object object) {
// The current object has exceeded the finalization deadline; abort!
String message = object.getClass().getName() + ".finalize() timed out after "
+ (MAX_FINALIZE_NANOS / NANOS_PER_SECOND) + " seconds";
Exception syntheticException = new TimeoutException(message);
// We use the stack from where finalize() was running to show where it was stuck.
syntheticException.setStackTrace(FinalizerDaemon.INSTANCE.getStackTrace());
Thread.UncaughtExceptionHandler h = Thread.getDefaultUncaughtExceptionHandler();
if (h == null) {
// If we have no handler, log and exit.
System.logE(message, syntheticException);
System.exit(2);
}
// Otherwise call the handler to do crash reporting.
// We don't just throw because we're not the thread that
// timed out; we're the thread that detected it.
h.uncaughtException(Thread.currentThread(), syntheticException)
}