간결한 프로세스로 백그라운드 작업 유지

많은 앱은 UI가 더 이상 표시되지 않을 때 대규모 애셋 팩 다운로드, 로컬 데이터베이스 동기화, 오디오 스트리밍, 운동 기록, 연결된 기기 세션 유지와 같은 합법적인 작업을 실행합니다. 이 백그라운드 작업이 포그라운드 UI 및 그래픽 엔진과 동일한 OS 프로세스 내에서 실행되면 전체 프로세스가 UI에서 할당한 모든 메모리와 함께 높은 중요도 수준(0과 250 사이의 oom_score_adj)으로 유지됩니다.

이 페이지에서는 이로 인해 앱과 기기의 나머지 부분에 어떤 문제가 발생하는지, android:process를 사용하여 백그라운드 작업을 전용 경량 프로세스로 이동하는 방법, adb shell 및 Perfetto를 사용하여 메모리 절약을 확인하는 방법을 설명합니다.

단일 프로세스 작업에서 UI 메모리를 유지하는 이유

Android는 개별 구성요소 (Activity, Service 또는 BroadcastReceiver)가 아닌 OS 프로세스 (pid)의 세분화 수준에서 메모리 및 프로세스 종료를 관리합니다. Activity Manager (OomAdjuster)는 각 프로세스에 해당 프로세스에서 가장 중요한 활성 구성요소 또는 수신 바인딩을 반영하는 단일 oom_score_adj 값을 할당합니다.

  • 사용자가 Activity와 상호작용하면 프로세스가 TOP 상태 (oom_score_adj = 0)로 실행됩니다.
  • 사용자가 다른 곳으로 이동하고 앱에 활성 백그라운드 구성요소가 없으면 프로세스는 PREV (oom_score_adj = 700) 또는 CACHED (oom_score_adj >= 900) 버킷으로 이동합니다. 캐시된 버킷에서 플랫폼은 cgroup v2 프리저로 프로세스를 고정하고 CachedAppOptimizer로 메모리를 압축하며 메모리 부족 시 lmkd가 프로세스를 먼저 종료하도록 합니다.
  • 그러나 동일한 프로세스가 포그라운드 서비스 (예: oom_score_adj = 200의 dataSync, mediaPlayback, connectedDevice 또는 location 유형)를 실행하거나 시스템 또는 다른 중요한 클라이언트가 바인딩하는 서비스를 호스팅하는 경우 전체 프로세스는 Activity가 중지된 후에도 높은 우선순위를 유지합니다.
UI 메모리를 oom_score_adj 200으로 고정하는 단일 프로세스 공동 배치와 UI 프로세스가 캐시로 떨어지는 동안 작업자만 oom_score_adj 200으로 유지하는 분할 프로세스 격리 비교
그림 1. 백그라운드 작업 중 단일 프로세스 코로케이션과 분할 프로세스 격리 비교

무거운 UI 프로세스가 oom_score_adj = 200에 머무르면 여러 대규모 메모리 카테고리가 상주합니다.

  • 중지된 Activity 및 뷰 계층 구조: Activity가 명시적으로 완료되지 않는 한, 사용자가 다른 앱에 있는 동안 Window, 뷰 트리, 레이아웃 구조, 뷰 상태는 Java 힙에서 계속 연결할 수 있습니다.
  • 네이티브 그래픽 컨텍스트 및 GPU 할당: 표준 HardwareRenderer는 GPU 컨텍스트와 캐시를 유지하는 반면 중지된 Activity는 캐시되지 않은 프로세스에 상주합니다. 자체 Vulkan (VkDevice, VkSwapchainKHR, 설명자 풀, 파이프라인 캐시) 또는 OpenGL ES (EGLContext, 텍스처, 꼭짓점 버퍼) 컨텍스트를 만드는 앱과 게임 엔진은 엔진이 중지될 때 그래픽 기기를 명시적으로 해체하지 않는 한 드라이버 구조, 셰이더 프로그램, 그래픽 버퍼도 유지합니다.
  • 힙 단편화 및 런타임 오버헤드: 앱이 onTrimMemory에서 캐시를 해제하더라도 부분적으로 채워진 네이티브 할당자 페이지, 더티 Zygote 쓰기 시 복사 페이지, 로드된 공유 라이브러리(.so), 런타임 메타데이터는 프로세스의 비공개 메모리에 상주합니다. 프로세스가 캐시된 기준점(oom_score_adj < 900)을 초과하므로 플랫폼은 프로세스를 정지하거나 캐시된 앱 압축을 실행하지 않습니다. 바인드된 서비스에서 이 효과를 자세히 살펴보려면 메모리 집약적 UI와 바인드된 서비스 공동 배치를 참고하세요.

