서비스 바인딩 및 프로세스 상태

Android의 앱 프로세스는 격리된 상태로 존재하지 않습니다. 애플리케이션은 다른 애플리케이션이나 시스템 자체에서 제공하는 서비스를 사용하는 경우가 많습니다. 한 프로세스가 서비스 바인딩을 통해 다른 프로세스에 연결되면 Android 프레임워크가 메모리를 관리하는 방식에 큰 영향을 미치는 종속 항목이 생성됩니다.

프로세스 상태 및 OOM 점수

Android 프레임워크는 프로세스 상태를 사용하여 실행 중인 각 프로세스의 중요도를 추적합니다. 이러한 상태는 OomAdjuster에서 -1000~1000 범위의 OOM 점수 조정 (oom_score_adj) 값을 할당하는 데 사용됩니다.

oom_score_adj가 낮을수록 프로세스가 더 중요하고 메모리 부족 종료 (LMK)에 의해 종료될 가능성이 적습니다.

일반적인 프로세스 상태

다음 표에는 가장 일반적인 프로세스 상태와 일반적인 oom_score_adj 값이 나와 있습니다. 최신 전체 목록은 Android 소스 코드의 android.app.ActivityManager 및 com.android.server.am.psc.Constants를 참고하세요.

프로세스 상태 (약어) 설명 일반 oom_score_adj
PER (영구) 항상 실행되어야 하는 시스템 프로세스 (예: 전화 통신) -800
TOP 사용자가 현재 상호 작용하고 있는 프로세스입니다. 0
VIS (표시) 프로세스에 표시되는 활동이 있습니다 (예: 반투명 대화상자 뒤). 100
PERC (지각 가능) 사용자가 알고 있는 백그라운드 프로세스 (예: 음악 재생) 200
FGS 포그라운드 서비스를 호스팅하는 프로세스입니다. 0~200 (다양함)
BTOP (바운드 상단) 상위 애플리케이션에 의해 바인드된 프로세스입니다. 100
BFGS 바인드된 포그라운드 서비스 (일반적으로 시스템 바인드) 0
PREV (이전) 사용자가 현재 프로세스 전에 있었던 마지막 프로세스입니다. 700
CACHED 안전하게 종료할 수 있는 백그라운드 앱입니다. 900~999

서비스 결합의 영향

클라이언트 프로세스 (예: TOP 상태의 앱)가 서버 프로세스의 서비스에 바인드되면 서버 프로세스는 종종 높은 우선순위를 상속합니다. 이렇게 하면 클라이언트가 필요로 하는 한 서비스가 계속 제공됩니다.

system_server를 통해 bindService()를 호출하여 프로세스 B를 BTOP로 승격하는 프로세스 A(상단)를 보여주는 다이어그램

BIND 플래그로 상속 제어

Context.BIND_AUTO_CREATE를 사용할 때 상속이 기본 동작입니다. 하지만 개발자는 bindService()의 다양한 플래그를 사용하여 바인딩이 타겟 프로세스의 중요도에 미치는 영향을 제어할 수 있습니다.

OOM 점수의 주요 바인드 플래그

다음 플래그는 시스템 전체 메모리 부족을 관리할 때 가장 관련성이 높습니다.

  • BIND_AUTO_CREATE: 가장 일반적인 플래그입니다. 바인딩이 있는 한 서비스 프로세스가 시작되고 활성 상태로 유지됩니다. 기본적으로 클라이언트와 일치하도록 서버 프로세스 우선순위도 높입니다.
  • BIND_NOT_FOREGROUND: 타겟 서비스의 프로세스가 포그라운드 일정 우선순위 (CPU 우선순위)로 올라가지 않도록 합니다. 하지만 메모리 우선순위 (oom_score_adj)는 여전히 허용됩니다. 이는 CPU 사이클을 위해 UI와 경쟁해서는 안 되지만 종료되지 않도록 보호해야 하는 백그라운드 작업에 유용합니다.
  • BIND_WAIVE_PRIORITY: 시스템이 타겟 프로세스의 예약 또는 메모리 관리 우선순위에 영향을 주지 않도록 지시하는 매우 강력한 플래그입니다. 서비스 프로세스는 LRU 목록의 일반 백그라운드 프로세스인 것처럼 관리되므로 바인드된 동안에도 OOM 종료가 가능합니다.
  • 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은 한 프로세스에서 기본 UI를 실행하고 :remote 프로세스에서 실행되는 RemoteService이 있습니다.

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 프로세스가 실행 중이며 oom_score_adj이 100인 BTOP (바운드 TOP) 상태입니다. 이는 일반적인 백그라운드 서비스 (500 이상)보다 훨씬 더 보호됩니다. <=Proc{...} 표기법은 이 우선순위 상승을 담당하는 프로세스를 보여줍니다.

