Привязки сервисов и состояния процессов

Приложения на Android не существуют изолированно. Часто они зависят от сервисов, предоставляемых другими приложениями или самой системой. Когда один процесс соединяется с другим через Service Binding , это создает зависимость, которая оказывает существенное влияние на то, как платформа Android управляет памятью.

Состояния процесса и показатели нехватки памяти

В Android-фреймворке используются состояния процессов для отслеживания важности каждого запущенного процесса. Затем эти состояния используются компонентом OomAdjuster для присвоения значения корректировки оценки нехватки памяти ( oom_score_adj ), варьирующегося от -1000 до 1000.

Более низкое oom_score_adj означает, что процесс более важен и с меньшей вероятностью будет завершен механизмом Low Memory Killer (LMK).

Общие состояния процесса

В следующей таблице показаны некоторые из наиболее распространенных состояний процесса и их типичные значения oom_score_adj . Полный и актуальный список см. в файлах android.app.ActivityManager и com.android.server.am.psc.Constants в исходном коде Android.

Состояние процесса (сокр.) Описание Типичная oom_score_adj
PER (Постоянный) Системные процессы, которые должны работать постоянно (например, телефония). -800
ВЕРШИНА Процесс, с которым пользователь в данный момент взаимодействует. 0
ВИС (Видимый) Процесс включает в себя видимую активность (например, за полупрозрачным диалогом). 100
PERC (ощутимый) Фоновый процесс, о котором пользователь знает (например, воспроизведение музыки). 200
ФГС Процесс, в котором размещена служба переднего плана. От 0 до 200 (варьируется)
BTOP (Bound Top) Процесс, связанный с приложением TOP. 100
БФГС Связанная служба переднего плана (обычно привязанная к системе). 0
ПРЕДЫДУЩИЙ (Previous) Последний процесс, в котором пользователь находился перед текущим. 700
КЭШИРОВАНО Фоновые приложения, которые можно безопасно завершить. 900–999

Влияние привязки сервисов

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

Диаграмма, показывающая вызов bindService() процессом A (TOP) через system_server, повышение привилегий процесса B до BTOP.

Управление наследованием с помощью флагов BIND

Наследование является поведением по умолчанию при использовании Context.BIND_AUTO_CREATE . Однако разработчики могут управлять тем, как привязка влияет на важность целевого процесса, используя различные флаги в bindService() .

Ключевые флаги BIND для оценки нехватки памяти (OOM).

При управлении нехваткой памяти в масштабах всей системы наиболее актуальны следующие флаги:

  • BIND_AUTO_CREATE : Наиболее распространенный флаг. Он гарантирует запуск и поддержание работоспособности процесса службы до тех пор, пока существует привязка. По умолчанию он также повышает приоритет процесса сервера до уровня клиента.
  • BIND_NOT_FOREGROUND : Предотвращает повышение приоритета планирования процесса целевой службы до приоритета планирования на переднем плане (приоритет ЦП). Однако при этом сохраняется возможность повышения приоритета памяти ( oom_score_adj ). Это полезно для фоновых задач, которые не должны конкурировать с пользовательским интерфейсом за ресурсы ЦП, но при этом должны быть защищены от завершения.
  • BIND_WAIVE_PRIORITY : Очень важный флаг, который указывает системе не влиять на приоритет планирования или управления памятью целевого процесса. Сервисный процесс будет управляться как обычный фоновый процесс в списке LRU, что делает его доступным для завершения из-за нехватки памяти даже во время привязки.
  • BIND_ABOVE_CLIENT : Указывает, что служба важнее самого клиентского приложения. Когда системе необходимо освободить память, она предпочтет завершить работу клиентского приложения, а не связанной службы. Это «более надежный» вариант, чем BIND_AUTO_CREATE поскольку он обеспечивает дополнительный уровень защиты службы за счет клиента.
  • BIND_NOT_PERCEPTIBLE : Снижает важность целевой службы до уровня ниже PERCEPTIBLE , позволяя системе освободить память для более важных процессов, воспринимаемых пользователем.

