Android app 崩溃 & Crash 分析(一)

如何收集崩溃日志的总结

  • 收集崩溃时的基本信息

    • 进程(前台进程还是后台进程)
    • 线程(是否是 UI 线程)
    • 崩溃堆栈(具体崩溃在系统的代码,还是我们自己的代码里面)
    • 崩溃堆栈类型(Java 崩溃、Native 崩溃 or ANR)
  • 收集崩溃时的系统信息

    • 机型、系统、厂商、CPU、ABI、Linux 版本等。(寻找共性)
    • Logcat。(包括应用、系统的运行日志,其中会记录 App 运行的一些基本情况)
  • 收集崩溃时的内存信息(OOM、ANR、虚拟内存耗尽等,很多崩溃都跟内存有直接关系)

    • 系统剩余内存。(系统可用内存很小 – 低于 MemTotal 的 10%时,OOM、大量 GC、系统频繁自杀拉起等问题都非常容易出现)
    • 虚拟内存(但是很多类似OOM、tgkill 等问题都是虚拟内存不足导致的)
    • 应用使用内存(得出应用本身内存的占用大小和分布)
    • 线程数()
  • 收集崩溃时的应用信息

    • 崩溃场景(崩溃发生在哪个 Activity 或 Fragment,发生在哪个业务中)
    • 关键操作路径(记录关键的用户操作路径,这对我们复现崩溃会有比较大的帮助)
    • 其他自定义信息(不同应用关心的重点不一样。例如运行时间、是否加载了补丁、是否是全新安装或升级等)

如何分析崩溃日志的总结

  • 确认重点(内存 & 线程 需特别注意,很多崩溃都是由于它们使用不当造成的)

    • 确认严重程度
    • 崩溃基本信息
      • Java 崩溃(比如 NullPPointerException 是空指针,OutOfMemoryError 是资源不足)
      • Native 崩溃(比较常见的是有 SIGSEGVSIGABRT
      • ANR(先看看主线程的堆栈,是否是因为锁等待导致。接着看看 ANR 日志中 iowaitCPUGCsystem 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 HookNative 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),我们可以得出应用本身内存的占用大小和分布。PSSRSS 通过 /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 崩溃。比较常见的是有 SIGSEGVSIGABRT,前者一般由于空指针、非法指针造成,后者主要因为 ANR 和调用 abort() 退出所导致。
    • ANR。先看看主线程的堆栈,是否是因为锁等待导致。接着看看 ANR 日志中 iowaitCPUGCsystem 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 HookNative 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 日志、远程诊断、动态分析等高级手段,可以帮助我们实现这些。

参考链接

  • https://time.geekbang.org/column/article/70966
  • https://blog.csdn.net/qq_17766199/article/details/84789495#t1
  • https://blog.csdn.net/qq_17766199/article/details/85716750

你可能感兴趣的:(android)