5. 백그라운드로 보내기

기기에서 홈 버튼을 누릅니다. 상태를 다시 확인합니다.

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

이제 클라이언트 프로세스가 더 이상 TOP이 아니므로 두 프로세스 모두 우선순위가 낮은 상태 (PREV/oom_score_adj 700)로 전환되었습니다. (참고: 상태 덤프의 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을 사용하면 UI 프로세스와 관련하여 원격 프로세스의 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 로드, 런타임 초기화, 애플리케이션 객체 생성)의 비용이 많이 드는 오버헤드를 방지할 수 있습니다. 백그라운드 이벤트 스트림을 처리해야 하는 앱의 빈번한 콜드 스타트를 방지하기 위한 다른 바인딩도 있습니다.

다음은 일반적인 기기에서 관찰할 수 있는 실제 사례입니다.

VoiceInteractor

사용자는 디지털 어시스턴트가 휴대전화 OS에 내장되어 음성 핫워드나 빠른 입력 동작으로 즉시 호출할 수 있고 상호작용이 원활하고 매끄럽기를 기대합니다.

어시스턴트 트리거 (예: Google Pixel 휴대전화의 'OK Google' 핫워드)가 발생하면 디지털 어시스턴트가 즉시 응답해야 합니다. 이를 위해 system_server는 사용자가 선택한 음성 상호작용 서비스에 대한 영구 바인딩을 유지합니다.

Google 앱의 인터랙터 프로세스에 바인딩된 system_server를 보여주는 다이어그램

프로세스 상태를 확인하면 (예: dumpsys activity processes 사용) BFGS (바인드된 포그라운드 서비스) 상태의 com.google.android.googlequicksearchbox:interactor와 같은 프로세스가 system_server (UID 1000)의 바인딩에 의해 활성 상태로 유지되는 것을 확인할 수 있습니다.

NotificationListenerService

일부 시스템-앱 바인딩의 목표는 지연 시간이 아니라 빈번한 콜드 스타트를 방지하는 것입니다. NotificationListenerService은 새 알림이 게시되거나 삭제될 때 시스템으로부터 호출을 수신하는 서비스로, 대표적인 예입니다. 일반적인 스마트폰 사용자는 하루 종일 수백 개의 알림을 받을 수 있습니다. 시스템이 알림 리스너에서 바인드 해제되면 해당 앱의 프로세스가 캐시된 상태로 전환될 가능성이 높고 LMK에 의해 종료될 수 있습니다.

다음 알림이 도착하면(몇 초 후일 수 있음) 시스템은 이벤트를 전달하기 위해 앱 프로세스를 처음부터 다시 콜드 스타트해야 합니다. 이러한 종료 및 콜드 스타트의 지속적인 사이클은 프로세스를 백그라운드에서 바인딩되고 활성 상태로 유지하는 것보다 훨씬 더 많은 CPU와 배터리를 소비합니다.

런처의 '-1 화면' (뉴스 피드)

최신 런처 앱은 일반적으로 핵심 탐색 기능 (홈 아이콘 및 위젯)을 런처 화면 중 하나에서 사용할 수 있고 런처 UX와 원활하게 통합되는 뉴스 피드와 결합합니다. 뉴스 피드는 다른 앱에서 제공할 수 있습니다. 예를 들어 Google Pixel에서는 런처가 Google 앱에서 제공하는 피드와 통합됩니다.

