ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Android系统核心服务AMS深度解析:架构、职责与进程管理

Android系统核心服务AMS深度解析:架构、职责与进程管理

做Android开发的人,迟早会遇到一个绕不开的名字——AMS。ActivityManagerService,很多三年五年的开发者一听到这几个字母就有点心虚,因为它太底层、太庞杂,网上资料要么是源码注释翻译,要么是通篇Fragment/ActivityRecord对象图,看两段就劝退了。我自己也是磕磕绊绊,从只会调startActivity到能顺着一次点击把链路捋到SystemServer,中间踩了不少坑。这系列文章就是想把AMS从定位、架构到核心流程讲清楚,第一期先把骨架搭起来,让你知道AMS是谁、管什么、长什么样、从哪儿来。内容尽量通俗,但该有的源码细节我会给出指向,方便你自己去验证。

如果你还在应用层写业务,这篇会帮你建立系统级的全局视角;如果你已经开始啃framework,这篇可以当一份导读地图,省得在源码里乱撞。我尽量用“人和人打交道”的方式来讲AMS,而不是堆砌术语。

1. AMS到底是什么:先说清楚它的职责边界

1.1 一次点击背后,谁在“安排”一切

先来个最贴近日常的场景:你手指点在桌面上某个App图标上,系统启动这个应用,界面就出来了。中间发生了什么?看起来只是Launcher调了一个startActivity,但真正干活的其实是系统里一个常驻的系统服务,它就是ActivityManagerService。

AMS不是一个普通的后台线程,也不是某个App内部的管理器。它是运行在系统进程SystemServer里的一个核心服务,相当于Android世界的“管家”。桌面App负责把页面画出来,但“哪个App可以启动”“它的Activity长什么样”“什么时候该暂停、什么时候该恢复”“内存不够了先杀谁”,这些都是AMS在决策。

你可以把AMS理解成一个调度中心:应用里的Activity只是演员,而AMS是导演,谁登场、谁下场、按什么顺序切换、舞台怎么分配,都由它说了算。应用层感觉不到它的存在,是因为系统通过Binder把这套能力封装成了透明的调用接口,你只需要调用startActivity,剩下的事有人替你安排。

1.2 它在系统架构里的具体位置

从系统架构看,Android从上到下大致是应用层、Framework框架层、系统运行库层、Linux内核层。AMS属于Framework层里的SystemServer进程,与它平级的还有WindowManagerService、PackageManagerService、ActivityTaskManagerService(ATMS,后来从AMS中拆出来的)等。

这里要先提醒一句:不同Android版本里AMS的代码结构变化很大。Android 10之后,谷歌把Activity相关的任务栈管理拆到了ActivityTaskManagerService,AMS本身则更聚焦在进程管理、权限校验、组件调度等事务上。很多老博客用Android 8甚至更早版本的源码讲AMS,对象关系图都不一样,你看的时候一定先确认源码分支版本,否则对着不同结构会越看越乱。

对于学习来说,不必过度纠结“到底AMS负责哪几行代码”,更重要的是理解它的职责分区。如果把框架层比作一家公司,AMS就是行政总监,WMS管窗口布局、PMS管包安装卸载、NotificationManagerService管通知,各司其职,而AMS管的是“Activity这整个组件的生命周期和任务栈编排”。

1.3 AMS核心职责,我总结下来就五件事

第一件事是调度Activity生命周期:启动、暂停、停止、销毁,这些状态切换不是Activity自己拍板,而是AMS发消息驱动,应用进程只是配合执行。

第二件事是管理和调度任务栈:你要知道Android多任务视图里那些“卡片”是怎么来的,其实就是AMS维护的TaskRecord在起作用,它记录了一组Activity的组合关系,栈怎么压入、怎么弹出,都有规则。

第三件事是进程管理:AMS根据每个App当前的状态给进程评分,内存吃紧时低内存回收机制会按这个评分选择性地杀进程,这就是为什么你在后台挂着的应用过一阵子就“冷启动”了。