간소화된 백그라운드 프로세스가 앱을 보호하는 방법

백그라운드 상태에서 포그라운드 크기의 메모리를 유지하면 백그라운드 작업이 위험해집니다.

  • 백그라운드 메모리 한도는 포그라운드 한도보다 더 엄격합니다. Android 17 이상에서는 포그라운드 서비스가 실행되는 동안을 포함하여 프로세스에 표시되는 UI가 없는 동안 메모리 제한기가 앱에 더 작은 표시되지 않는 메모리 한도를 적용합니다. 이 한도는 앱의 모든 프로세스에 함께 적용됩니다. 포그라운드 서비스를 실행하는 프로세스가 여전히 UI 및 그래픽 메모리를 보유하고 있는 경우 해당 메모리는 더 작은 한도에 포함됩니다. 앱이 한도를 초과하면 커널은 페이지를 회수하는 동안 앱을 제한하므로 실행 중인 다운로드, 동기화 또는 재생 속도가 느려집니다. 스왑이 소진된 후에도 앱이 계속 할당하면 시스템에서 앱을 종료합니다. 앱이 영향을 받았는지 확인하려면 앱 메모리 한도를 참고하세요.
  • 메모리가 부족하면 큰 프로세스가 먼저 종료됩니다. lmkd가 메모리를 확보해야 하고 중요도 버킷 (ro.lmk.kill_heaviest_task)에서 가장 무거운 프로세스를 선택하도록 구성된 경우 수백 MB의 UI 메모리도 보유하는 포그라운드 서비스 프로세스가 동일한 중요도의 더 작은 프로세스보다 먼저 종료되고 백그라운드 작업도 함께 중지됩니다. kswapd와 lmkd 상호작용을 참조하세요.
  • 캐시된 UI 상태는 고정, 압축, 플래시에 다시 쓰기가 가능합니다. 포그라운드 UI 상태를 캐시된 프로세스로 이동하도록 허용하면 OS가 프로세스를 고정하고 메모리를 zRAM으로 압축하며 콜드 페이지를 플래시 저장소에 다시 쓸 수 있습니다. UI 상태는 그대로 유지되면서 상주 메모리 사용량이 줄어들어 사용자가 돌아올 때까지 백그라운드 작업과 캐시된 UI가 모두 유지될 가능성이 가장 높습니다.
  • 프로세스별 메모리 예산이 실용적으로 바뀝니다. 백그라운드 작업만 실행하는 프로세스에는 작고 예측 가능한 작업 세트가 있으므로 UI를 제한하지 않고 엄격한 메모리 예산을 제공할 수 있습니다.

두 번째 런타임에는 작은 기준 비용이 있으므로 두 번째 프로세스를 실행하면 앱의 총 메모리 사용량이 증가하는 것처럼 보일 수 있습니다. 이 비교는 단일 프로세스가 백그라운드 작업으로 전환될 때마다 모든 UI 및 그래픽 상태를 완전히 해제할 수 있는 경우에만 유효하며, 앱과 UI 도구 키트는 실제로 이를 안정적으로 실행할 수 없습니다. OS가 별도의 캐시된 UI 프로세스를 정지, 압축, 다시 쓰기하도록 하면 일반적인 경우에 실제 메모리를 훨씬 적게 사용하고 앱을 표시되지 않는 한도 내로 유지하며 앱과 기기 전반에서 메모리 부족으로 인한 종료를 줄일 수 있습니다.

