Android 系统稳定性问题相关梳理

# 稳定性简述

Android 系统是一个开源的系统,稳定性对于每一个定制系统的手机厂商来讲都至关重要,因为稳定性在很大程度上决定着用户的体验,从稳定性的表现上来讲主要为:死机、自动开关机、黑屏、冻屏、闪退、无响应等情况。这些稳定性的问题在使用过程中,即便是仅发生过一到两次,但是影响是比较差的。Google 在 Android Vitals 的统计数据中提到,用户在安装一个软件后当日应用 ANR 发生次数超过一次的,用户卸载该软件的概率几乎为 1。 由此可见,稳定性在 Android 的系统和应用中是一个很重要的方向,稳定在影响着用户对系统(软件)的忠诚度和信赖程度,同时也会对口碑产生较大的影响。

从技术层面去划分这类问题的话,分为两个大类方向: Timeout 和 Crash . Timeout 是指某一操作或指令长时间无法完成,Crash 是指异常崩溃,类别划分如下:

stb

在应用程序运行在 System 进程的时候,准确来讲应该是 System Not Responding ,但是通常都会一起归到 ANR 大类,毕竟在用户使用层面上,都是程序无响应。

# Timeout > ANR

Timeout 是指在 Android 系统中某一任务长时间无法完成,对于不同的 Timeout 系统对指定的不同的超时阀值,超过限定时间极为 Timeout .

在 Timeout 中,Service、Broadcast 、Provider比较熟悉,Input 指的是在输入事件中所产生的超时无响应,一般 APP 进程在执行一些才走超过一定的时间没有执行完,会弹出『应用无响应』(Application Not Responding,ANR)对话框。对于 ANR 发生的触发原理,有些是因为需要执行的时间过长,稍微等待一下多给一些时间应用依旧能正常运行,而有些则是发生了死锁,再给再多的事件也无法正常运行。

  • Service Timeout:比如前台服务在20s内未执行完成;
  • broadcast Timeout:比如前台广播在10s内未执行完成
  • ContentProvider Timeout:内容提供者执行超时
  • InputDispatching Timeout: 输入事件分发超时5s,包括按键和触摸事件。
    # Service Timeout

Service Timeout 是”ActivityManager”线程中的 AMS.MainHandler 收到 SERVICE_TIMEOUT_MSG 时所触发的超时异常。

Service 分为前台Service 和后台 Service

  • 前台 Service ,超时 SERVICE_TIMEOUT = 20s
  • 后台 Service,超时事件为 SERVICE_BACKGROUND_TIMEOUT = 200s

由变量 ProcessRecrd.execServicesFg 来决定是否是前台启动

# BroadcastReceiver Timeout

BroadcastReceiver Timeout 是“ ActivityManger”线程中的 BroadcastQueue.BroadcastHandler 收到 BROADCAST_TIMEOUT_MSG 消息时触发。

广播队列分为 foreground 和 background 队列

  • 对于前台广播,超时 BROADCAST_FG_TIMEOUT = 10s;
  • 对于后台广播,超时 BROADCAST_FG_TIMEOUT = 60s;
# ContentProvider Timeout

ContentProvider Timeout 是位于“ActivityManager”线程中的 AMS.MainHandler 收到 CONTENT_PROVIDER_PUBLISH_TIMEOUT_MSG 消息时触发。

ContentProvider 超时 CONTENT_PROVIDER_PUBLISH_TIMEOUT = 10s

区别于 Service 和 BroadcastQueue ,由 Provider 进程启动过程相关

可以参考四大组件启动流程相关资料

# 小结

当系统出现 ANR 时候,会调用 AMS.appNotResponding() 方法,上述 Provider 除外,具体过程可以查阅 Android ANR 信息收集过程来学习。

Provider 在其进程启动时 publish 过程可能会出现 ANR , 则会直接杀进程以及清理相应信息,而不会弹出 ANR 的对话框 . appNotRespondingViaProvider() 过程会走 appNotResponding() ,很少使用,由用户自定义超时时间.

Timeout 时长汇总

类型 Timeout 时长
前台服务 20s
后台服务 200s
前台广播 10s
后台广播 60s
ConentProvider 10s
Input 事件分发 5 s

超时检测机制

  • Service 在超过一定事件没有执行完相应操作来触发移除延时消息,则会触发 ANR
  • 有序广播的总执行时间超过 2receiver 个数 timeout 时长,则会触发 ANR
  • 有序广播的某一个 receiver 执行过程超过 Timeout 时长,则会触发 ANR

# Timeout > WatchDog

