Android 应用冻结机制与防冻结方案分析

目录


1. 背景介绍

从 Android 11 (API 30) 开始,Android 系统引入了应用冻结 (App Freezing) 机制,作为 Cached App Optimizer 的一部分。该机制旨在通过冻结后台缓存应用来减少系统资源消耗,延长电池续航时间,同时保持应用在内存中以便快速恢复。

问题场景

在实际应用中,某些后台服务(如计时器、倒计时小工具)在运行一段时间后会突然停止工作,查看日志发现应用被系统主动冻结:

1
ActivityManager: freezing 29669 com.example.stopwatch

这种冻结会导致:

  • 后台计时停止
  • 定时任务无法执行
  • 网络连接被挂起
  • 用户体验受影响

2. 应用冻结机制详解

2.1 什么是应用冻结

应用冻结是一种进程状态管理技术,通过 Linux cgroup freezer 机制暂停进程的执行:

  • 冻结 (Frozen): 进程被暂停执行,不消耗 CPU 资源,但保留在内存中
  • 解冻 (Unfrozen): 进程恢复执行,从暂停点继续运行
  • 与杀死的区别: 冻结的进程仍在内存中,解冻后可立即恢复;被杀死的进程需要重新启动

2.2 冻结触发条件

系统在以下条件下会冻结应用(源码位置:CachedAppOptimizer.java):

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
private void freezeProcess(final ProcessRecord proc) {
synchronized (mProcLock) {
pid = proc.getPid();

// 条件1: ADJ 必须 >= CACHED_APP_MIN_ADJ (900)
if (proc.mState.getCurAdj() < ProcessList.CACHED_APP_MIN_ADJ
|| opt.shouldNotFreeze()) {
if (DEBUG_FREEZER) {
Slog.d(TAG_AM, "Skipping freeze for process " + pid
+ " " + name + " curAdj = " + proc.mState.getCurAdj()
+ ", shouldNotFreeze = " + opt.shouldNotFreeze());
}
return;
}

// 条件2: 不能处于冻结覆盖状态
if (mFreezerOverride) {
opt.setFreezerOverride(true);
return;
}

// 条件3: 进程未被冻结
if (pid == 0 || opt.isFrozen()) {
return;
}

Slog.d(TAG_AM, "freezing " + pid + " " + name);
}
}

核心条件总结

  1. ADJ >= 900 (CACHED_APP_MIN_ADJ)
  2. shouldNotFreeze() == false
  3. 默认延迟时间: 600 秒(10 分钟)
1
2
// 默认冻结延迟时间配置
static final long DEFAULT_FREEZER_DEBOUNCE_TIMEOUT = 600_000L; // 10 minutes

2.3 OOM Adjustment 机制

OOM Adjustment (简称 ADJ) 是 Android 进程优先级管理的核心机制,值越小优先级越高:

ADJ级别 名称 取值 说明 是否会被冻结
NATIVE_ADJ native -1000 Native 进程
SYSTEM_ADJ sys -900 System Server
PERSISTENT_PROC_ADJ pers -800 Persistent 进程
PERSISTENT_SERVICE_ADJ psvc -700 Persistent 服务
FOREGROUND_APP_ADJ fg 0 前台应用
PERCEPTIBLE_RECENT_FOREGROUND_APP_ADJ 50 最近在前台的应用
VISIBLE_APP_ADJ vis 100 可见应用
PERCEPTIBLE_APP_ADJ prcp 200 可感知应用(如音乐播放)
PERCEPTIBLE_LOW_APP_ADJ prcl 250 低优先级可感知应用
BACKUP_APP_ADJ bkup 300 备份进程
HEAVY_WEIGHT_APP_ADJ hvy 400 重量级后台进程
SERVICE_ADJ svc 500 服务进程
HOME_APP_ADJ home 600 桌面进程
PREVIOUS_APP_ADJ prev 700 上一个应用
SERVICE_B_ADJ svcb 800 B List 服务
CACHED_APP_MIN_ADJ cch 900 缓存应用最小值 ✅ 冻结阈值
CACHED_APP_LMK_FIRST_ADJ 950 LMK 首选杀死
CACHED_APP_MAX_ADJ 999 缓存应用最大值
UNKNOWN_ADJ 1001 未知状态

