Как работать с кешированными и замороженными приложениями

При использовании Binder для связи между процессами будьте особенно осторожны, когда удаленный процесс находится в кешированном или замороженном состоянии. Вызовы кешированных или замороженных приложений могут привести к их сбою или ненужному потреблению ресурсов.

Состояния приложений, сохраненные в кеше и замороженные

Android поддерживает приложения в разных состояниях, чтобы управлять системными ресурсами, такими как память и ЦП.

Состояние в кеше

Если в приложении нет видимых пользователю компонентов, таких как действия или сервисы, оно может быть переведено в кешированное состояние. Подробнее о процессах и жизненном цикле приложений… Приложения, сохраненные в кеше, остаются в памяти устройства на случай, если пользователь захочет вернуться к ним, но они не должны работать в фоновом режиме.

При связывании одного процесса приложения с другим, например с помощью bindService, статус серверного процесса повышается до уровня важности клиентского процесса (за исключением случаев, когда указан Context#BIND_WAIVE_PRIORITY). Например, если клиент не находится в кешированном состоянии, то и сервер тоже.

И наоборот, состояние серверного процесса не определяет состояние его клиентов. Таким образом, у сервера могут быть подключения Binder к клиентам, чаще всего в форме обратных вызовов, и пока удаленный процесс находится в кешированном состоянии, сервер не кешируется.

При разработке API, в которых обратные вызовы инициируются в процессе с повышенными привилегиями и передаются приложениям, рекомендуется приостанавливать отправку обратных вызовов, когда приложение переходит в состояние кэширования, и возобновлять ее, когда оно выходит из этого состояния. Это позволяет избежать ненужной работы в процессах приложений, хранящихся в кеше.

Чтобы отслеживать, когда приложения переходят в кешированное состояние или выходят из него, используйте ActivityManager.addOnUidImportanceListener:

Java

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

Kotlin

// in ActivityManager or Context
activityManager.addOnUidImportanceListener({ uid, importance ->
  // ...
}, IMPORTANCE_CACHED)

Замороженное состояние

Система может заморозить кешированное приложение, чтобы сэкономить ресурсы. Когда приложение заморожено, оно не получает процессорного времени и не может выполнять никакие действия. Подробнее о заморозке приложений в кеше…

Когда процесс отправляет синхронную (не oneway) транзакцию Binder другому удаленному процессу, который заморожен, система завершает удаленный процесс. Это предотвращает зависание вызывающего потока в вызывающем процессе на неопределенное время в ожидании разморозки удаленного процесса, что может привести к голоданию потока или взаимной блокировке в вызывающем приложении.

Когда процесс отправляет асинхронную (oneway) транзакцию связывания в замороженное приложение (обычно путем уведомления обратного вызова, который обычно является методом oneway), транзакция буферизуется до тех пор, пока удаленный процесс не будет разморожен. Если буфер переполнен, процесс приложения-получателя может завершиться сбоем. Кроме того, буферизованные транзакции могут устареть к тому времени, когда процесс приложения будет разморожен и обработает их.

Чтобы не перегружать приложения устаревшими событиями или не заполнять их буферы, вы должны приостановить отправку обратных вызовов, пока процесс приложения-получателя заморожен.

Чтобы отслеживать, когда приложения замораживаются или размораживаются, используйте IBinder.addFrozenStateChangeCallback:

Java

// 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
}

Kotlin

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

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

// Update remoteFrozen when the remote process freezes or unfreezes
binder.addFrozenStateChangeCallback(myExecutor) { isFrozen ->
    remoteFrozen.set(isFrozen)
}

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

C++

Для кода платформы, использующего API связывателя C++:

#include <binder/Binder.h>
#include <binder/IBinder.h>

// The binder token of the remote process
android::sp<android::IBinder> binder = service->getBinder();

// Keep track of frozen state
std::atomic<bool> remoteFrozen = false;