在 ANR 之外,Timeout 家族还有一个成员就是 WatchDog Timeout,最为常见的就是运行在 system 进程中的 “watchdog” 线程,还有运行在各个 APP 进程 (包括 system 进程) 的“ FinalizerWatchdogDaemon”,该线程用于监控执行 GC 过程中,守护线程 “FinalizerDaemon” 回收某个对象过长的监视器,当然除此之外还有 dex2oat,wifi 等 WatchDog。

当发生 ANR 或者 WatchDog Timeout 后,需要手机系统相关信息,用于分析和修复异常,过程中 Trace 的输出是核心环节,另外该过程会清空 /data/anr/traces.txt 的老文件,而原来的 traces 信息一般会先输出到 dropbox ,不过在某些特定的命令或者情况下,也会丢失 traces .

WatchDog 是在 Android 系统中用于定时检测关键硬件是否正常工作,类似的,在 Framwork 层也有一个软件 WatchDog 用于检测关键系统服务是否发生死锁事件,WatchDog 的主要功能是分析核心服务和重要线程是否处于 Blocked 状态,它是一个运行在 system_server 进程名为 “watchdog” 的线程。

监视 reboot 广播

监视 mMonitors 关键系统服务是否死锁

当发生 WatchDog Timeout 情况下,会杀死 system_server 触发上层重启,在以下情况下,即使触发了 WatchDog ,也不会杀掉 system_server 进程:

  • monkey:设置 IActivityController,拦截 systemNotResponding 事件,比如 monkey
  • hang:执行 am hang 命令,不重启
  • debugger:连接 debugger 的情况,不重启

WatchDog 监控的线程

线程名 对应handler 说明 Timeout
main new Handler(Looper.getMainLooper()) 当前主线程 1min
android.fg FgThread.getHandler 前台线程 1min
android.ui UiThread.getHandler UI线程 1min
android.io IoThread.getHandler I/O线程 1min
android.display DisplayThread.getHandler display线程 1min
ActivityManager AMS.MainHandler AMS线程 1min
PowerManagerService PMS.PowerManagerHandler PMS线程 1min
PackageManager PKMS.PackageHandler PKMS线程 10min

WatchDog 监控同步锁

能够被Watchdog监控的系统服务都实现了Watchdog.Monitor接口,并实现其中的monitor()方法。运行在android.fg线程, 系统中实现该接口类主要有:

  • ActivityManagerService
  • WindowManagerService
  • InputManagerService
  • PowerManagerService
  • NetworkManagementService
  • MountService
  • NativeDaemonConnector
  • BinderThreadMonitor
  • MediaProjectionManagerService
  • MediaRouterService
  • MediaSessionService
  • BinderThreadMonitor

输出信息

WatchDog 在 check 过程中出现 1min 阻塞的情况,则会输出:

  1. AMS.dumpStackTraces : 输出 system_server 和 3个 native 进程的 traces
    • 会输出两次:① 在超时 30s 的地方 ② 在超时 1min
  2. WD.dumpStackTraces: 输出 system_server 进程中所有线程的 Kernel stack;
    • 节点/proc/%d/task获取进程内所有的线程列表
    • 节点/proc/%d/stack获取kernel的栈
  3. doSysRq, 触发kernel来dump所有阻塞线程,输出所有CPU的backtrace到kernel log;
    • 节点/proc/sysrq-trigger
  4. dropBox,输出文件到/data/system/dropbox,内容是trace + blocked信息
  5. 杀掉system_server,进而触发zygote进程自杀,从而重启上层framework。

# Traces In ART

在 Android 5.0 以后,Google 更换了使用已久的 Dalvik 虚拟机,改为使用 ART ,即 Android Rutime . ART 的机制与 Dalvik 不同,在 Dalvik 下,应用每次运行的时候,字节码需要通过即时的编译器 ( Just In Time,JIT )将程序转化为机器码,这样会拖慢应用的运行效率,在 ART 环境下,应用第一次安装应用,会进行一道预编译( AOT, Ahead Of Time),在应用第一次安装的时候,程序的字节码就会被转化成机器码,从而使其真正成为本地应用,不过这样带来的一个比较大的缺点,就是系统在应用过多的情况下预编译的时间会较长。

在 ART 中,所有的 Java 进程都在 ART 之上,所以在应用发生 ANR 的时候,其中最终的一个环节就是向目标进程发送 SIGNAL_QUIT 信号,传统的 Linux 是终止程序并输出 core,对于 Android 进程而言就是收到信号时,ART 会捕获该信号,并输出相应的 traces 信息到 /data/anr/traces.txt .

