EN

Android 代码内存优化建议 - OnTrimMemory 优化

字数统计: 3.1k阅读时长: 11 min
2015/07/20
loading

Android 内存优化系列文章:

  1. Android代码内存优化建议-Android官方篇
  2. Android代码内存优化建议-Java官方篇
  3. Android代码内存优化建议-Android资源篇
  4. Android代码内存优化建议-OnTrimMemory优化

onTrimMemory(int level) 从 Android 4.0(API 14)提供,用于提示应用在合适的时机减少不必要的内存占用。合理缩减缓存有助于系统保留更多后台进程,但不能保证应用不被终止,也不能保证下次返回一定更快。

本文仍用问答方式介绍回调的作用、接入和资源清理。重点是按资源的用途决定保留什么、释放什么,并确保返回界面时能够恢复。

技术复核:2026-10-13。 Android 14(API 34)起不再发送五种旧内存压力级别,相关常量在 API 35 弃用;UI_HIDDEN 与 BACKGROUND 仍可用于状态变化时的缓存管理。下文的 Launcher/Contacts 代码按 AOSP android-4.4.4_r2 核对,作为历史例子保留,并补上适用于当前回调的缓存示例。

0. OnTrimMemory回调的作用?

回调通过 level 提供清理线索。它不等于“每次剩余 RAM 变化就通知”,也不是“进程即将死亡前最后一次保存数据的机会”。只减少自己不再需要的资源,不要为了响应一个级别破坏正在运行的业务。

当前仍使用的两个级别均从 API 14 提供:

级别 含义与处理方向
TRIM_MEMORY_UI_HIDDEN 当前进程不再显示 UI,可缩减只用于界面的可重建缓存;后台播放等工作仍可能继续。
TRIM_MEMORY_BACKGROUND 进程进入后台 LRU 状态,可以进一步释放容易恢复的资源;不能仅据此断言设备已严重缺内存。

下面五项只用于理解旧系统代码,API 34 起不再通知,API 35 标为弃用:

历史级别 引入版本 历史含义
TRIM_MEMORY_RUNNING_MODERATE API 16 进程仍在运行时,设备开始出现内存压力。
TRIM_MEMORY_RUNNING_LOW API 16 运行中的进程收到更高内存压力提示。
TRIM_MEMORY_RUNNING_CRITICAL API 16 运行中的进程收到严重内存压力提示。
TRIM_MEMORY_MODERATE API 14 进程接近后台 LRU 列表中部。
TRIM_MEMORY_COMPLETE API 14 进程接近后台 LRU 列表尾部。

这些旧提示也不保证“应用不会被杀”。处理级别时采用适合自己策略的阈值比较,不要假定一定依次收到所有级别。依据 ComponentCallbacks2 和 Android 16 对应源码。

1. 哪些组件可以实现OnTrimMemory回调?

Application、Activity、Service、ContentProvider 提供 onTrimMemory(int level),继承这些组件时可按需覆写,并遵守父类调用约定。

这里要区分两种 Fragment:

  • 旧 android.app.Fragment 有这个回调,从 API 14 提供;该系统 Fragment API 在 API 28 弃用。
  • 当前 androidx.fragment.app.Fragment 没有同名的可直接覆写回调。可由宿主或统一的缓存管理器处理,也可以显式注册独立的回调对象,按相应作用域注销。不要把系统 Fragment 的方法清单直接套到 AndroidX。

普通类只是实现 ComponentCallbacks2,不会因此自动收到系统通知;还需要用 Context.registerComponentCallbacks() 注册,并在其作用域结束时 unregisterComponentCallbacks()。使用 Application 级注册会延长回调对象的持有期,不能随意把 Activity 或 View 留在里面。方法名是小写开头的 onTrimMemory,不是 OnTrimMemory。

2. OnTrimMemory回调中可以释放哪些资源?

架构阶段就应明确资源所有权、容量预算和恢复成本。适合清理的通常是不用的内存缓存、可重建的预览、已经结束用途的监听或异步请求。不要清掉尚未保存的用户状态,也不要释放后台业务仍在使用的数据。

磁盘缓存和内存缓存要分开:删除缓存文件主要释放存储空间,不等于回收 Java 堆,回调里也不适合做大量同步磁盘 IO。仅把 View 设成 GONE 不会让 View 对象消失;要检查父容器、监听器和任务队列的持有关系。Fragment 的 View 引用还应遵循其自身 View 生命周期,不能全部等到 trim 回调。

下面先保留两个历史实现,再说明当前可以采用的接入方式。

2.1 例子:释放不常用到的View.

以下是 AOSP Launcher2,android-4.4.4_r2 中的方法片段,字段与调用关系按该标签统一;它们依赖 Launcher2 的其余代码,不能当成独立应用粘贴运行。旧 TRIM_MEMORY_MODERATE 在 Android 14+ 不再通知,所以也不能把这条触发路径直接作为当前方案。

