☰
Android Activity启动全流程解析:从AMS到窗口显示
2026/10/11 6:59:35 网站建设 项目流程

平时做 Android 开发,大多数人第一次接触到 AMS(ActivityManagerService)这个名词,都是在查 bug 的时候。要么是启动页面卡了,要么是后台拉起失败,要么是 singleTop 不生效,往日志里一翻,全是 AMS 的调用栈,但你真要说清这个启动流程究竟是怎么一步步走到 onCreate 的,很多人是说不完整的。

我之前排查一个冷启动黑屏的问题,明明 Activity 代码一行没写错,可是启动窗口切换时机总是不对。后来没办法,只能从Context.startActivity一路把 Framework 的调用链啃了一遍,才彻底搞清楚 AMS 在 activity 启动过程中承担的角色。这篇文章我不打算只贴源码,而是把这条链路拆开,每一段都弄明白为什么要这么设计,以及在实战中这些机制会如何影响你写的代码。

如果你只想知道"调用 startActivity 之后发生了什么",跟着我往下看就行。我会把从 App 进程到 system_server,再到新进程创建、窗口添加,直到onCreate回调的完整代码流程,按真实执行顺序捋一遍。

1. 先弄清楚:一次 startActivity 从 App 到系统进程要穿过多少层

很多开发者的误区是:认为startActivity是 App 内部的事情,跟系统进程关系不大。实际上 Activity 是跑在你的 App 进程里,但 Activity 的启动决策权不在 App 手里,而在系统进程 system_server 里。所以一次启动请求本质上是一个跨进程调用,中间至少要经过三层。

在应用进程这一侧,现有代码如下:

// 以 android.app.Activity 的入口为例 public void startActivity(Intent intent, Bundle options) { getApplication().getAutofillManager().notifyActivityStarted(); if (options != null) { mActivityTaskManager.startActivity( mMainThread.getApplicationThread(), getOpPackageName(), intent, null, null, getActivityToken(), options, isTopOfTask(), mCallingUid); } }

这段代码的关键在于mActivityTaskManager。在 Android 10 及之后的版本,它实际是ActivityTaskManager,内部持有IActivityTaskManager这个 Binder 接口;在更早的 Android 8/9 时代,这里直接对接的是ActivityManager.getService(),也就是 AMS 的 Binder 代理。

1.1 你调用的 startActivity 并不直接在 AMS 里执行

从Activity.startActivity开始,真正干活的是Instrumentation.execStartActivity。Instrumentation 这个东西很多人只在写测试时见过,其实它才是应用启动 Activity 的"前台执行者"。

execStartActivity内部做了一系列检查,比如校验调用者的进程是否合法、解析出来的 ActivityInfo 是否符合调用身份,并且——这里有一个被很多人忽略的 hook 点——它会遍历当前进程注册的所有ActivityMonitor。如果你在做 UI 自动化,通过ActivityMonitor拦截指定 Intent 并阻断 Activity 启动,靠的就是这个机制。

public ActivityResult execStartActivity(...) { IActivityTaskManager service = ActivityTaskManager.getService(); int result = service.startActivity(whoThread, who.getOpPackageName(), intent, intent.resolveTypeIfNeeded(who.getContentResolver()), token, target, outIssue, options); checkStartActivityResult(result, intent); return null; }

跨进程就发生在service.startActivity这一行。经过 Binder,调用来到了 system_server 进程里的ActivityTaskManagerService(原生 Android 10+ 是 ATMS,但为表述方便,业界仍习惯统称 AMS;厂商内核和一些资料里仍使用 AMS 命名,理解时就按 klass 对应的核心服务对象来)。此时运行线程是 system_server 的 Binder 线程,不是主线程。

1.2 Binder 之后:system_server 里的真入口