전용 프로세스에서 백그라운드 작업 격리

백그라운드 작업 중에 UI 및 그래픽 상주를 제거하려면 UI 및 그래픽 엔진을 기본 프로세스에 유지하면서 전용 경량 프로세스에서 장기 실행 백그라운드 구성요소를 실행합니다.

프로세스 매니페스트 선언 호스팅된 구성요소 백그라운드 작업 중에 사용자가 UI를 나갈 때의 상태
기본 UI 프로세스 기본(com.example.app) Activity, UI 뷰, 이미지 캐시, Vulkan 또는 OpenGL ES 렌더기, 오디오 및 장면 엔진 PREV로 떨어진 후 CACHED(oom_score_adj >= 900)로 떨어집니다. 고정, 압축되어 플래시에 다시 기록됩니다.
백그라운드 작업자 프로세스 android:process=":bg" 포그라운드 Service, 백그라운드 애셋 다운로더, 동기화 작업자 또는 연결된 기기 관리자 작업자의 자체 힙 및 코드만 있고 UI 뷰나 GPU 컨텍스트가 없는 FGS (oom_score_adj = 200)에서 실행됩니다.

프로세스 및 메모리 예산 선언

AndroidManifest.xml에서 android:process=":bg"를 설정하여 백그라운드 Service를 비공개 하위 프로세스에 할당합니다. 선행 콜론 (:)은 앱의 UID로 범위가 지정된 비공개 프로세스를 만듭니다.

Android 17 QPR2 (API 수준 37.2) 이상에서는 프로세스 분할을 <processes> 블록 내의 프로세스별 앱 메모리 예산과 결합하여 운영체제가 포그라운드 UI 프로세스를 제한하지 않고 백그라운드 작업자에 엄격한 작업 세트 상한을 적용하도록 합니다.

<manifest xmlns:android="http://schemas.android.com/apk/res/android">

    <application
        android:name=".HeavyApplication"
        android:label="@string/app_name">

        <!-- App-wide budgets that cover every process, including :bg -->
        <memory-budget android:maxMb="512" />
        <memory-budget
            android:maxMb="128"
            android:state="background" />

        <processes>
            <!-- Tight budget for the headless background worker process -->
            <process android:process=":bg">
                <memory-budget android:maxMb="32" />
            </process>
        </processes>

        <activity
            android:name=".MainActivity"
            android:exported="true" />

        <service
            android:name=".AssetDownloadService"
            android:process=":bg"
            android:foregroundServiceType="dataSync"
            android:exported="false" />

    </application>
</manifest>

:bg의 메모리 사용량은 자체 예산과 앱 전체 예산에 모두 반영됩니다. Android 17 QPR2 이전 버전을 실행하는 기기에서는 시스템이 <memory-budget>를 무시하지만 android:process=":bg"는 모든 API 수준에서 작동합니다.

Application.onCreate에서 초기화 게이트

Android는 Application 클래스를 인스턴스화하고 패키지에 대해 생성된 모든 프로세스에서 Application.onCreate를 호출합니다. android:process=":bg"를 채택할 때 흔히 발생하는 문제는 무거운 UI 툴킷, 이미지 로드 풀, 애널리틱스 SDK 또는 네이티브 엔진 초기화 (System.loadLibrary)를 보호되지 않는 Application.onCreate 메서드 내에 남겨두는 것입니다. 이렇게 하면 피하려는 동일한 기준 메모리 사용량으로 :bg 프로세스가 확장됩니다.

onCreate 내에서 Android 9 (API 수준 28) 이상에서 사용할 수 있는 Application.getProcessName를 확인하고 현재 프로세스에 필요한 종속 항목만 초기화합니다.

Kotlin

class HeavyApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        val processName = Application.getProcessName()
        if (processName != null && processName.endsWith(BG_PROCESS_SUFFIX)) {
            initBackgroundWorkerProcess()
            return
        }
        initMainUiProcess()
    }

    private fun initBackgroundWorkerProcess() {
        // Initialize only lightweight HTTP, storage, and sync primitives.
        // Avoid loading native rendering libraries or allocating UI caches.
    }

    private fun initMainUiProcess() {
        // Initialize UI frameworks, image caches, and native engines.
        System.loadLibrary("heavy_engine_jni")
    }

    companion object {
        private const val BG_PROCESS_SUFFIX = ":bg"
    }
}

자바

public final class HeavyApplication extends Application {
    private static final String BG_PROCESS_SUFFIX = ":bg";

    @Override
    public void onCreate() {
        super.onCreate();
        String processName = Application.getProcessName();
        if (processName != null && processName.endsWith(BG_PROCESS_SUFFIX)) {
            initBackgroundWorkerProcess();
            return;
        }
        initMainUiProcess();
    }

    private void initBackgroundWorkerProcess() {
        // Initialize only lightweight HTTP, storage, and sync primitives.
        // Avoid loading native rendering libraries or allocating UI caches.
    }

    private void initMainUiProcess() {
        // Initialize UI frameworks, image caches, and native engines.
        System.loadLibrary("heavy_engine_jni");
    }
}

Jetpack 앱 시작과 같은 콘텐츠 제공자를 통해 초기화되는 라이브러리는 기본 프로세스에서만 초기화됩니다. android:process 없이 선언된 콘텐츠 제공자는 기본 프로세스에서 실행되기 때문입니다. :bg의 코드가 이러한 라이브러리에 종속되는 경우 initBackgroundWorkerProcess에서 명시적으로 초기화합니다.

Unity와 같은 게임 엔진으로 빌드하는 경우 UnityPlayer와 같은 엔진 클래스를 참조하지 않는 자바 또는 Kotlin으로 작성된 일반 Android 라이브러리나 플러그인에서 백그라운드 서비스를 구현합니다. 그러면 :bg 프로세스가 엔진 네이티브 라이브러리를 로드하지 않습니다. 확인하려면 :bg 프로세스의 adb shell cat /proc/<pid>/maps에 libunity.so 또는 libil2cpp.so가 나열되지 않는지 확인합니다.

UI 프로세스를 고정하지 않고 통신

AssetDownloadService가 :bg에서 실행되는 동안 사용자가 MainActivity를 열면 UI에서 실시간 다운로드 진행률을 표시해야 하는 경우가 많습니다. 프로세스 중요도는 클라이언트에서 사용하는 서비스 또는 콘텐츠 제공자를 호스팅하는 프로세스로 흐르므로 각 종속 항목의 방향이 중요합니다.

  • 작업자에 대한 UI 바인딩은 안전합니다. MainActivity가 AssetDownloadService에 바인딩되면 시스템은 :bg를 표시되는 UI의 중요도로 높입니다. 사용자가 나가면 UI 프로세스가 캐시되고 바인딩은 더 이상 아무것도 승격하지 않습니다.
  • UI 프로세스에 종속된 작업자는 안전하지 않습니다. :bg의 코드가 서비스에 바인딩되거나 기본 프로세스에서 실행되는 콘텐츠 제공자를 쿼리하면 시스템은 기본 프로세스를 작업자의 중요도 상승으로 올리고 UI 메모리는 다시 고정된 상태로 유지됩니다. 대신 UI가 작업자에 콜백(예: Messenger 또는 AIDL 콜백 인터페이스)을 등록하도록 합니다. 바인더 객체를 콜백으로 전달해도 두 프로세스의 중요도는 변경되지 않습니다.

바인딩을 해제하기 전에 콜백을 등록 취소하여 시스템이 UI 프로세스를 정지한 후 작업자가 UI 프로세스를 호출하지 않도록 합니다. 다음 예는 UI가 표시되는 동안에만 작업자에 바인딩됩니다.

Kotlin

class MainActivity : Activity() {
    private var downloadService: IBinder? = null

    private val downloadConnection = object : ServiceConnection {
        override fun onServiceConnected(
                name: ComponentName,
                service: IBinder
        ) {
            downloadService = service
            // Register a Messenger or AIDL callback for progress updates.
        }

        override fun onServiceDisconnected(name: ComponentName) {
            // The binding stays active and reconnects if the worker restarts.
            downloadService = null
        }
    }