// Define a callback class
class MyFrozenStateCallback : public android::IBinder::FrozenStateChangeCallback {
public:
    explicit MyFrozenStateCallback(std::atomic<bool>* frozenState) : mFrozenState(frozenState) {}
    void onFrozenStateChanged(bool isFrozen) override {
        mFrozenState->store(isFrozen);
    }
private:
    std::atomic<bool>* mFrozenState;
};

// Update remoteFrozen when the remote process freezes or unfreezes
if (binder != nullptr) {
    binder->addFrozenStateChangeCallback(android::sp<android::IBinder::FrozenStateChangeCallback>::make(
        new MyFrozenStateCallback(&remoteFrozen)));
}

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

Как использовать RemoteCallbackList

Класс RemoteCallbackList помогает управлять списками обратных вызовов IInterface, которые регистрируют удаленные процессы. Этот класс автоматически обрабатывает уведомления о завершении работы связывателя и предоставляет варианты обработки обратных вызовов для замороженных приложений.

При создании RemoteCallbackList можно указать замороженное правило вызываемого абонента:

  • FROZEN_CALLEE_POLICY_DROP: обратные вызовы в зависшие приложения игнорируются. Используйте это правило, если события, произошедшие, когда приложение было в кеше, не важны для него, например события датчиков в реальном времени.
  • FROZEN_CALLEE_POLICY_ENQUEUE_MOST_RECENT: если во время заморозки приложения транслируется несколько обратных вызовов, в очередь добавляется только последний из них, и он выполняется, когда приложение размораживается. Это полезно для обратных вызовов на основе состояния, когда важно только последнее обновление состояния, например обратный вызов, уведомляющий приложение о текущей громкости мультимедиа.
  • FROZEN_CALLEE_POLICY_ENQUEUE_ALL: все обратные вызовы, транслируемые, когда приложение заморожено, ставятся в очередь и выполняются, когда приложение размораживается. Будьте осторожны при использовании этого правила, поскольку оно может привести к переполнению буфера, если в очередь будет добавлено слишком много обратных вызовов, или к накоплению устаревших событий.

В следующем примере показано, как создать и использовать экземпляр RemoteCallbackList, который отбрасывает обратные вызовы в замороженные приложения:

Java

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

Kotlin

val callbacks =
    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.getCallingUid(), чтобы получить UID и сопоставить его с состоянием важности, отслеживаемым слушателем. Это позволяет системным службам узнать, находится ли вызывающее приложение в кэшированном состоянии.

  • IBinder.addFrozenStateChangeCallback. Когда системный сервис получает объект binder от приложения (например, в рамках регистрации обратных вызовов), он должен зарегистрировать FrozenStateChangeCallback в этом конкретном экземпляре IBinder. Это позволяет напрямую уведомлять системную службу о том, что процесс приложения, в котором размещен связыватель, заморожен или разморожен.

Рекомендации для системных сервисов

Мы рекомендуем всем системным сервисам, которые могут взаимодействовать с приложениями, отслеживать состояние кеширования и заморозки процессов приложений, с которыми они обмениваются данными. Если этого не сделать, возможны следующие последствия:

  • Потребление ресурсов. Выполнение задач для приложений, которые находятся в кеше и не видны пользователю, может привести к нерациональному использованию системных ресурсов.
  • Сбои в работе приложений. Синхронные вызовы связывателя к замороженным приложениям приводят к сбоям в их работе. Асинхронные вызовы связывателя в замороженные приложения приводят к сбою, если переполняется буфер асинхронных транзакций.
  • Неожиданное поведение приложений. Размороженные приложения сразу же получают все буферизованные асинхронные транзакции Binder, отправленные им, пока они были заморожены. Приложения могут находиться в замороженном состоянии неограниченное время, поэтому буферизованные транзакции могут быть очень старыми.

Системные сервисы часто используют RemoteCallbackList для управления удаленными обратными вызовами и автоматической обработки неактивных процессов. Чтобы обрабатывать замороженные приложения, расширьте существующее использование RemoteCallbackList, применив правило замороженного вызываемого объекта, как описано в разделе Использование RemoteCallbackList.