ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Android进阶分水岭:四大组件工作过程与系统调度机制解析

Android进阶分水岭:四大组件工作过程与系统调度机制解析 Android开发者啃到《Android开发艺术探索》第九章基本就到了一个很有意思的节点前面几章讲的是View、动画、IPC、Window这些更像是“术”让你把某个具体功能或机制用明白而到了“四大组件的工作过程”它开始把Activity、Service、BroadcastReceiver、ContentProvider这四兄弟从“API黑盒”拉开成一整套“系统调度链条”。我第一次读这一章的时候正好在做一个进程常驻保活相关的需求每天被各种手机厂商杀后台搞得焦头烂额翻完第九章才真正看明白系统是怎么决定“谁该活、谁该杀”的那种感觉不是某个具体API能教你的。这一章适合谁适合已经写了几个月App、能独立做业务功能但一遇到启动闪退、后台被杀、ANR、跨进程调用就被卡住的人也适合准备系统性梳理Android framework、应对大厂面试的人。它不是教你写某个页面而是帮你把“我调了一个方法之后系统到底发生了什么”这件事彻底想清楚。1. 为什么说这一章是Android进阶的分水岭1.1 这一章到底讲了什么四大组件的工作过程往大了说是“从调用方发起一个动作到系统完成组件调度、进程创建、生命周期回调”的完整链路。举例来说你点击按钮跳转一个新页面表面上看是startActivity(intent)一行代码但背后至少牵扯到你的App进程通过Binder把请求发给系统进程里的AMSActivityManagerServiceAMS校验Intent、计算任务栈、决定是否需要创建新进程再通过Binder回调你的App进程让主线程执行onCreate、onStart、onResume。这一整条链路里AMS不是唯一主角还有PMSPackageManagerService、ActivityThread、ApplicationThread、Zygote等一票角色。学习这一章之前你可能知道“Activity有三种启动模式”、知道“ContentProvider可以用来跨进程访问数据”但不知道这些能力是怎么被系统组织起来的。读完之后你会建立一张地图四大组件不是四个孤立的类它们各自对应着系统服务中的一套记录结构比如Activity对应ActivityRecord和TaskRecordService对应ServiceRecordContentProvider对应ProviderRecord。每当系统需要操作某个组件它先找到对应的“注册记录”再根据进程状态决定“唤起进程”还是“直接回调”。这就是为什么很多人说第九章读完才算真正踏进Android framework的大门。1.2 系统服务为什么值得你先认识要理解组件工作过程绕不开“系统服务”这个概念。你手机上跑着两个世界一个是各种App所在的普通应用进程另一个是system_server这个系统进程。AMS、PMS、WMS这些重量级服务几乎都居住在system_server进程里。App没办法直接调用这些服务的方法于是Android引入了Binder这套跨进程通信机制把系统服务的能力“远程暴露”给App调用。这里有个关键认知ActivityManager.getService()拿到的其实是一个Binder代理对象你调用它的时候参数会被序列化跨越进程边界送到system_server由真正的AMS执行。反过来AMS想通知App进程做点什么也不能直接调App代码它手里握着另一个Binder代理也就是App进程通过ActivityThread注册过来的ApplicationThread。所以你会发现整个组件工作过程本质上就是一套“来回Binder调用”的握手协议。理解这个背景之后很多现象就有了第一层解释为什么冷启动比热启动慢那么多因为冷启动不只是创建Activity整个App进程都可能要先被Zygote fork出来再初始化Application、ContentProvider、主线程消息队列最后才轮到你的Activity。为什么有时候bindService感觉比startService复杂因为它多了一组Binder回传和跨进程接口绑定。系统服务不是教科书里的抽象名词而是你真的可以在日志里、在dumpsys输出里看到的调度中心。2. 先搭好总纲从ActivityThread看App进程的“骨架”2.1 ActivityThread并不是一个线程关于ActivityThread有个特别容易误导人的名字它名字里带个“Thread”但它在Java层并不是一个Thread对象。准确一点说ActivityThread是App进程的主线程逻辑入口它内部持有Looper负责不断从主线程MessageQueue里取出消息并分发执行。每个Android应用进程启动后第一个要创建的Java对象基本就是ActivityThread。ActivityThread的核心职责可以概括为三件事一是作为App进程的“主控制器”接收AMS通过ApplicationThread发来的各种指令并执行二是管理主线程的消息循环三是维护四大组件在当前进程里的实例列表比如mActivities、mServices、mLocalProviders组件生命周期发生变化时更新这些列表。这里要特别区分一组容易混淆的角色ActivityThread和ApplicationThread不是一回事。ApplicationThread是ActivityThread里的一个内部类它继承了IApplicationThread.Stub本质上是一个Binder实体。App进程启动后要把它注册给AMSAMS以后想让你创建Activity、创建Service、绑定ContentProvider、分发广播都会调用ApplicationThread上的对应方法。你可以这么理解ActivityThread是整个App进程的“管家”ApplicationThread是管家放在门口的那个“传话信箱”系统服务的消息只投递到信箱里管家再从信箱取出来分派下去。2.2 BinderApp与世界对话的“电话线”Binder如果没接触过很容易被各种“AIDL”“代理”“Stub”这些词搞晕。我用一个生活化类比解释Binder就像一根电话线App这头拿着电话听筒BinderProxysystem_server那头有真正接电话的人Binder实体比如AMS自己。你说话的内容会被打包成Parcel格式沿着线路传过去对方听完再回你一个Parcel结果。中间没有共享内存的直接访问一切都是“请求-响应”。在组件工作过程里Binder贯穿始终App调系统ActivityManager.getService().startActivity(...)这是拿AMS的代理对象发起远程调用。系统调AppAMS通过App注册的ApplicationThread代理调用scheduleLaunchActivity、scheduleCreateService等方法。跨进程绑定Service服务端通过onBind返回一个IBinder客户端拿到这个IBinder后才能直接调用服务端暴露的接口。我见过不少读者在AIDL上学得一头雾水但把第九章对应进程间调用的位置反复看几遍之后AIDL里的Stub、Proxy、asInterface全都有了实际对应的调度场景。这就是为什么我特别建议把Binder和组件工作过程放在一起理解Binder是方法组件工作过程是场景。3. 四大组件工作链路逐一拆解3.1 Activity的启动一场由AMS全程调度的接力赛Activity的启动是四大组件流程里最典型、也最复杂的。我们把场景限定在一款老版本Android系统上新版本虽然代码路径有调整但主链路仍然可对照。第一步发生在App进程内部。startActivity经过ContextWrapper、ContextImpl最终进入Instrumentation.execStartActivity它调用ActivityManager.getService().startActivity把Intent、调用者身份、请求码等参数交给AMS。这一步就是App把“我要开一个Activity”的诉求从本进程传递到系统进程。第二步交给AMS处理。AMS收到请求后会做一系列检查并借用ActivityStarter后来版本拆出来的类去决定启动策略目标Activity是否已注册、调用者有没有权限、应该放在哪个任务栈、是否要复用已有实例、启动模式是否会改变栈结构等等。这些计算结果会体现在ActivityRecord和TaskRecord里。如果启动模式是singleTask系统会先查找已有的TaskRecord决定是否需要清理栈顶Activity然后再展示新的Activity。第三步最关键目标Activity所在进程可能根本不存在。比如你从桌面点开一个尚未运行的AppAMS发现目标进程是空的它就要通过Process.start让Zygote fork出一个新进程。新进程创建完毕后入口是ActivityThread.main()随后ActivityThread调用attach方法通过Binder把ApplicationThread注册给AMS相当于对AMS说“我的进程好了可以给我派活了”。第四步是AMS在收到attachApplication后再把之前搁置的Activity启动请求派发下来。AMS通过ApplicationThread的scheduleLaunchActivity方法节流这个方法在App进程里最终会向主线程消息队列发送一条启动Activity的消息。主线程取出消息后TransactionExecutor开始执行一个事务进入handleLaunchActivity这里会通过performLaunchActivity完成一系列“初始化”动作反射创建Activity实例、通过Instrumentation回调onCreate、创建PhoneWindow并设置ContentView、通知生命周期状态变为Started、Resumed。到这一步实际上界面还没有真正画出来直到handleResumeActivity阶段WindowManager才会把DecorView添加进窗口。这里有两个值得细品的点。第一冷启动和热启动的差距主要来自第三步进程创建、Application初始化、ContentProvider初始化都在这一步这也是为什么优化冷启动通常优先盯Application里的逻辑。第二Activity生命周期回调不是AMS直接调的而是AMS把需求发送回来App主线程按照Handler消息的先后顺序逐个执行。这也解释了为什么耗时操作不该放主线程——它阻塞的不只是你的代码而是整个主线程消息队列AMS后续派发过来的onResume、输入事件、广播回调全都会被排队等着。3.2 Service的工作过程启动与绑定的两条线Service比Activity稍微简单一些但它有两条独立的使用路径startService和bindService。两条路径在很多地方共享进程调度的逻辑但生命周期回调差异很大网络上有大量面试题专门对比它们。先说启动型。context.startService最终会通过Binder进入AMS的startService方法AMS把请求交给ActiveServices去处理。ActiveServices会先查找或创建对应的ServiceRecord然后检查Service所在进程是否存在。进程不存在时和Activity那边的做法一样先创建进程。进程就绪并通过attachApplication上报之后AMS会把之前暂存的启动请求补派下来调用到ApplicationThread的scheduleCreateService。App进程处理这条消息时通过反射创建Service实例调用其onCreate这一步执行完后系统再通过scheduleServiceArgs触发onStartCommand。再说绑定型。bindService的调用同样先进AMS但ActiveServices会做额外的判断这个ServiceRecord是否已经被启动过如果之前没有任何记录需要先创建Service实例并回调onCreate然后找到或创建客户端与Service之间的连接记录也就是ConnectionRecord。之后AMS请求App进程执行scheduleBindServiceApp进程调用Service的onBind拿到返回的Binder对象再通过Binder回传给AMSAMS把它转交给客户端。客户端等到onServiceConnected回调后才能真正调用到远端接口。有一条经验值得记住同一个Service先被startService启动后又bindService那么它生命周期的结束方式要两边同时满足。stopService只能停掉启动型的标记如果还有连接绑定着Service不会销毁反过来解绑所有连接后如果启动型标记还存在Service依然会继续活着直到收到stopService或系统主动回收。这种混合场景在实际项目里非常容易踩坑尤其是当你把一个音乐播放Service同时暴露给多个页面绑定时。Service本身还有一层和进程优先级相关的重要属性系统在内存紧张时要决定优先杀谁一般前台进程优先于可见进程可见进程优先于服务进程。而一个正在执行到前台服务的进程会被提升到较高优先级。第六章或第九章学完后你会理解为什么startForeground能让进程“抗杀”一些因为它把当前进程的OOM级别往上提了一档虽然不能保证100%不被杀但至少提高了存活概率。这也是保活方向的人一定要啃明白的底层逻辑。3.3 BroadcastReceiver注册方式决定分发路线广播机制的流程可以拆成“注册-发送-分发”三段来看。注册方式不同会影响系统在分发时选择哪条路径。动态注册是指代码里调registerReceiver这条路最终会进入AMS的registerReceiver。AMS内部维护了一张匹配表把你注册的IntentFilter和ReceiverDispatcher关联起来后面有广播发出时AMS就能根据Action进行匹配。这里需要注意的是动态广播注册在Activity里时Android 13之后要求显式声明RECEIVER_EXPORTED/RECEIVER_NOT_EXPORTED标志旧代码如果不处理注册非系统广播时会直接抛异常。把这一章读明白后你会自动理解这个标志的含义它本质上是让系统在分发时判断外部其他应用能不能把广播发给你这个Receiver。静态注册是指在Manifest里声明receiver。安装App时PMS解析Manifest将静态广播接收者信息登记在系统里。从Android 8.0开始绝大多数隐式静态广播都被限制在Manifest里注册了如果静态Receiver里的action是随机自定义的系统不会再分发给它。原因是系统统计发现隐式广播在后台唤醒App太频繁不仅耗电还容易被恶意应用“嗅探”所以做了收紧。实话说这套限制现在已经是Android开发的常识了但很多人只记住了“不能用”不知道为什么会这样设计。从第九章的视角看静态广播接收者会在广播分发时被AMS检查是否匹配隐式广播规则不满足的直接跳过。发送广播时sendBroadcast进入AMS的broadcastIntentLocked系统要在这里做权限检查并决定将广播放入前台广播队列还是后台广播队列。匹配到的接收者会被逐条取出来通过ApplicationThread的scheduleReceiver派发到对应App进程。App进程收到后主线程会执行ReceiverDispatcher中的回调逻辑最终调用Receiver对象的onReceive。有个核心结论值得记住onReceive运行在主线程耗时操作会导致主线程卡顿进一步可能触发广播接收者ANRBroadcastReceiver没有在规定时间内执行完。有序广播里每个接收者都有超时限制你在onReceive里跑网络请求纯属给自己挖坑。我见过不止一次有人把动态注册的Receiver里的onReceive当成异步线程用以为系统会等他执行完结果就是各种ANR和灵异问题。3.4 ContentProvider进程启动比Application还早的“隐形组件”ContentProvider的启动流程是四大组件里最特殊的因为它的初始化时机非常早而且很多App并不直接感知它的存在。先解释一个反直觉的细节通过ContentResolver.query()访问其他进程的ContentProvider时整个访问链路是这样的App进程调用acquireProvider通过Binder向AMS的getContentProvider发起请求AMS查找是否已有该authority对应的ContentProviderRecord和Provider实例如果目标进程还没启动AMS会先创建进程进程启动后AMS通过ApplicationThread的scheduleInstallContentProviders把该进程要负责安装的provider列表发下去最终执行installProvider反射创建ContentProvider对象并回调onCreate。在这个过程里ContentProvider.onCreate的执行时机早于Application.onCreate。很多人第一次知道这个顺序会很惊讶自己写了一个Application在onCreate里做了全量初始化然后某天发现一个ContentProvider在Application初始化之前就被调用了于是各种空指针满天飞。原因就在这里系统在进程启动时会先完成Provider的安装再执行Application的onCreate。如果你在设计SDK时用ContentProvider作为初始化入口比如Google的Firebase有很多SDK就是利用一个空ContentProvider在App启动早期完成初始化那么你就要明白这种“偷跑”的时机其实是你有意选择的。ContentProvider天然支持跨进程。从实现机制上看真正跨进程的是你通过query、insert、update、delete、call方法传入的参数和返回值这个过程中ContentProvider实例本身在宿主进程里只有一个但不同调用方进程会持有不同的Binder代理。所以如果你的ContentProvider实现里保存了大量可变状态你还要考虑多进程并发问题系统默认不会帮你处理同一个进程内、或跨进程调用时的同步。我这里想补一个和Service类似但很容易被忽略的细节一个App进程里每个ContentProvider实例只会被创建一次但当同一个Provider被多个不同进程使用而且App配置了android:multiprocesstrue时不同进程会各自创建一份Provider实例。这个标志非常冷门但是面试和跨进程数据同步里出现过不少次值得去源码里看看它到底控制了什么。4. 把原理用在真机上用日志和命令重新“看”组件启动4.1 打开ActivityManager日志观察Activity启动过程看源码归看源码纸上谈兵总是不够直观。我强烈建议你连一台手机或模拟器把系统日志打开亲手启动一个Activity再看日志里的时间线。这样你会比纯看书多一层“原来日志里对应的是这一步”的实感。现在Android系统里Activity启动相关的日志分散在ActivityTaskManager、ActivityManager、WindowManager等模块中。你可以先这么打开日志开关adb shell setprop log.tag.ActivityTaskManager VERBOSE adb shell setprop log.tag.ActivityManager VERBOSE adb logcat -c adb logcat -v time | grep -E ActivityTaskManager|ActivityManager|Start proc然后手动点击桌面图标启动一个测试App你会看到类似START u0 {cmpcom.example/.MainActivity}这样的日志接着可能看到Move activity task to front之类的内容再往后就是Displayed com.example/.MainActivity: 1s234ms。这里的Displayed时间基本就是用户感受到的启动耗时。除了看日志dumpsys是另一个宝藏命令。启动几个Activity之后执行adb shell dumpsys activity activities你会看到当前任务栈里每个ActivityRecord的详细信息包括TaskRecord的id以及每个Activity处于什么状态。再结合书中讲的launchMode、任务栈分配逻辑你就能把抽象的概念落地到真实系统状态上。刚开始接触时不要被一大坨输出吓到只挑mResumedActivity、Hist开头的任务栈信息看足够了。再用adb shell dumpsys activity providers查ContentProvider信息用adb shell dumpsys activity services查Service信息都是同样的办法先找到关键字段再对着源码理解。4.2 真机调试环境准备与常见踩坑如果你打算用真机来观察这些流程环境准备上经常有几个小坑。第一手机连不上adb。常见原因是开发者选项没有打开或者USB调试授权弹窗没点确认。小米手机尤其典型需要在“设置 - 我的设备 - 全部参数”里连续点击“MIUI版本”几次才能开启开发者选项然后进“开发者选项”里打开“USB调试”。连接后可以用adb devices验证看到device状态就是连接成功如果显示unauthorized说明手机上的授权弹窗没确认。第二Android Studio一直提示找不到设备。除驱动问题外建议把adb重启一下adb kill-server再adb start-server。如果你用的是Windows厂商驱动没装好也会导致设备列表为空。第三日志太多找不到重点。看组件启动流程时建议先adb logcat -c清空日志再干净操作一遍流程。也可以加过滤比如只关心ActivityTaskManager、AM_、ActivityManager开头的日志。这里我给一个小技巧Android日志里有几个特别关键的tag比如ActivityTaskManager负责Activity状态变化ActivityManager里会有Start procCHG表示组件生命周期切换。你可以在Android Studio的Logcat过滤栏里输入ActivityTaskManager|ActivityManager观察一次完整启动流程。这套“真机观测”的方法比瞎猜startActivity内部发生了什么高效得多。我当年第一次看到Start proc日志的时候才真正理解了“进程由AMS创建”这句话的份量。5. 常见问题速查与我的读书建议5.1 常见问题速查表下面这些是我在实际开发、带新人、回答社区提问时遇到频率非常高的问题。把问题、原因、排查方向列出来方便你以后对照使用。现象可能原因排查与解决方向Activity启动异常慢冷启动进程创建、Application初始化重、ContentProvider onCreate耗时看Displayed时间检查performLaunchActivity前的初始化逻辑优化Application和ContentProviderService在后台被系统杀死进程优先级较低、厂商激进清理使用前台服务提升进程优先级理解startForeground检查内存限制必要时拆分进程广播onReceive不执行静态注册隐式广播被限制、Android 13动态注册未设置RECEIVER_EXPORTED、Action不匹配检查目标版本限制改用动态注册并声明暴露属性发送显式广播或使用带包名的隐式广播广播onReceive里一跑网络就ANRonReceive执行在主线程且系统对广播执行有超时限制耗时操作放到goAsync()或后台线程或者直接改用Service组件ContentProvider被调用时Application还没初始化系统安装ContentProvider先于Application.onCreate不要在Application里依赖Provider的初始化结果SDK初始化入口要了解执行顺序bindService返回成功但调用接口空指针客户端没有等待onServiceConnected或Service返回Binder为null确认onBind返回非null客户端在回调和serviceConnected回调里取Binder明明动态注册了ReceiverApp退出后收不到系统广播活动Activity销毁后Receiver被反注册或进程被杀生命周期内注册/反注册要成对需要长期收广播用静态注册或前台服务方案5.2 怎么读这一章收益最高书读百遍其义自见但方法不对读三遍都可能还卡在代码细节里。我的建议是分三轮来读。第一轮只看时序图。第九章里有大量跨进程调用的时序图你把每个组件的工作过程当作一节“接力赛”来记住谁先发起、谁转手给谁、最后交到谁手里。这一轮不要盯着源码一行行看先抓住主干例如Activity那节就记“App - AMS - 进程创建 - ApplicationThread - ActivityThread - Activity”。主干记住了后面往里填细节才不迷路。第二轮打开源码跟随调用栈。建议在Android Studio里直接下载对应系统版本的SDK源码或者浏览器打开官方Android代码搜索站从ContextImpl.startActivity或者ActivityManagerService.startActivity这种明确的入口方法开始跟踪。这一轮主要看“哪里是进程边界的跨越”你会发现代码在频繁调用Binder.transact和onTransact跨进程的跃迁点非常醒目。如果能配合Logcat里的关键日志比如ActivityTaskManager日志效果会更好。第三轮自己画一遍图。不要觉得画图浪费时间这一步收益是最大的。画的时候不要照着书抄而是合上书凭记忆画“App进程和system_server进程之间有哪些Binder调用”画完再对照书查漏。你会发现漏掉的那几步往往就是你以后面试答不出来的那几个点。顺带提一句很多新手会在意Android Studio源码环境的搭建。如果你只是想读framework源码完全不需要去下载整个AOSP源码树。直接在Android Studio的SDK Manager里勾选“Sources for Android”下载对应系统版本的源码或者用在线代码搜索网站就够用了。不要在这个环节消耗过多精力重点还是要把主线链路啃干净。至于热词里那些“android studio每次新建项目都要下载gradle”之类的环境问题和读这本书的关系不大先把gradle下载问题用镜像或离线包解决掉就好别让它打断你的学习节奏。第九章读完之后你会有一个特别明显的感觉以后再看系统日志、再遇到启动优化和ANR问题脑子里关于“系统在做什么”是清晰的而不是靠经验瞎猜。这可能就是进阶路上“心里有谱”和“纯粹靠试”的最大区别。我个人花了好几晚重画这些时序图后来做后台任务调度方案设计时很多决策都直接受益于这一章的沉淀。建议你也找个周末连着真机、开着日志、翻着源码把四个组件的链路亲手走一遍。
返回列表