第四件事是组件权限校验与Intent解析:你启动了别的应用里的Activity,AMS要检查是否存在、目标组件是否允许外部访问、Intent能不能解析到对应的组件,这些校验都在AMS这里完成。

第五件事是系统消息中枢:很多系统级广播、进程间状态同步的入口都在AMS,比如开机后PackageManager扫描完所有应用,要通知AMS“包列表准备好了”,这中间就有复杂的流程调用。

理解这五件事,你就能明白为什么AMS是所有Android系统服务里最核心的一个。它像人体的中枢神经,不受它支配的系统服务几乎不存在。

2. 认识AMS的核心数据结构:记住这几个关键角色

2.1 ActivityRecord、TaskRecord、ActivityStack

要深入AMS源码,你早晚会碰到这几个类:ActivityRecord、TaskRecord、ActivityStack、ActivityStackSupervisor。它们之间的关系,我建议你用一套“实体-容器”的思维去记。

ActivityRecord是Activity在系统侧的“档案袋”,一个Activity实例在系统侧对应一个ActivityRecord,里面记录了它的componentName、Intent、启动模式、当前状态、所在进程等元信息。它不等同于应用里的Activity对象,更准确的比喻是“护照:Activity是本人,ActivityRecord是护照上的记录页”。

TaskRecord(Android 10后叫Task)是任务栈的载体,一个Task里包含一组ActivityRecord,它们在界面上就是同一个“返回流”。比如你从首页进详情页再进支付页,这三个页面通常在一个Task里,按下返回键时ActivityRecord一个接一个从Task中弹出。

ActivityStack是更高一层的容器,容纳了一组Task。Android中有前台栈、后台栈、Home栈等不同堆栈,用来组织处于不同状态的Task。在更早些的版本里,ActivityStackSupervisor负责协调所有这些栈以及它们与AMS之间的交互。简单理解,ActivityStack是“舞台”,“Task”是节目单,ActivityRecord是具体的演员。

这里最容易踩的坑是把“Task”和“ActivityStack”搞混。你可以这样记:Task是纵向的返回栈关系,Stack是横向的前后台分组。同一个Stack可以包含多个Task,前台只有一个Stack,但屏幕上可能同时存在多个可见的Task形态(比如分屏)。

2.2 ProcessRecord:AMS眼里没有App,只有进程

对于AMS来说,它很少直接操作一个“App”的概念,而是操作ProcessRecord。ProcessRecord是某个应用进程在系统侧的代表,里面保存了进程优先级、pid、uid、当前运行的Activity列表、ANR相关信息、已绑定的服务等。

这里有个需要纠正的印象:很多人以为Activity和进程是一一对应的,其实不是。同一个应用的所有Activity、Service、BroadcastReceiver都可以跑在同一个进程里,而不同应用如果在Manifest里配置了相同的sharedUserId或通过某种方式指定了同进程,它们也可以共有同一个进程。AMS只关心进程级的状态,不会真正去管“哪个类在跑”——这是应用进程内部的事情。

在进程管理上,AMS维护了几个关键列表,比如mLruProcesses(按最近使用情况排序的进程表),这个列表直接影响进程被杀的顺序。进程更新前台状态、某个组件绑定、某个服务启动,都会改变它在LRU列表中的位置。

顺带一提,很多内存优化方案里说的“进程被杀死后重新拉起”,本质是AMS根据最近任务、恢复策略等手段重新创建进程并恢复Activity状态的过程,而不是“原来的对象复活了”。搞清楚ProcessRecord,你才能理解后续我们讲进程优先级时相关机制的本质。

2.3 Handler、消息循环与线程模型

AMS运行在SystemServer进程中,它的主线程是一个消息循环线程,所有对Activity的调度动作最终都会通过Handler发送到主线程的消息队列里执行。为什么要这么做?因为AMS要面对来自无数App进程的并发Binder调用,如果每个请求都直接操作内部数据结构,不加锁、不排队,状态早就乱套了。