关键点

  • ADJ < 900: 不会被冻结
  • ADJ >= 900: 会在一定时间后被冻结
  • ADJ 值由 OomAdjuster 动态计算

2.4 冻结流程源码分析

步骤1: OomAdjuster 计算 ADJ

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
// OomAdjuster.java
@GuardedBy({"mService", "mProcLock"})
private void maybeUpdateIsolatedFreeze(ProcessRecord app, @OomAdjReason int oomAdjReason,
boolean immediate) {
if (!mCachedAppOptimizer.useFreezer()) {
return;
}

if (app.mOptRecord.isFreezeExempt()) {
return;
}

final ProcessCachedOptimizerRecord opt = app.mOptRecord;

// 如果已冻结且 shouldNotFreeze 变为 true,立即解冻
if (opt.isFrozen() && opt.shouldNotFreeze()) {
mCachedAppOptimizer.unfreezeAppLSP(app,
CachedAppOptimizer.getUnfreezeReasonCodeFromOomAdjReason(oomAdjReason));
return;
}

final ProcessStateRecord state = app.mState;

// 核心逻辑:ADJ >= 900 且 shouldNotFreeze = false 时冻结
if (state.getCurAdj() >= FREEZER_CUTOFF_ADJ && !opt.isFrozen()
&& !opt.shouldNotFreeze()) {
if (!immediate) {
mCachedAppOptimizer.freezeAppAsyncLSP(app);
} else {
mCachedAppOptimizer.freezeAppAsyncAtEarliestLSP(app);
}
} else if (state.getSetAdj() < FREEZER_CUTOFF_ADJ) {
mCachedAppOptimizer.unfreezeAppLSP(app,
CachedAppOptimizer.getUnfreezeReasonCodeFromOomAdjReason(oomAdjReason));
}
}

步骤2: CachedAppOptimizer 执行冻结

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// CachedAppOptimizer.java
private void freezeProcess(final ProcessRecord proc) {
// ... 前置检查 ...

try {
// 冻结 Binder 接口
freezeBinder(pid, true, FREEZE_BINDER_TIMEOUT_MS);

// 冻结进程
Process.setProcessFrozen(pid, proc.uid, true);

opt.setFrozen(true);
opt.setFreezeUnfreezeTime(SystemClock.uptimeMillis());

mFrozenProcesses.put(pid, proc);
} catch (Exception e) {
Slog.e(TAG_AM, "Unable to freeze " + pid + " " + proc.processName);
}
}

3. 防冻结方案详解

3.1 ContentProvider 方案

原理

通过声明 ContentProvider 并调用系统 API getContentProviderExternal(),使系统认为该 Provider 具有外部进程依赖,从而将 ADJ 提升到 0。

关键机制

步骤1: 外部进程句柄机制

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
// ContentProviderRecord.java
public class ContentProviderRecord {
// 记录外部进程对 Provider 的持有
ArrayMap<IBinder, ExternalProcessHandle> externalProcessTokenToHandle;
int externalProcessNoHandleCount;

public void addExternalProcessHandleLocked(IBinder token, int callingUid, String callingTag) {
if (token == null) {
externalProcessNoHandleCount++;
} else {
if (externalProcessTokenToHandle == null) {
externalProcessTokenToHandle = new ArrayMap<>();
}
ExternalProcessHandle handle = externalProcessTokenToHandle.get(token);
if (handle == null) {
handle = new ExternalProcessHandle(token, callingUid, callingTag);
externalProcessTokenToHandle.put(token, handle);
handle.startAssociationIfNeeded(this);
}
handle.mAcquisitionCount++;
}
}

public boolean hasExternalProcessHandles() {
return (externalProcessTokenToHandle != null || externalProcessNoHandleCount > 0);
}
}

步骤2: getContentProviderExternal 添加句柄

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// ContentProviderHelper.java
private ContentProviderConnection incProviderCountLocked(ProcessRecord r,
final ContentProviderRecord cpr, IBinder externalProcessToken, int callingUid,
String callingPackage, String callingTag, boolean stable, boolean updateLru,
long startTime, ProcessList processList, @UserIdInt int expectedUserId) {

// 关键:如果 r == null(通过 getContentProviderExternal 调用),添加外部句柄
if (r == null) {
cpr.addExternalProcessHandleLocked(externalProcessToken, callingUid, callingTag);
return null;
}

// ... 正常的 Provider 连接逻辑 ...
}