Практическое занятие: наблюдение за эффектами связывания.

Мы воспользуемся приложением MemoryLab , чтобы продемонстрировать, как привязка из приложения TOP влияет на состояние отдельного процесса.

1. Запустите MemoryLab

Следующая команда запускает приложение. После открытия убедитесь, что приложение остается на переднем плане (пока не нажимайте кнопку «Домой» и не переключайтесь между приложениями).

adb shell am start -n com.android.memorylab/.MainActivity

2. Идентификация процессов

Перед привязкой проверьте состояние процесса. MemoryLab запускает свой основной пользовательский интерфейс в одном процессе, а RemoteService работает в процессе с параметром :remote .

adb shell dumpsys activity processes com.android.memorylab

Пример выходного фрагмента:

  Process OOM control (154 total, non-act at 7, non-svc at 7):
    Proc #0: fg       T/A/TOP  LCMNFUATI  t: 0 13470:com.android.memorylab/u0a417 (top-activity)
        oom: max=1001 curRaw=0 setRaw=0 cur=0 set=0
        state: cur=TOP  set=TOP   lastRss=0.00 lastCachedRss=0.00

В состоянии TOP вы увидите основной процесс com.android.memorylab . Процесс :remote еще не запущен.

3. Привязка триггера

Отправьте широковещательное сообщение в приложение, чтобы активировать привязку сервиса:

adb shell am broadcast -a com.android.memorylab.LEAK_BINDER

4. Наблюдайте повышенное состояние

Проверьте еще раз состояния процесса:

adb shell dumpsys activity processes | grep -A 10 "com.android.memorylab"

Пример выходного фрагмента:

  Proc #  1: vis      F/ /BTOP ---NFUATI  t: 0 13560:com.android.memorylab:remote/u0a417 (service)
    com.android.memorylab/.RemoteService<=Proc{13470:com.android.memorylab/u0a417}
    oom: max=1001 curRaw=100 setRaw=100 cur=100 set=100
    state: cur=BTOP set=BTOP  lastRss=0.00 lastCachedRss=0.00

Процесс :remote теперь запущен и находится в состоянии BTOP (Bound TOP) с oom_score_adj равным 100. Это значительно более защищено, чем типичная фоновая служба (которая имела бы приоритет 500 или выше). Обозначение <=Proc{...} показывает, какой процесс отвечает за это повышение приоритета.

5. Отправить в фоновый режим

Нажмите кнопку HOME на устройстве. Еще раз проверьте состояния:

adb shell dumpsys activity processes | grep -A 10 "com.android.memorylab"

Пример выходного фрагмента:

    Proc #  2: prev     b/ /LAST --------I  t: 0 13560:com.android.memorylab:remote/u0a417 (service)
        com.android.memorylab/.RemoteService<=Proc{13470:com.android.memorylab/u0a417}
        oom: max=1001 curRaw=700 setRaw=700 cur=700 set=700
        state: cur=LAST set=LAST  lastRss=0.00 lastCachedRss=0.00
    Proc #  1: prev     b/ /LAST --------I  t: 0 13470:com.android.memorylab/u0a417 (previous)
        oom: max=1001 curRaw=700 setRaw=700 cur=700 set=700
        state: cur=LAST set=LAST  lastRss=209MB lastCachedRss=0.00

Теперь оба процесса перешли в состояние с более низким приоритетом ( PREV / oom_score_adj 700 ), поскольку клиентский процесс больше не является TOP . (Примечание: LAST в дампе состояния относится к внутреннему состоянию LAST_ACTIVITY , которое соответствует PREV в сводках высокого уровня).

Анализ с помощью procstats

Инструмент procstats предоставляет исторический обзор этих штатов.

# View stats for MemoryLab over the last hour
adb shell dumpsys procstats --hours 1 com.android.memorylab