홈 화면에서 왼쪽으로 스와이프하여 뉴스 피드를 볼 때 전환이 원활해야 합니다. 런처는 뉴스 피드를 제공하는 앱의 서비스 인터페이스에 바인딩하고 런처가 활성 상태인 동안 바인딩을 활성 상태로 유지하여 이를 달성합니다. 이렇게 하면 피드 콘텐츠가 메모리에 렌더링되어 표시되지 않을 때도 준비된 상태로 유지됩니다.

기타 일반적인 예

  • 런처 (HOME_APP_ADJ): 런처 앱 (홈)에는 우선순위 목록에 자체 특수 슬롯이 있습니다. 서비스에 항상 바인딩되는 것은 아니지만 HOME_APP_ADJ (일반적으로 600)이 할당됩니다. 사용자가 자주 돌아오기 때문에 시스템은 런처를 활성 상태로 유지하는 것을 선호합니다. 실제로 시스템은 런처를 종료하는 것보다 이전에 사용한 앱 (PREV_APP_ADJ = 700)을 종료하는 것을 선호합니다. 런처를 종료하면 사용자가 앱을 종료할 때 런처가 콜드 스타트될 때까지 기다려야 하므로 사용자 환경이 느려지기 때문입니다.
  • 입력 방식 편집기 (IME): 입력 시 시스템은 선택한 키보드 앱 (예: Gboard)에 바인딩됩니다. 이렇게 하면 키보드가 일시적으로 숨겨지더라도 키보드 프로세스가 상승된 상태로 유지됩니다. 이렇게 하면 다른 텍스트 필드를 탭할 때 키보드가 즉시 다시 표시됩니다.
  • NFC 결제: 결제를 위해 휴대전화를 탭하면 시스템이 NFC 결제 서비스 (예: Google 월렛)에 바인딩됩니다. 이러한 거래에는 판매자 터미널의 엄격한 실시간 요구사항이 있는 경우가 많습니다. 결제 앱을 콜드 스타트해야 하는 경우 트랜잭션 시간이 초과되어 실패할 수 있습니다.

절충점 및 성능 급락

바인딩은 성능과 정확성에 필요하지만 시스템 메모리 상태에 비용이 발생합니다.

  • 유연성 감소: 바인드된 모든 프로세스는 LMK가 쉽게 종료할 수 없는 프로세스입니다. 이렇게 하면 시스템에서 압박을 받는 상황에서 메모리를 확보하는 데 사용할 수 있는 캐시된 프로세스의 '쿠션'이 줄어듭니다.
  • 성능 급락 악화: 너무 많은 프로세스가 바인드되면 시스템에 종료 가능한 백그라운드 프로세스가 거의 없을 수 있습니다. 메모리 압력이 증가하면 시스템은 더 중요한 프로세스를 강제 종료하거나 페이지 캐시를 스래싱해야 하므로 훨씬 빠르게 '성능 절벽에서 떨어집니다'.

일반적인 서비스 결합 안티패턴

서비스 바인딩은 oom_score_adj를 직접 상승시키므로 바인딩을 획득하거나 구조화하는 방식에 미묘한 수명 주기 실수가 있으면 몇 시간 동안 권한이 있는 상태 (BTOP, BFGS 또는 PERC)에서 많은 양의 메모리가 고정될 수 있습니다. 바인드된 서비스를 설계하거나 감사할 때 다음과 같은 일반적인 안티 패턴을 주의하세요.

장기 실행 클라이언트에서 unbindService()를 잊음

Application 싱글톤, 백그라운드 관리자 또는 onStop()에 일치하는 unbindService() 없이 onStart()에서 bindService()를 호출하는 Activity에서 서비스에 바인딩하면 ServiceConnection가 누수됩니다.

바인딩이 활성 상태로 유지되는 한 타겟 프로세스는 클라이언트의 상승된 우선순위를 상속합니다. 클라이언트가 영구 시스템 구성요소나 포그라운드 앱인 경우 바인드된 서비스 프로세스가 BFGS 또는 BTOP에 무기한 고정되어 서비스가 완전히 유휴 상태일 때도 LMK가 메모리를 회수하지 못합니다 (procstats에서 100% 에 가깝게 표시됨).