Launcher.java:

1
2
3
4
5
6
7
@Override
public void onTrimMemory(int level) {
super.onTrimMemory(level);
if (level >= ComponentCallbacks2.TRIM_MEMORY_MODERATE) {
mAppsCustomizeTabHost.onTrimMemory();
}
}

AppsCustomizeTabHost.java:

1
2
3
4
public void onTrimMemory() {
mContent.setVisibility(GONE);
mAppsCustomizePane.clearAllWidgetPages();
}

AppsCustomizePagedView.java:

1
2
3
4
5
6
7
8
9
10
11
public void clearAllWidgetPages() {
cancelAllTasks();
int count = getChildCount();
for (int i = 0; i < count; i++) {
View v = getPageAt(i);
if (v instanceof PagedViewGridLayout) {
((PagedViewGridLayout) v).removeAllViewsOnPage();
mDirtyPageContent.set(i, true);
}
}
}

PagedViewGridLayout.java:

1
2
3
4
5
6
@Override
public void removeAllViewsOnPage() {
removeAllViews();
mOnLayoutListener = null;
setLayerType(LAYER_TYPE_NONE, null);
}

这条路径不仅隐藏内容,还取消预览任务、移除 widget 页的子 View、清理布局监听和硬件层设置,并标记页面需要重新填充。GONE 本身不能做到这些;移除引用也不保证 GC 或进程占用数字立刻下降。今天可借鉴的是“清理可重建内容并安排恢复”的职责划分,而不是照搬旧阈值或桌面内部类。

2.2 例子: 清除缓存

以下片段来自 ContactsCommon 的 ContactPhotoManagerImpl(android-4.4.4_r2),省略了调试日志:

1
2
3
4
5
6
7
8
9
10
11
12
@Override
public void onTrimMemory(int level) {
if (level >= ComponentCallbacks2.TRIM_MEMORY_MODERATE) {
clear();
}
}

public void clear() {
mPendingRequests.clear();
mBitmapHolderCache.evictAll();
mBitmapCache.evictAll();
}

对应源码 清的是待处理请求表和两级内存缓存。清空请求表不等于中断已开始的 IO,后台结果也可能重新填充缓存。迁移这种思路时,要同时处理任务取消、过期结果和再次加载;TRIM_MEMORY_MODERATE 仍只是历史触发条件。

2.3 当前示例:有预算的UI图片缓存

下面是一个完整的 API 19+ 示例,缓存可重新生成、与界面配置有关的软件 Bitmap。调用方传入测量后确定的正数像素预算;getAllocationByteCount() 从 API 19 提供。这个预算统计像素分配容量,不含所有键、对象头和容器开销,也不是进程内存的硬上限。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import android.app.Application;
import android.content.ComponentCallbacks2;
import android.content.res.Configuration;
import android.graphics.Bitmap;
import android.util.LruCache;

public final class UiBitmapCache implements ComponentCallbacks2 {
private final Application application;
private final LruCache<String, Bitmap> cache;

public UiBitmapCache(Application application, int maxPixelBytes) {
this.application = application;
cache = new LruCache<String, Bitmap>(maxPixelBytes) {
@Override protected int sizeOf(String key, Bitmap value) {
return value.getAllocationByteCount();
}
};
application.registerComponentCallbacks(this);
}

public Bitmap get(String key) {
return cache.get(key);
}

public void put(String key, Bitmap bitmap) {
cache.put(key, bitmap);
}

@Override public void onTrimMemory(int level) {
if (level >= TRIM_MEMORY_UI_HIDDEN) {
cache.evictAll();
}
}

@Override public void onConfigurationChanged(Configuration newConfig) {
cache.evictAll();
}

@Override public void onLowMemory() {
cache.evictAll();
}

public void dispose() {
application.unregisterComponentCallbacks(this);
cache.evictAll();
}
}

这个示例在 UI_HIDDEN 或更高等级时清空同一份 UI 缓存,因此不需要重复写两个相同分支。旧 RUNNING_* 级别不在此例的触发范围。get() 返回 null 时,需要重新生成或加载图片。配置变化也清空,以便重建与新配置匹配的内容;onLowMemory() 仅用于旧系统兼容,API 34+ 不会调用它。

在主线程创建和管理这个对象,通常由 Application 持有。若它的作用域短于进程,结束时调用 dispose(),同时停止再向它提交任务或数据;不要在切换每个 Activity 时反复注册新对象。入缓存后不应由外部随意 recycle(),evictAll() 只移除缓存的引用,不会回收其他使用方仍持有的 Bitmap。

