关于 Service ANR的简单分析
这段时间遇到的 bug 中很多时候是多线程引起的 bug,特别是同事春节前还在问为什么无障碍服务的主线程可以长时间的进行选人的操作,而春节附近就发现了主线程长时间的进行选人的操作并不稳定,会偶发性的出现 ANR 崩溃,最近一个版本的更新中把这个代码进行了修改。虽然 bug 是解决了,但是原理还是没搞清楚。所以昨天我专门看了一下 Android 主线程这方面的相关知识,今天在这里通过几个问题搞清楚这块的知识点。
1,首先我们先看看主线程源码,根据 Looper.loop() 源码可知里面是一个死循环在遍历消息队列获取消息。看到这里我们下意识的会发出第一个疑问,Android 中为什么主线程不会因为 Looper.loop() 里的死循环卡死?搞清楚这个问题就得先了解 Android 的进程/线程,android 进程:每个 app 运行时前首先创建一个进程,该进程是由 Zygote fork 出来的,用于承载 App 上运行的各种Activity/Service等组件。进程对于上层应用来说是完全透明的,App 程序都是运行在 Android Runtime。大多数情况一个 App 就运行在一个进程中,除非在 AndroidManifest.xml 中配置 Android:process 属性,或通过 native 代码 fork 进程。线程:线程对于 Android 开发来说非常常见,比如每次 new Thread().start 都会创建一个新的线程。该线程与 App 所在进程之间资源共享,从Linux角度来说进程与线程除了是否共享资源外,并没有本质的区别,都是一个task_struct 结构体,在 CPU 看来进程或线程无非就是一段可执行的代码,CPU 采用 CFS 调度算法,保证每个task都尽可能公平的享有 CPU 时间片。对于线程既然是一段可执行的代码,当可执行代码执行完成后,线程生命周期便该终止了,线程退出。而对于主线程,不能运行一段时间把任务队列运行完后就退出,那么如何保证能一直存活呢?简单做法就是可执行代码是能一直执行下去的,死循环便能保证不会被退出,当然并非简单地死循环,无消息时会休眠。所以主线程的死循环也不是特别消耗 CPU 资源,因为主线程大多说的时候都是出于休眠状态,不会大量消耗 CPU 资源。
2,我们清楚知道主线程死循环处理消息不会造成卡死后,紧接着会有第二个疑问产生,那是为什么造成程序 ANR 奔溃的呢?是因为想 Android 把所有的任务栈都设置成一个一个的消息队列,每一个消息的处理时间是有时间限制的,当一个消息开始处理是就会设置一个延迟发送的超时 handler,如果不在延迟时间内把这个 Handler 移除掉,这个 Handler 就会通知 system_server 进程该消息超时,然后 App 发生 ANR 奔溃。
3,知道了 ANR 奔溃的原因,要找到上面bug的原因,我们再看看 Service。Service Timeout是位于 ”ActivityManager” 线程中的 AMS.MainHandler 收到 SERVICE_TIMEOUT_MSG 消息时触发。对于 Service 有两类:前台服务,则超时时间是 20s;后台服务,则超时时间是200s。由变量 ProcessRecord.execServicesFg 来决定是否前台启动。春节前后左右发生的 ANR 奔溃 bug,选取了 35 个联系人后执行时间在 20 秒左右。通过这个现象可以确定当时那台手机的无障碍服务是由后台服务转为前台服务导致的。
4,最后一个问题就是,什么情况下后台服务会转为前台服务呢?我们通过官方文档可以知道以下有四种情况只要出现一种就会导致后台服务转成前台服务。1,正在屏幕上运行一个对用户可见的Activity,但不在前台(它的 onPause() 方法已被调用)。 例如,如果前景活动显示为允许在其后面看到先前活动的对话框,则可能发生这种情况。2,有一个作为前台服务运行的服务,通过 Service.startForeground() (这是要求系统将服务视为用户知道的事情,或者对他们来说基本可见)。3,正在托管系统正在使用的用于用户知晓的特定功能的服务,例如动态壁纸,输入法服务等。4,托管绑定到可见(非前台)Activity的Service。(目标进程持有一个绑定到可见 Activity 的 Service)。