这里的核心机制是:AMS接收到的Binder请求可能来自任何线程,但真正执行逻辑的地方是系统主线程。它把请求包装成消息,投递到主线程Handler的MessageQueue,然后串行地逐条执行。我建议你记住这个模型,因为很多ANR问题的排查思路,追根溯源都会拉回到这条消息链路上:某一条系统消息处理超时,导致后续的调度请求全部排队,最终表现为“应用无响应”。

这个设计也解释了为什么AMS源码里充斥着大量类似mActivityTaskManagerInternal、mHandler.removeMessages(...)的调用——它需要精确地控制某段逻辑在什么时机执行、如何取消定时任务。如果你在看源码时看到某个方法直接调用了另一个进程的接口,那通常是Binder IPC调用;如果是通过内部Handler发送消息,那多半是跨线程调度。把这两条线索理清了,源码主干基本就顺了。

3. AMS从哪儿来:系统启动时发生了什么

3.1 SystemServer的“演员表”

AMS不是某次点击时被临时创建的,它是系统开机过程中SystemServer进程初始化出来的常驻服务。要理解AMS的起源,你需要先知道SystemServer的角色:Android系统在启动时,会先由init进程启动Zygote进程,Zygote再fork出SystemServer进程。SystemServer是所有Java层系统服务的“宿主”,服务列表几十上百个,它们都在这里被依次创建并注册。

这些服务的启动顺序不是随意的,而是分阶段的:先是基础的LED服务、电池服务、振动服务等,然后是AMS、WMS、PMS这些重量级角色,最后是一些非关键服务。为什么AMS这么靠后?因为它需要前面某些服务已经就绪,比如电源管理服务准备好之后,AMS才能拿到唤醒锁去管理Activity显示;用户管理服务准备好了,AMS才能做用户相关的权限判断。

在Android 10往后的代码里,AMS的启动入口已经分散了:ActivityTaskManagerService在startActivityTaskManagerService()方法里创建,AMS则在后续的startActivityManagerService()里创建。你可以从SystemServer的run()方法看起,一路追踪下去就能看到完整的初始化瀑布流。

3.2 AMS的构造过程,拆开看就三步

AMS构造函数里做的一件事是创建自己的内部Handler和线程,为后续的消息循环做准备。然后是创建ActivityTaskManagerService的实例,把任务栈相关的逻辑单独交给它。最后是各种内部状态的初始化,比如LRU进程列表、最近任务列表等。

之后SystemServer会调用AMS的startSystemServerProcess()之类的流程,让AMS知道自己现在运行在system_server进程里,并设置必要的权限上下文。接下来是setSystemProcess(),这个方法会把AMS注册到全局ServiceManager,并写入系统进程对应的ProcessRecord。这一步完成之后,其他进程才能通过ServiceManager查询方式拿到AMS的Binder代理。

还需要将一些系统包(如framework-res.apk)预置到AMS的包管理状态中,这样后续的组件解析才有东西可查。最后调用installSystemProviders()安装一些系统Provider,比如SettingsProvider,这样后面的系统组件的默认参数才有存取的地方。

我建议大家不要死记这些方法名,而是理解这条链:创建实例→注册到上下文→写入进程信息→安装系统Provider→系统就绪。这样你单独去看某一版本的源码时,不至于被几十个方法名的顺序绕晕。

3.3 开机完成后AMS等待“开工”

到这里AMS并没有真正“开始工作”,它只是把“办公桌”摆好了。开机过程还在继续,当PackageManager扫描完磁盘上所有APK,把应用包信息收集完毕后,系统会通过回调把“包列表准备好了”这个信号传给AMS。AMS拿到信号后开始接受并处理来自应用的绑定请求、startActivity请求等。

这里有个细节:在AMS完全就绪前,如果某个进程提前发来了请求会发生什么?实际上Zygote和SystemServer启动完成后,系统进程自己会先启动一个Home应用(通常在Launcher启动完成后),其他App进程还不会被真正唤醒。AMS要确保系统进入稳定可交互状态后,才开始分发应用层的调度指令。