步骤3: OomAdjuster 提升 ADJ

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
// OomAdjuster.java
private int computeOomAdjLSP(ProcessRecord app, int cachedAdj, ProcessRecord topApp,
boolean doingAll, long now, boolean cycleReEval, boolean computeClients,
@OomAdjReason int oomAdjReason, boolean couldRecurse) {

// 检查 Provider 是否有外部进程依赖
for (int iproc = ppr.numberOfPublishedProviders() - 1; iproc >= 0; iproc--) {
ContentProviderRecord cpr = ppr.getPublishedProviderAt(iproc);

// 如果有外部进程句柄,提升到前台应用级别
if (cpr.hasExternalProcessHandles()) {
if (adj > FOREGROUND_APP_ADJ) {
adj = FOREGROUND_APP_ADJ; // ADJ = 0
state.setCurRawAdj(adj);
schedGroup = SCHED_GROUP_DEFAULT;
state.setAdjType("ext-provider");
state.setAdjTarget(cpr.name);
}
if (procState > PROCESS_STATE_IMPORTANT_FOREGROUND) {
procState = PROCESS_STATE_IMPORTANT_FOREGROUND;
state.setCurRawProcState(procState);
}
}
}
}

完整实现代码

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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
/**
* ADJ 调整器 - 通过 ContentProvider 防止应用被冻结
* 原理:通过 getContentProviderExternal() 使 Provider 具有外部进程依赖,
* 从而将应用 ADJ 提升到 FOREGROUND_APP_ADJ (0)
*/
public class AdjAdjuster extends ContentProvider {
private static final String TAG = "AdjAdjuster";
private static final String AUTHORITY = "com.example.countdown.provider.AdjAdjuster";

// 引用计数管理
private static final Set<Integer> sKeySet = new HashSet<>();
private static final IBinder sToken = new Binder();

/**
* 开始防冻结保护
* @param key 场景标识,用于引用计数
*/
public static void onStart(int key) {
synchronized (sKeySet) {
sKeySet.add(key);

// 只在第一次调用时获取 Provider
if (sKeySet.size() == 1) {
try {
// 调用系统 API,将自己的 Provider 标记为具有外部依赖
// 需要权限: ACCESS_CONTENT_PROVIDERS_EXTERNALLY
ActivityManager.getService().getContentProviderExternal(
AUTHORITY,
UserHandle.USER_CURRENT,
sToken,
TAG
);
Log.d(TAG, "Anti-freeze protection enabled");
} catch (RemoteException e) {
Log.e(TAG, "Failed to enable anti-freeze protection", e);
}
}
}
}

/**
* 停止防冻结保护
* @param key 场景标识
*/
public static void onStop(int key) {
synchronized (sKeySet) {
sKeySet.remove(key);

// 只在所有引用都释放后才真正移除
if (sKeySet.isEmpty()) {
try {
ActivityManager.getService().removeContentProviderExternal(
AUTHORITY,
sToken
);
Log.d(TAG, "Anti-freeze protection disabled");
} catch (RemoteException e) {
Log.e(TAG, "Failed to disable anti-freeze protection", e);
}
}
}
}

// ========== ContentProvider 空实现 ==========
@Override
public boolean onCreate() {
return false;
}

@Nullable
@Override
public Cursor query(@NonNull Uri uri, @Nullable String[] projection,
@Nullable String selection, @Nullable String[] selectionArgs,
@Nullable String sortOrder) {
return null;
}

@Nullable
@Override
public String getType(@NonNull Uri uri) {
return null;
}

@Nullable
@Override
public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
return null;
}

@Override
public int delete(@NonNull Uri uri, @Nullable String selection,
@Nullable String[] selectionArgs) {
return 0;
}

@Override
public int update(@NonNull Uri uri, @Nullable ContentValues values,
@Nullable String selection, @Nullable String[] selectionArgs) {
return 0;
}
}

AndroidManifest.xml 配置:

1
2
3
4
5
6
7
8
9
<!-- 声明 Provider -->
<provider
android:name=".provider.AdjAdjuster"
android:authorities="com.example.countdown.provider.AdjAdjuster"
android:exported="false"
android:enabled="true" />

