处理已缓存和冻结的应用

使用 binder 在进程之间进行通信时,如果远程进程处于缓存或冻结状态,请特别注意。对已缓存或冻结的应用进行调用可能会导致这些应用崩溃或不必要地消耗资源。

缓存和冻结的应用状态

Android 会将应用保持在不同的状态,以管理内存和 CPU 等系统资源。

缓存状态

当应用没有用户可见的组件(例如 activity 或服务)时,可以将其移至缓存状态。如需了解详情,请参阅进程和应用生命周期。缓存的应用会保留在内存中,以防用户切换回这些应用,但这些应用不应处于活跃工作状态。

当从一个应用进程绑定到另一个应用进程时(例如使用 bindService),服务器进程的进程状态会提升到至少与客户端进程一样重要(除非指定了 Context#BIND_WAIVE_PRIORITY)。例如,如果客户端未处于缓存状态,则服务器也不会处于缓存状态。

相反,服务器进程的状态不会决定其客户端的状态。因此,服务器可能与客户端存在 binder 连接(最常见的是以回调的形式),并且当远程进程处于缓存状态时,服务器不会被缓存。

在设计回调源自提升权限的进程并传递给应用的 API 时,请考虑在应用进入缓存状态时暂停调度回调,并在应用退出此状态时恢复调度。这样可以避免在缓存的应用进程中执行不必要的工作。

如需跟踪应用何时进入或退出缓存状态,请使用 ActivityManager.addOnUidImportanceListener

// in ActivityManager or Context
activityManager.addOnUidImportanceListener(
    new UidImportanceListener() { ... },
    IMPORTANCE_CACHED);

冻结状态

系统可以冻结缓存的应用,以节省资源。应用被冻结后,将无法获得 CPU 时间,也无法执行任何工作。如需了解详情,请参阅缓存应用冷冻器

当某个进程向另一个处于冻结状态的远程进程发送同步(非 oneway)binder 事务时,系统会终止该远程进程。这样可防止调用进程中的调用线程在等待远程进程解除冻结时无限期挂起,从而避免调用应用中出现线程饥饿或死锁。

当进程向冻结的应用发送异步 (oneway) binder 事务时(通常是通过通知回调,而回调通常是 oneway 方法),该事务会被缓冲,直到远程进程解除冻结。如果缓冲区溢出,接收方应用进程可能会崩溃。此外,当应用进程解冻并处理缓冲的交易时,这些交易可能已过时。

为避免应用因过时的事件而不堪重负或其缓冲区溢出,在接收方应用的进程冻结时,您必须暂停调度回调。

如需跟踪应用何时被冻结或解冻,请使用 IBinder.addFrozenStateChangeCallback

// The binder token of the remote process
IBinder binder = service.getBinder();

// Keep track of frozen state
AtomicBoolean remoteFrozen = new AtomicBoolean(false);

// Update remoteFrozen when the remote process freezes or unfreezes
binder.addFrozenStateChangeCallback(
    myExecutor,
    new IBinder.FrozenStateChangeCallback() {
        @Override
        public void onFrozenStateChanged(boolean isFrozen) {
            remoteFrozen.set(isFrozen);
        }
    });

// When dispatching callbacks to the remote process, pause dispatch if frozen:
if (!remoteFrozen.get()) {
    // dispatch callback to remote process
}

使用 RemoteCallbackList

RemoteCallbackList 类是一个辅助类,用于管理远程进程注册的 IInterface 回调列表。此类会自动处理 binder 死亡通知,并提供用于处理对冻结应用的调用的选项。

构建 RemoteCallbackList 时,您可以指定冻结的被调用方政策:

  • FROZEN_CALLEE_POLICY_DROP:系统会默默舍弃对冻结应用的回调。 如果应用在缓存期间发生的事件对应用并不重要,例如实时传感器事件,请使用此政策。
  • FROZEN_CALLEE_POLICY_ENQUEUE_MOST_RECENT:如果应用处于冻结状态时广播了多个回调,则只有最近的回调会被加入队列,并在应用解除冻结状态时传送。这对于基于状态的回调非常有用,因为在这种情况下,只有最新的状态更新才重要,例如,通知应用当前媒体音量的回调。
  • FROZEN_CALLEE_POLICY_ENQUEUE_ALL:当应用处于冻结状态时,所有广播的回调都会排队,并在应用解除冻结状态时传送。请谨慎使用此政策,因为如果排队的回调过多,可能会导致缓冲区溢出或过时事件累积。

以下示例展示了如何创建和使用会舍弃对冻结应用的回调的 RemoteCallbackList 实例:

RemoteCallbackList<IMyCallbackInterface> callbacks =
        new RemoteCallbackList.Builder<IMyCallbackInterface>(
                        RemoteCallbackList.FROZEN_CALLEE_POLICY_DROP)
                .setExecutor(myExecutor)
                .build();

// Registering a callback:
callbacks.register(callback);

// Broadcasting to all registered callbacks:
callbacks.broadcast((callback) -> callback.onSomeEvent(eventData));

如果您使用 FROZEN_CALLEE_POLICY_DROP,则只有在托管回调的进程未冻结时,系统才会调用 callback.onSomeEvent()

系统服务和应用互动

系统服务通常使用 binder 与许多不同的应用进行交互。由于应用可能会进入缓存和冻结状态,因此系统服务必须特别注意妥善处理这些互动,以帮助保持系统稳定性和性能。

系统服务已经需要处理因各种原因导致应用进程被终止的情况。这包括停止代表他们执行工作,以及不再尝试向已终止的进程继续传递回调。考虑应用被冻结的情况是现有监控责任的延伸。

从系统服务跟踪应用状态

system_server 中运行或作为原生守护程序运行的系统服务也可以使用前面介绍的 API 来跟踪应用进程的重要性和冻结状态:

  • ActivityManager.addOnUidImportanceListener:系统服务可以注册监听器来跟踪 UID 重要性变化。当从应用接收到 binder 调用或回调时,服务可以使用 Binder.getCallingUid() 获取 UID,并将其与监听器跟踪的重要性状态相关联。这可让系统服务知道调用应用是否处于缓存状态。

  • IBinder.addFrozenStateChangeCallback:当系统服务从应用接收到 binder 对象(例如,作为回调注册的一部分)时,应在该特定 IBinder 实例上注册 FrozenStateChangeCallback。当托管相应 binder 的应用进程冻结或解冻时,直接通知系统服务。

针对系统服务的建议

我们建议所有可能与应用互动的系统服务跟踪其通信的应用进程的缓存和冻结状态。否则可能会导致:

  • 资源消耗:为已缓存且用户不可见的应用执行工作可能会浪费系统资源。
  • 应用崩溃:对冻结应用的同步 binder 调用会导致应用崩溃。 如果冻结应用的异步 binder 调用导致其异步事务缓冲区溢出,则会导致崩溃。
  • 意外的应用行为:解冻的应用会立即接收在冻结期间发送给它们的任何已缓冲的异步 binder 事务。应用可以在冻结状态下停留无限期的时间,因此缓冲的事务可能会非常过时。

系统服务通常使用 RemoteCallbackList 来管理远程回调并自动处理已终止的进程。如需处理冻结的应用,请按照使用 RemoteCallbackList中所述,应用冻结的被调用方政策,从而扩展 RemoteCallbackList 的现有用法。