从“写代码的人”的角度理解AMS启动过程,最大的价值在于:它让你知道AMS不是一个抽象概念,而是一个有完整初始化顺序、有明确依赖关系的Java对象。以后你看到线上崩溃日志里出现ActivityManagerService.failure之类的字样,就不会一头雾水,至少你能判断它是处在初始化阶段还是运行阶段。

4. 一次startActivity的完整旅程:把链路走一遍

4.1 从应用侧发起,到AMS收到请求

Activity启动的入口在应用进程侧,最常见的是ContextImpl.startActivity(),它最终会走Instrumentation.execStartActivity()。这里Instrumentation像是一个“应用侧的特派员”,负责把启动指令发给系统侧,同时还能监控启动过程中的异常。

在execStartActivity中调用ActivityTaskManager.getService().startActivity(),这个getService()拿到的就是一个Binder代理,实际对应的远端是ActivityTaskManagerService(Android 10之后的实现)。这个调用会通过Binder驱动跨进程传输到system_server进程,在这里,系统侧才拿到了真正的Intent、调用者包名、用户ID等参数。

这个地方要额外理解的是,跨进程调用不是“方法同步执行”,而是“把请求打包发给远端进程等待返回”。Binder线程池中的某个线程拦截到请求,解包后交到系统侧线程池或直接调用目标方法。所以你在源码里能看到很多onTransact相关的异常,这些就是Binder传输层在处理请求时暴露出来的问题。

从应用发起进程到AMS校验结束,整条路径里有一句话要记住:应用调用方的startActivity最终都会变成系统侧ActivityTaskManagerService的一次本地调用,而系统侧返回的RemoteException会被Instrumentation捕获并转成应用层异常。

4.2 AMS侧的关键处理:校验、解析与任务栈决策

ActivityTaskManagerService收到请求后,会经过很长一段处理链。首先是权限校验:调用方有没有权限启动目标Activity?目标Activity是否允许被外部启动?这里涉及exported属性、权限声明等。如果不满足,直接抛SecurityException。

接着是Intent解析:系统要解析出Intent到底对应哪个组件。如果是显式Intent,直接拿到componentName;如果是隐式Intent,系统要遍历已安装的包匹配IntentFilter。匹配结果可能有好几个,用户没指定默认选择时,系统会弹出ResolverActivity让用户选择,这也解释了为什么你点击一个链接时偶尔会弹出“选择打开方式”的对话框。

之后进入核心的ActivityStarter环节。这里会决定任务栈怎么处理,涉及启动模式(standard、singleTop、singleTask、singleInstance)、flags、调用方的taskAffinity等参数。比如标记了NEW_TASK的会把Activity压入新任务栈,singleTop的栈顶复用要判断当前栈顶Activity是否是同一个组件。

任务栈决策是AMS体系里最“玄学”的部分之一,因为多任务切换、返回栈顺序、分屏模式都会干扰最终结果。我的经验是:遇到栈异常时,优先确认Intent的flag和启动模式以及目标Activity的taskAffinity,这三个参数的组合基本决定了Activity会去哪。

4.3 进程不存在时:AMS如何“从无到有”拉起一个进程

如果目标Activity所属的进程还没启动,AMS需要先创建进程。这个动作不能由AMS直接fork,而是AMS通过Socket向Zygote发送请求,让Zygotefork出一个新进程,并在新进程中执行ActivityThread.main()。

这里的逻辑很有意思:AMS不会重复创建已经存在的进程。它会先检查目标进程是否已存在(通过ProcessRecord查询),如果存在,直接走“进程已就绪”的流程;只有不存在时,才会与Zygote通信。进程创建完成后,应用进程里会执行ActivityThread初始化,并注册一个ApplicationThread对象到AMS那边,这样AMS就拿到了和应用进程双向通信的通道。

再往后,AMS通过ApplicationThread通知ActivityThread创建和启动目标Activity,ActivityThread会通过ClassLoader加载Activity类、创建实例、调用attach、走生命周期回调。你在onCreate里写的代码,都是在ActivityThread上执行的,而AMS始终在远端盯着状态的变化。