同时也可以通过指令来获取指定进程的 traces 信息,例如输出 pid = 1101 的进程信息:

adb shell kill -3 1101

执行完毕后 traces 信息的结果会保存到文件 /data/anr/tarces.txt . 可以根据 traces 来分析运行过程,定位问题点。

# Crash

App Crash ,全称 Application Crash ,按照层级划分可以分为 native crash 和 framework crash(含 app crash), 在应用开发中我们经常会使用 try...catch...finally 语句来捕获异常,在没有有效的 catch exception 情况下,应用的 crash 会由系统来捕获,进入 crash 流程。

在 Android 系统中,上层应用均是由 zygote 复刻而来,分为 system_server 系统进程和各种应用进程,这些应用在创建之初就会设置捕获异常的处理器,当系统抛出未捕获的异常时,最终都会交给异常处理器来处理。

  • 对于 system_server 进程,system_server 启动过程中由 RuntimeInit.java 的commonInit 方法设置 UncaughtHandler ,用于捕获异常
  • 对于应用进程,类似 system_server ,不过是在应用进程创建的过程中调用 RuntimeInit.java 的 commonInit 方法设置 UncaughtHandler
Crash 流程方法调用关系
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 获取进程数据
AMP.handleApplicationCrash
AMS.handleApplicationCrash
// 找到应用所在的进程
AMS.findAppProcess
// 其中的 addErrorToDropBox 将 crash 的信息输出到目录 /data/system/dropbox
AMS.handleApplicationCrashInner
AMS.addErrorToDropBox
// 调用 makeAppCrashingLocked,继续处理 crash 流程
// 发送消息SHOW_ERROR_MSG,弹出提示crash的对话框,等待用户选择
AMS.crashApplication
AMS.makeAppCrashingLocked
// 获取当前用户下的 crash 应用的 error receiver
// 忽略当前 app 的广播接收
AMS.startAppProblemLocked
// ①
ProcessRecord.stopFreezingAllLocked
ActivityRecord.stopFreezingScreenLocked
WMS.stopFreezingScreenLocked
WMS.stopFreezingDisplayLocked
AMS.handleAppCrashLocked
mUiHandler.sendMessage(SHOW_ERROR_MSG)
// 杀掉进程
Process.killProcess(Process.myPid());
System.exit(10);

方法 ① 位置方法注释:

  1. 处理屏幕旋转相关逻辑;
  2. 移除冻屏的超时消息;
  3. 屏幕旋转动画的相关操作;
  4. 使能输入事件分发功能;
  5. display冻结时,执行gc操作;
  6. 更新当前的屏幕方向;
  7. 向mH发送configuraion改变的消息。

上述过程中最为核心的为AMS.handleAppCrashLocked的主要功能:

  1. 当同一进程1分钟之内连续两次crash,则执行的情况下:
    • 对于非persistent进程:
      • ASS.handleAppCrashLocked, 直接结束该应用所有activity
      • AMS.removeProcessLocked,杀死该进程以及同一个进程组下的所有进
      • ASS.resumeTopActivitiesLocked,恢复栈顶第一个非finishing状态的activity
    • 对于persistent进程,则只执行
      • ASS.resumeTopActivitiesLocked,恢复栈顶第一个非finishing状态的activity
  2. 否则,当进程没连续频繁crash
    • ASS.finishTopRunningActivityLocked,执行结束栈顶正在运行activity
Java 层 Crash 小结

上述过程其实是 framwork 层关于 crash 的处理流程,对于应用遇到 crash 时,本身并没有捕获异常的话,会进入的如下的流程:

  1. 在发生 crash 的进程,进程创建时便准备好 defaultUncaughtHandler ,用来处理 Uncaught Exception,并输出当前 Crash 的基本信息
  2. 调用当前进程中的 AMP.handleApplicationCrash ; 经过 binder ipc 机制,传递到 system_server进程
  3. 进入 system_server进程,调用 binder 服务端执行 .handleApplicationCrash
  4. 从 mProcessNames 查找目标进程的 PorcessRecord 对象,并将进程 crash 信息输出到目录 /data/system/dropbox
  5. 执行 makeAppCrashingLocked
    • 创建当前用户下的 crash 应用的 errror receiver ,并忽略当前应用的广播
    • 停止当前进程中所有 Activity 的 WMS 冻结屏幕信息,并执行一些屏幕相关操作
  6. 再执行 handleAppCrashLocked 方法
    • 当 1 分钟内统一进程连续 crash 两次,并且为非 persistent进程,则接触该应用的所有Activity ,并杀死该进程以及同一个进程组下的所有进程,然后再恢复栈顶第一个非 finishing 状态的 Activity
    • 当 1 分钟内同一进程连续 crash 两次并且为 persistent 进程,则只恢复栈顶第一个非 finishing 状态的 Activity
    • 当 1 分钟内同一进程 未发生连续两次 crash 时,则执行结束栈顶正在运行的 Activity
  7. 通过 mUIHandle 发送 SHOW_ERROR_MSG ,弹出 crash 对话框
  8. 完成 system_server 进程的相关执行,回到 crash 进程开始执行杀掉当前进程的操作
  9. Crash 进程 over ,通过 binder 死亡通告,告诉 system_server 进程来执行 appDiedLocked()
  10. 执行清理用用相关的 activity/service/ContentProvider/receiver 组件信息