Пример выходного фрагмента:

  *   com.android.memorylab / u0a417 / v37:
      *   Prc com.android.memorylab / u0a417 / v37:
             TOTAL: 0.89% (0.00-0.00-0.00/0.00-0.00-0.00/210MB-210MB-210MB over 1)
               Top: 0.89% (0.00-0.00-0.00/0.00-0.00-0.00/210MB-210MB-210MB over 1)
      *   Prc com.android.memorylab:remote / u0a417 / v37:
             TOTAL: 0.19%
           Bnd Top: 0.19%

Здесь Bnd Top указывает процент времени, которое удаленный процесс провел в состоянии TOP , будучи привязанным к приложению.

Захват и анализ привязок с помощью Perfetto

В то время как dumpsys предоставляет моментальный снимок состояния, Perfetto позволяет увидеть точный момент возникновения привязки и то, как в реальном времени изменяется оценка нехватки памяти (OOM).

1. Запишите трассировку

Используйте конфигурацию, включающую linux.process_stats и категорию am atrace:

adb shell perfetto -c - --txt -o /data/misc/perfetto-traces/service_bindings.perfetto-trace <<EOF
buffers: { size_kb: 65536 }
data_sources: {
    config {
        name: "linux.process_stats"
        process_stats_config { proc_stats_poll_ms: 100 }
    }
}
data_sources: {
    config {
        name: "linux.ftrace"
        ftrace_config { ftrace_events: "am/am_proc_bound" }
    }
}
duration_ms: 15000
EOF

2. Запрос переходов оценки OOM

С помощью PerfettoSQL можно увидеть, как изменился показатель нехватки памяти (OOM) удаленного процесса относительно процесса пользовательского интерфейса:

SELECT ts, p.name, value AS oom_score_adj
FROM counter c
JOIN process_counter_track t ON c.track_id = t.id
JOIN process p USING (upid)
WHERE p.name LIKE 'com.android.memorylab%'
  AND t.name = 'oom_score_adj'
ORDER BY ts;

3. Определите события связывания.

Чтобы точно узнать, когда была установлена ​​зависимость привязки и какой процесс её инициировал, используйте следующий запрос:

SELECT
    s.ts,
    p.name AS process_name,
    t.name AS thread_name,
    s.name AS slice_name
FROM slice s
JOIN thread_track tt ON s.track_id = tt.id
JOIN thread t USING (utid)
JOIN process p USING (upid)
WHERE s.name LIKE 'bindService:{com.android.memorylab%';

Привязки системы к приложению

Сама система Android часто использует привязки к сервисам сторонних приложений для обеспечения основной функциональности. Зачастую целью таких привязок является снижение задержки . Поддерживая процесс активным и в памяти, система избегает дорогостоящих накладных расходов, связанных с «холодным запуском» (загрузка APK-файла, инициализация среды выполнения и создание объекта Application) при критически важном взаимодействии с пользователем. Существуют и другие привязки, предотвращающие частые «холодные запуски» для приложений, которым необходимо обрабатывать потоки фоновых событий.

Вот несколько реальных примеров, которые вы можете наблюдать на типичном устройстве:

VoiceInteractor

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

Когда срабатывает триггер голосового помощника (например, ключевое слово «OK Google» на телефонах Google Pixel), цифровой помощник должен отреагировать мгновенно. Для этого system_server поддерживает постоянную привязку к службе голосового взаимодействия , выбранной пользователем.

Диаграмма, показывающая привязку system_server к процессу взаимодействия приложения Google.

Если вы проверите состояния процессов (например, с помощью dumpsys activity processes ), вы можете увидеть процесс типа com.google.android.googlequicksearchbox:interactor в состоянии BFGS (Bound Foreground Service), поддерживаемый привязкой от system_server (UID 1000).

NotificationListenerService