整个链路串起来,一次startActivity实际上经历了好几次跨进程通信:应用进程→系统进程→Zygote→新进程→应用进程。这也是为什么冷启动比热启动慢很多——多了一次完整的进程fork和初始化。

5. AMS的进程大权:怎么管理、怎么杀

5.1 进程优先级是怎么评出来的

Android中每个正在运行的进程都有优先级,但这个优先级不是Linux内核的传统优先级,而是AMS根据进程当前的状态和应用行为综合评定的一个“adj”值。adj约大,进程越容易被回收;adj约小,进程越重要,越不会被杀。

比如:前台Activity所在进程的adj最小,优先级最高;可见但非前台的Activity所在进程其次;有前台服务的进程排第三;有后台Service的进程再靠后;空进程优先级最低。这套规则分布在AMS的computeOomAdjLocked()里,它会遍历所有进程,反复评估,最终算出每个进程对应的adj值和对应的内存回收级别。

你手机上可能上百个进程同时存在,但内存是有限资源,Android系统的设计哲学就是“尽量让当前正在使用的App运行流畅,牺牲后台进程”。所以你会发现,平时后台进程被杀掉是常态,不是系统“坏了”。

理解adj机制对你的App开发有实际意义:如果你的App要做后台长任务,就必须想办法提高进程优先级,比如利用前台Service或者配合通知,让系统认为这个进程有用户可感知的内容,而不是在后台偷偷干活。

5.2 LRU列表与进程回收的双通道

AMS维护了一个LRU列表,根据进程最近被使用的时间排序,最近使用的进程排前面,其他靠后。这个列表的作用是配合adj共同决定回收顺序。即使两个进程adj相同,LRU靠后的会先被回收。

这里有个容易误解的点:很多人以为“杀掉进程”是AMS自己动手杀,其实不是。AMS只负责计算每个进程应该处于什么样的状态,并更新内核驱动的参数(通过写/proc/.../oom_score_adj),真正执行杀动作的是内核里的lowmemorykiller(低内存回收机制)或AMS发起的Process.killProcess()。

当系统内存低于某个阈值时,lowmemorykiller会根据每个进程的oom_score_adj选择性地杀进程。所以进程被杀了,你看到日志往往是一片低内存统计,而不是一句“我决定杀了你”。

你的App如果死得莫名其妙,第一步要看一下Logcat里有没有lowmemorykiller相关日志,大概率就是内存紧张时被系统选中了。想避免这个问题,最有效的手段就是降低进程对内存的占用,而不是祈祷系统“开恩”。

5.3 进程恢复与状态保存:AMS的“记忆能力”

AMS除了会杀进程,还要负责在进程被重新创建时帮你恢复现场。你肯定经历过:一个App在后台被杀了,重新打开时页面还在原来的位置,甚至滚动位置都还在。这看起来神奇,实际上是AMS在杀进程之前保存了Activity状态快照,重新创建进程后恢复。

这套机制的载体是ActivityRecord.savedState,如果你重写了Activity的onSaveInstanceState,系统可以在进程被杀前调用它保存数据;如果没有重写,系统只能恢复Activity的类名和Intent,视图细节就丢了。

这解释了为什么很多开发者遇到“进程被杀后数据丢失”问题:不重写onSaveInstanceState,进程被系统回收后,重新创建时只能拿到Intent,而Intent里没有的数据自然就丢了。所以遇到这类问题,第一件事就是检查你是否正确处理了实例状态保存和恢复。AMS只提供“恢复的能力”,要不要恢复完整,取决于应用自己。

6. 调试AMS的实用姿势:直接看系统怎么说

6.1 用dumpsys activity读取AMS的心跳

如果你和我一样喜欢动手验证,最常见的工具就是adb shell dumpsys activity。这条命令会把AMS当前的状态全部打印出来,包括栈信息、任务信息、ActivityRecord列表、进程状态、LRU列表、内存信息、ANR记录等等。