    override fun onStart() {
        super.onStart()
        val intent = Intent(this, AssetDownloadService::class.java)
        bindService(intent, downloadConnection, Context.BIND_AUTO_CREATE)
    }

    override fun onStop() {
        super.onStop()
        if (downloadService != null) {
            // Unregister the progress callback here, before unbinding.
            downloadService = null
        }
        // Unbind the ServiceConnection even if onServiceConnected hasn't run
        // yet.
        unbindService(downloadConnection)
    }
}

자바

public final class MainActivity extends Activity {
    private IBinder downloadService;

    private final ServiceConnection downloadConnection =
            new ServiceConnection() {
                @Override
                public void onServiceConnected(
                        ComponentName name, IBinder service) {
                    downloadService = service;
                    // Register a Messenger or AIDL callback for progress
                    // updates.
                }

                @Override
                public void onServiceDisconnected(ComponentName name) {
                    // The binding stays active and reconnects if the worker
                    // restarts.
                    downloadService = null;
                }
            };

    @Override
    protected void onStart() {
        super.onStart();
        Intent intent = new Intent(this, AssetDownloadService.class);
        bindService(intent, downloadConnection, Context.BIND_AUTO_CREATE);
    }

    @Override
    protected void onStop() {
        super.onStop();
        if (downloadService != null) {
            // Unregister the progress callback here, before unbinding.
            downloadService = null;
        }
        // Unbind the ServiceConnection even if onServiceConnected hasn't run
        // yet.
        unbindService(downloadConnection);
    }
}

바인딩 플래그가 프로세스 전반에서 oom_score_adj에 미치는 영향에 관한 자세한 내용은 서비스 결합 및 프로세스 상태를 참고하세요.

공유된 디스크 상태를 안전하게 조정

기본 프로세스와 :bg는 별도의 런타임 및 파일 설명자가 있는 별도의 주소 공간에서 실행되므로 다음 사항이 적용됩니다.

  • SQLite 및 Room: 두 프로세스가 데이터베이스를 동시에 읽고 쓸 수 있도록 미리 쓰기 로깅 (대부분의 기기에서 Room의 기본 저널 모드)을 사용합니다. :bg에서 enableMultiInstanceInvalidation를 호출하지 마세요. Room은 기본 프로세스에서 무효화 서비스를 호스팅하므로 작업자가 기본 프로세스에 바인딩되고 UI 메모리를 작업자의 높은 중요도로 유지합니다. 대신 작업자가 UI가 등록하는 콜백을 통해 변경사항을 보고하도록 하고 UI가 시작될 때 데이터베이스를 다시 쿼리하도록 합니다.
  • 환경설정: 프로세스 간에 SharedPreferences를 공유하지 않습니다. 멀티 프로세스 코드에서 DataStore를 사용하거나 IPC를 통해 명시적으로 상태를 전달합니다.
  • 애셋 파일: 다운로드한 애셋 팩을 :bg의 스테이징 파일에 쓰고, 쓴 후에는 :bg에서 fdatasync를 호출한 다음 posix_fadvise(fd, 0, 0, POSIX_FADV_DONTNEED)를 호출하고 (메모리 페이지를 OS에 반환 참고) 파일 이름을 원자적으로 바꿉니다. 정지될 수 있는 UI 프로세스를 호출하는 대신 UI가 다음에 시작되고 작업자에 바인딩될 때 결과를 선택하도록 합니다.

adb 셸에서 효과 관찰

포그라운드 프로세스와 백그라운드 프로세스를 분할하면 메모리 사용량 증가가 격리되는지 확인하려면 홈 화면으로 이동한 후 Activity Manager 프로세스 상태와 dumpsys meminfo를 검사하면 됩니다. 이 섹션의 샘플 출력은 MemoryLab 샘플 앱에서 가져온 것입니다. 이 앱은 두 구성 모두에서 UI 프로세스에서 동일한 64MB 네이티브 할당을 실행하고 분할 포그라운드 서비스를 :remote 프로세스에서 호스팅합니다.