Для некоторых системных привязок к приложениям цель состоит не в снижении задержки, а в предотвращении частых «холодных» перезапусков. Ярким примером является NotificationListenerService — служба, которая получает вызовы от системы при отправке или удалении новых уведомлений. Типичный пользователь смартфона может получать сотни уведомлений в течение дня. Если система отключится от обработчика уведомлений, процесс этого приложения, скорее всего, перейдет в кэшированное состояние и может быть завершен LMK.

Когда придёт следующее уведомление — возможно, через несколько секунд — системе придётся заново запускать процесс приложения, чтобы доставить это событие. Этот постоянный цикл завершения и перезапуска будет потреблять гораздо больше ресурсов процессора и заряда батареи, чем просто поддержание процесса в фоновом режиме.

Экран "-1" в лаунчере (лента новостей)

Современные приложения-лаунчеры обычно объединяют основные функции навигации (значки на главном экране и виджеты) с новостной лентой, доступной на одном из экранов лаунчера и органично интегрированной в пользовательский интерфейс лаунчера. Новостная лента может предоставляться другим приложением. Например, на Google Pixel лаунчер интегрируется с лентой, предоставляемой приложением Google.

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

Другие распространенные примеры

  • Лаунчер (HOME_APP_ADJ) : Приложение-лаунчер (Home) имеет свой собственный слот в списке приоритетов. Хотя оно не всегда привязано к службе, ему присваивается значение HOME_APP_ADJ (обычно 600 ). Система предпочитает поддерживать работу лаунчера, поскольку пользователь часто к нему возвращается. Фактически, система предпочла бы завершить работу ранее использованного приложения ( PREV_APP_ADJ = 700 ), чем завершить работу лаунчера, поскольку завершение работы лаунчера приведет к замедлению работы при выходе из любого приложения, так как пользователю придется ждать, пока лаунчер перезапустится.
  • Редактор методов ввода (IME) : Во время набора текста система привязывается к выбранному вами приложению клавиатуры (например, Gboard). Это позволяет поддерживать процесс клавиатуры в режиме повышенных прав, даже если клавиатура временно скрыта. Это гарантирует мгновенное повторное появление клавиатуры при нажатии на другое текстовое поле.
  • Платежи NFC : Когда вы прикладываете телефон для оплаты, система подключается к сервису NFC-платежей (например, Google Wallet). Эти транзакции часто предъявляют строгие требования к времени выполнения со стороны торгового терминала. Если платежному приложению потребуется «холодный запуск», транзакция может завершиться по истечении времени ожидания и не состояться.

Компромиссы и обрыв производительности

Хотя привязки необходимы для производительности и корректной работы, они негативно сказываются на состоянии памяти системы.

  • Сниженная гибкость : Каждый связанный процесс — это процесс, который LMK не может легко завершить. Это уменьшает «резерв» кэшированных процессов, который система может использовать для освобождения памяти под нагрузкой.
  • Усугубление проблемы резкого снижения производительности : если задействовано слишком много процессов, система может обнаружить, что у нее практически не осталось фоновых процессов, которые можно завершить. При увеличении нагрузки на память система «сорвется с обрыва производительности» гораздо быстрее, поскольку ей придется завершать более важные процессы или использовать кэш страниц.

Распространенные антипаттерны привязки услуг

Поскольку привязки сервисов напрямую повышают oom_score_adj , незначительные ошибки в жизненном цикле, связанные с получением или структурированием привязок, могут закрепить большие объемы памяти в привилегированных состояниях ( BTOP , BFGS или PERC ) на несколько часов. Обратите внимание на эти распространенные антипаттерны при проектировании или аудите привязанных сервисов.

В долгоживущих клиентах забыта unbindService()

Привязка к службе из синглтона Application , фонового менеджера или Activity , которая вызывает bindService() в onStart() без соответствующего unbindService() в onStop() приводит к утечке ServiceConnection .