比如你想看当前前台Activity是谁,可以执行adb shell dumpsys activity top,它会打印栈顶的Activity详情,包括它的包名、组件名、所在的TaskRecord ID、状态等。还有一个高频操作是查看进程的adj情况:adb shell dumpsys activity processes,它会列出所有进程的pid、uid、adj、进程状态等关键信息。

调试时可以先把输出重定向到文件再慢慢看,否则终端刷屏太严重。用adb shell dumpsys activity > dumpsys_activity.txt保存到本地,然后用编辑器搜索关键字,效率会高很多。我强烈建议做一次:立刻去查一下你手机上当前各个进程的adj值,你会瞬间理解“前台进程、可见进程、服务进程”这些概念的实际排序。

6.2 追踪Activity生命周期:用ams日志或Debug工具

系统日志里有很多和AMS相关的输出,但不是默认全打出来的。你可以在设置里打开“调试”相关选项,或者通过adb shell setprop log.tag.ActivityTaskManager DEBUG,让ATMS输出更详细的日志。对于AMS本身,也可以针对性设置log.tag.ActivityManager。

另外Android Studio自带的“Layout Inspector”和“Profiler”只适合应用层调试,真正要看AMS视角的状态,还是依赖dumpsys和systrace。如果你在做启动优化或卡顿优化,建议学一下systrace,它能把系统各组件包括AMS的关键动作投递到时间线上,一眼看出启动过程中哪一步耗时最长。

调试经验说一条:当Activity无法启动或莫名其妙跳回上一个页面时,先别急着看业务代码。先执行adb shell dumpsys activity top确认栈顶是什么,再执行adb shell dumpsys activity intents看看最近的intent记录。很多时候不是业务逻辑错了,而是栈里的Activity状态不符合预期。

6.3 实战案例:一次Activity启动失败的排查路径

我做性能优化时遇到过一个问题:某App时不时出现点击按钮后界面闪一下就回桌面,没有任何崩溃日志。查看logcat时发现一条明显异常:Permission Denial: starting Intent ... requires ...。定位到问题后,发现是目标Activity的exported设置为false,而外部调起方不属于同一应用,最终在AMS校验阶段被拦截了。

另外一个常踩的坑是android:taskAffinity设置不当导致Activity被放到了错误的Task里。表现是打开一个页面后,返回键没办法回到上一个页面,而是跳回了桌面。用dumpsys查看后发现,这个Activity的TaskRecord与期望的不在同一个Stack,原因是设置了与其他页面不同的taskAffinity,系统为它新建了一个Task。

这类问题在业务代码里排查很久不出来的几率很高,因为代码逻辑明明是对的。从AMS视角去查,一眼就能定位。所以我的建议是:任何“启动异常、返回异常、栈混乱”的问题,都要第一时间怀疑启动模式/任务栈配置,而不是怀疑生命周期回调没写对。

7. 第二期预告:接下来拆Activity启动核心链路的源码

第一期的内容偏“地图式”的框架梳理,把AMS的定位、数据结构、启动过程、Activity启动链路、进程管理和调试手段都过了一遍。你可以拿这份脑图去看源码,方向就不会偏。不过真正把AMS吃透,还得落到源码细节上,尤其是ActivityStarter那段任务栈决策逻辑和Zygote进程启动的完整流程。

下一期我打算拆一条更细的线:从一次startActivity调用进入到ActivityStarter内部,逐步分析任务栈的入栈、复用、单例模式处理逻辑,并额外聊聊“为什么很多系统启动慢其实卡在AMS的消息队列上”。如果你手头正想做冷启动优化,可以先把这一期里的进程创建链路和adj知识消化掉,下一期讲线程调度时你会更有感觉。

我自己最初学AMS的时候犯过一个错误:总想一次性把所有方法背下来,结果越看越迷糊。后来换成“按场景去阅读源码”的方式——比如就盯着“启动一个Activity要经历哪些校验”“杀掉一个后台进程的完整路径”这两个问题去读代码,反而很快就把主干理顺了。你也别慌,这套体系本身庞大,能坚持挖下去,收获绝对值得。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进