在 system_server 进程内,ActivityTaskManagerService.startActivity有这么几步逻辑:

  • 首先会判断调用者是否来自隔离进程,如果是,直接拒绝;
  • 然后根据调用的 binder 信息拿到调用方的 uid、pid,用于后续权限判定;
  • 接着调用mActivityTaskSupervisor里面的checkActivityStarting,检查是否触发屏幕熄灭、锁屏等场景下的启动限制;
  • 再经过mController的拦截回调。如果 system_server 层面注册了全局的IActivityController(比如 monkey 测试时、开发者选项里的强制停止流程),这里会给 controller 一次否决或重定向启动请求的机会;
  • 最终才把具体的工作交给ActivityStarter。

注意这里的线程模型:Binder 线程进入系统服务后,很多流程会通过mHhandler 再切到 system_server 的主线程执行。别小看这个切换,它直接决定了 AMS 处理启动请求时的"串行化"特性——所有 Activity 启动决策在 AMS 主线程里是按顺序处理的,所以你看到的startActivity相关日志基本都是同一个线程在打。

1.3 三层结构,各自管什么

层级代表类所在进程核心职责
应用侧Activity / InstrumentationApp 进程准备 Intent、确认发起者、分发结果码
服务侧ActivityTaskManagerService / AMSsystem_server权限校验、进程共存、决策启动
决策器ActivityStarter / ActivitySupervisor / Task 相关system_server解析 Intent、匹配 Task、决定启动模式

所以可以这么理解:Application 是提申请的,AMS 是审批的,ActivityStarter 是具体干事的人。很多疑难 bug,本质上都是这三个角色之间预期不一致造成的。

2. ActivityStarter:启动请求的"总闸",它筛掉了哪些非法启动

到了 system_server 这边,真正负责任务分配的类就是ActivityStarter(Google 在 Android 7.0 引入)。AMS 里的startActivityLocked早就拆出来了,把配置解析、模式判定、权限检查、栈操作全塞给了这个类。

execute()方法是整个流程的入口,执行链路可以简化为:

ActivityStarter.execute() -> 前置检查与参数初始化 -> computeLaunchingTaskFlags() // 计算最终的 flags -> setActivityTypeAndCheckFlag() -> startActivityInner() -> 校验权限与 ActivityInfo -> 查找或创建 Task -> 复用 ActivityRecord / 新创建 -> resumeTargetStackIfNeeded()

这套逻辑不是什么黑魔法,它其实回答了一个核心问题:这个 Intent 到底应该怎么启动?

2.1 组件解析与权限检查:为什么有的 App 启动不了别的 App 的页面

在设计启动流程时,系统必须回答三个安全性问题:

  1. 谁能发起启动?这由 uid、pid 以及 Intent 中携带的callerPackage决定;
  2. 目标 Activity 是否存在、是否对外可见?通过resolveActivity和AppOpsManager的权限判定;
  3. 目标 Activity 是否能被这个调用者启动?基于隐式 Intent 的包可见性规则、Intent Filter 的匹配,以及 ActivityInfo 的exported属性。

举例来说,Android 11 之后强制启用包可见性机制,如果你没有在 manifest 中声明<queries>,通过getPackageManager().queryIntentActivities是查不到很多第三方应用的 Activity 的,但 AMS 在 system_server 侧拥有全部包信息,它不受这种限制。这就是系统层与 App 层的差异。不是"代码写错了",而是你面对的系统数据边界不同。

同时,Activity 启动还受后台启动限制约束。在ActivityStarter的校验链里有一个判断:如果目标进程不在前台,且没有 SYSTEM_ALERT_WINDOW 等特权,系统会执行appop检查,直接拦截后台启动。这也是为什么你一在锁屏或后台收到的推送中试图直接startActivity,经常会看到 "Background activity start" 相关日志。

2.2 Intent 解析:隐式跳转是怎么变成具体 Activity 的

  • Intent 匹配:Action、Category、Data 三项必须命中,系统返回的是符合条件的 ResolveInfo 列表;
  • 无显式 Component 时,默认询问用户;
  • 结果是 ActivityInfo,它包含完整的目标类名、主题、进程名等信息。

