Handle cached and frozen apps

When using binder to communicate between processes, take special care when the remote process is in a cached or frozen state. Calls into cached or frozen apps can cause them to crash or consume resources unnecessarily.

Cached and frozen app states

Android maintains apps in different states to manage system resources like memory and CPU.

Cached state

When an app has no user-visible components, such as activities or services, it can be moved to the cached state. See Processes and app lifecycle for details. Cached apps are kept in memory in case the user switches back to them, but they aren't expected to be actively working.

When binding from one app process to another, such as using bindService, the process state of the server process is elevated to be at least as important as the client process (except when Context#BIND_WAIVE_PRIORITY is specified). For example, if the client isn't in the cached state, neither is the server.

Conversely, the state of a server process doesn't determine that of its clients. So a server might have binder connections to clients, most commonly in the form of callbacks, and while the remote process is in the cached state, the server isn't cached.

When designing APIs where callbacks originate in an elevated process and are delivered to apps, consider pausing dispatch of callbacks when an app enters the cached state, and resuming when it exits this state. This prevents unnecessary work in cached app processes.

To track when apps enter or exit the cached state, use ActivityManager.addOnUidImportanceListener:

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

Frozen state

The system can freeze a cached app to conserve resources. When an app is frozen, it receives zero CPU time and can't do any work. For more details, see Cached apps freezer.

When a process sends a synchronous (not oneway) binder transaction to another remote process that is frozen, the system kills the remote process. This prevents the calling thread in the calling process from hanging indefinitely while waiting for the remote process to be unfrozen, which could cause thread starvation or deadlocks in the calling app.

When a process sends an asynchronous (oneway) binder transaction to a frozen app (commonly by notifying a callback, which is typically a oneway method), the transaction is buffered until the remote process is unfrozen. If the buffer overflows, the recipient app process can crash. Additionally, buffered transactions might become stale by the time the app process is unfrozen and processes them.

To avoid overwhelming apps with stale events or overflowing their buffers, you must pause dispatching callbacks while the recipient app's process is frozen.

To track when apps are frozen or unfrozen, use 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
}

Use RemoteCallbackList

The RemoteCallbackList class is a helper for managing lists of IInterface callbacks that remote processes register. This class automatically handles binder death notifications and provides options for handling callbacks to frozen apps.

When building RemoteCallbackList, you can specify a frozen callee policy:

  • FROZEN_CALLEE_POLICY_DROP: Callbacks to frozen apps are silently dropped. Use this policy when events that happened while the app was cached aren't important to the app, for example, real-time sensor events.
  • FROZEN_CALLEE_POLICY_ENQUEUE_MOST_RECENT: If multiple callbacks are broadcast while an app is frozen, only the most recent one is enqueued and delivered when the app is unfrozen. This is useful for state-based callbacks where only the most recent state update matters, for example, a callback notifying the app of the current media volume.
  • FROZEN_CALLEE_POLICY_ENQUEUE_ALL: All callbacks broadcast while an app is frozen are enqueued and delivered when the app is unfrozen. Be cautious with this policy, because it can lead to buffer overflows if too many callbacks are enqueued, or to the accumulation of stale events.

The following example shows how to create and use a RemoteCallbackList instance that drops callbacks to frozen apps:

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));

If you use FROZEN_CALLEE_POLICY_DROP, the system invokes callback.onSomeEvent() only if the process hosting the callback isn't frozen.

System services and app interactions

System services often interact with many different apps using binder. Because apps can enter cached and frozen states, system services must take special care to handle these interactions gracefully to help maintain system stability and performance.

System services already need to handle situations where app processes are killed for various reasons. This involves stopping work on their behalf and not attempting to continue delivering callbacks to dead processes. The consideration of apps being frozen is an extension of this existing monitoring responsibility.

Track app states from system services

System services, running in system_server or as native daemons, can also use the APIs described earlier to track the importance and frozen state of app processes:

  • ActivityManager.addOnUidImportanceListener: System services can register a listener to track UID importance changes. When receiving a binder call or callback from an app, the service can use Binder.getCallingUid() to get the UID and correlate it with the importance state tracked by the listener. This lets system services know if the calling app is in a cached state.

  • IBinder.addFrozenStateChangeCallback: When a system service receives a binder object from an app (for example, as part of a registration for callbacks), it should register a FrozenStateChangeCallback on that specific IBinder instance. This directly notifies the system service when the app process hosting that binder becomes frozen or unfrozen.

Recommendations for system services

We recommend that all system services that might interact with apps track the cached and frozen state of the app processes they communicate with. Failing to do so can lead to:

  • Resource consumption: Performing work for apps that are cached and not user-visible can waste system resources.
  • App crashes: Synchronous binder calls to frozen apps crash them. Asynchronous binder calls to frozen apps lead to a crash if their async transaction buffer overflows.
  • Unexpected app behavior: Unfrozen apps wi immediately receive any buffered asynchronous binder transactions that were sent to them while they were frozen. Apps can spend an indefinite period in the freezer, so buffered transactions can be very stale.

System services often use RemoteCallbackList to manage remote callbacks and automatically handle dead processes. To handle frozen apps, extend the existing usage of RemoteCallbackList by applying a frozen callee policy as described in Use RemoteCallbackList.