해결 방법: 바인딩 범위를 바인딩이 필요한 구성요소의 수명 주기로 엄격하게 제한하거나 비활성 기간이 지난 후 unbindService()를 호출하는 유휴 시간 제한을 구현합니다. 개별 RPC 호출마다 바인딩 해제 및 재바인딩을 피하세요. 이렇게 하면 프로세스 스래싱과 반복되는 바인더 설정 오버헤드가 발생합니다. 대신 짧은 유휴 타이머 (예: 5~30초) 뒤에 작업 버스트를 병합하세요.

메모리 집약적인 UI와 바인드된 서비스 공동 배치

기본적으로 APK의 모든 구성요소는 동일한 프로세스에서 실행됩니다. 앱이 기본 Activity과 동일한 프로세스에서 경량 바인드 서비스 (예: NotificationListenerService, 위젯 제공자 또는 시스템이나 런처가 바인드하는 플러그인 서비스)를 노출하는 경우 전체 프로세스가 서비스의 상승된 상태 (BFGS 또는 PERC, 일반적으로 200 이하의 oom_score_adj)를 상속합니다.

사용자가 앱의 UI를 열면 프로세스에서 큰 뷰 계층 구조, 디코딩된 비트맵, 그래픽 버퍼를 할당합니다. 사용자가 이동하면 활성 서비스 바인딩으로 인해 프로세스가 상승된 상태로 유지되므로 프로세스가 CACHED (900 이상의 oom_score_adj)로 떨어지지 않습니다. 이로 인해 다음과 같은 두 가지 문제가 발생합니다.

  • 백그라운드 메모리 압축 없음: 시스템의 CachedAppOptimizer는 프로세스가 CACHED 상태로 전환된 후에만 압축합니다.
  • 캐시된 LRU 메모리 트림 또는 LMK 회수 없음: 서비스 결합이 프로세스를 상승된 상태로 유지하는 동안 시스템은 캐시된 LRU 목록(TRIM_MEMORY_BACKGROUND 이상)에 연결된 백그라운드 트림 콜백을 전달하지 않습니다. 앱이 TRIM_MEMORY_UI_HIDDEN 또는 Activity.onStop()에서 UI 리소스를 명시적으로 해제하지 않으면 LMK가 쉽게 회수할 수 없는 권한이 있는 OOM 점수에서 최대 UI 할당이 RAM에 고정됩니다.

해결 방법: UI가 더 이상 표시되지 않을 때 Activity.onStop(), ComponentCallbacks2.onTrimMemory(TRIM_MEMORY_UI_HIDDEN), Application.ActivityLifecycleCallbacks 또는 ProcessLifecycleOwner를 사용하여 UI 캐시, 디코딩된 비트맵, 뷰 참조를 명시적으로 삭제합니다. 또는 시스템이 기본 UI 프로세스를 CACHED 상태로 이동하여 메모리를 독립적으로 압축하거나 회수할 수 있도록 android:process 매니페스트 속성을 사용하여 항상 바인드된 서비스를 별도의 경량 프로세스로 이동합니다.

우선순위 포기 플래그 누락

BIND_AUTO_CREATE만으로 bindService()를 호출하면 호출자의 전체 일정 및 메모리 우선순위가 타겟 서비스로 트랜스퍼됩니다. 포그라운드 앱이 BIND_AUTO_CREATE만 사용하여 백그라운드 분석, 로깅 또는 미리 가져오기 서비스에 바인드되면 의도치 않게 해당 백그라운드 작업자가 BTOP로 승격됩니다.

해결 방법: 포그라운드 UI와 동일한 보호 수준이 필요하지 않은 보조 서비스 또는 최선을 다하는 서비스에 바인딩할 때는 BIND_AUTO_CREATE를 BIND_WAIVE_PRIORITY, BIND_NOT_FOREGROUND 또는 BIND_NOT_PERCEPTIBLE와 결합하여 시스템이 캐시된 LRU 목록에서 타겟 프로세스를 계속 관리할 수 있도록 합니다.


← 지역 | ↑ 위로 | 시스템 전체 →