问题一:
App 内嵌入了一个 H5 单页应用。用户反馈:App 切到后台随便打开另一个 App 后,再切回我们的 App 时,H5 页面会重新加载,而不是停留在用户切后台前浏览的页面。
更诡异的是:从多任务键(Recent Apps)切回去没问题,只有点击桌面图标切回去才会重新加载。
定位两种切回方式的差异
| 切回方式 | 触发的生命周期 | Activity 是否重建 |
|---|---|---|
| 多任务键切换 | onRestart() → onStart() → onResume() |
❌ 不重建 |
| 点击桌面图标 | onCreate() → initView() → ... |
✅ 全量重建 |
点击桌面图标时 SplashActivity (启动页)被重新创建了。
根因分析
App 的启动流程
AndroidManifest.xml
┌─────────────────────────────────────────┐
│ SplashActivity │
│ intent-filter: MAIN + LAUNCHER │
│ launchMode: 默认(standard) │
│ │
│ onCreate() { │
│ loadData() // 业务逻辑 │
│ MainActivity // 跳转主页 │
│ finish() // 启动完就销毁自己 │
│ } │
└─────────────────────────────────────────┘
正常冷启动流程:
[启动] → [SplashActivity] → [MainActivity(含 WebView)] → SplashActivity finish()
任务栈变为: [MainActivity]
点击桌面图标的实际行为
Android Launcher 发现目标 App 已有任务栈在后台运行,但 SplashActivity 声明了 MAIN + LAUNCHER 且 launchMode="standard",系统会:
- 重新创建一个新的 SplashActivity 实例,压在已有栈的顶部
- 新 SplashActivity 走完整初始化 →
MainActivity.open()→ 又创建一个新的 MainActivity
结果:
java
[旧 MainActivity(WebView 完好)] → [新 SplashActivity] → [新 MainActivity(WebView 重新加载)]
↑
用户看到的是这个
旧 MainActivity 的 WebView 状态完好(按返回键甚至还能看到它),但用户被新 Activity 挡住了视线。
这就是「多任务切换不重载、点图标重载」的根本原因。
补充:为什么有的手机没这个问题?
测试中发现绝大多数手机点击图标都能正常回到原界面,只有一台手机必现。这是因为 Android 没有强制规定 Launcher 启动 App 的行为。
不同厂商/桌面的实现差异:
正常手机(大多数):
Launcher 检测到目标 App 后台已有任务栈 → 使用 moveTaskToFront 或带上 FLAG_ACTIVITY_BROUGHT_TO_FRONT 的 Intent → 直接把已有栈拉到前台,不创建 Activity。
问题手机:
Launcher 不做已有栈检测,直接用 ACTION_MAIN + CATEGORY_LAUNCHER 裸发 Intent → 系统按默认 standard 模式创建新的 SplashActivity → 触发重载。
常见的触发场景:
- 部分厂商魔改的系统桌面(某些 ROM 特定机型)
- 用户安装了第三方桌面(Nova Launcher、微软 Launcher 等)
- 开发者选项中开启了「不保留活动」(Don't keep activities)
- 手机开启了「应用分身」「双开」等特性
这也恰好印证了方案一(isTaskRoot)比方案二(singleTask)更合适------它是防御式检查,正常手机走不到这个分支、零影响;问题手机命中分支、一口兜住。
解决方案
方案一:isTaskRoot 判断(推荐)
在 SplashActivity.onCreate() 最前面加一行判断:
kotlin
// SplashActivity.kt
override fun onCreate(savedInstanceState: Bundle?) {
// 如果自己不是任务栈根节点,说明 App 已在运行
if (!isTaskRoot) {
finish()
return
}
// ... 原有初始化逻辑 ...
}
原理:
isTaskRoot判断当前 Activity 是不是所在任务栈的栈底(第一个 Activity)- 正常冷启动:SplashActivity 是栈底 →
isTaskRoot = true→ 走正常流程 - 后台切回点了图标:已存在栈
[TaskDetailActivity],新 SplashActivity 不是栈底 →isTaskRoot = false→ 直接 finish,露出旧栈
| 场景 | isTaskRoot | 行为 |
|---|---|---|
| 首次冷启动 | true | 正常初始化 |
| 进程被杀死后重启 | true | 正常初始化 |
| 后台存活 + 点图标 | false | 直接 finish,回到旧栈 |
优点: 3 行代码,意图明确,不改变 Manifest,不影响其他流程。
方案二:singleTask 启动模式
在 AndroidManifest.xml 中为 SplashActivity 添加:
xml
<activity
android:name=".ui.SplashActivity"
android:launchMode="singleTask"
... />
原理: singleTask 模式下,如果任务栈已存在,系统不会创建新实例,而是把已有任务拉到前台并调用 onNewIntent()。
缺点:
- 需要在
onNewIntent()中处理业务等逻辑 - 改动面更大,需要回归测试认证流程
- 行为变更隐式依赖系统,可读性不如方案一
方案三:同时增加 WebView 状态持久化(兜底)
以上方案解决 Activity 不被重建的问题。但进程仍可能被系统杀死。作为兜底,可增加 WebView 状态持久化:
kotlin
// MainActivity.kt
override fun onStop() {
super.onStop()
// 保存 WebView 状态到文件
val stateFile = File(filesDir, "webview_state")
val stateBundle = Bundle()
mBind.webView.saveState(stateBundle)
stateFile.writeBytes(stateBundle.toByteArray())
// 备用:保存当前 URL 到 SharedPreferences
prefs.edit().putString("last_url", mBind.webView.url).apply()
}
// onCreate() 中
if (savedInstanceState != null) {
// 恢复 WebView 状态
restoreWebViewState()
return // 不执行 loadUrl()
}
总结
| 层面 | 问题 | 措施 | 优先级 |
|---|---|---|---|
| Activity 栈管理 | 点图标启动新 SplashActivity | isTaskRoot 判断,非栈底直接 finish |
P0 |
singleTask 启动模式 |
需处理业务逻辑,改动面大 | 在newIntent中做好处理 | P1 |
| 进程死亡兜底 | 系统杀进程后 WebView 状态丢失 | saveState/restoreState 持久化 |
P2 |
三个措施,推荐第一个,可以解决 H5 页面的重新加载问题。