一、背景原因
1.1 问题背景
在 Android 应用的线上运行过程中,我们遇到了两个主要问题:
Background 崩溃率偏高:当应用处于后台时,系统在内存压力下会触发
onTrimMemory回调,此时如果应用仍在进行资源密集型操作(如布局 inflate、资源解析等),可能导致 SIGSEGV 崩溃。特别是在TRIM_MEMORY_RUNNING_CRITICAL(level = 40) 时,系统可能已经清理了资源缓存,但应用代码仍在尝试访问这些资源,导致段错误。进程被 kill 后重启不走 SplashActivity:部分手机在低内存时系统会 kill 后台进程,当用户重新打开应用时,系统会尝试恢复之前的 Activity 栈,导致应用跳过 SplashActivity 直接进入之前的页面。这不仅影响用户体验,还可能导致应用状态不一致的问题。
1.2 调研目标
- 了解不同 Android 版本和厂商对
onTrimMemory的处理差异 - 研究进程被 kill 后系统恢复 Activity 栈的机制
- 找到在低内存时主动 kill 进程的最佳实践,确保下次启动是冷启动
- 降低 background 崩溃率
二、onTrimMemory 触发时机与 Level 说明
2.1 触发时机
onTrimMemory 是 Android 系统在内存压力大时通知应用释放资源的回调方法。系统会根据应用的状态(前台/后台)和内存压力程度,传递不同的 level 值。
2.2 Level 常量说明
前台应用相关 Level(已废弃,API 34+ 不再通知)
| Level 常量 | 值 | 说明 | 状态 |
|---|---|---|---|
TRIM_MEMORY_RUNNING_MODERATE |
5 | 进程不是可消耗的后台进程,但设备内存运行在中等偏低水平。运行中的进程应该释放一些不需要的资源供其他地方使用。 | 已废弃(API 34+) |
TRIM_MEMORY_RUNNING_LOW |
10 | 进程不是可消耗的后台进程,但设备内存运行在较低水平。运行中的进程应该释放不需要的资源,以允许该内存在其他地方使用。 | 已废弃(API 34+) |
TRIM_MEMORY_RUNNING_CRITICAL |
15 | 进程不是可消耗的后台进程,但设备内存运行在极低水平,即将无法保持任何后台进程运行。运行中的进程应该释放尽可能多的非关键资源,以允许该内存在其他地方使用。接下来会发生的是调用 onLowMemory() 来报告后台无法保留任何内容,这种情况可能会开始显著影响用户。 |
已废弃(API 34+) |
后台应用相关 Level
| Level 常量 | 值 | 说明 | 状态 |
|---|---|---|---|
TRIM_MEMORY_UI_HIDDEN |
20 | 进程曾经显示用户界面,现在不再显示。此时应该释放与 UI 相关的大型分配,以允许更好地管理内存。 | 可用 |
TRIM_MEMORY_BACKGROUND |
40 | 进程已进入 LRU 列表。这是清理可以高效快速重建的资源的良好机会(如果用户返回应用)。 | 可用 |
TRIM_MEMORY_MODERATE |
60 | 进程位于后台 LRU 列表的中间位置;释放内存可以帮助系统保持列表中的其他进程运行,以获得更好的整体性能。 | 已废弃(API 34+) |
TRIM_MEMORY_COMPLETE |
80 | 进程接近后台 LRU 列表的末尾,如果很快找不到更多内存,它将被杀死。 | 已废弃(API 34+) |
2.3 关键理解
- TRIM_MEMORY_UI_HIDDEN (20):应用刚切到后台时触发,此时应该释放 UI 相关资源
- TRIM_MEMORY_BACKGROUND (40):应用在后台 LRU 列表中,系统建议释放可重建的资源
- TRIM_MEMORY_MODERATE (60) 和 TRIM_MEMORY_COMPLETE (80):应用在后台 LRU 列表的中间或末尾,系统内存压力很大,应用很可能被系统 kill
三、调研结果
3.1 测试环境
| Android 版本 | 设备型号 | 厂商 |
|---|---|---|
| Android 16 | 华为荣耀 | 华为 |
| Android 12 | 华为 mate60pro+ | 华为 |
| Android 10 | 小米3 | 小米 |
3.2 onTrimMemory 执行情况
| Android 版本 | App 退后台后 onTrimMemory 的执行情况 |
|---|---|
| Android 16(华为荣耀) |
TRIM_MEMORY_UI_HIDDEN 后立刻 TRIM_MEMORY_BACKGROUND(大概率,偶尔只执行 TRIM_MEMORY_UI_HIDDEN) |
| Android 12(华为 mate60pro+) | TRIM_MEMORY_UI_HIDDEN |
| Android 10(小米3) | TRIM_MEMORY_UI_HIDDEN |
3.3 进程被 kill 后恢复行为调研
前台应用场景
| 操作方式 | 是否在 recent 里 | 重启是否恢复 Activity 栈 | 测试结果(Android 16/12/10) |
|---|---|---|---|
| 崩溃后 | 是 | 否 | 三个版本行为一致 |
| 用代码 kill 进程后 | 是 | 否 | 三个版本行为一致 |
| 先删除 Activity 栈再用代码 kill 进程后 | 否 | 否 | 三个版本行为一致 |
后台应用场景
| 操作方式 | 是否在 recent 里 | 重启是否恢复 Activity 栈 | 测试结果(Android 16/12/10) |
|---|---|---|---|
| 崩溃后 | 是 | 否 | 三个版本行为一致 |
| 用代码 kill 进程后 | 是 | 是 ⚠️ | 三个版本行为一致 |
| 先删除 Activity 栈再用代码 kill 进程后 | 否 | 否 | 三个版本行为一致 |
⚠️ 关键发现:后台应用直接 kill 进程会恢复 Activity 栈,这是导致跳过 SplashActivity 的根本原因。
3.4 关键发现
后台应用直接 kill 进程会恢复 Activity 栈:当应用在后台时,如果直接使用
Process.killProcess()kill 进程,系统会在 recent 中保留应用,并且重启时会恢复之前的 Activity 栈。这是导致跳过 SplashActivity 的主要原因。先清理 Activity 栈再 kill 进程可以避免恢复:无论是前台还是后台,如果先调用
finishAndRemoveTask()和killAllActivity()清理 Activity 栈,再 kill 进程,系统不会在 recent 中保留应用,也不会恢复 Activity 栈。崩溃不会恢复 Activity 栈:无论是前台还是后台,应用崩溃后系统不会恢复 Activity 栈,这是因为崩溃时系统没有机会保存状态。
四、最佳实践方案
4.1 实现方案
基于调研结果,我们实现了以下方案:
4.1.1 在 onTrimMemory 中主动 kill 进程
@Override
public void onTrimMemory(int level) {
super.onTrimMemory(level);
// 如果应用在后台且内存压力大,主动kill进程,避免资源被清理导致的崩溃
if (level >= Activity.TRIM_MEMORY_BACKGROUND) {
// 检查应用是否在后台
boolean isBackground = !SystemUtil.isRunningForeground(this) || HyAppLifeCycle.background;
if (isBackground) {
// 调用 killProcess 方法
killProcess();
}
}
// 其他清理操作...
if (level >= Activity.TRIM_MEMORY_UI_HIDDEN) {
TtfIconConverter.Companion.getInstance(mContext).clearCache();
Glide.get(this).onTrimMemory(level);
}
}
4.1.2 killProcess 方法实现
// 标记是否正在执行清理和 kill 进程的逻辑,避免多次收到 onTrimMemory 重复处理
private static volatile boolean isKillingProcess = false;
public void killProcess(){
// 如果已经在执行清理和 kill 进程的逻辑,不重复处理
if (isKillingProcess) {
LogUtil.d("HyApp", "killProcess: 已在执行 kill 进程逻辑,跳过重复处理");
return;
}
// 设置标记,避免重复处理
isKillingProcess = true;
LogUtil.postBuglyException(new Throwable("killProcess: kill self after 500ms!"));
// 延迟一小段时间再处理,确保日志上报完成
// 在延迟回调中清理 Activity 栈和 kill 进程
new Handler(Looper.getMainLooper()).postDelayed(() -> {
try {
// 先清理 Activity 栈,避免系统保存状态,确保下次启动是冷启动
try {
FragmentActivity topActivity = ActivityStackManager.getInstance().getTopActivity();
if (topActivity != null) {
// 使用 finishAndRemoveTask 清除任务栈,避免系统恢复
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
topActivity.finishAndRemoveTask();
} else {
topActivity.finish();
}
}
// 清理所有 Activity
ActivityStackManager.getInstance().killAllActivity();
} catch (Exception e) {
LogUtil.e("HyApp", "清理 Activity 栈失败", e);
}
// 执行 kill 操作
LogUtil.d("HyApp", "killProcess: 执行 kill 进程");
android.os.Process.killProcess(android.os.Process.myPid());
} catch (Exception e) {
LogUtil.e("HyApp", "延迟处理 kill 进程逻辑时发生异常", e);
// 发生异常时重置标记
isKillingProcess = false;
}
}, 500);
}
4.2 方案要点
4.2.1 触发条件
-
Level 判断:
level >= Activity.TRIM_MEMORY_BACKGROUND(40)- 覆盖
TRIM_MEMORY_BACKGROUND(40)、TRIM_MEMORY_MODERATE(60)、TRIM_MEMORY_COMPLETE(80) - 这些 level 都表示系统内存压力大,应用在后台时主动 kill 是合理的
- 覆盖
-
后台检查:确保应用在后台时才执行 kill 操作
- 使用
SystemUtil.isRunningForeground()检查应用是否在前台 - 使用
HyAppLifeCycle.background作为补充判断
- 使用
4.2.2 防重复处理
- 使用
volatile boolean isKillingProcess标记,避免多次收到onTrimMemory时重复处理 - 在开始处理时设置标记,在异常时重置标记
4.2.3 清理 Activity 栈
-
先清理再 kill:在 kill 进程前先清理 Activity 栈
- 使用
finishAndRemoveTask()(Android 5.0+) 或finish()清除任务栈 - 调用
killAllActivity()清理所有 Activity - 这样可以避免系统保存状态,确保下次启动是冷启动
- 使用
4.2.4 延迟执行
- 延迟 500ms 再执行 kill 操作
- 确保日志上报完成
- 给清理操作足够的时间
- 如果用户在延迟期间切回前台,可以在延迟回调中再次检查(可选)
4.3 方案优势
- 降低崩溃率:在内存压力大时主动退出,避免资源被清理导致的 SIGSEGV 崩溃
- 确保冷启动:先清理 Activity 栈再 kill 进程,确保下次启动走 SplashActivity
- 用户体验:只在后台时 kill,对用户无感知
- 资源释放:主动退出可以释放内存,帮助系统恢复
4.4 注意事项
- 数据保存:确保在 kill 进程前,关键数据已经保存
- 日志记录:记录 kill 原因,便于问题追踪
- 异常处理:添加异常处理,确保即使清理失败也能正常退出
-
版本兼容:注意
finishAndRemoveTask()需要 Android 5.0+
五、总结
5.1 调研结论
-
不同 Android 版本和厂商对 onTrimMemory 的处理有差异:
- Android 16(华为荣耀)会在退后台时连续触发
TRIM_MEMORY_UI_HIDDEN和TRIM_MEMORY_BACKGROUND - Android 12 和 Android 10 通常只触发
TRIM_MEMORY_UI_HIDDEN
- Android 16(华为荣耀)会在退后台时连续触发
-
后台应用直接 kill 进程会恢复 Activity 栈:
- 这是导致跳过 SplashActivity 的根本原因
- 必须先清理 Activity 栈再 kill 进程
-
先清理 Activity 栈再 kill 进程可以避免恢复:
- 无论是前台还是后台,都能确保下次启动是冷启动
- 不会在 recent 中保留应用
5.2 实施方案
通过在 onTrimMemory 中检测到内存压力大且应用在后台时,主动清理 Activity 栈并 kill 进程,可以:
- 降低 background 崩溃率:避免在资源被清理时访问已释放的内存
- 确保冷启动:避免系统恢复 Activity 栈,确保每次启动都走 SplashActivity
- 提升用户体验:在后台时主动退出,对用户无感知,且有助于系统恢复
5.3 后续优化建议
- 监控效果:上线后监控 background 崩溃率的变化
- 数据统计:统计主动 kill 进程的次数和场景
- 用户反馈:关注用户反馈,确保方案不影响正常使用
- 持续优化:根据线上数据持续优化触发条件和清理逻辑