1. 프로세스 상태 및 OOM 점수 비교

백그라운드 워크로드를 시작하고 홈 버튼을 눌러 Activity를 중지한 후 Activity Manager를 쿼리합니다.

adb shell dumpsys activity processes com.example.app | grep -E "Proc #|oom:|state:"

단일 프로세스 아키텍처에서는 Activity이 중지되더라도 기본 프로세스가 승격된 상태로 유지됩니다. 사용자가 나간 후 약 15초 동안 활동 관리자는 포그라운드 서비스를 실행하는 프로세스를 oom_score_adj = 50(fg-service-act)에서 유지한 다음 200 (fg-service)로 낮춥니다. 다음 출력은 홈을 누르고 20초 후에 캡처되었습니다.

    Proc #1: prcp     F/S/FGS  ---NFUATI  t: 0 13794:com.android.memorylab/u0a284 (fg-service)
        oom: max=1001 curRaw=200 setRaw=200 cur=200 set=200
        state: cur=FGS  set=FGS   lastRss=0.00 lastCachedRss=0.00

분할 프로세스 아키텍처에서는 작업자 프로세스만 oom_score_adj = 200에 유지되고 기본 UI 프로세스는 PREV(oom_score_adj = 700)로, 나중에는 캐시된 버킷(oom_score_adj >= 900)으로 떨어집니다.

    Proc #3: prcp     F/S/FGS  ---NFUATI  t: 0 13544:com.android.memorylab:remote/u0a284 (fg-service)
        oom: max=1001 curRaw=200 setRaw=200 cur=200 set=200
        state: cur=FGS  set=FGS   lastRss=0.00 lastCachedRss=0.00
    Proc #1: prev     b/ /LAST -------TI  t: 0 13312:com.android.memorylab/u0a284 (previous)
        oom: max=1001 curRaw=700 setRaw=700 cur=700 set=700
        state: cur=LAST set=LAST  lastRss=0.00 lastCachedRss=0.00

이전 앱 제한 시간이 만료되면 기본 프로세스가 캐시된 버킷 (cch, oom_score_adj = 900, previous-expired)으로 이동하고 암시적 CPU 시간 (I)을 비롯한 모든 프로세스 기능 (---------)을 잃습니다. 이 시점에서 시스템은 프로세스를 정지하고 작업자 프로세스를 중단하지 않고 언제든지 압축하거나 회수할 수 있습니다.

2. 프로세스 간 dumpsys meminfo 비교

승격된 작업자의 dumpsys meminfo 요약을 UI 프로세스와 비교합니다.

adb shell dumpsys meminfo com.example.app:bg
adb shell dumpsys meminfo com.example.app

oom_score_adj = 200의 MemoryLab 작업자 프로세스에 대한 샘플 App Summary 출력:

 App Summary
                       Pss(KB)                        Rss(KB)
                        ------                         ------
           Java Heap:      380                          13840
         Native Heap:     1548                           2832
                Code:      132                          55816
               Stack:      200                            208
            Graphics:        0                              0
       Private Other:      496
              System:     1926
             Unknown:                                    4168

           TOTAL PSS:     4682            TOTAL RSS:    76864       TOTAL SWAP PSS:      441

Private Other에서 64MB 네이티브 할당을 보유하는 oom_score_adj = 700의 MemoryLab UI 프로세스에 대한 샘플 App Summary 출력:

 App Summary
                       Pss(KB)                        Rss(KB)
                        ------                         ------
           Java Heap:     1316                          19816
         Native Heap:    30408                          31596
                Code:     3148                         115796
               Stack:      656                            664
            Graphics:        0                              0
       Private Other:    67044
              System:    13687
             Unknown:                                   71092

           TOTAL PSS:   116259            TOTAL RSS:   238964       TOTAL SWAP PSS:      259