清空不等于暂停生产数据,仍要取消不再需要的预取或忽略过期结果,避免后台任务马上把缓存填回。需要更平滑的恢复体验时,可以按实测改为缩减预算或分层保留;不要把“隐藏就清空所有缓存”推广到播放、上传或其他业务缓存。接口细节见 LruCache。

3. OnTrimMemory和onStop的关系?

onStop() 针对一个 Activity;TRIM_MEMORY_UI_HIDDEN 针对当前进程不再显示 UI。进程内从 Activity A 切到 B 时,A 可以停止,但整个进程仍有可见 UI。多进程应用还需分别判断,不能把一个进程的回调当成全应用可见性广播。

属于单个页面可见期的资源,可以跟随 onStart/onStop 成对管理;跨页面共享且有容量上限的缓存可以由更长生命周期的缓存管理器统一处理。并非所有 UI 资源都必须等 UI_HIDDEN 才释放。

不要依赖“UI_HIDDEN 总在 onStop 之前调用”的顺序。 它们是不同范围的通知,公开接口没有给出可用于同步业务逻辑的统一先后保证。清理应可重复执行、能恢复,也不能依赖 trim 回调作为进程结束前必达的钩子。

4. OnTrimMemory和OnLowMemory的关系?

onLowMemory() 是更早的粗粒度低内存通知。旧文把它与 COMPLETE 相比,是为了说明历史清理强度,并不是两者触发条件完全相同。

从 API 34 起 onLowMemory 不再调用,API 35 标为弃用,参见 ComponentCallbacks。因此不能将其作为现代系统收不到旧 trim 压力级别时的替代。当前应结合仍支持的状态回调、组件生命周期和日常缓存预算管理内存。

5. 为什么要调用OnTrimMemory?

更准确地说,是为什么要响应这个回调。手动传入一个 level 只是在执行本地清理方法,不会向系统声明已经进入对应的进程状态。

减少不用的占用有助于系统保留后台进程,但回收策略还取决于组件状态、依赖关系与全局压力,不是一个固定的“LRU 从低到高、先杀大进程”规则。原文的“保留进程就是热启动、固定快 2–3 倍”也不成立:进程还在而 Activity 需要重建时,仍可能经历温启动;缓存清理过多又会增加恢复成本。

启动应区分冷、温、热状态,并在同一设备、版本和操作路径上测量,参见 应用启动时间。降低存活内存和减少下次恢复耗时,需要一起权衡。

6. 有哪些典型的使用场景?

6.1 常驻内存的应用

Launcher 等长时间运行的应用,可以清理当前不用的预览页和界面缓存,并保留快速重建能力。但“桌面、电话、安全中心”等名称不等于进程不会被终止;是否常驻及其优先级取决于系统角色和实现。应响应系统通知和自身生命周期,不要靠业务代码反复手动调用回调保活。

6.2 有后台Service运行的应用

音乐、下载等场景中,UI 不可见时后台工作仍可能需要数据。可以缩减只用于展示的封面或预览缓存,但不能一并释放播放器、传输缓冲或仍在使用的连接。

Service 也不保证进程常驻;按当前后台执行与前台服务规则安排工作,内存策略由各资源的实际用途决定。验证时覆盖退到后台、返回界面、内部页面切换和配置变化,并记录实际收到的级别;不要在 Android 14+ 等待已停发的五种旧压力回调来证明清理生效。

关于我 && 博客

下面是个人的介绍和相关的链接,期望与同行的各位多多交流,三人行,则必有我师!

  1. 博主个人介绍 :里面有个人的微信和微信群链接。
  2. 本博客内容导航 :个人博客内容的一个导航。
  3. 个人整理和搜集的优秀博客文章 - Android 性能优化必知必会 :欢迎大家自荐和推荐 (微信私聊即可)
  4. Android性能优化知识星球:付费社群已于 2026 年停止运营,不再招收新成员;此处保留历史介绍。

一个人可以走的更快 , 一群人可以走的更远

微信扫一扫

CATALOG
  1. 1. 0. OnTrimMemory回调的作用?
  2. 2. 1. 哪些组件可以实现OnTrimMemory回调?
  3. 3. 2. OnTrimMemory回调中可以释放哪些资源?
    1. 3.1. 2.1 例子:释放不常用到的View.
    2. 3.2. 2.2 例子: 清除缓存
    3. 3.3. 2.3 当前示例:有预算的UI图片缓存
  4. 4. 3. OnTrimMemory和onStop的关系?
  5. 5. 4. OnTrimMemory和OnLowMemory的关系?
  6. 6. 5. 为什么要调用OnTrimMemory?
  7. 7. 6. 有哪些典型的使用场景?
    1. 7.1. 6.1 常驻内存的应用
    2. 7.2. 6.2 有后台Service运行的应用
  • 关于我 && 博客