Пока эта привязка остается активной, целевой процесс наследует повышенный приоритет клиента. Если клиент является постоянным системным компонентом или приложением переднего плана, привязанный процесс службы остается зафиксированным в BFGS или BTOP на неопределенный срок (отображаясь на уровне около 100% в procstats ), что предотвращает освобождение памяти LMK даже тогда, когда служба полностью простаивает.

Решение : Ограничьте область действия привязок жизненным циклом компонента, которому они необходимы, или реализуйте тайм-аут простоя, который вызывает unbindService() после периода бездействия. Избегайте отмены и повторной привязки при каждом отдельном вызове RPC, что приводит к перегрузке процесса и повторным накладным расходам на настройку Binder; вместо этого объединяйте всплески работы за коротким таймером простоя (например, от 5 до 30 секунд).

Размещение привязанного сервиса совместно с ресурсоемким пользовательским интерфейсом.

По умолчанию все компоненты APK работают в одном процессе. Если ваше приложение предоставляет легковесный связанный сервис (например, NotificationListenerService , поставщик виджетов или плагин, к которому привязывается система или средство запуска) в том же процессе, что и ваша основная Activity , весь процесс наследует повышенное состояние сервиса ( BFGS или PERC , обычно oom_score_adj равное 200 или ниже).

Когда пользователь открывает пользовательский интерфейс вашего приложения, процесс выделяет большие иерархии представлений, декодированные растровые изображения и графические буферы. Когда пользователь покидает приложение, процесс не переходит в CACHED ( oom_score_adj равно 900 или выше), поскольку активная привязка службы поддерживает процесс на повышенном уровне. Это приводит к двум взаимосвязанным проблемам:

  • Отсутствие фоновой компаундировки памяти : CachedAppOptimizer приложений системы выполняет компаундировку процессов только после того, как они переходят в состояние CACHED .
  • Отсутствие кэшированной LRU-очистки памяти или освобождения LMK : система не предоставляет обратные вызовы фоновой очистки, привязанные к кэшированному списку LRU ( TRIM_MEMORY_BACKGROUND и выше), пока привязка службы удерживает процесс в состоянии с повышенными правами. Если ваше приложение явно не освобождает ресурсы пользовательского интерфейса при TRIM_MEMORY_UI_HIDDEN или Activity.onStop() , пиковые выделения ресурсов пользовательского интерфейса остаются зафиксированными в оперативной памяти с привилегированным показателем нехватки памяти (OOM), где LMK не может легко их освободить.

Решение : Явно удаляйте кэш пользовательского интерфейса, декодированные растровые изображения и ссылки на представления, когда ваш пользовательский интерфейс перестает быть видимым, используя Activity.onStop() , ComponentCallbacks2.onTrimMemory(TRIM_MEMORY_UI_HIDDEN) , Application.ActivityLifecycleCallbacks или ProcessLifecycleOwner . В качестве альтернативы переместите постоянно привязанную службу в отдельный, легковесный процесс, используя атрибут android:process в манифесте, чтобы система могла перевести ваш основной процесс пользовательского интерфейса в состояние CACHED для независимой уплотнения или освобождения памяти.

Опускание флагов, отменяющих приоритет.

Вызов bindService() только с BIND_AUTO_CREATE передает весь приоритет планирования и памяти вызывающей стороны целевой службе. Когда приложение переднего плана привязывается к фоновой службе аналитики, логирования или предварительной выборки, используя только BIND_AUTO_CREATE , оно непреднамеренно повышает статус этого фонового рабочего процесса до BTOP .

Решение : При привязке к вспомогательным службам или службам с наилучшими усилиями, которым не требуется тот же уровень защиты, что и пользовательскому интерфейсу на переднем плане, объедините BIND_AUTO_CREATE с BIND_WAIVE_PRIORITY , BIND_NOT_FOREGROUND или BIND_NOT_PERCEPTIBLE , чтобы система могла по-прежнему управлять целевым процессом из кэшированного списка LRU.


← Местоположение | ↑ Вверх | В масштабах всей системы →