ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

深入理解Android Activity启动流程:从Binder到任务栈的完整闭环

2026/9/10 7:07:36 拓冰建站 浏览量
深入理解Android Activity启动流程:从Binder到任务栈的完整闭环 做Android开发这些年只要牵涉到页面跳转、冷启动优化、ANR定位最后几乎都会绕回到同一个问题上启动Activity时系统到底做了什么。很多同学背了一堆生命周期顺序onPause、onStop、onCreate背得滚瓜烂熟但一到线上问题就懵了——为什么有时候onNewIntent不回调为什么adjustResize不生效为什么后台拉起Activity会被系统拦掉这些说白了都是因为只记了结论没理解启动流程本身。这篇就基于我长期排查问题、读源码的经验把Activity从startActivity到界面真正展示的完整链路拆开讲清楚包括涉及的进程通信、核心数据结构、生命周期事务的驱动方式以及实际开发中可以直接用来排查问题的命令和思路。不管是刚接触源码的初级开发还是已经被线上问题折磨过几轮的进阶选手这篇都能提供一些可落地的参考。1. 整体设计与核心思路拆解1.1 一条启动链路牵扯的三方角色Activity启动绝不是当前页面调一下startActivity新页面就自己蹦出来这么简单。它本质上是一次跨进程协同最少涉及三方发起方所在的应用进程、负责全局调度与状态管理的系统服务进程主要是AMS/ATMS以及即将承载新页面的目标应用进程如果目标应用还没启动还要先完成进程创建。用生活中的例子来类比你想去商场里一个新的餐厅吃饭启动一个新Activity你不能自己直接推门进去得先通过商场服务台系统服务查询这家餐厅在几楼、有没有营业、需不需要排队。服务台确认之后通知餐厅接待员目标进程的ActivityThread准备座位然后你才被引导过去。整个过程中商场服务台负责认路和发号施令餐厅负责执行。放到系统里**ATMSActivityTaskManagerService**早期其实叫AMSActivityManagerService后来谷歌把任务管理这块单独拆了出来成了ActivityTaskManagerService但从Binder通信层面看应用进程最终还是通过AMS的代理接口把请求送进系统服务。搞清楚了这三方后面看代码就不会迷路。1.2 为什么要用Binder而不是直接方法调用Android应用进程和系统服务不在同一个进程这就引出第二个关键点它们之间靠Binder驱动。Binder是Android整个IPC机制的底座startActivity的调用链上至少有两次经典的Binder事务第一次应用进程通过ActivityManager.getService().startActivity()把请求发给system_server进程里的ATMS第二次如果目标Activity所在进程没有创建系统会通过Process.start()向zygote发起创建进程的请求新进程起来之后通过ActivityThread.main()进入应用消息循环。之后系统服务要把启动Activity的指令下发到应用进程走的仍然是Binder只不过这里用的是IApplicationThread这个接口由ActivityThread内部实现。理解Binder在这一流程中的角色你才能真正明白为什么启动慢会是性能问题的大头一次跨进程通信至少一到两次sched切换启动一个Activity如果链路上一口气走了十几次Binder调用耗时自然叠加。发起方应用进程 │ │ Binder: IActivityTaskManager.startActivity ▼ system_serverATMS/AMS │ │ Binder: IApplicationThread.scheduleLaunchActivity ▼ 目标应用进程ActivityThread │ │ 本地消息队列 ▼ Activity.onCreate / onStart / onResume1.3 一个容易被忽略的线程模型问题另一个非常核心的设计是应用进程里Activity所有生命周期回调都发生在主线程UI线程的消息循环里。系统服务通过Binder把启动Activity的请求发送给应用进程时ActivityThread内部收到的其实是一个ClientTransaction对象里面包含了需要执行的一系列ClientTransactionItem比如LaunchActivityItem、ResumeActivityItem。这些item不会立刻执行而是被封装成Message抛到主线程的Handler队列中等主线程空闲时再逐个处理。这一点解释了无数面试题和线上问题的根源为什么主线程卡顿会导致启动慢不是系统服务执行得慢而是消息队列里积压了太多消息launch Activity的消息排不到前面去。反过来你也就知道了为什么在主线程做耗时操作会被系统判死刑因为紧随其后的生命周期事务根本没法执行。2. 核心数据结构与重要概念2.1 ActivityRecord、TaskRecord、ActivityStack读启动流程源码时几个数据结构绕不开理解了它们系统的思路就一目了然ActivityRecord一个Activity运行时的记录包含Intent、包名、进程信息、生命周期状态等。可以说系统里一个Activity实例对应的管理实体就是ActivityRecord。TaskRecord一组ActivityRecord的集合对应我们开发时感知的任务栈/回退栈。TaskRecord里维护了一个ActivityRecord的列表栈顶即当前正在显示的Activity。ActivityStack用来管理一组TaskRecord的容器主要处理诸如哪个Task在最前面如何做启动窗口的切换这类展示层事务。WindowContainer更顶层的东西Android 10以后把窗口和Activity的管理统一到了WindowContainer的树状层级里这里不展开但你要知道TaskRecord和ActivityRecord本身都是WindowContainer的子类天然具备层级关系。从taskAffinity到launchMode最终影响的其实就是ActivityRecord被放进哪个TaskRecord、以什么姿态放进去。你在Manifest里配的singleTask、singleInstance启动时传的FLAG_ACTIVITY_NEW_TASK全是在ATMS做任务查找和栈操作时起作用的。2.2 生命周期状态机与ClientTransaction系统侧给Activity定义了多个状态比如INITIALIZING、STARTED、RESUMED、PAUSED、STOPPED等。源码里ActivityState这个枚举定义得很直观但普通人看启动流程只需要抓住一条主线系统侧不断通过状态机推进Activity的状态再通过ClientTransaction把状态变化翻译成应用进程里对应的生命周期回调。ClientTransaction的设计值得一提。它里面包含了一个ActivityLifecycleItem表示最终要到达的生命周期状态和一个ListClientTransactionItem中间要执行的一系列动作比如LaunchActivityItem、ResumeActivityItem。系统把这些封装好之后一次性发给应用进程应用进程再用TransactionExecutor按顺序执行。这样一来一次Binder调用就能完成整个启动链路上多个生命周期回调的调度减少了进程通信次数。2.3 进程、任务与栈的层级关系画个笼统的层级图就是一个ActivityManagerService管理多个ActivityStack一个ActivityStack管理多个TaskRecord一个TaskRecord管理多个ActivityRecord每个ActivityRecord对应应用侧唯一的一个Activity实例。这个层级关系看起来复杂但本质上是容器套容器掌握了这个模型再看到taskAffinity导致Task被切换FLAG_ACTIVITY_CLEAR_TOP把栈顶之上全干掉之类的行为你就会知道都是哪一个层级上的操作。3. 启动流程的关键路径逐段拆解3.1 从startActivity到ATMS拿最常见的情况举例Activity A在主线程里执行了startActivity(intent)。这里有一个隐性知识点如果你是在普通Context比如ApplicationContext里启动Activity必须给Intent加FLAG_ACTIVITY_NEW_TASK否则会直接抛异常。原因很简单——Activity必须挂在某个Task上而ApplicationContext没有Activity所在的Task可依附系统只能要求你开一个新任务。接下来看调用链Activity.startActivity()最终会走到Activity.startActivityForResult()。Instrumentation.execStartActivity()登场它负责拿着Intent去问系统这个Activity能起吗。Instrumentation这个名字很多人只在插桩测试里见过其实它是应用进程与系统服务之间一个很关键的执行代理。ActivityTaskManager.getService().startActivity()开始跨进程到这里控制权交给了system_server。细节上execStartActivity里还会校验调用者的权限比如你带了一个不存在的Component系统会在这里抛出ActivityNotFoundException。再比如Android 10以后对后台启动Activity的限制有一部分校验逻辑也会沿着这条路径提前拦截。3.2 ATMS如何找到正确的ActivityRecord和TaskRecord进入系统服务之后核心处理在ActivityTaskManagerService.startActivity()它内部会穿到ActivityStarter.execute()。ActivityStarter是启动流程里一个非常关键的类负责解析Intent、检查Activity信息、确定启动方式、查找到正确的Task并最终把要启动的Activity塞进任务体系里。这里有几个关键分支Intent解析如果是隐式Intent系统要通过IntentFilter匹配到确切的Activity组件匹配不到就抛异常。显式Intent则直接解析ComponentName。LaunchMode判定解析Activity在Manifest里声明的launchMode再叠加Intent里的Flags综合决定这次启动是创建新实例、复用到栈顶还是清空上方Activity。Task查找根据Activity的taskAffinity、launchMode等属性去找有没有合适的TaskRecord可以复用。没有就新建。这些逻辑最终收敛到一个方法ActivityStarter.startActivityInner()。它向ActivityStackSupervisor上报要开始对ActivityRecord执行真正的启动动作。3.3 生命周期状态编排与ClientLifecycleManager在Android 10之前系统对Activity生命周期状态的调度散落在各种方法里代码维护很痛苦。后来谷歌把从当前状态推进到目标状态这件事统一交给了ClientLifecycleManager由它构造一个ClientTransaction再调用IApplicationThread.scheduleTransaction()把事务发给应用进程。具体到一次冷启动目标进程可能完全不存在那就必须先走进程创建流程ATMS发现目标进程没起来要告诉AMS或者直接用进程管理模块通过Zygote fork新进程。Zygote是Android系统的进程孵化器本身是init进程启动的Native进程所有应用进程都由它fork而来。fork之后新进程入口是ActivityThread.main()在这里会创建主线程的Looper和Handler然后把自己attach到系统服务上。attach的作用是告诉AMS我的进程已经准备好了可以开始往我这边派发任务。attach完成后系统拿着应用进程的ApplicationThread代理继续走ClientLifecycleManager的scheduleTransaction把之前构建好的ClientTransaction投递给新进程。新进程的ActivityThread拿到事务后会把它交给主线程Handler最后在TransactionExecutor.execute()里真正开始执行。3.4 应用进程侧TransactionExecutor与生命周期回调执行TransactionExecutor做的事情比想象中要谨慎。它拿到ClientTransaction之后并不是一股脑按顺序执行list里的item而是先根据目标生命周期状态确定执行顺序。举个例子如果目标是ON_RESUME中间需要经历ON_CREATE、ON_START、ON_RESUME它会把所有要执行到目标状态的必要的生命周期回调排列好再逐步调用。这里最值得注意的细节是onPause和onStop的触发并不一定发生在新Activity启动的那次事务里。比如A启动BA的onPause会在B启动前期就被调度这样B才能在A之上稳定地获取焦点但A的onStop往往会晚一些可能等B完全启动、绘制完成之后系统再回调A的onStop。理解了这个时序差你再去排查A切到B之后A的某些资源还没释放之类的问题就不会手足无措。LaunchActivityItem执行时ActivityThread会通过反射创建目标Activity实例然后调用attach()建立Activity与Window、WindowManager等系统的联系再依次执行onCreate、onStart、onPostCreate等回调。这里还有一套与Window相关的逻辑Activity创建的同时会创建PhoneWindow并设置WindowManager。setContentView实际上就是把布局扔给PhoneWindow处理最后在onResume之后WindowManager才真正把DecorView添加到窗口上界面才可见。3.5 涉及的关键类速查类/接口作用Activity / ActivityThread应用侧入口承载生命周期回调的实体Instrumentation应用进程内执行Activity启动的代理ActivityTaskManager / ActivityManager负责与AMS/ATMS通信的Binder客户端ActivityStarter系统侧解析并编排启动逻辑的核心类TaskRecord / ActivityRecord / ActivityStack系统侧管理任务栈与Activity状态的实体ClientLifecycleManager / ClientTransaction生命周期事务的封装与下发TransactionExecutor应用侧执行生命周期回调的调度器ApplicationThread / IApplicationThread应用进程与系统服务的Binder回调接口4. 启动模式与Flags对流程的影响4.1 launchMode对Task的干预如果启动流程只是新建Activity压栈展示那系统设计就太简单了。实际上随着APP页面越来越复杂开发者需要精细控制Task的复用和清理于是就有了launchMode和一堆操作Task的Flags。四种launchMode的行为差异核心在于如何复用ActivityRecord、要不要清空Task:standard每次创建一个新的ActivityRecord压入当前Task。默认模式最符合直觉。singleTop如果当前Task栈顶已经存在同一个Activity实例不会新建而是回调它的onNewIntent否则新建。singleTask系统会在目标Activity的taskAffinity对应的Task里查找是否存在该Activity实例。存在则把Task调到前台并清空该Activity之上的所有其他Activity然后回调onNewIntent不存在则新建Task或复用一个空的Task。singleInstance比singleTask更激进该Activity所在的Task里只能有它一个Activity。通常用于来电界面、闹钟这类需要独占任务的场景。用错launchMode是线上问题的高发区。最常见的是给分享页配了singleTask用户从A页面分享到微信再回到APP时发现A页面之上的所有页面都被清了。你会觉得我没写过清理代码啊但实际上就是singleTask在维护任务栈一致性时把上方的记录全移除了。4.2 Intent Flags的叠加规则更易踩坑Flags是另一个维度的控制手段而且和launchMode是叠加生效的。你需要重点掌握的几个FLAG_ACTIVITY_NEW_TASK在新的Task中启动Activity常与taskAffinity配合使用。FLAG_ACTIVITY_CLEAR_TOP如果目标Activity已经存在于Task中则清空它之上的所有Activity并把它带到前台。FLAG_ACTIVITY_SINGLE_TOP相当于launchMode的singleTop。FLAG_ACTIVITY_CLEAR_TASK启动前把Task清空。FLAG_ACTIVITY_REORDER_TO_FRONT把已有的Activity调整到栈顶但不清空栈里的其他实例。实际开发里我建议在起页面之前明确想清楚这次跳转我要的是复用还是新建 如果只是简单的页面推进尽量用默认行为。一上来就全套Flag叠加后面排查问题时完全是噩梦。4.3 taskAffinity与进程和栈的隐式关联taskAffinity比很多人以为得更隐蔽。它不只是singleTask的辅助属性还会影响FLAG_ACTIVITY_NEW_TASK启动时选择哪个Task以及allowTaskReparenting时Activity会不会搬家。很多大厂APP里出现从通知栏点进来返回之后回到了某个完全不搭界的中间页大概率就是taskAffinity设置导致的Task切换。举一个真实案例某APP从MainActivity跳到了WebViewActivityWebViewActivity配了singleTask并且没有特别设置taskAffinity。因为Manifest里Application没有设taskAffinity默认就是包名。这时在MainActivity的任务栈里WebViewActivity可以完美压栈不会有问题。但如果某个第三方SDK启动了同一个WebViewActivity而且它在一个taskAffinity完全不同的Task里那么singleTask的WebViewActivity会基于自己所属的包名去找Task结果可能跳到另一个Task去。这个问题排查起来特别绕因为Activity明明在当前栈里已经有了。5. 冷启动与生命周期联动的实战问题5.1 冷启动从点击图标到首帧显示Activity启动流程和APP冷启动流程并不是完全一回事但两者有大量重叠。冷启动指的是进程不存在从Launcher点击图标或者从系统接收一个Intent开始到MainActivity第一帧真正显示出来。整个冷启动可以分成几个阶段进程创建system_server通过Zygote fork出新进程。这里有一个常见指标Process.preLoad耗时主要是ClassLoader、资源、系统服务代理的预加载。Application初始化ActivityThread进入main之后首先创建Application执行attachBaseContext和onCreate。Activity创建与首帧执行MainActivity的onCreate、onStart、onResume。onResume返回之后系统发出第一帧绘制请求。注意这里说的首帧不是Activity完全可见而是DecorView完成了测量、布局、绘制并提交给SurfaceFlinger。显示SurfaceFlinger合成并上屏。性能优化的很多手段都围绕这几个阶段展开减少Application.onCreate里的任务、用启动器预加载、延迟初始化、懒加载、ContentProvider优化等。之所以要强调Activity启动流程是因为你在做启动优化时如果不清楚onCreate、onStart、onResume在启动链路中的位置很可能优化目标打偏。比如你花了大力气把onCreate的耗时减掉了但如果首帧绘制卡在View的measure/layout或者卡在Application初始化那优化效果依然不明显。5.2 后台启动限制对流程的干预Android 10开始系统对后台启动Activity做了严格限制。所谓后台是指应用进程当前没有可见Activity或者处于某些不被系统认为前台的状态。限制的直接效果是你在后台执行startActivity可能被系统静默拦截连异常都不抛。了解这个限制必须结合启动流程来看解读Intent、查找Task这些步骤照常但在ActivityStarter真正向应用进程下发生命周期事务之前系统会先判断发起方的进程状态和有没有可见窗口。如果不符合要求启动请求会被标记为不允许记录到log里不会执行。实际开发里最常见的冲突场景是推送SDK在收到离线消息后尝试直接拉起某个Activity或者长连接回调在进程没有可见界面时跳首页。这些行为在旧版本Android上一直能跑到了Android 10就神秘失效。正确做法是改用通知栏或者使用系统允许的例外场景比如应用有SYSTEM_ALERT_WINDOW权限、有可见的activity、最近任务里存在该任务等。但注意这些豁免条件在不同版本上细节有差异需要用dumpsys activity实际查看进程的oom_adj和对应的ActivityRecord状态来判断能否拉起。5.3 为什么onNewIntent经常不到达这个问题出现频率非常高。很多人误以为只要设置了singleTask或singleTop再次启动时onNewIntent就必然会回调。实际上onNewIntent是否回调取决于系统在复用已有ActivityRecord时走的具体分支对于singleTop只有新的启动请求目标是栈顶Activity时才走onNewIntent如果目标Activity在栈顶之下singleTop根本不会复用而是新建一个实例。对于singleTask复用时会把位于目标Activity之上的页面全部清掉然后回调onNewIntent。但如果目标Activity根本不在同一个Task里系统可能新建Task或复用别的Task行为又不一样。此外从Android 10之后某些从通知栏或外部唤起的场景是由系统直接把Intent附加到Task上绕过了常规的startActivity路径这时候onNewIntent也可能不回调你需要检查Activity.getIntent()的pendingIntent行为。这个点极其隐蔽我见过线上反馈从通知点进来页面数据不刷新排查到最后才发现是onNewIntent没走系统直接复用了已有Activity的实例还在旧Intent上继续展示。6. 生命周期回调与启动流程的对应关系6.1 一个完整的冷启动回调时序下面是一个标准冷启动、进程不存在、从Zygote fork到MainActivity显示的回调顺序以Activity A为起点启动Activity B为例A被创建ApplicationonCreate- A的onCreate-onStart-onResume此时A完全可见。A调用startActivity(B)。系统先让A进入Paused状态AonPause被调用。如果B所在进程不存在创建进程初始化Application。B实例创建BonCreate-onStart-onResumeB完全可见。系统再让A进入Stopped状态AonStop被调用。如果A被完全覆盖且用户没再看见它之后可能还会走onDestroy但这取决于A是否被销毁比如内存不足或者被finish或者Task被清理。这个顺序经常被误解很多人以为A的onStop一定在B的onResume之前实际上从Android 7.0开始系统的调度顺序已经变成先确保新Activity能赶紧展示再收拾旧的。所以A的onStop往往迟于B的onResume。6.2 onSaveInstanceState与启动流程的关系系统在Activity被销毁但状态需要保留时会调用onSaveInstanceState。启动流程中它出现的时机也很有讲究比如A启动B如果系统预测A可能因为内存不足被回收它会提前调用A的onSaveInstanceState保存UI状态。因此你在onCreate的savedInstanceState参数里能恢复数据如果A没有被回收savedInstanceState大概率是null。结合启动流程你还需要注意配置变更旋转屏幕会让系统销毁并重建Activity但这个过程走的是ActivityThread.handleRelaunchActivity它和普通的startActivity链路不一样。它不会创建新的Task、不需要走ActivityStarter而是在同一个ActivityRecord上重新执行onDestroy-onCreate这样一轮生命周期。很多人在配置变更时发现onCreate里的intent还是老的其实是因为系统重建时会把之前的Intent重新传进去你需要在onCreate里及时读取最新的state。6.3 启动模式对时序的实际影响launchMode不仅影响谁在栈顶也直接影响生命周期的回调顺序。举一个例子Astandard启动BsingleTask。B在另一个Task里已经存在。这时系统会先把B所在的Task移至前台回调B的onNewIntent。同时A所在的Task退到后台A会依次走onPause、onStop。你的A如果依赖onStop来释放摄像头、麦克风等资源就要考虑清楚从A跳B时A到底会不会立刻收到onStop这取决于B能否快速显示和B的启动方式。如果是单进程、页面较复杂onStop延迟几百毫秒甚至更久都有可能。7. 实际排查工具与技巧7.1 dumpsys activity让你看清系统视角掌握了启动流程最高效的验证方式就是直接用系统工具看运行时状态。adb shell dumpsys activity有几个子命令非常实用dumpsys activity activities打印所有Task栈和ActivityRecord的当前状态。dumpsys activity processes查看所有运行进程的oom_adj、pid、进程里包含的Activity。dumpsys activity top查看当前焦点Activity及其所在的Task。dumpsys activity service查看Service绑定和调度情况。排查页面为什么起不来这类问题时我会走一遍这个流程从日志里找到ATMS打印的START日志关键词类似START u0 {intent} from uid ...。如果这里直接报错看是ActivityNotFoundException、SecurityException还是后台限制。到dumpsys activity activities里确认ActivityRecord是否创建如果创建了但状态卡在STOPPED或INITIALIZING说明应用进程侧没收到事务或者事务执行中途卡住。到dumpsys activity processes里看目标进程是否存在、pid有没有变化、线程堆栈是否有主线程阻塞。这套方法比纯看日志要靠谱得多因为它直接反映了系统侧的状态。7.2 adb shell am start 手动模拟启动想快速验证某个Activity在某个launchMode下的行为不需要写代码用adb shell am start就能模拟。比如adb shell am start -n com.example.app/.MainActivity adb shell am start -n com.example.app/.MainActivity --activity-single-top adb shell am start -a android.intent.action.VIEW -d https://example.com结合dumpsys activity activities观察Task和ActivityRecord的变化你就知道FLAG、launchMode到底怎么影响栈结构。遇到微信分享回跳后页面状态不对一类的问题时这一招尤其有效可以快速排除是不是Intent传递的问题。7.3 systrace/Perfetto看启动耗时分布当问题从能不能启动变成为什么这么慢就要上性能分析工具。现在主流推荐用Perfettosystrace的替代者抓取方式# 在较新的Android设备上 adb shell perfetto -o /data/misc/perfetto-traces/trace.perfetto-trace -t 10s sched freq idle am wm gfx view抓完之后在Perfetto UI里重点看几个片段AppProcessCreate进程创建耗时。bindApplicationApplication初始化耗时。ActivityTaskManager里的LaunchActivity、Displayed相关标记。Choreographer的帧耗时。主线程出现长时间HandleMessage占用基本可以定位到代码里的耗时块如果卡在Binder调用上则要看系统服务侧的繁忙程度。7.4 通过日志快速定位启动失败的几类原因症状常见原因排查方向点击无反应后台启动限制检查应用是否有可见窗口查看ATMS START日志里是否带有blocked标志崩溃且没有异常目标Activity设置了exportedfalse检查Manifest导出配置页面跳转后瞬间闪退Application初始化异常看logcat中的FATAL EXCEPTION重点看ContentProvider和Application.onCreateonNewIntent未回调Task里Activity不在栈顶dumpsys查看ActivityRecord和Task的栈顶情况返回键回到老页面而不是首页多个Task被复用检查taskAffinity和FLAG_ACTIVITY_NEW_TASK的配合情况8. 高级实践自定义方案与优化经验8.1 基于启动流程做冷启动优化启动优化本质上是压缩从进程创建到首帧显示的链路时间。按我的经验优先级是这样Application初始化阶段最需要收敛。所有ContentProvider里的初始化都发生在Application.onCreate之前如果你的SDK在ContentProvider里做了大量懒加载设计一定要查一遍启动阶段实际执行了哪些。能改成懒加载就改改不掉的想办法放到子线程但要注意子线程加载和主线程使用之间的同步。首帧Activity的onCreate只做必要的视图初始化和数据展示。常见误区是把网络请求、数据库操作都放在Activity.onCreate里导致首帧被拖慢。正确做法是主线程先渲染骨架数据到了再填充。**启动窗口StartingWindow**优化。Android 12上系统提供了启动画面SplashScreenAPI可以控制启动窗口的显示内容和持续时间。如果你不做任何配置系统会用默认主题和图标生成一个启动界面首帧上来后退出。这个窗口和Activity的启动流程是强相关的充分利用它可以在视觉上大幅优化点击到出内容的等待感。预加载Zygote相关的类一般App用不上但对大厂来说尽量减少冷启动时fork进程后的类加载压力能稳定压缩几十毫秒。8.2 跳页提速的三个行之有效的方法除了冷启动日常页面跳转提速也有很多手段。我这里列三个比较有效的减少主线程事务排队跳转前不要在主线程做耗时操作比如复杂计算、大对象创建、SharedPreferences写盘等。这些操作会导致scheduleLaunchActivity事务被执行的时间延后。用startActivityForResult替代onActivityResult的全局分发如果你在一个复杂页面跳转后需要回传数据尽量用明确的requestCode resultCode路径避免在onResume里反复读取全局变量导致逻辑混乱和额外性能损耗。适当使用单例/对象池复用页面数据对于列表页跳详情页这种模式详情页每次重新创建onCreate里如果都要查一次数据库或请求网络体验会差很多。可以把分页数据和列表位置状态暂存在内存缓存里页面重建时直接从缓存恢复减少首帧等待。8.3 后台Activity被回收后的恢复策略系统在内存吃紧时可能杀掉后台Activity所在进程。用户回到这个界面时系统会尝试按Task里的ActivityRecord做恢复如果进程已死系统会重建进程再用之前保存的savedInstanceState恢复Activity。这个恢复流程本质上是启动流程里启动已存在但进程已死的ActivityRecord的特殊分支。恢复时TargetActivity的onCreate会收到非空的savedInstanceState。要想不丢状态你需要在onSaveInstanceState里保存关键UI状态列表位置、输入内容、滚动位置。在onCreate里判断savedInstanceState能恢复就及时恢复。避免在重建后的Activity里再依赖原来进程里的静态变量。静态变量在进程被杀后全部消失这是很多后台切回后白屏问题的直接原因。这一步一定要配合启动流程理解ActivityRecord还在Task里系统只需要重启进程重新创建Activity即可。它不是一次普通启动不会走ActivityStarter里新建Task的逻辑而是走ActivityStackSupervisor.realStartActivityLocked之类的恢复路径。所以你onCreate里的Intent、启动模式都和首次启动不一样。9. 常见问题与排查思路实录9.1 点击图标无响应但是其他App正常这种情况要优先怀疑system_server本身是否卡住。因为所有App的启动都要通过system_server的ATMS调度。如果它主线程卡死比如系统服务里有人做了慢查询你会看到大量App的启动请求都排在后面。排查方式就是看系统日志里有没有ANR in system_server或者用adb shell top看CPU占用。如果只有本App无响应再看dumpsys activity processes里本App进程是否存在。如果进程活着但Activity没起来多半是主线程卡死。抓一次ANR trace看主线程堆栈卡在哪个方法上即可定位。9.2 startActivity抛异常Calling startActivity from outside of an Activity context这条异常上文已经提过是因为使用了ApplicationContext或ServiceContext启动Activity但没有设置FLAG_ACTIVITY_NEW_TASK。系统为了保护Task结构在ContextImpl.startActivity里做了校验如果context不是Activity类型就必须有NEW_TASK标志。解决方式就是补充标志Intent intent new Intent(context, TargetActivity.class); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(intent);但注意加了NEW_TASK之后新Activity会进入一个新Task。如果你的设计是希望它和主流程Task在一起就要考虑taskAffinity否则返回时会直接离开整个任务栈体验很怪。9.3 onActivityResult回调延迟甚至不回调onActivityResult被废弃之后AndroidX Activity 1.3.0加入了ActivityResult APIs但仍有大量老代码在用。它与启动流程的关联在于系统在startActivityForResult时记录了发起方ActivityRecord并在目标Activity finish时回传结果。如果启动活动走的是singleTask复用路径并且目标Activity和发起方不在同一个Task那么返回时结果可能不会准确回传。更隐蔽的问题在于启动后发起方Activity因为内存被杀恢复后的新实例其实是一个新的ActivityRecord但系统仍持有旧的ActivityRecord引用结果回传时会找不到原来的回调对象。这就是为什么旧版Fragment里onActivityResult经常失灵。迁移到ActivityResultLauncher之后因为底层用了新的回调注册机制这类问题少了很多。9.4 启动窗口Splash一直不退Android 12上如果采用默认启动窗口首帧绘制完成后系统会自动移除启动窗口。但如果你做了自定义主题把启动窗口的windowBackground设置成了一个复杂drawable或者调用了SplashScreen相关API但返回时机不对启动窗口可能长时间不消失。排查上建议先确认首帧是否及时。如果首帧很慢启动窗口当然一直展示如果首帧已出但启动窗口还在多半是主题或API配置问题。你用Perfetto抓trace时可以看到StartingWindow从创建到移除的时间节点对照Activity首帧的时间点很快能分清是哪一边的问题。9.5 多进程应用与启动流程的冲突如果你的App开了多进程要尤其注意只有当目标Activity所在进程没有创建时系统才会fork新进程。如果你在A进程里启动了B进程的ActivityB进程要额外创建而B进程里的Application也会再次执行onCreate。这可能触发一些耗时逻辑拖慢Activity启动。更麻烦的是多进程之间不能共享内存状态所以静态变量、单例全都得小心处理。判断应用开了几个进程最简单的方式是adb shell ps | grep 包名看到多个PID即说明多进程。最后的实践心得Activity启动流程这块读源码的时候我最大的感受是不要一开始就陷进ActivityTaskManagerService那一大坨代码里先把应用进程、系统服务、Binder、消息队列这几个核心概念在脑子里立住再顺着一次startActivity的数据流往下追会轻松很多。真到了线上排查也别慌先看系统侧状态再判断是系统调度问题还是应用主线程问题一步步缩小范围。你现在遇到的绝大多数诡异跳页、生命周期混乱、启动崩溃基本都能在这条链路里找到答案。最后再分享一个我自己常用的习惯每次提交和Activity跳转相关的代码都顺手跑一遍adb shell dumpsys activity activities哪怕功能正常也要确认Task里各ActivityRecord的状态是否符合预期。很多问题在早期就是从那多出来的一条记录开始的。