작업자 프로세스는 코드 및 런타임 페이지의 대부분을 Zygote와 공유하므로 5MB 미만의 PSS가 필요합니다. Vulkan 또는 OpenGL ES로 렌더링하는 앱에서는 UI 프로세스의 Graphics 행에 GPU 메모리도 포함됩니다. 이 샘플은 소프트웨어 렌더링이 있는 Cuttlefish 가상 기기에서 캡처되었으므로 두 프로세스에서 Graphics는 0입니다.

3. 프로세스별 cgroup 계정 검사

UID 아래의 각 프로세스에는 앱의 상위 cgroup 내에 자체 cgroup v2 디렉터리가 있습니다. 다음 명령어는 앱의 모든 프로세스에 관한 oom_score_adj, memory.current, 앱 전체 memory.high를 나열합니다.

adb shell '
  for PID in $(pidof com.example.app com.example.app:bg); do
    CG=/sys/fs/cgroup$(sed -n "s/^0:://p" /proc/$PID/cgroup)
    echo "pid=$PID ($(tr -d "\0" < /proc/$PID/cmdline))" \
      "adj=$(cat /proc/$PID/oom_score_adj)" \
      "memory.current=$(cat $CG/memory.current)" \
      "app.memory.high=$(cat ${CG%/*}/memory.high)"
  done
'

홈을 누른 후 MemoryLab의 샘플 출력:

pid=15301 (com.android.memorylab) adj=700 memory.current=103260160 app.memory.high=966787072
pid=16518 (com.android.memorylab:remote) adj=200 memory.current=3608576 app.memory.high=966787072

작업자 프로세스는 4MB 미만으로 청구되고 UI 프로세스는 약 98MB로 청구됩니다. 각 프로세스에는 자체 memory.current가 있지만 메모리 제한기는 앱의 모든 프로세스를 포함하는 상위 cgroup에 한도를 설정합니다. 두 줄 모두 동일한 app.memory.high인 966,787,072바이트를 표시하며, 이는 다음 출력에서 보이지 않는 한도인 922MB입니다.

메모리 제한기가 기기에 적용하는 표시되는 한도와 표시되지 않는 한도를 보려면 다음 명령어를 실행합니다.

adb shell am memory-limiter status

RAM이 4GB인 기기의 샘플 출력입니다. 여기서 표시되지 않는 한도는 표시되는 한도의 절반입니다.

Memory limiter
  enabled                  limits=true              killing=true             ignored=none
  visible:                 memHigh=1843MB           memMax=2048MB            swapMax=1024MB
  not-visible:             memHigh=922MB            memMax=1024MB            swapMax=1024MB

Perfetto에서 효과 관찰

Perfetto를 사용하면 사용자가 포그라운드 UI에서 홈 화면으로 이동하는 동안 백그라운드 작업이 계속 진행될 때 두 프로세스에서 oom_score_adj 및 RSS의 타임라인을 시각화할 수 있습니다.

1. 트레이스 기록

앱에서 백그라운드 작업을 시작하고 홈 화면으로 이동하는 동안 프로세스 메모리 카운터, GPU 메모리, oom_score_adj 변경사항을 캡처하는 15초 트레이스를 기록합니다.

adb shell perfetto -c - --txt -o /data/misc/perfetto-traces/process-splitting.perfetto-trace <<'EOF'
buffers: {
  size_kb: 32768
  fill_policy: DISCARD
}
data_sources: {
  config {
    name: "linux.process_stats"
    process_stats_config {
      scan_all_processes_on_start: true
      proc_stats_poll_ms: 250
    }
  }
}
data_sources: {
  config {
    name: "android.gpu.memory"
  }
}
data_sources: {
  config {
    name: "linux.ftrace"
    ftrace_config {
      ftrace_events: "oom/oom_score_adj_update"
      ftrace_events: "gpu_mem/gpu_mem_total"
    }
  }
}
duration_ms: 15000
EOF
adb pull /data/misc/perfetto-traces/process-splitting.perfetto-trace

2. Perfetto UI에서 프로세스 트랙 검사