<!-- 声明权限(系统应用已具备,无需额外申请) -->
<uses-permission android:name="android.permission.ACCESS_CONTENT_PROVIDERS_EXTERNALLY" />

使用示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 在需要防冻结的场景启动时调用
public class CountdownService extends Service {
private static final int KEY_COUNTDOWN_SERVICE = 1;

@Override
public void onCreate() {
super.onCreate();
// 启动防冻结保护
AdjAdjuster.onStart(KEY_COUNTDOWN_SERVICE);
}

@Override
public void onDestroy() {
// 停止防冻结保护
AdjAdjuster.onStop(KEY_COUNTDOWN_SERVICE);
super.onDestroy();
}
}

优势

  1. 改动极小: 只需添加一个 Provider 类和 AndroidManifest 声明
  2. 完全自控: 应用内部可随时启停,无需外部进程配合
  3. ADJ 最低: ADJ = 0,等同前台应用,不会被冻结或杀死
  4. 零业务侵入: 不影响原有业务逻辑
  5. 引用计数: 支持多场景同时使用,安全可靠

劣势

  1. 仅限系统应用: 需要 ACCESS_CONTENT_PROVIDERS_EXTERNALLY 权限(signature|privileged)
  2. 需要主动管理: 必须手动调用 onStart()onStop(),忘记调用会导致应用长期占用高优先级

3.2 前台服务方案

原理

通过启动前台服务(Foreground Service)提升应用优先级,前台服务的 ADJ 通常为 PERCEPTIBLE_APP_ADJ (200)。

实现代码

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
public class AntiFreezeService extends Service {
private static final int NOTIFICATION_ID = 1001;
private static final String CHANNEL_ID = "anti_freeze_channel";

@Override
public void onCreate() {
super.onCreate();
createNotificationChannel();
startForeground(NOTIFICATION_ID, createNotification());
}

private void createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = new NotificationChannel(
CHANNEL_ID,
"防冻结服务",
NotificationManager.IMPORTANCE_LOW
);
NotificationManager manager = getSystemService(NotificationManager.class);
manager.createNotificationChannel(channel);
}
}

private Notification createNotification() {
return new NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("服务运行中")
.setContentText("计时器正在运行")
.setSmallIcon(R.drawable.ic_timer)
.build();
}

@Override
public IBinder onBind(Intent intent) {
return null;
}
}

优势

  1. 标准方案: Android 原生支持,API 稳定
  2. 适用所有应用: 不需要特殊权限
  3. 用户感知: 通知栏显示,用户知道服务在运行

劣势

  1. 必须显示通知: 无法隐藏,占用通知栏空间
  2. ADJ 较高: ADJ = 200,虽不会被冻结但优先级低于 ContentProvider 方案
  3. 用户可关闭: 用户可通过通知栏停止服务
  4. 需要修改业务逻辑: 需要将功能迁移到服务中

3.3 省电白名单方案

原理

将应用加入省电白名单(Power Save Whitelist),系统会设置 shouldNotFreeze = true

源码机制

1
2
3
4
5
6
7
8
9
// OomAdjuster.java
private int computeOomAdjLSP(ProcessRecord app, ...) {
if (!couldRecurse || !cycleReEval) {
// 如果 UID 在白名单中,设置 shouldNotFreeze
final UidRecord uidRec = app.getUidRecord();
app.mOptRecord.setShouldNotFreeze(uidRec != null && uidRec.isCurAllowListed());
}
// ...
}

实现方式

方式1: 用户手动添加

1
设置 -> 电池 -> 电池优化 -> 选择应用 -> 不优化

方式2: 代码请求(需要用户授权)

1
2
3
4
5
6
7
8
9
PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
String packageName = getPackageName();