至此,完成应用 Crash 后系统的执行过程,当一个应用连续 Crash 时,如果是 1 分钟内连续两次 Crash 的非 persistent 进程,会被认定为 bad 进程,那么如果第三次从后台启动该进程(通过 Intent.getFlags 来判断),则会拒绝该进程;Crash 达到两次的非 persistent 进程,便会直接杀死该进程,即便允许自启的 Service 也会在被杀之后再拒绝再次启动。

# Native Crash

对于 APP 或者 Framework 的Crash,往往通过抛出未捕获的异常而导致 Crash,对于 Kernel ,很多情况是发生 Kernel panic ,内核崩溃的原因往往是驱动或者硬件的问题。

对于 Native Crash,也就是所谓的 NE ,发生在 Native 层,是 C/C++ 层面的异常,Native 介于 Framework 和 Linux 之间,对于 Native 层的 Crash,当有异常发生时,Kernel 会发送相应的 signal , 当进程捕获到对应 signal,通知 debuggerd 调用 ptrace 来获取有价值的信息。

Native Crash

(上图来源:http://gityuan.com/images/stability/native_crash.jpg )

  1. Kernel 发送 signal 给 target 进程
  2. target 进程通过 debuggerd_signal_handler 来捕获 signal
    • 建立和 debuggerd 进程的 socket 通道
    • 将 action = DEBUGGER_ACTION_CRASH 的消息发送给 debuggerd 服务端
    • 阻塞等待debuggerd服务端的回应数据
  3. debuggerd 作为守护进程,一直等待 socket client 的连接,收到 action = DEBUGGER_ACTION_CRASH 的消息,则执行到 handle_request 位置,通过 fork 创建子线程来进行各种 dump 相关操作
  4. 新创建的进程通过 Socket 和 system_server 进程中的 NativeCrashListener 线程建立 Socket 通道,并向其发送 Native Crash 信息
  5. NativeCrashListener 线程通过创建新的名为 “NativeCrashReport”的子线程来执行 AMS的 handleApplicationCrashInner 方法
  6. 在途中 perform_dump 是整个 debuggerd 的核心工作,该方法内部调用 engrave_tombston,是一个守护进程的功能内容,此外整个过程还需要与 target 进程通信来获取 target 进程更多的信息。
  7. 在 AMS 中的 handleApplicationCrashInner ,其工作方式和 Java 层的 Crash 处理流程一只。

# 拓展总结

对于 Java 层的 Crash ,通常是抛出一个未捕获的异常 uncaughtExcception 而导致的崩溃,但是如果说把所有的异常都进行 catch ,这样是否应用或者系统就不会出问题了呢?这个不能有肯定的答案,在某些情况下,异常需要深入分析 Root Cause ,从根源来解决 BUG 的出现点,而不是简单的捕获到异常。

在 Native 层的 Crash ,通常是由于进程收到 signal 信号而引发的崩溃,当进程收到信号,并会触发处理函数,通过 Socket 发送信息到 debuggerd 进程,debuggerd 进程收到时间后通过 ptrace attach 到目标进程,获取 cpu/memory/trace 等关键信息后 dettach . Native Crash 情况比较多,发生较多的是 SIGSEGV 段错误异常,往往是内存出现异常,比如无法访问权限不足的内存地址。

对于 Kernel Crash,难分析,很多时候是由硬件导致。例如 CPU、驱动等问题……

# 参考资料

Gityuan博客:http://gityuan.com

《Android 高级进阶》

很多内容参考借鉴上述地方资料,主要作为技术笔记和总结的存在

特此感谢上述两部分资料的作者

坚持原创技术分享,您的支持将鼓励我继续创作!
0%