对于显式 Intent 就简单很多,但不管哪种,最终都要生成一个完整的ActivityInfo,这个对象里包含了线程、进程、包路径、屏幕方向、软键盘模式、主题资源等几十项属性,后面创建 ActivityRecord 都要用。

在实际调试中,最好把 Intent 的 action、data 和 category 打全日志再排查隐式跳转。别只打印 class 名字,我曾经排查过一个"点击通知跳转打开的是旧页面"的问题,最后发现是 data 里的 URI scheme 信息被某个 Tinker 热修框架改了。

2.3 启动模式与 task 归属的初筛

computeLaunchingTaskFlags这一步做的是"启动模式 + Intent flags"的融合计算:

  • 如果 Intent 带了FLAG_ACTIVITY_NEW_TASK,意味着 Activity 需要一个新 Task 或者进入对应 Task;
  • 如果没带,但启动模式是singleTask或singleInstance,系统会补上FLAG_ACTIVITY_NEW_TASK;
  • 若启动模式是singleTop且栈顶实例匹配,则走onNewIntent而非重新创建;
  • FLAG_ACTIVITY_CLEAR_TOP与singleTask组合时,会把目标 Activity 之上的所有 Activity 清理掉。

这个判定其实非常精细,它要结合mStartActivity.getTaskDescription、目前的目标栈、以及 target root task 是否存在来决定是否"创建一个全新的 Task"。

我画过一张表,标注不同启动模式下 Activity 与 Task 的匹配逻辑,这是排查栈混乱问题最常用的参照:

启动模式该 Activity 在哪个 Task 中查找找到后处理未找到时的行为
standard当前获取焦点的 Task重新创建新实例,多次出现直接在目标 Task 创建
singleTop当前 Task栈顶就复用并回调 onNewIntent重新创建实例
singleTask全局查找已有实例清除其上所有实例并 onNewIntent创建新 Task 或进入指定 Task
singleInstance全局查找任何访问过该 Activity 的 Task复用该独立 Task,唯一实例创建仅有一个 Activity 的新 Task

注意,singleTop和standard的启动顺序是先从mRootWindowContainer拿到焦点 Task,再在其上叠加;而singleTask的检索范围是整个用户的 task 集合。所以不同模式之间的性能、时序差异非常明显,这点不弄清楚,后面实战踩坑基本无从谈起。

3. 从 ActivityRecord 到 Task:Activity 的"户口登记"如何完成

当ActivityStarter.startActivityInner执行到核心阶段时,系统要创建或复用一个ActivityRecord。你可以把 ActivityRecord 理解为一个正在被"管理层"跟踪的 Activity 身份档案。一个 Activity 实例不一定对应一个 ActivityRecord,但每个 ActivityRecord 都会和一个或一组 Task 关联。

3.1 ActivityRecord 的构造:远不止一个 Intent

ActivityRecord r = new ActivityRecord.Builder(mService) .setCaller(app) .setLaunchedFromPackage(callingPackage) .setIntent(intent) .setActivityInfo(aInfo) .setProcessName(procName) .build();

这段代码看似简单,但 Builder 背后在做的事情非常多:

  • setLaunchedFromPackage记录了是谁调起的,这直接决定返回栈从哪归属;
  • setActivityInfo把 manifest 里定义的screenOrientation、configChanges、launchMode等全部搬进 record;
  • setComponentSpecified标记 Intent 是否显式指定了组件;
  • setState先设为INITIALIZING,后续转为STARTING、RESUMED等。

这里我要专门提醒一点:ActivityRecord 里保存的intent是经过intent.migrateExtraStreamToClipData和sanitizeActivityIntent清洗过的。如果你在 Activity 的getIntent()里拿到了一个跟传进去不完全一样的 Intent,不用震惊,系统层已经帮你做过一轮兼容处理了。

3.2 从 Task 到 RootTask:到底插到哪个"栈"