if (!pm.isIgnoringBatteryOptimizations(packageName)) {
Intent intent = new Intent();
intent.setAction(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
intent.setData(Uri.parse("package:" + packageName));
startActivity(intent);
}

方式3: 系统预配置(厂商)

1
2
3
4
<!-- frameworks/base/core/res/res/xml/power_profile.xml -->
<array name="config_allowlistedAppsForPowerWhitelist">
<item>com.example.app</item>
</array>

优势

  1. 无需改代码: 通过配置实现
  2. 厂商可控: OEM 可预置白名单
  3. 适用所有应用: 不限系统应用

劣势

  1. 需要系统配置或用户授权: 普通应用难以自动实现
  2. 不同 ROM 差异大: 各厂商实现可能不同
  3. 用户可撤销: 用户可随时移出白名单

3.4 Persistent 应用方案

原理

在 AndroidManifest.xml 中声明 android:persistent="true",使应用成为系统常驻进程,ADJ = -800。

实现方式

1
2
3
4
5
6
<application
android:name=".MyApplication"
android:persistent="true"
android:label="@string/app_name">
<!-- ... -->
</application>

源码机制

1
2
3
4
5
6
7
8
9
10
11
// OomAdjuster.java
private int computeOomAdjLSP(ProcessRecord app, ...) {
if (state.getMaxAdj() <= FOREGROUND_APP_ADJ) {
// Persistent 应用的 maxAdj 通常设置为 PERSISTENT_PROC_ADJ (-800)
state.setAdjType("fixed");
state.setCurRawAdj(state.getMaxAdj());
state.setCurProcState(ActivityManager.PROCESS_STATE_PERSISTENT);
// ...
return app.mState.getCurRawAdj();
}
}

优势

  1. 最高优先级: ADJ = -800,系统级保护
  2. 改动极小: 只需添加一个属性
  3. 自动重启: Crash 后系统会立即重启

劣势

  1. 仅限系统签名应用: 需要 android:sharedUserId="android.uid.system" 或平台签名
  2. 资源占用高: 常驻内存,占用系统资源
  3. Crash 频繁重启: 可能导致系统不稳定

3.5 Service 绑定方案

原理

通过绑定到其他不会被冻结的进程的服务,利用 shouldNotFreeze 的传播机制。

源码机制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// OomAdjuster.java
private boolean computeServiceHostOomAdjLSP(ServiceRecord s, ProcessRecord app, ...) {
// ...

// shouldNotFreeze 会通过绑定关系传播
if (client.mOptRecord.shouldNotFreeze()) {
// Propagate the shouldNotFreeze flag down the bindings.
if (app.mOptRecord.setShouldNotFreeze(true, dryRun)) {
return true;
}
}

// ...
}

实现示例

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
// 服务端(不会被冻结的进程)
public class KeeperService extends Service {
private final IBinder binder = new Binder();

@Override
public IBinder onBind(Intent intent) {
return binder;
}
}

// 客户端(需要防冻结的应用)
public class MyActivity extends Activity {
private ServiceConnection connection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
// 绑定成功,shouldNotFreeze 会传播过来
}

@Override
public void onServiceDisconnected(ComponentName name) {
}
};

@Override
protected void onStart() {
super.onStart();
Intent intent = new Intent(this, KeeperService.class);
bindService(intent, connection, Context.BIND_AUTO_CREATE);
}

@Override
protected void onStop() {
unbindService(connection);
super.onStop();
}
}

优势

  1. 灵活性高: 可以根据需要动态绑定/解绑
  2. 适用所有应用: 不需要特殊权限

劣势

  1. 需要另一个进程配合: 必须有一个不被冻结的进程提供服务
  2. 依赖其他进程状态: 如果服务进程被杀死,保护失效
  3. 管理复杂: 需要处理绑定生命周期

4. 方案对比与评估

综合对比表

维度 ContentProvider 前台服务 省电白名单 Persistent Service 绑定
ADJ 值 0 200 不改变(设置 shouldNotFreeze) -800 继承客户端
防冻结效果 ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐
防 LMK 效果 ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐
权限要求 系统权限 用户授权/系统配置 系统签名
适用范围 系统应用 所有应用 所有应用 系统签名应用 所有应用
改动成本 ⭐⭐⭐ ⭐⭐⭐⭐
维护成本 ⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐
用户感知 有(通知)
可控性 ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐
资源占用 无额外占用 高(常驻)
ROM 兼容性 ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐

详细评估

4.1 ContentProvider 方案

适用场景:

  • ✅ 系统应用(具备 ACCESS_CONTENT_PROVIDERS_EXTERNALLY 权限)
  • ✅ 需要最高优先级(ADJ = 0)
  • ✅ 希望完全自主控制启停
  • ✅ 无需用户感知

