如何收集崩溃日志的总结
-
进程(前台进程还是后台进程)
-
线程(是否是 UI 线程)
-
崩溃堆栈(具体崩溃在系统的代码,还是我们自己的代码里面)
-
崩溃堆栈类型(Java 崩溃、Native 崩溃 or ANR)
-
机型、系统、厂商、CPU、ABI、Linux 版本等。(寻找共性)
-
Logcat
。(包括应用、系统的运行日志,其中会记录
App
运行的一些基本情况)
-
收集崩溃时的内存信息(OOM、ANR、虚拟内存耗尽等,很多崩溃都跟内存有直接关系)
-
系统剩余内存。(系统可用内存很小 – 低于 MemTotal 的 10%时,OOM、大量 GC、系统频繁自杀拉起等问题都非常容易出现)
-
虚拟内存(但是很多类似OOM、tgkill 等问题都是虚拟内存不足导致的)
-
应用使用内存(得出应用本身内存的占用大小和分布)
-
线程数()
-
崩溃场景(崩溃发生在哪个 Activity 或 Fragment,发生在哪个业务中)
-
关键操作路径(记录关键的用户操作路径,这对我们复现崩溃会有比较大的帮助)
-
其他自定义信息(不同应用关心的重点不一样。例如运行时间、是否加载了补丁、是否是全新安装或升级等)
如何分析崩溃日志的总结
-
确认重点(
内存
&
线程
需特别注意,很多崩溃都是由于它们使用不当造成的)
-
Java 崩溃(比如
NullPPointerException
是空指针,
OutOfMemoryError
是资源不足)
-
Native 崩溃(比较常见的是有
SIGSEGV
和
SIGABRT
)
-
ANR(先看看主线程的堆栈,是否是因为锁等待导致。接着看看 ANR 日志中
iowait
、
CPU
、
GC
、
system server
等信息,进一步确定是
I/O
问题,或是
CPU
竞争问题,还是由于大量
GC
导致卡死)
-
Logcat
。从 Logcat 中我们可以看到当时系统的一些行为跟手机的状态,当从一条崩溃日志中无法看出问题的原因,或者得不到有用信息时,不要放弃,建议查看相同崩溃点下的更多崩溃日志。
-
查找共性(机型、系统、ROM、厂商、ABI)
-
复现问题
针对系统崩溃
eg:
java.util.concurrent.TimeoutException:
android.os.BinderProxy.finalize() timed out after 10 seconds
at android.os.BinderProxy.destroy(Native Method)
at android.os.BinderProxy.finalize(Binder.java:459)
-
查找可能的原因
。(但通过操作路径和日志,我们可以找到一些怀疑的点)
-
尝试规避
(查看可疑的代码调用,是否使用了不恰当的 API,是否可以更换其他的实现方式规避)
-
Hook 解决
(
Java Hook
和
Native Hook
)
从哪收集 Crash 信息?
-
崩溃现场是我们的“第一案发现场”,它保留着很多有价值的线索。在这里我们挖掘到的信息越多,下一步分析的方向就越清晰。
-
操作系统是整个崩溃过程的“旁观者”
,也是我们最重要的“证人”,也是我们最重要的“证人”。
-
一个好的崩溃捕获工具知道应该采集哪些系统信息
,也知道在什么场景要深入挖掘哪些内容。
1.1 崩溃信息
从崩溃的基本信息,我们可以对崩溃有初步的判断。
-
进程名、线程名
。崩溃的进程是前台进程还是后台进程,崩溃是不是发生在 UI 线程。
-
崩溃堆栈和类型
。崩溃是属于 Java 崩溃、Native 崩溃,还是 ANR,对于不同类型的崩溃我们关注的点也不太一样。特别需要看崩溃堆栈的栈顶,看具体崩溃在系统的代码,还是我们自己的代码里面。
Process Name: 'com.sample.crash'
Thread Name: 'MyThread'
java.lang.NullPointerException
at ...TestsActivity.crashInJava(TestsActivity.java:275)
1.2 系统信息
-
Logcat
。这里包括应用、系统的运行日志。由于系统权限问题,获取到的
Logcat
可能只包含与当前
App
相关的。其中系统的
event logcat
会记录
App
运行的一些基本情况,记录在文件
/system/etc/event-log-tags
中。
system logcat:
10-25 17:13:47.788 21430 21430 D dalvikvm: Trying to load lib ...
event logcat:
10-25 17:13:47.788 21430 21430 I am_on_resume_called: 生命周期
10-25 17:13:47.788 21430 21430 I am_low_memory: 系统内存不足
10-25 17:13:47.788 21430 21430 I am_destroy_activity: 销毁 Activty
10-25 17:13:47.888 21430 21430 I am_anr: ANR 以及原因
10-25 17:13:47.888 21430 21430 I am_kill: APP 被杀以及原因
-
机型、系统、厂商、CPU、ABI、Linux 版本等。–> 寻找共性
-
设备状态:是否 root、是否是模拟器。一些问题是由 Xposed 或多开软件造成,对这部分问题我们要区别对待。
1.3 内存信息
OOM、ANR、虚拟内存耗尽等,很多崩溃都跟内存有直接关系。
-
系统剩余内存
。关于系统内存状态,可以直接读取文件 /proc/meminfo。当系统可用内存很小(低于 MemTotal 的 10%)时,OOM、大量 GC、系统频繁自杀拉起等问题都非常容易出现。
-
应用使用内存
。包括 Java 内存、
RSS(Resident Set Size)
、
PSS(Proportional Set Size)
,我们可以得出应用本身内存的占用大小和分布。
PSS
和
RSS
通过
/proc/self/smap
计算,可以进一步得到例如 apk、dex、so 等更加详细的分类统计。
-
虚拟内存
。虚拟内存可以通过
/proc/self/status
得到,通过
/proc/self/maps
文件可以得到具体的分布情况。有时候我们一般不太重视虚拟内存,但是很多类似OOM、tgkill 等问题都是虚拟内存不足导致的。
opened files count 812:
0 -> /dev/null
1 -> /dev/log/main4
2 -> /dev/binder
3 -> /data/data/com.crash.sample/files/test.config
...
-
线程数
。当前线程数大小可以通过上面的
status
文件得到,一个线程可能就占 2MB 的虚拟内存,过多的线程会对虚拟内存和文件句柄带来压力。根据我的经验来说,如果线程数超过 400 个就比较危险。需要将所有的线程id 以及对应的线程名输出到日志中,进一步排查是否出现了线程相关的问题。
threads count 412:
1820 com.sample.crashsdk
1844 ReferenceQueueD
1869 FinalizerDaemon
...
1.4 应用信息
-
崩溃场景
。崩溃发生在哪个 Activity 或 Fragment,发生在哪个业务中。
-
关键操作路径
。不同于开发过程详细的打点日志,我们可以记录关键的用户操作路径,这对我们复现崩溃会有比较大的帮助。
-
其他自定义信息
。不同的应用关心的重点可能不太一样,比如网易云音乐会关注当前播放的音乐,QQ 浏览器会关注当前打开的网址或频。此外例如运行时间、是否加载了补丁、是否是全新安装或升级等信息也非常重要。
2. 崩溃分析
2.1 确认重点
-
Java 崩溃
。比如
NullPPointerException
是空指针,
OutOfMemoryError
是资源不足,这个时候需要去进一步查看日志中的 “内存信息”和“资源信息”。
-
Native 崩溃
。比较常见的是有
SIGSEGV
和
SIGABRT
,前者一般由于空指针、非法指针造成,后者主要因为
ANR
和调用
abort()
退出所导致。
-
ANR
。先看看主线程的堆栈,是否是因为锁等待导致。接着看看 ANR 日志中
iowait
、
CPU
、
GC
、
system server
等信息,进一步确定是
I/O
问题,或是
CPU
竞争问题,还是由于大量
GC
导致卡死。
-
Logcat
。从 Logcat 中我们可以看到当时系统的一些行为跟手机的状态,
当从一条崩溃日志中无法看出问题的原因,或者得不到有用信息时,不要放弃,建议查看相同崩溃点下的更多崩溃日志
。
-
各个资源情况
。结合崩溃的基本信息,我们接着看看是不是跟 “内存信息” 有关,是不是跟“资源信息”有关。比如是物理内存不足、虚拟内存不足,还是文件句柄 fd 泄漏了。
内存与线程相关的信息都需要特别注意,很多崩溃都是由于它们使用不当造成的。
2.2 查找共性
如果使用了上面的方法还是不能有效定位问题,我们可以尝试查找这类崩溃有没有什么共性。找到了共性,也就可以进一步找到差异。
机型、系统、ROM、厂商、ABI,这些采集到的系统信息都可以作为维度聚合,共性问题例如是不是因为安装了 Xposed,是不是只出现在 x86 的手机,是不是只有三星这款机型,是不是只在 Android 5.0 的系统上。应用信息也可以作为维度来聚合,比如正在打开的链接、正在播放的视频、国家、地区等。
2.3 尝试浮现
底气主要是因为在稳定的复现路径上面,我们可以采用增加日志或使用 Debugger、GDB 等各种各样的手段或工具做进一步分析。
3 系统崩溃
-
查找可能的原因
。通过上面的共性归类,我们先看看是某个系统版本的问题,还是某个厂商特定 ROM 的问题。虽然崩溃日志可能没有我们自己的代码,但通过操作路径和日志,我们可以找到一些怀疑的点。
-
尝试规避
。查看可疑的代码调用,是否使用了不恰当的 API,是否可以更换其他的实现方式规避。
-
Hook 解决
。这里分为
Java Hook
和
Native Hook
。以我最近解决的一个系统崩溃为例,我们发现线上出现一个 Toast 相关的系统崩溃,它只出现在 Android 7.0 的系统中,看起来是在 Toast 显示的时候窗口的 token 已经无效了。这有可能出现在 Toast 需要显示时,窗口已经销毁了。
android.view.WindowManager$BadTokenException:
at android.view.ViewRootImpl.setView(ViewRootImpl.java)
at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java)
at android.view.WindowManagerImpl.addView(WindowManagerImpl.java4)
at android.widget.Toast$TN.handleShow(Toast.java)
为什么 Android 8.0 的系统不会有这个问题?在查看 Android 8.0 的源码后我们发现有以下修改:
try {
mWM.addView(mView, mParams);
trySendAccessibilityEvent();
} catch (WindowManager.BadTokenException e) {
/* ignore */
}
考虑再三,我们决定参考 Android 8.0 的做法,直接 catch 住这个异常。这里的关键在于寻找 Hook 点,这个案例算是相对比较简单的。Toast 里面有一个变量叫 mTN,它的类型为 handler,我们只需要代理它就可以实现捕获。
如果你做到了我上面说的这些,
95% 以上的崩溃都能解决或者规避,大部分的系统崩溃也是如此
。
当然总有一些疑难问题需要依赖到用户的真实环境,我们希望具备类似动态跟踪和调试的能力。
xlog 日志、远程诊断、动态分析
等高级手段,可以帮助我们实现这些。