ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Service ANR触发链路全解析:从startService到系统弹窗的底层原理

Service ANR触发链路全解析:从startService到系统弹窗的底层原理 最近一次线上事故让我印象挺深用户反馈下载功能卡在“准备中”过了大约20秒屏幕弹出了“某应用无响应是等待还是关闭”。我查日志第一眼就看见了一行关键词ActivityManager: ANR in com.example.app (com.example.app/.download.DownloadService) ActivityManager: Executing service com.example.app/.download.DownloadService单独看这段日志没研究过系统底层的同学很容易懵Service代码里没有死循环主线程也没有卡死在某个函数为什么系统还是坚定不移地给了一个ANR事实上Service ANR不是简单看“你的Service业务跑没跑完”而是看“启动一个Service后有没有在限定时间内向AMS回报完成信号”。这套触发流程藏得很深从应用层调startService()到系统弹ANR对话框中间隔了跨进程Binder、系统侧调度、主线程消息队列、延迟消息定时器好几道环节。这篇文章我就把Service ANR的触发链路完整梳理一遍系统侧ActiveServices如何调度、应用进程ActivityThread如何执行、超时定时器如何精准命中一个“看起来还在跑”的Service最后配合我踩过的几个真实案例说说怎么排查、怎么从根上避免。无论你是正在啃framework源码的开发者还是每天被ANR问题追着跑的应用层同学这篇文章应该都能给你一张比较完整的地图。1. Service ANR到底在“管”什么1.1 先看懂ANR的全称和管辖范围ANR的全称是Application Not Responding直白翻译就是“应用不响应”。在Android系统里它不是在笼统地监控“应用卡不卡”而是非常具体地给三类任务设了超时红线ANR类型监控对象典型关键日志Input ANR输入事件在5秒内没被完整派发处理Input event dispatching timed outBroadcast ANRBroadcastReceiver的onReceive在限定时间内没执行完Timeout executing broadcastService ANRService启动阶段限定时间内没向AMS回报完成Executing service ...每个应用进程都有一个主线程Android的组件生命周期回调全部跑在这条线程上这是自始至终绕不开的基础设定。AMS通过Binder把一个任务比如“创建并启动这个Service”发送到应用进程后任务会作为一条Message插入主线程消息队列末尾主线程从队列里一条条取出来执行执行完之后应用进程又要通过Binder把结果回报给系统侧。Service ANR盯的就是这个“系统发出指令 - 主线程执行完毕 - 回报回执”的完整闭环。1.2 为什么会单独为Service设超时机制回到开头那段日志日志里写的是Executing service这句抱怨不是来自应用的某个回调而是来自系统进程的ActivityManagerService。它记录了一个ServiceRecord把executingStart字段按下去了然后等着应用进程来把它清零。如果应用迟迟不去清零系统侧就会觉得“我安排给你的启动任务一直没有完成信号。”现实中踩过这个坑的都有体会主线程可能不是闲着而是在执行一条很重的同步逻辑比如网络请求一直等响应、读大文件、解析复杂JSON。线程在跑、Looper在跑但已经完成了的“启动Service”任务并没有被回执。系统可不管你在忙什么它只关心自己的时间表。所以Service ANR机制本质上就是一套“启动确认超时机制”和Input ANR的“事件派发超时”逻辑相通只是监测对象换成了服务启动任务。1.3 前台20秒、后台200秒的设计逻辑Service ANR超时时间在系统侧写得很明确前台服务20秒后台服务200秒很多人第一次看到这个数字会觉得奇怪后台服务给10倍时间是不是意味着“后台应用可以随便卡”不是这恰恰体现了Android系统的调度哲学。前台服务用户能直接感知到通知栏里挂着进度条用户正盯着它干活系统自然给出更苛刻的响应预期后台服务没有前台界面和通知的压力用户感知不到具体执行进度系统倾向于多等一会儿避免因为偶发的主线程繁忙就直接杀掉后台进程。但等太久也不行进程要是彻底卡死了还无人发现内存和系统资源就会被白白占着所以200秒是一条更宽的死线。理解这一点是抓住Service ANR触发链路的第一把钥匙前台服务不是拿到免死金牌反而比后台服务更容易触发ANR。2. 一次Service启动的完整旅程executingStart是怎么被按下去的从代码层面看Service ANR的整个生命周期绕不开ActiveServices这个系统侧内部类。这一章我们跟着一次startService()调用走一遍把它从客户端到系统侧再到应用主线程的路径拆开。2.1 第一棒应用进程通过Binder走进AMSContextImpl.startService()内部最终会通过ActivityManager.getService()拿到AMS的Binder代理然后调用startService()。这个调用是跨进程的从应用进程进入系统进程走到ActivityManagerService.startServiceLocked()。AMS自己并不直接处理Service调度而是把工作委托给内部类ActiveServices。到这里这趟旅程从应用层进入了系统层。这个跨界很重要因为跨进程意味着双方之间不是同一个内存空间你没法用普通函数调用的思路去理解“谁等待谁”。系统进程发出指令后不会干等它继续运转用定时器盯着结果应用进程收到的是另一个进程发来的消息消息要进主线程队列什么时候执行完全取决于主线程队列的状态。2.2 第二棒ActiveServices安排调度按下“开始计时”的开关ActiveServices拿到启动请求后先看目标Service所属的进程是否存在。如果进程没有启动系统会先去AMS的startProcessLocked()拉一个进程起来把启动请求挂到一个待启动队列里进程起来并完成attachApplicationLocked()后再去执行真正的启动动作。这段路里的分支很多但最终所有路径都会汇合到realStartServiceLocked()private final void realStartServiceLocked(ServiceRecord r, ProcessRecord app, boolean execInFg) throws RemoteException { ... app.thread.scheduleCreateService(r, r.serviceInfo, mAm.compatibilityInfoForPackageLocked(r.serviceInfo.applicationInfo), app.getReportedProcessState()); // 记录启动服务的开始时间 r.lastActivity SystemClock.uptimeMillis(); r.executingStart SystemClock.uptimeMillis(); ... // 这里挂一个延迟消息这把“计时器”才真正按下去了 scheduleServiceTimeoutLocked(app); }这段代码里有三个关键动作app.thread.scheduleCreateService(...)通过进程的IApplicationThread Binder接口向应用进程发送“请创建Service”的指令r.executingStart SystemClock.uptimeMillis()给这个ServiceRecord打上一个时间戳作为此后判断是否超时的基准点scheduleServiceTimeoutLocked(app)给AMS的Handler发送一条延迟消息形成定时检查的“闹钟”。executingStart就是那枚“开始计时”的开关。它被按下的一瞬间系统侧就默认这个Service已经处于“正在执行启动”的状态接下来就看应用进程是否来把它清零。2.3 第三棒ActivityThread在应用进程里执行Service应用进程这边收到的CREATE_SERVICE消息由ActivityThread.handleCreateService()处理。它做的是所有Android开发者都能背出来的那套流程private void handleCreateService(CreateServiceData data) { ... java.lang.Object service null; try { java.lang.ClassLoader cl packageInfo.getClassLoader(); service cl.loadClass(data.info.name).newInstance(); } catch (Exception e) { ... } try { ... service.attach(context, this, data.info.name, data.token, packageInfo.getApplication(), data.info); // 真正的生命周期回调全部在主线程上执行 service.onCreate(); mServices.put(data.token, service); service.onStartCommand(data.args, data.flags, data.startId); } catch (Exception e) { ... } // 关键一步执行完毕向系统侧回报完成 ActivityManager.getService().serviceDoneExecuting( data.token, SERVICE_DONE_EXECUTING_START, 0, 0); }注意到没有onCreate()、onStartCommand()、serviceDoneExecuting()这三件事是在同一个主线程消息处理过程中依次执行完的。如果onStartCommand()里放着一条同步网络请求主线程会一直卡在网络IO上卡多久最后那个serviceDoneExecuting()的回报就会晚多久。也正因如此onStartCommand()里的一行阻塞代码就足以让整个Service进入超时风险区。2.4 回执即拆弹executingStart什么时候归零AMS收到应用进程的serviceDoneExecuting()后会调用ActiveServices.serviceDoneExecutingLocked()。里面最关键的操作很简单if (r.executingStart ! 0) { r.executingStart 0; ... }executingStart0这个动作相当于把炸弹的引信拔掉了。定时器消息哪怕此刻已经在AMS的Handler队列里排着等它真正被处理时发现这个ServiceRecord的executingStart已经是0就会判定“任务已经完成”安静离开。这套机制可以打一个比方系统像一个下订单的顾客它把“给我创建并启动这个Service”的订单发出后同时按下手里的秒表executingStart。应用进程必须在上菜时限内把菜端上来并说一句“菜齐了”serviceDoneExecuting顾客才把秒表清零不催不闹。如果到时间了没听到那句“菜齐了”顾客就开始查后厨是不是瘫痪了。3. 超时判定的源码级拆解定时器如何精准命中“卡住”的Service理解了executingStart的语义后我们再深入超时定时器本身。这层逻辑在ActiveServices和AMS的Handler里是整个触发链路里最需要精读的部分。3.1 scheduleServiceTimeoutLocked定时器挂上的具体时刻前面已经看到realStartServiceLocked()在通知应用进程创建Service之后紧接着就调用了scheduleServiceTimeoutLocked()。它的实现是这样的void scheduleServiceTimeoutLocked(ProcessRecord proc) { if (proc.execServicesFg || proc.execServicesCount 0) { Message msg mAm.mHandler.obtainMessage( ActivityManagerService.SERVICE_TIMEOUT_MSG); msg.obj proc; mAm.mHandler.sendMessageDelayed(msg, proc.execServicesFg ? SERVICE_TIMEOUT : SERVICE_BACKGROUND_TIMEOUT); } }这里出现了两个关键常量static final int SERVICE_TIMEOUT 20 * 1000; static final int SERVICE_BACKGROUND_TIMEOUT SERVICE_TIMEOUT * 10;还需要理解execServicesFg的含义。如果一个进程内正在等待确认的任务里包含前台服务这个flag会被置为true整个进程都按20秒标准来如果都是后台服务就按200秒标准来。它细化到进程粒度了——只要这个进程有一个前台Service在启动阶段没回执你其它后台Service也一并享受20秒的超时待遇。这个细节我在项目里实际验证过进程里混跑前台服务和后台任务时后台任务稍一卡顿ANR来得比预期快得多。3.2 serviceTimeout()超时消息到达后做了什么AMS的Handler收到SERVICE_TIMEOUT_MSG消息后会调用ActiveServices.serviceTimeout()来判断该不该判ANR。核心逻辑不复杂void serviceTimeout(ProcessRecord proc) { ... ServiceRecord timeoutRecord null; long now SystemClock.uptimeMillis(); long maxTime now - (proc.execServicesFg ? SERVICE_TIMEOUT : SERVICE_BACKGROUND_TIMEOUT); for (int i 0; i proc.services.size(); i) { ServiceRecord sr proc.services.valueAt(i); if (sr.executingStart ! 0 maxTime sr.executingStart) { timeoutRecord sr; break; } } if (timeoutRecord ! null) { ... mAm.appNotResponding(proc, null, Executing service timeoutRecord.shortName); } }它遍历目标进程名下所有ServiceRecord条件有两个一是executingStart不为0说明还没收到执行完成回执二是maxTime sr.executingStart说明已经超过了允许的时限窗口。两个条件同时满足就拿着这个ServiceRecord去调用appNotResponding()启动正式的ANR流程。所以判断逻辑非常清晰定时器不是去“看谁的代码执行得慢”而是去看“有没有ServiceRecord还停留在没回执的状态并且停留时间已经超过了20秒或200秒”。这也是为什么我说Service ANR的本质是启动确认超时。3.3 临界状态下的竞态回执和超时消息谁先到这套机制里有一个必须理解的边界竞态如果serviceDoneExecuting()的应用进程回执先到达AMS并清零了executingStart随后超时消息才被Handler处理系统会发现没有超时记录不做任何ANR处理如果超时消息先被处理系统就会立即发起ANR流程。虽然正常情况下这个窗口只有毫秒级但主线程卡顿情况下消息处理的先后顺序完全可以颠倒过来。实际排查时偶尔会遇到一种“明明代码逻辑很快完成了却还是ANR”的诡异现象其中一个原因就是主线程卡顿导致serviceDoneExecuting()的Binder调用迟迟没发出而系统侧的超时消息已经被Handler取出并处理。也就是说应用进程侧“表面上完成”不等于“系统侧知道完成”单靠业务代码里打了log判断“我执行完了”并不靠谱等Binder方法真正落地才作数。3.4 常见误区Service ANR不是“运行超时”而是“启动确认超时”这里要特意区分一个常见的误解。很多人把Service ANR理解为“Service跑的时间太长而触发的”于是代码里一旦遇到后台长任务就担心ANR。实际上只要onStartCommand()正常返回、serviceDoneExecuting()已经回执你的Service后面就是跑十分钟、一小时都不会触发Service ANR。系统只会关注“启动确认”这个阶段有没有及时完成不会对已经完成启动的Service做运行时长监控。反过来更隐蔽的坑是onStartCommand()里同步执行了耗时逻辑哪怕后台线程池很空闲只要主线程被这条耗时逻辑占据回执就会延后后台任务的200秒也会被吃掉。所以Service ANR和“Service运行时长”是两个维度的概念前者是启动握手协议后者是纯粹的业务执行策略。4. 从超时判定到弹窗ANR事件是如何被推向用户的超时判定只是第一步appNotResponding()之后还有一连串动作决定这个ANR最终以什么形式呈现给用户。4.1 appNotResponding的完整动作链ActivityManagerService.appNotResponding()大致做的事情包括收集当前进程的CPU占用率、线程状态、进程优先级等信息写入日志触发一次系统级dump把各线程堆栈写到/data/anr/下的traces文件判断当前进程是否拥有焦点窗口、屏幕是否锁屏、用户是否能看到对话框在满足条件下弹出ANR对话框选项通常包括“等待”和“关闭应用”如果进程后续一直没有恢复系统还会在一段时间后直接杀死进程并清理资源。这些动作在应用层的直观感受是界面先卡住然后弹窗选“等待”可能过一会儿恢复选“关闭”则整个进程结束。但要注意弹窗只是ANR处理的一种表现进程能不能活下来还要看主线程是否从卡顿中恢复。4.2 有焦点和无焦点时的关键差异不是每个ANR都会弹窗。系统侧有一个判断如果ANR发生时应用没有任何可见界面比如一个纯后台Service在跑系统默认用户没有可交互的入口就会倾向于不弹窗直接在后台dump完信息后把进程杀掉。这种情况下用户感知不到弹窗但系统日志里已经有完整的ANR记录。还有一类情况是锁屏或者系统UI处于特殊状态时系统也会选择静默处理。所以排查ANR问题时不能只依赖用户“有没有看见弹窗”来定位日志里的ANR in ...关键字和traces文件才是真正靠谱的证据。后台ANR往往比前台ANR更危险因为它没有弹窗缓冲进程直接被回收业务中断得毫无提示。4.3 和Input ANR、Broadcast ANR的区别同是ANR三条路径经常被搞混但它们的触发对象和排查入口完全不同对比项Service ANRInput ANRBroadcast ANR监测主体Service启动确认回执输入事件派发完成BroadcastReceiver的onReceive执行超时时间前台20秒、后台200秒5秒左右前台广播约10秒后台广播约60秒主要卡点onCreate/onStartCommand同步耗时主线程忙导致输入事件无法派发onReceive里做了耗时操作日志特征Executing serviceInput event dispatching timed outTimeout executing broadcast遇到“点了没反应系统弹窗”这类表现时日志里的关键字能帮你第一时间分清是哪条线上出了问题。Service ANR的弹窗表现往往伴随着后台定时任务的延迟而Input ANR更常表现为界面事件响应断裂这两者虽然都源于主线程问题但追踪入口完全不一样。5. 实战复盘我踩过的三种Service ANR场景理论链路讲完了落到实战。这几年我在不同项目里处理过不少Service ANR真正有代表性的其实就几种展开讲讲当时的现象和排查过程。5.1 案例一onStartCommand里的同步网络请求当时做一个下载功能下载服务的onStartCommand()里有一段获取下载地址的逻辑用的还是OkHttp同步调用Override public int onStartCommand(Intent intent, int flags, int startId) { // 此处通过同步方式请求下载地址网络差时单次超时30秒 String url okHttpClient.newCall(request).execute().body().string(); startDownload(url); return START_STICKY; }网络正常时这段代码不到一秒钟就过了所以前期从没测出问题。某次弱网环境下请求重试了30秒主线程一直卡在OkHttp的getResponseWithInterceptorChain()上最终日志里出现20秒级别的Service ANR。查traces文件时主线程堆栈停在okhttp3.RealCall.getResponseWithInterceptorChain的等待处一眼定位。这个案例如果只看业务层面会很难理解明明请求最终会成功为什么系统不给我更多时间原因就是本章前面讲的系统只等待启动确认窗口超过20秒不等你。修复方式也直接把获取下载地址的逻辑全部改成异步enqueue()拿到结果再决定下载状态不阻塞主线程。5.2 案例二共享锁把主线程拖进泥潭另一个项目里有一个经常性ANR奇怪的是所有生命周期回调里都没什么重活。反复看traces后发现主线程停在Object.wait()上。顺着锁对象一路追发现流程是主线程在onCreate()里等一个后台线程写入的缓存数据后台线程写缓存前又会持有一把锁而主线程恰好也在等这把锁。两条线程互相等待形成典型的锁竞争死锁。这个案例最大的教训是Service的生命周期回调不只是你自己写的代码在跑中间可能间接产生跨线程等待。排查时如果只盯着自己的业务代码很难发现“主线程等一个线程而那个线程又在等主线程”这样的环路。主线程上任何形式的锁等待都是高危操作尤其当等待对象是另一个线程控制的东西时风险呈指数级上升。5.3 案例三看起来什么都没做却还是ANR还有一类让我印象深刻的ANRService的onCreate和onStartCommand加起来执行时间不超过100毫秒业务逻辑一眼看去毫无问题可系统还是报Executing service超时。深挖后发现问题出在主线程消息队列的积压上。当时应用启动后有一个比较重的UI初始化流程主线程连续执行了多次大数据量列表刷新和布局计算耗时接近5秒后续的CREATE_SERVICE消息就一直在队列里排队。主线程处理完积压消息再执行Service启动时系统侧的定时器已经走了太久最终导致整体超过20秒。这类问题暴露了一个关键认知你的回调开始执行时间不代表系统期待你开始执行的时间。executingStart在系统发出指令时就按下了而不是在主线程真正开始创建Service时才按下。主线程其它任务越重Service启动任务被推迟得越久ANR风险越大。这也是为什么我在排查时总喜欢把Looper日志打开看主线程消息分发耗时的原因。5.4 排查三板斧traces、logcat、dumpsys activity如果说有什么固定的排查套路可以作为默认动作我总结成三步看logcat中的关键字。过滤ActivityManager和ANR第一时间找到Executing service xxx、ANR in xxx这几行确定是Service ANR拿到发生时间和进程名。拉traces文件。不同Android版本的路径略有差异但大都在/data/anr/目录下。重点看主线程的堆栈如果停在网络调用上多半是同步IO如果停在MessageQueue.next()等待消息多半是队列积压如果停在锁等待上就顺藤摸瓜查锁关系。用dumpsys确认ServiceRecord状态。adb shell dumpsys activity services能看到系统侧记录的ServiceRecord状态包括executingStart时间、foreground标记。这个信息能帮你确认系统是在哪个时间点开始计时的和logcat日志的时间线对上。这套三板斧在绝大多数Service ANR排查里都够用。要记得同时看CPU load信息有时ANR不是代码问题而是系统负载太高导致主线程根本分配不到CPU时间片那种情况代码写得再干净也没用。6. 如何让Service业务远离ANR我的方法论技术和原理都讲完最后说说我这几年沉淀下来的几条写Service的铁律。6.1 生命周期回调只做“轻量登记”别做“重量执行”我把Service定位成一个调度外壳不在onCreate、onStartCommand里做任何可能超过几百毫秒的同步操作。拿到参数后立即记录状态、启动后台任务然后快速返回。所有耗时逻辑丢给线程池、协程或HandlerThread。这不是过度设计而是把ANR超时窗口的占用减到最小给系统回执留足余量。6.2 能选WorkManager就不选常驻Service如果任务本身是后台数据同步、定期上传之类的场景我会优先考虑WorkManager。它自带约束条件充电、联网、延迟、失败重试、电量优化策略底层的调度时机由系统统一管理开发者不需要自己维护Service的启停和生命周期也就不存在Service启动确认超时的问题。原生Service保留给必须长时间前台运行、用户能明确感知的场景比如播放音乐、前台下载、主动上传。6.3 给主线程装一个“心电图”Service ANR归根到底大概率是主线程出了状况所以我主张在每个应用里都保留一套主线程卡顿监控。最简单可靠的是基于Looper的setMessageLogging()方案打印每条消息的执行的起始和结束时间也可以直接用开源的卡顿监控库实时统计主线程单条消息分发耗时超过2秒就上报。有了这个机制很多崩溃发生前的主线程卡顿轨迹都能提前发现根本不用等ANR日志出来再回过头去猜。6.4 前台服务的特殊注意点前台服务比普通Service多一层风险不仅要照顾20秒的启动确认还要在5秒内调用startForeground()并显示通知否则会被系统判定为“前台服务未及时显示通知”而直接拒绝或杀死。两个时间窗口叠加在一起对主线程的整洁度要求非常高。启动前台服务时的所有准备工作都要提前做好比如把关键数据预先放在内存里不要在onCreate里临时读数据库、加载大图等重操作这能避免两个超时机制同时被引爆。最后再分享一个我反复跟团队强调的检查习惯每次写完一个Service先问自己三个问题——onCreate和onStartCommand里有没有可能超过几百毫秒的同步操作主线程有没有可能在等待其它线程释放锁这个Service如果必须长期存在它抢占的CPU和主线程时间是不是真的必要三个问题想清楚大多数Service ANR在联调之前就能提前消灭。真遇到线上疑难的也记住那句话日志里的Executing service才是判断Service ANR的钥匙找到它再顺着traces找堆栈比你对着满屏日志瞎猜高效得多。
返回列表