不适用场景:

  • ❌ 第三方应用(无权限)
  • ❌ 需要跨进程共享状态

技术特点:

  • 优先级最高,等同前台应用
  • 完全应用内部控制,无需外部配合
  • 代码简单,易于维护
  • 支持引用计数,可多场景使用

推荐指数: ⭐⭐⭐⭐⭐(系统应用首选)


4.2 前台服务方案

适用场景:

  • ✅ 所有类型应用
  • ✅ 需要用户知晓服务状态
  • ✅ 可以接受通知栏显示

不适用场景:

  • ❌ 需要隐藏运行
  • ❌ 对通知栏占用敏感

技术特点:

  • Android 标准方案,API 稳定
  • 用户可见,体验较好
  • ADJ = 200,优先级较高但不如 ContentProvider

推荐指数: ⭐⭐⭐⭐(第三方应用首选)


4.3 省电白名单方案

适用场景:

  • ✅ OEM 厂商预置应用
  • ✅ 客供应用(厂商配置白名单)
  • ✅ 需要用户主动授权的场景

不适用场景:

  • ❌ 需要自动实现的场景
  • ❌ 对不同 ROM 兼容性要求高

技术特点:

  • 无需修改应用代码
  • 厂商可控,适合客供场景
  • 依赖系统配置或用户授权

推荐指数: ⭐⭐⭐(辅助方案)


4.4 Persistent 方案

适用场景:

  • ✅ 核心系统服务
  • ✅ 需要系统级保护
  • ✅ Crash 后必须立即重启

不适用场景:

  • ❌ 非系统签名应用
  • ❌ 对资源占用敏感
  • ❌ 应用不够稳定(频繁 Crash)

技术特点:

  • 最高优先级(ADJ = -800)
  • 系统级保护,不会被杀死
  • 资源占用高,影响系统稳定性

推荐指数: ⭐⭐(仅限核心系统服务)


4.5 Service 绑定方案

适用场景:

  • ✅ 多进程应用
  • ✅ 已有不被冻结的进程可提供服务
  • ✅ 需要灵活控制

不适用场景:

  • ❌ 单进程应用
  • ❌ 无法保证服务进程不被杀死
  • ❌ 希望简单实现

技术特点:

  • 利用 shouldNotFreeze 传播机制
  • 依赖其他进程状态
  • 管理复杂,需要处理生命周期

推荐指数: ⭐⭐(特殊场景)


5. 实践建议

5.1 方案选择决策树

1
2
3
4
5
6
7
┌─ 是系统应用?
│ ├─ 是 ──→ 使用 ContentProvider 方案(最优)
│ └─ 否 ──┬─ 可以显示通知?
│ ├─ 是 ──→ 使用前台服务方案
│ └─ 否 ──┬─ 是客供应用?
│ ├─ 是 ──→ 联系厂商加入省电白名单
│ └─ 否 ──→ 考虑业务重构或使用前台服务

5.2 系统应用推荐实践

步骤1: 添加 ContentProvider

1
2
3
4
// 1. 创建 AdjAdjuster.java
public class AdjAdjuster extends ContentProvider {
// ... (完整代码见 3.1 节)
}

步骤2: 配置 AndroidManifest.xml

1
2
3
4
<provider
android:name=".provider.AdjAdjuster"
android:authorities="${applicationId}.provider.adjuster"
android:exported="false" />

步骤3: 在合适的时机调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class CountdownService extends Service {
private static final int KEY_SERVICE = 1;

@Override
public void onCreate() {
super.onCreate();
AdjAdjuster.onStart(KEY_SERVICE);
}

@Override
public void onDestroy() {
AdjAdjuster.onStop(KEY_SERVICE);
super.onDestroy();
}
}

5.3 第三方应用推荐实践

步骤1: 使用前台服务

1
2
3
4
5
6
7
public class TimerService extends Service {
@Override
public void onCreate() {
super.onCreate();
startForeground(NOTIFICATION_ID, createNotification());
}
}