创建好 ActivityRecord 之后,紧跟着就是寻找合适的Task。这个逻辑在startActivityInner里分为几类:

  • 如果目标 Task 已存在(通过 Task ID、Affinity、Intent 里的 taskId 等),复用它;
  • 如果不存在,就需要创建新的 Task,创建的权利和规则在 RootWindowContainer 里;
  • 对于带有 NEW_TASK 标志的情况,如果找不到复用 Task 就直接创建;
  • 对于非 NEW_TASK 的请求,通常直接把 ActivityRecord 放到当前 focus Task 的顶部(前提是当前有前台任务)。

在 Android 10 之前,这个查找顺序是mStack > mTask > mActivity;之后引入了RootWindowContainer,层级变成Display > RootTask > Task > ActivityRecord。但很多厂商源码还保留旧命名,读代码时注意不要混淆。

搞清楚这个层级关系,对理解"为什么我的页面跑到了别的任务栈里"非常重要。比如你启动了一个singleTask页面,但它的 taskAffinity 跟当前前台 App 完全不同,AMS 就会选择新建一个 RootTask 把它放进去,而不是叠加在你当前的栈上。

3.3 onNewIntent 的触发条件

不少人认为singleTop就等于onNewIntent,这其实是误解。要让onNewIntent生效,前提是"新 Intent 与现有 ActivityRecord 相匹配"。这个匹配逻辑由ActivityStarter.startActivityInner里的mLastStartActivityResult相关判断决定。

具体来说,有这么几种情况会走onNewIntent:

  • singleTop:目标 Activity 就在栈顶;
  • singleTask/singleInstance:目标 Activity 已经存在于指定 Task 中;
  • 手动添加了FLAG_ACTIVITY_SINGLE_TOP且栈顶命中;
  • 添加了FLAG_ACTIVITY_CLEAR_TOP且目标 Activity 在栈中非栈顶位置——系统会先销毁它上面的 Activity 和它自己,再以新 Intent 重新创建?这里有一颗暗雷。注意,纯CLEAR_TOP且没带 SINGLE_TOP 时,系统会销毁再重建,并不会触发 onNewIntent!它内部走到ActivityStarter.deliverNewIntents之前就会因为intent是否匹配而改变走向。

我在线上无数次看到开发者误以为加了CLEAR_TOP就会调用onNewIntent,结果在 MainActivity 里打了个Log,怎么都不打。这个坑下面专门会说到。

3.4 当 Task 需要新进程时不存在的 Activity

回到流程,如果 ActivityRecord 对应的 App 进程还没启动,AMS 不会立刻回调 onNewIntent 或 onCreate,它会先冻结一切,去走进程启动这条线。这一步我放到下一章讲,因为这里的时序是整个启动链路里最容易被误解的:不是先启动 App 进程再决定 Task 归属,而是先在 system_server 里把 ActivityRecord 和 Task 关系都定好,再去拉进程。反过来下面的理解会把前因后果搞错。

4. 冷启动时的进程争夺战:zygote fork 与 ActivityThread 的诞生

很多人以为冷启动流程是先跑 Application 的onCreate,再跑启动页 Activity。从最终回调时序看确实如此,但从 AMS 内部看,流程是:先决定"这个 Activity 应该出现在哪个 Task 上",紧接着发现"宿主进程还没起来",于是先去创建进程,等进程启动完成并回连 system_server 后,才真正进入 Activity 的launchActivity阶段。

4.1 进程不存在时的处理分支

在ActivityStarter.startActivityInner完成栈任务分配后,会走到RootWindowContainer.resumeFocusedTasksTopActivities,其中会调用ActivityStackSupervisor.startSpecificActivityLocked。这里的逻辑:

final WindowProcessController wpc = mService.getProcessController(procName, uid); if (wpc == null || wpc.hasThread() == false) { // 进程不存在或尚未就绪 final ProcessRecord temp = new ProcessRecord(...); mService.startProcessAsync(hostingRecord, procName, uid, processRecord, ...); } else { // 进程已存在,直接进入 realStartActivityLocked realStartActivityLocked(record, wpc, r); }

这里的hostingRecord类型是HostingRecord("activity", r.intent.getComponent()),它会作为后台统计进程启动原因的字段,这也是为什么后来dumpsys activity processes能看到启动宿主。

如果判定为进程不存在,AMS 会通过ProcessList.startProcessLocked进入 zygote 通道:

ProcessList.startProcessLocked() -> zygoteProcess.start() -> openZygoteSocketIfNeeded() -> socket 通信发送 fork 请求 -> Zygote 进程收到后执行 ZygoteProcess.zygoteServer -> forkSystemServer / forkAndSpecialize -> 子进程入口跑到 ActivityThread.main()

很多人以为 Binder 是 App 与 system_server 之间的唯一通道,实际上 zygote 用了另一个机制:本地 socket。system_server 会把目标进程的运行参数(uid、gid、seInfo、className 等)通过 socket 写到 zygote,zygote 完成 fork 后,子进程的入口直接是ActivityThread.main()。

4.2 ActivityThread.main 与 Application 创建

新进程诞生后的执行路径相当快,顺序如下:

  1. ActivityThread.main():创建主线程 Looper、初始化ActivityThread对象、创建ApplicationThread(这是 Application 进程与 AMS 通信的 Binder 通道);
  2. 调用thread.attach(false, startSeq),通过 Binder 回到 system_server;
  3. system_server 侧收到attachApplicationLocked,把之前等待的 ActivityRecord 拿出来,执行realStartActivityLocked;
  4. 然后ApplicationThread.scheduleLaunchActivity以 Handler 消息形式发出;
  5. App 主线程 handler 处理LAUNCH_ACTIVITY消息,进入handleLaunchActivity。

这里有一个关键细节:Application 的创建不在进程启动后立刻自动执行,而是在attachApplicationLocked返回时触发。更准确地说,LoadedApk.makeApplication(false)会创建 Application 实例并调用它的onCreate,然后 AMS 才会继续做 activity 部分的启动。所以你在 Application 里设置全局单例,Activity 里直接能用,是因为这两个时序天然有先后。

4.3 scheduleLaunchActivity 到 handleLaunchActivity

进入ActivityThread.handleLaunchActivity时,一切准备工作已经就绪:

  • performLaunchActivity通过mInstrumentation.newActivity反射创建 Activity 实例;
  • 同时mInstrumentation.callActivityOnCreate回调 onCreate;
  • onCreate 完成之后立即是 onStart;
  • 接着如果活动有焦点,会走 onResume。

这里也有一个被反复问到的点:为什么onCreate里拿不到可靠的宽高?因为窗口还没有完成第一帧绘制。在performLaunchActivity内部,activity.attach这一步会把PhoneWindow创建出来、初始化WindowManager,并传入mToken。这个 token 其实是系统进程里的IApplicationTokenBinder 代理,是 Activity 窗口与 WMS 沟通的凭证。窗口的真正可见需要等待handleResumeActivity之后WindowManagerGlobal.addView执行完毕。

4.4 冷启动时序速查表

步骤执行方关键回调/动作
1. 发起启动App 进程startActivity -> Instrumentation
2. 权限与模式判定system_serverActivityStarter 校验
3. Task 归属system_serverActivityRecord 入栈
4. fork 子进程zygoteActivityThread.main 开始执行
5. 回连 AMSApp 进程ApplicationThread.attach
6. 创建 ApplicationApp 进程Application.onCreate
7. 创建 ActivityApp 进程Activity.onCreate / onStart
8. 绘制/可见App 进程 + WMSonResume -> 首帧绘制

只要把这个表格背熟,大部分启动时序问题都能对上号。

5. 窗口与黑屏问题的真相:starting window 与首帧时间窗口

写 Android 的几乎都遇到过"启动黑屏"或"启动白屏",这其实不是 bug,而是系统性设计造成的。搞清楚 AMS 在这一阶段的窗口管理逻辑,你会明白产生黑屏的必然性,以及怎么从代码层面干预它。

5.1 启动窗口(Starting Window)是谁创建的

很多开发者不知道,"启动窗口"这个名字有歧义。它并不是由 Activity 自己创建的,而是系统为 Activity 预生成的SplashScreen或StartingWindow,用来在 Activity 真正首帧渲染完成前,给用户一个"应用正在启动"的视觉反馈。

WindowManager 在 ActivityRecord 提交了addStartingWindow请求后,使用PhoneWindowManager去加载应用的主题样式。Android 12 开始,Google 把这段逻辑收敛成了系统级的SplashScreen库,启动窗口的绘制由core-splashscreen控制,主题属性直接影响启动窗口的背景色。

于是,判断冷启动黑屏还是白屏的因素就是:

  • 启动窗口的背景主题是否拥有背景;
  • 主题是否设置了windowIsTranslucent;
  • 第一帧的绘制速度是否足够快,能在超时前覆盖启动窗口。

如果你去dumpsys window windows看,会发现冷启动期间存在两个窗口:一个是 splash screen 窗口,另一个是 MainActivity 自己的窗口。当 Activity 完成第一帧绘制后,WMS 收到relayout结果,把 splash 窗口移除,完成视觉交接。

5.2 黑屏/白屏常见成因

我在实际排查过程中整理出了三类高发原因:

原因一:主题没有设置合适的启动背景

如果你设置了一个透明主题(windowIsTranslucent = true),系统认为你的 Activity 不希望在启动时展示任何背景,那启动窗口就显示为透明/黑屏,等 Activity 自己慢慢画。对于需要"看起来秒开"的应用,这往往是反效果。

原因二:首帧耗时太长

AMS 不会无限等待首帧,它有个超时机制。如果你的onCreate里做了耗时操作,比如同步读数据库、拉大图,WMS 会长时间保留 splash 窗口,视觉上就是"卡在启动画面"。真机上的表现还会叠加厂商 Launch 优化(预加载、防抖机制),导致表现更加诡异。

原因三:relaunch 旋转导致的黑窗口

屏幕旋转、配置变更时,AMS 会触发relaunchActivityLocked,期间可能销毁旧窗口、创建新窗口,这个间隙中如果没有正确配置configChanges或没有设置快照,就会黑屏。

5.3 首帧与 onResume 的关系

我在这里刻意把 onResume 和"可见"分开。你平时看到的onResume回调并不意味着页面已经完成绘制,它只是表示 Activity 已经获得焦点。真正的画面呈现要靠ViewRootImpl.performTraversals完成后,WMS 才会认为该窗口已drawn。

所以黑屏优化有两个方向:

  • 提前首帧:把启动页的布局尽量简单,减少 onCreate 里的耗时逻辑,尤其是不要在首帧前做大图解码;
  • 用系统启动窗口做折衷:设置与页面主色调一致的windowSplashScreenBackground,让用户无感衔接,这也成了 Android 12+ 上最主流的启动优化方案。

6. 实战避坑:我在排查启动问题时反复踩过的几个点

讲完主链路,下面聊点实打实的经验。这些东西书上不一定写得清楚,但你在项目里绝对用得上。

6.1 singleTop 失效,实际原因在 Task 归属

有个经典场景:从通知栏点击推送,打开 MainActivity,你希望如果 MainActivity 已经位于当前前端任务栈顶就复用,于是给 MainActivity 设置了singleTop。结果测试反馈,每次点击通知都重新创建了一个 MainActivity。

原因其实就在 Task 归属。由于通知是从PendingIntent发送的,通常 Intent 被填了FLAG_ACTIVITY_NEW_TASK。这个 flag 和singleTop一起使用时,系统会先去查找是否存在对应 ActivityRecord 的 Task;如果当前通知对应的 Task 与你 MainActivity 所在的 Task 不一致(比如带上了taskAffinity的差异),那它找到的"栈顶"是另一个 Task 的栈顶,你 MainActivity 在别的 Task 里就算真的在栈顶也没用。

所以在做这类需求时,一定要确保:

  • Intent 的taskAffinity与 MainActivity 所在任务一致;
  • 要么初始化 PendingIntent 时设置setTaskClazzName相关参数;
  • 要么干脆用singleTask并配合FLAG_ACTIVITY_CLEAR_TOP,让系统强制归置到对应 Task。

6.2 Application context 启动时的 NEW_TASK 问题

从 ApplicationContext 调startActivity时,系统会强制补FLAG_ACTIVITY_NEW_TASK。这背后原因很简单:ApplicationContext 没有 ActivityToken,它不属于任何 Task,如果不建新 Task,系统不知道把 Activity 放哪里。

这个设计的坑在于:如果你用 ApplicationContext 启动了一个本来需要在当前任务栈顶叠加的页面,就会出现"新任务栈"现象。尤其在一些内部跳转逻辑里,Activity 会莫名变成独立任务,返回键行为极其怪异。排查这种问题,直接看dumpsys activity activities里的任务清单,你会发现多出的 RootTask 就是你 ApplicationContext 启动出来的。

6.3 后台启动 Activity 的限制比你想的更复杂

从 Android 10 开始,直接后台启动 Activity 会被系统按包策略拦截。很多开发者通过SYSTEM_ALERT_WINDOW权限绕开限制,但权限申请时机不对照样没用;而 Android 11+ 上系统对"来自通知的 PendingIntent"和"来自前台服务"的启动资格判定逻辑完全不同。排查后台拉起失败时,日志中会出现 "Background activity start", 后面紧跟callerUid、targetUid等字段。我看到过有人因为RemoteViews里的 PendingIntent 忘记设置setPackage,结果点击通知后整整一屏 Log 全是权限拒绝,实际上是服务侧包名不匹配导致的。

6.4 启动窗口相关主题配置

最后给一份我常用的启动窗口配置清单,避免冷启动白屏/黑屏:

<style name="AppTheme" parent="Theme.Material.DayNight.NoActionBar"> <item name="android:windowBackground">@color/app_background</item> <item name="android:windowSplashScreenBackground">@color/app_background_primary</item> <item name="android:windowSplashScreenAnimatedIcon">@mipmap/ic_launcher</item> </style>

如果你用的是 Android 12 以下,必须同时保证windowBackground存在的值不透明——否则也容易在启动阶段看到黑洞洞的窗口。很多"启动了但黑屏"的问题,其实就是这张表里的东西没配对。

6.5 使用日志快速定位启动问题

我平时最依赖的命令组合:

adb shell dumpsys activity activities # 看 Task 与 ActivityRecord adb shell dumpsys window windows # 看窗口层级和 splash 状态 adb shell logcat -b events | grep am_ # 看 Activity Manager 生命周期事件

系统产生的am_activity_launch_time、am_proc_start、am_kill事件能直接对应上文流程的各个阶段。遇到任何启动时序问题,先纵向看这三个事件的时间点,基本就能把问题锁定在 App 层还是系统层。

把这些环节吃透之后,Activity 启动对你来说就不再是黑盒了。做启动优化时,你能精确到"首帧之前系统已经帮我们展示了什么";排查 task 混乱时,你能基于启动模式表反推根因。这套代码流程虽然牵扯的类很多,但本质就是"申请-审批-归档-建进程-窗口绘制"五件事,遇到任何一个启动 bug,按这个链路一层层排查,基本都能落地。最后再分享一个小技巧:遇到诡异启动问题,先打开开发者选项里的"不保留活动"和"后台进程限制",把系统变量的干扰排除掉,再去分析 AMS 日志,会省掉大量无关干扰。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询