Perfetto UI에서 트레이스를 열고 com.example.app 및 com.example.app:bg의 프로세스 그룹을 펼칩니다.

  1. 두 프로세스의 oom_score_adj 및 mem.rss.anon 트랙을 고정하여 타임라인 상단에 나란히 표시합니다.
  2. 사용자가 홈을 누르는 타임스탬프를 찾습니다.
    • 작업자 프로세스에서 oom_score_adj는 200에 유지되고 mem.rss.anon는 평평하고 작게 유지됩니다.
    • UI 프로세스에서 oom_score_adj는 0에서 200로 상승하고 Activity가 중지되는 동안 약 1초 동안 유지된 후 700(PREV)로 상승합니다. 이전 앱 제한 시간이 지나면 캐시된 범위로 이동하여 시스템이 작업자에 영향을 주지 않고 메모리를 압축하고 회수할 수 있습니다.
UI 프로세스 및 작업자 프로세스의 Perfetto 트랙 UI 프로세스 oom_score_adj는 Home 이후 0에서 200으로, 그런 다음 700으로 상승하는 반면 작업자 프로세스는 약 19MB의 익명 RSS로 200에 머무릅니다.
그림 2. 사용자가 홈을 누른 후 프로세스를 분할합니다.

그림 2에서 UI 프로세스는 Activity가 중지되는 동안 잠시 200에 머물렀다가 700로 상승하는 반면 작업자는 약 19MB의 익명 RSS로 200에 머무릅니다. 비교를 위해 그림 3은 공동 배치 구성의 35초 trace를 보여줍니다. 여기서 단일 프로세스는 50에 머무른 다음 모든 UI 메모리와 함께 200에 머무릅니다.

단일 공동 배치 프로세스의 Perfetto 트랙. oom_score_adj는 Home 이후 0에서 50으로, 약 15초 후에는 200으로 증가하는 반면 익명 RSS는 높은 상태를 유지합니다.
그림 3. 사용자가 홈을 누른 후의 공동 배치 프로세스입니다.

이러한 뷰를 살펴보려면 Perfetto UI에서 분할 트레이스와 공동 배치 트레이스를 엽니다.

3. PerfettoSQL로 증가된 메모리 사용량 쿼리

쿼리 (SQL) 탭에서 다음 PerfettoSQL 쿼리를 실행하여 각 oom_score_adj 수준에서 앱의 각 프로세스가 보유한 평균 익명 및 파일 RSS를 측정합니다.

INCLUDE PERFETTO MODULE android.memory.process;

SELECT
  process_name,
  score AS oom_score_adj,
  ROUND(SUM(dur) / 1e9, 2) AS duration_sec,
  ROUND(SUM(anon_rss * dur) / SUM(dur) / 1048576.0, 1) AS avg_anon_mb,
  ROUND(SUM(file_rss * dur) / SUM(dur) / 1048576.0, 1) AS avg_file_mb
FROM memory_oom_score_with_rss_and_swap_per_process
WHERE process_name GLOB 'com.example.app*'
GROUP BY process_name, score
ORDER BY process_name, score;

그림 2의 분할 trace에 대한 샘플 출력:

process_name                  oom_score_adj  duration_sec  avg_anon_mb  avg_file_mb
com.android.memorylab                     0          4.11         93.3        110.4
com.android.memorylab                   200          1.06        126.8        110.5
com.android.memorylab                   700          9.72        115.1        110.5
com.android.memorylab:remote              0          0.01         NULL         NULL
com.android.memorylab:remote            200         12.87         18.9         53.7

그림 3의 공동 배치된 트레이스에 대한 샘플 출력:

process_name           oom_score_adj  duration_sec  avg_anon_mb  avg_file_mb
com.android.memorylab              0          4.10         96.0        110.4
com.android.memorylab             50         16.04        117.6        110.5
com.android.memorylab            200         14.68        116.5        110.6

분할 구성에서는 처음 1초 후에 약 19MB의 익명 메모리만 oom_score_adj = 200에 유지됩니다. 공동 배치 구성에서는 포그라운드 서비스가 실행되는 동안 약 117MB가 50에 유지된 후 200에 유지됩니다.


← OS에 메모리 페이지 반환 | ↑ 위로