步骤2: 提供用户手动加入白名单的入口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private void requestBatteryOptimizationExemption() {
PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
if (!pm.isIgnoringBatteryOptimizations(getPackageName())) {
new AlertDialog.Builder(this)
.setTitle("电池优化")
.setMessage("为保证计时器正常运行,建议关闭电池优化")
.setPositiveButton("去设置", (dialog, which) -> {
Intent intent = new Intent();
intent.setAction(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
intent.setData(Uri.parse("package:" + getPackageName()));
startActivity(intent);
})
.setNegativeButton("取消", null)
.show();
}
}

5.4 调试方法

查看进程 ADJ:

1
2
3
4
5
6
7
8
# 查看所有进程的 ADJ
adb shell dumpsys activity processes | grep -A 5 "com.your.package"

# 实时监控 ADJ 变化(方式1:循环查询)
while true; do adb shell dumpsys activity processes | grep -A 5 com.your.package; sleep 1; done

# 或者使用 watch 命令(如果系统支持)
watch -n 1 "adb shell dumpsys activity processes | grep -A 5 com.your.package"

查看冻结状态:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 查看所有冻结的进程(在 CachedAppOptimizer 部分)
adb shell dumpsys activity | grep -A 20 "Apps frozen"

# 输出示例:
# Apps frozen: 3
# 1234567890: 12345 com.example.app1
# 1234567891: 12346 com.example.app2 (sticky)
# 1234567892: 12347 com.example.app3
# 格式:<冻结时间戳>: <PID> <进程名> [(sticky)]

# 或者查看完整的 CachedAppOptimizer 信息
adb shell dumpsys activity | grep -A 100 "CachedAppOptimizer"

# 手动冻结/解冻进程(测试用)
adb shell am freeze <package-name>
adb shell am unfreeze <package-name>

查看 OOM Adj 日志:

1
adb logcat -s ActivityManager:D | grep -E "freezing|unfreezing|ext-provider"

5.5 常见问题

Q1: ContentProvider 方案会增加内存占用吗?

A: 几乎不会。Provider 本身是空实现,只是通过外部句柄机制改变了 ADJ 值,不会额外占用内存。

Q2: 忘记调用 onStop() 会有什么影响?

A: 应用会一直保持 ADJ = 0 的高优先级,占用系统资源。建议使用 try-finally 或在组件生命周期方法中确保调用。

Q3: ContentProvider 方案在 Android 12+ 上是否仍然有效?

A: 是的。该机制是 AOSP 的基础设计,在 Android 12、13、14 上均有效。

Q4: 可以同时使用多个防冻结方案吗?

A: 可以,但没有必要。系统会取最低的 ADJ 值,使用 ContentProvider 方案已经是最高优先级。


6. 参考文献

源码文件

  • frameworks/base/services/core/java/com/android/server/am/CachedAppOptimizer.java
  • frameworks/base/services/core/java/com/android/server/am/OomAdjuster.java
  • frameworks/base/services/core/java/com/android/server/am/ContentProviderHelper.java
  • frameworks/base/services/core/java/com/android/server/am/ContentProviderRecord.java
  • frameworks/base/services/core/java/com/android/server/am/ProcessList.java

相关文档

相关配置

1
2
3
4
5
6
7
8
// 关键常量
ProcessList.CACHED_APP_MIN_ADJ = 900 // 冻结阈值
ProcessList.FOREGROUND_APP_ADJ = 0 // 前台应用 ADJ
ProcessList.PERCEPTIBLE_APP_ADJ = 200 // 可感知应用 ADJ
ProcessList.PERSISTENT_PROC_ADJ = -800 // Persistent 应用 ADJ

// 默认冻结延迟
CachedAppOptimizer.DEFAULT_FREEZER_DEBOUNCE_TIMEOUT = 600_000L // 10 分钟

总结

应用冻结机制是 Android 系统为优化资源使用而引入的重要特性。对于需要后台长期运行的应用,选择合适的防冻结方案至关重要:

  1. 系统应用: 优先使用 ContentProvider 方案,改动小、效果好、完全可控
  2. 第三方应用: 使用 前台服务方案,符合 Android 规范,用户体验好
  3. 客供应用: 可联系厂商加入 省电白名单,无需修改代码
  4. 核心系统服务: 考虑 Persistent 方案,但需谨慎使用

无论选择哪种方案,都应该充分理解其原理和限制,在保证功能的同时,兼顾系统资源和用户体验。


文档版本: 1.0
最后更新: 2025-01-15
作者: Android Framework 团队
适用版本: Android 11+