Duy trì hoạt động ở chế độ nền bằng một quy trình tinh gọn

Nhiều ứng dụng thực hiện công việc hợp lệ khi giao diện người dùng không còn hiển thị, chẳng hạn như tải các gói tài sản lớn xuống, đồng bộ hoá cơ sở dữ liệu cục bộ, phát trực tuyến âm thanh, ghi lại các bài tập thể dục hoặc duy trì phiên thiết bị đã kết nối. Khi công việc trong nền này chạy trong cùng một quy trình hệ điều hành với giao diện người dùng trên nền trước và công cụ đồ hoạ, toàn bộ quy trình sẽ duy trì ở mức độ quan trọng cao (oom_score_adj giữa 0 và 250) cùng với tất cả bộ nhớ mà giao diện người dùng đã phân bổ.

Trang này giải thích lý do khiến việc này gây hại cho ứng dụng của bạn cũng như phần còn lại của thiết bị, cách chuyển công việc trong nền sang một quy trình gọn nhẹ chuyên dụng bằng android:process và cách xác minh mức tiết kiệm bộ nhớ bằng adb shell và Perfetto.

Lý do công việc trong một quy trình duy nhất giữ lại bộ nhớ giao diện người dùng

Android quản lý việc chấm dứt bộ nhớ và quy trình ở mức độ chi tiết của một quy trình hệ điều hành (pid), chứ không phải các thành phần riêng lẻ (Activity, Service hoặc BroadcastReceiver). Trình quản lý hoạt động (OomAdjuster) chỉ định cho mỗi quy trình một giá trị oom_score_adj duy nhất phản ánh thành phần hoạt động quan trọng nhất hoặc liên kết sắp tới trong quy trình đó:

  • Khi người dùng tương tác với Activity, quy trình sẽ chạy ở trạng thái TOP (oom_score_adj = 0).
  • Khi người dùng rời khỏi ứng dụng và ứng dụng không có thành phần nền nào đang hoạt động, quy trình sẽ chuyển sang bộ chứa PREV (oom_score_adj = 700) hoặc CACHED (oom_score_adj >= 900). Trong bộ nhớ đệm, nền tảng sẽ đóng băng quy trình bằng trình đóng băng cgroup phiên bản 2, nén bộ nhớ bằng CachedAppOptimizer và cho phép lmkd chấm dứt quy trình trước tiên khi có áp lực về bộ nhớ.
  • Tuy nhiên, nếu cùng một quy trình cũng chạy một dịch vụ nền trước (chẳng hạn như các loại dataSync, mediaPlayback, connectedDevice hoặc location, tại oom_score_adj = 200) hoặc lưu trữ một dịch vụ mà hệ thống hoặc một ứng dụng quan trọng khác liên kết, thì toàn bộ quy trình sẽ duy trì ở mức ưu tiên cao đó ngay cả sau khi Activity dừng.
So sánh giữa việc đặt chung một quy trình duy nhất giữ cho bộ nhớ giao diện người dùng được ghim
       ở oom_score_adj 200 và việc tách biệt quy trình chỉ giữ cho
       worker ở oom_score_adj 200 trong khi quy trình giao diện người dùng giảm xuống mức được lưu vào bộ nhớ đệm.
Hình 1. So sánh việc cùng vị trí trong một quy trình với việc tách biệt quy trình trong khi làm việc ở chế độ nền.

Khi một quy trình giao diện người dùng nặng vẫn ở trạng thái oom_score_adj = 200, một số danh mục bộ nhớ lớn vẫn còn lưu trú:

  • Activity đã dừng và hệ phân cấp view: Trừ phi Activity đã hoàn tất rõ ràng, Window, cây khung hiển thị, cấu trúc bố cục và trạng thái khung hiển thị của nó vẫn có thể truy cập được trên vùng nhớ khối xếp Java trong khi người dùng đang ở trong một ứng dụng khác.
  • Bối cảnh đồ hoạ gốc và mức phân bổ GPU: HardwareRenderer tiêu chuẩn giữ bối cảnh GPU và bộ nhớ đệm trong khi Activity đã dừng hoạt động trong một quy trình không được lưu vào bộ nhớ đệm. Các ứng dụng và công cụ phát triển trò chơi tạo bối cảnh Vulkan (VkDevice, VkSwapchainKHR, nhóm mô tả, bộ nhớ đệm quy trình) hoặc OpenGL ES (EGLContext, kết cấu, vùng đệm đỉnh) riêng cũng giữ lại cấu trúc trình điều khiển, chương trình đổ bóng và vùng đệm đồ hoạ, trừ phi công cụ phát triển trò chơi này huỷ bỏ thiết bị đồ hoạ một cách rõ ràng khi dừng.
  • Phân mảnh vùng nhớ khối xếp và hao tổn thời gian chạy: Ngay cả khi ứng dụng giải phóng bộ nhớ đệm trong onTrimMemory, các trang phân bổ gốc được điền một phần, các trang ngầm sao chép (copy-on-write) Zygote có sửa đổi, các thư viện chia sẻ đã tải (.so) và siêu dữ liệu thời gian chạy vẫn nằm trong bộ nhớ riêng tư của quy trình. Vì quy trình này nằm trên ngưỡng lưu vào bộ nhớ đệm (oom_score_adj < 900), nên nền tảng sẽ không đóng băng quy trình hoặc chạy tính năng nén ứng dụng đã lưu vào bộ nhớ đệm trên quy trình đó. Để tìm hiểu kỹ hơn về hiệu ứng này đối với các dịch vụ ràng buộc, hãy xem bài viết Đồng định vị một dịch vụ ràng buộc với giao diện người dùng sử dụng nhiều bộ nhớ.

Cách một tiến trình nền tinh gọn bảo vệ ứng dụng của bạn

Việc giữ bộ nhớ có kích thước nền trước ở trạng thái nền sẽ khiến công việc nền của bạn gặp rủi ro:

  • Giới hạn bộ nhớ trong nền nghiêm ngặt hơn giới hạn bộ nhớ trên nền trước. Trong Android 17 trở lên, Trình giới hạn bộ nhớ sẽ áp dụng giới hạn bộ nhớ không hiển thị nhỏ hơn cho ứng dụng của bạn khi không có quy trình nào của ứng dụng hiển thị giao diện người dùng, kể cả khi một dịch vụ nền trước đang chạy. Giới hạn này áp dụng cho tất cả các quy trình của ứng dụng. Nếu quy trình chạy dịch vụ trên nền trước của bạn vẫn giữ giao diện người dùng và bộ nhớ đồ hoạ, thì bộ nhớ đó sẽ được tính vào giới hạn nhỏ hơn. Khi ứng dụng của bạn vượt quá giới hạn, hạt nhân sẽ điều tiết ứng dụng trong khi thu hồi các trang của ứng dụng, làm chậm quá trình tải xuống, đồng bộ hoá hoặc phát đang chạy. Nếu ứng dụng của bạn tiếp tục phân bổ sau khi hết bộ nhớ hoán đổi, hệ thống sẽ chấm dứt ứng dụng đó. Để kiểm tra xem ứng dụng của bạn có bị ảnh hưởng hay không, hãy xem phần Giới hạn bộ nhớ của ứng dụng.
  • Các quy trình lớn hơn sẽ bị chấm dứt trước khi bộ nhớ bị áp lực. Khi lmkd phải giải phóng bộ nhớ và được định cấu hình để chọn quy trình nặng nhất trong một bộ chứa quan trọng (ro.lmk.kill_heaviest_task), một quy trình dịch vụ nền trước cũng chứa hàng trăm MB bộ nhớ giao diện người dùng sẽ bị chấm dứt trước các quy trình nhỏ hơn có cùng mức độ quan trọng và công việc trong nền của bạn sẽ dừng lại cùng với quy trình đó. Xem phần Tương tác giữa kswapd và lmkd.
  • Trạng thái giao diện người dùng được lưu vào bộ nhớ đệm có thể bị đóng băng, nén và ghi lại vào bộ nhớ flash. Việc cho phép trạng thái giao diện người dùng trên nền trước chuyển sang một quy trình được lưu vào bộ nhớ đệm sẽ giúp hệ điều hành đóng băng quy trình đó, nén bộ nhớ của quy trình đó vào zRAM và ghi các trang không hoạt động của quy trình đó trở lại bộ nhớ flash. Mức sử dụng bộ nhớ thường trú của ứng dụng sẽ giảm trong khi trạng thái giao diện người dùng vẫn giữ nguyên, giúp cả công việc trong nền và giao diện người dùng được lưu vào bộ nhớ đệm có cơ hội tốt nhất để tồn tại cho đến khi người dùng quay lại.
  • Ngân sách bộ nhớ cho mỗi quy trình trở nên thiết thực. Một quy trình chỉ chạy công việc ở chế độ nền có một tập hợp hoạt động nhỏ, có thể dự đoán được, vì vậy, bạn có thể cung cấp cho quy trình đó một ngân sách bộ nhớ chặt chẽ mà không làm hạn chế giao diện người dùng.

Việc chạy quy trình thứ hai có thể làm tăng tổng mức sử dụng bộ nhớ của ứng dụng, vì thời gian chạy thứ hai có chi phí cơ sở nhỏ. Phép so sánh đó chỉ đúng nếu một quy trình duy nhất có thể giải phóng hoàn toàn tất cả trạng thái giao diện người dùng và đồ hoạ bất cứ khi nào quy trình đó chuyển sang hoạt động ở chế độ nền, điều mà các ứng dụng và bộ công cụ giao diện người dùng không thể thực hiện một cách đáng tin cậy trong thực tế. Việc cho phép hệ điều hành đóng băng, thu gọn và ghi lại một quy trình giao diện người dùng riêng biệt được lưu vào bộ nhớ đệm sẽ sử dụng ít bộ nhớ thực hơn trong trường hợp thông thường, giữ cho ứng dụng của bạn nằm trong giới hạn không hiển thị và giảm tình trạng chấm dứt do bộ nhớ thấp cho ứng dụng của bạn và trên toàn thiết bị.

Tách biệt công việc trong nền trong một quy trình riêng

Để loại bỏ việc lưu trú giao diện người dùng và đồ hoạ trong khi làm việc ở chế độ nền, hãy chạy các thành phần nền tồn tại lâu trong một quy trình gọn nhẹ chuyên dụng, đồng thời giữ giao diện người dùng và công cụ đồ hoạ trong quy trình chính:

Quy trình Nội dung khai báo trong tệp kê khai Thành phần được lưu trữ Trạng thái khi người dùng rời khỏi giao diện người dùng trong khi tác vụ nền đang chạy
Quy trình giao diện người dùng chính Mặc định (com.example.app) Activity, chế độ xem giao diện người dùng, bộ nhớ đệm hình ảnh, trình kết xuất Vulkan hoặc OpenGL ES, công cụ âm thanh và cảnh Giảm xuống PREV rồi đến CACHED (oom_score_adj >= 900). Đóng băng, nén và ghi trở lại bộ nhớ flash.
Tiến trình chạy trong nền android:process=":bg" Service trên nền trước, trình tải thành phần xuống trong nền, trình thực thi đồng bộ hoá hoặc trình quản lý thiết bị đã kết nối Chạy ở FGS (oom_score_adj = 200) chỉ với vùng nhớ khối xếp và mã của worker, không có khung hiển thị giao diện người dùng hoặc ngữ cảnh GPU.

Khai báo ngân sách bộ nhớ và quy trình

Chỉ định Service nền cho một quy trình con riêng tư bằng cách đặt android:process=":bg" trong AndroidManifest.xml. Dấu hai chấm ở đầu (:) sẽ tạo một quy trình riêng tư trong phạm vi UID của ứng dụng.

Trong Android 17 QPR2 (cấp độ API 37.2) trở lên, hãy kết hợp việc phân tách quy trình với ngân sách bộ nhớ ứng dụng theo từng quy trình bên trong khối <processes> để hệ điều hành thực thi giới hạn nghiêm ngặt về tập hợp hoạt động trên trình chạy trong nền mà không ràng buộc quy trình giao diện người dùng trên nền trước:

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

Mức sử dụng bộ nhớ trong :bg được tính vào cả ngân sách riêng của nó và ngân sách trên toàn ứng dụng. Trên các thiết bị chạy phiên bản cũ hơn Android 17 QPR2, hệ thống sẽ bỏ qua <memory-budget>, trong khi android:process=":bg" hoạt động trên tất cả các cấp độ API.

Khởi chạy cổng trong Application.onCreate

Android tạo thực thể cho lớp Application và gọi Application.onCreate trong mọi quy trình được tạo cho gói của bạn. Một lỗi thường gặp khi áp dụng android:process=":bg" là để lại các bộ công cụ giao diện người dùng nặng, nhóm tải hình ảnh, Analytics SDK hoặc khởi chạy công cụ gốc (System.loadLibrary) bên trong một phương thức Application.onCreate không được bảo vệ. Làm như vậy sẽ khiến quy trình :bg tăng lên với cùng mức sử dụng cơ sở mà bạn đang cố gắng tránh.

Kiểm tra Application.getProcessName (có trong Android 9 (API cấp 28) trở lên) bên trong onCreate và chỉ khởi chạy các phần phụ thuộc mà quy trình hiện tại yêu cầu:

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

Java

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

Các thư viện khởi chạy thông qua một nhà cung cấp nội dung, chẳng hạn như Khởi động ứng dụng Jetpack, chỉ khởi chạy trong quy trình chính, vì nhà cung cấp nội dung được khai báo mà không có android:process sẽ chạy trong quy trình chính. Nếu mã trong :bg phụ thuộc vào một thư viện như vậy, hãy khởi chạy thư viện đó một cách rõ ràng trong initBackgroundWorkerProcess.

Nếu bạn xây dựng bằng một công cụ phát triển trò chơi như Unity, hãy triển khai dịch vụ nền trong một thư viện hoặc trình bổ trợ Android thuần tuý được viết bằng Java hoặc Kotlin không tham chiếu đến các lớp công cụ như UnityPlayer. Sau đó, quy trình :bg sẽ không bao giờ tải các thư viện gốc của công cụ. Để xác nhận, hãy kiểm tra để đảm bảo rằng adb shell cat /proc/<pid>/maps cho quy trình :bg không liệt kê libunity.so hoặc libil2cpp.so.

Giao tiếp mà không cần ghim quy trình giao diện người dùng

Khi người dùng mở MainActivity trong khi AssetDownloadService chạy ở :bg, giao diện người dùng thường cần hiển thị tiến trình tải xuống trực tiếp. Tầm quan trọng của quy trình chuyển từ ứng dụng khách sang quy trình lưu trữ dịch vụ hoặc nhà cung cấp nội dung mà ứng dụng đó sử dụng, vì vậy, hướng của mỗi phần phụ thuộc đều quan trọng:

  • Việc liên kết giao diện người dùng với worker là an toàn. Khi MainActivity liên kết với AssetDownloadService, hệ thống sẽ nâng :bg lên mức quan trọng của giao diện người dùng hiển thị. Khi người dùng rời đi, quy trình giao diện người dùng sẽ được lưu vào bộ nhớ đệm và liên kết không còn nâng cấp bất kỳ thứ gì nữa.
  • Trình thực thi phụ thuộc vào quy trình giao diện người dùng là không an toàn. Nếu mã trong :bg liên kết với một dịch vụ hoặc truy vấn một trình cung cấp nội dung chạy trong quy trình chính, thì hệ thống sẽ nâng quy trình chính lên mức độ quan trọng cao hơn của trình thực thi và bộ nhớ giao diện người dùng sẽ được ghim lại. Thay vào đó, hãy yêu cầu giao diện người dùng đăng ký một lệnh gọi lại với worker, chẳng hạn như Messenger hoặc giao diện gọi lại AIDL. Việc truyền đối tượng liên kết dưới dạng lệnh gọi lại không làm thay đổi tầm quan trọng của một trong hai quy trình.

Huỷ đăng ký lệnh gọi lại trước khi huỷ liên kết để worker không bao giờ gọi vào quy trình giao diện người dùng sau khi hệ thống đóng băng quy trình đó. Ví dụ sau đây chỉ liên kết với worker khi giao diện người dùng hiển thị:

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

Java

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

Để biết thêm về cách cờ liên kết ảnh hưởng đến oom_score_adj trên các quy trình, hãy xem phần Liên kết dịch vụ và trạng thái quy trình.

Điều phối trạng thái trên ổ đĩa dùng chung một cách an toàn

Vì quy trình chính và :bg chạy trong các không gian địa chỉ riêng biệt với thời gian chạy và chỉ số mô tả tệp riêng biệt:

  • SQLite và Room: Sử dụng tính năng ghi nhật ký trước (chế độ ghi nhật ký mặc định trong Room trên hầu hết các thiết bị) để cả hai quy trình đều có thể đọc và ghi cơ sở dữ liệu cùng một lúc. Đừng gọi enableMultiInstanceInvalidation từ :bg. Room lưu trữ dịch vụ vô hiệu hoá trong quy trình chính, vì vậy, worker sẽ liên kết với quy trình chính và giữ bộ nhớ giao diện người dùng ở mức quan trọng cao của worker. Thay vào đó, hãy yêu cầu worker báo cáo các thay đổi thông qua lệnh gọi lại mà giao diện người dùng đăng ký, đồng thời yêu cầu giao diện người dùng truy vấn lại cơ sở dữ liệu khi bắt đầu.
  • Lựa chọn ưu tiên: Không chia sẻ SharedPreferences giữa các quy trình. Sử dụng DataStore trong mã đa quy trình hoặc truyền trạng thái một cách rõ ràng thông qua IPC.
  • Tệp tài sản: Ghi các gói tài sản đã tải xuống vào một tệp thử nghiệm trong :bg, gọi fdatasync rồi gọi posix_fadvise(fd, 0, 0, POSIX_FADV_DONTNEED) trong :bg sau khi ghi (như minh hoạ trong phần Trả lại trang bộ nhớ cho hệ điều hành) và đổi tên tệp thành tên chính thức. Hãy để giao diện người dùng nhận kết quả khi khởi động và liên kết với worker vào lần tiếp theo, thay vì gọi vào một quy trình giao diện người dùng có thể bị treo.

Quan sát hiệu ứng trong adb shell

Bạn có thể xác minh rằng việc phân tách các quy trình trên nền trước và nền sau sẽ tách biệt mức sử dụng bộ nhớ cao bằng cách kiểm tra trạng thái quy trình của Trình quản lý hoạt động và dumpsys meminfo sau khi chuyển đến Màn hình chính. Kết quả mẫu trong phần này được lấy từ ứng dụng mẫu MemoryLab. Ứng dụng này chạy cùng một mức phân bổ gốc 64 MB trong quy trình giao diện người dùng ở cả hai cấu hình và lưu trữ dịch vụ nền trước được phân tách trong một quy trình :remote.

1. So sánh trạng thái quy trình và điểm OOM

Bắt đầu khối lượng công việc ở chế độ nền, nhấn nút Màn hình chính để Activity dừng và truy vấn Trình quản lý hoạt động:

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

Trong cấu trúc một quy trình, quy trình chính vẫn được nâng cao ngay cả khi Activity của quy trình đó bị dừng. Trong khoảng 15 giây sau khi người dùng rời đi, Trình quản lý hoạt động sẽ giữ một quy trình chạy dịch vụ nền trước ở mức oom_score_adj = 50 (fg-service-act), sau đó hạ xuống mức 200 (fg-service). Kết quả sau đây được ghi lại sau khi nhấn nút Màn hình chính 20 giây:

    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

Trong cấu trúc phân tách quy trình, chỉ có quy trình thực thi duy trì ở mức oom_score_adj = 200, trong khi quy trình giao diện người dùng chính giảm xuống mức PREV (oom_score_adj = 700) và sau đó giảm xuống nhóm lưu vào bộ nhớ đệm (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

Sau khi hết thời gian chờ của ứng dụng trước đó, quy trình chính sẽ chuyển sang bộ chứa được lưu vào bộ nhớ đệm (cch, oom_score_adj = 900, previous-expired) và mất tất cả khả năng xử lý (---------), bao gồm cả thời gian của CPU ngầm định (I). Tại thời điểm đó, hệ thống sẽ đóng băng và có thể thu gọn hoặc thu hồi bất cứ lúc nào mà không làm gián đoạn quy trình của worker.

2. So sánh dumpsys meminfo giữa các quy trình

So sánh thông tin tóm tắt về dumpsys meminfo của worker có đặc quyền với quy trình giao diện người dùng:

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

Kết quả mẫu App Summary cho quy trình thực thi MemoryLab tại oom_score_adj = 200:

 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

Kết quả mẫu của App Summary cho quy trình giao diện người dùng MemoryLab tại oom_score_adj = 700, chứa mức phân bổ gốc 64 MB trong Private Other:

 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

Quy trình của worker cần ít hơn 5 MB PSS vì quy trình này chia sẻ hầu hết mã và trang thời gian chạy với Zygote. Trong một ứng dụng kết xuất bằng Vulkan hoặc OpenGL ES, hàng Graphics của quy trình giao diện người dùng cũng bao gồm bộ nhớ GPU. Mẫu này được ghi lại trên một thiết bị ảo Cuttlefish có tính năng kết xuất phần mềm, vì vậy, Graphics là 0 trong cả hai quy trình.

3. Kiểm tra việc tính toán cgroup theo quy trình

Mỗi quy trình trong UID của bạn đều có thư mục cgroup v2 riêng bên trong một cgroup mẹ cho ứng dụng của bạn. Lệnh sau đây liệt kê oom_score_adj, memory.current và memory.high trên toàn ứng dụng cho mọi quy trình của ứng dụng:

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
'

Kết quả mẫu cho MemoryLab sau khi nhấn vào nút Màn hình chính:

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

Quy trình của worker được tính phí dưới 4 MB, trong khi quy trình giao diện người dùng được tính phí khoảng 98 MB. Mỗi quy trình có memory.current riêng, nhưng Trình giới hạn bộ nhớ đặt giới hạn cho cgroup mẹ chứa tất cả quy trình của ứng dụng. Cả hai dòng đều cho thấy cùng một app.memory.high là 966.787.072 byte, tức là giới hạn không hiển thị 922 MB trong kết quả sau.

Để xem các giới hạn hiển thị và không hiển thị mà Trình giới hạn bộ nhớ áp dụng trên thiết bị, hãy chạy lệnh sau:

adb shell am memory-limiter status

Kết quả mẫu từ một thiết bị có RAM 4 GB, trong đó giới hạn không hiển thị bằng một nửa giới hạn hiển thị:

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

Quan sát hiệu ứng trong Perfetto

Perfetto cho phép bạn trực quan hoá dòng thời gian của oom_score_adj và RSS trên cả hai quy trình khi người dùng di chuyển từ giao diện người dùng trên nền trước sang Màn hình chính trong khi công việc trong nền vẫn tiếp tục.

1. Ghi lại dấu vết

Ghi lại dấu vết dài 15 giây để ghi lại bộ đếm bộ nhớ của quy trình, bộ nhớ GPU và oom_score_adj thay đổi trong khi bắt đầu một công việc ở chế độ nền trong ứng dụng và chuyển đến Màn hình chính:

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. Kiểm tra các bản nhạc của quy trình trong giao diện người dùng Perfetto

Mở dấu vết trong giao diện người dùng Perfetto và mở rộng các nhóm quy trình cho com.example.app và com.example.app:bg:

  1. Ghim các bản ghi oom_score_adj và mem.rss.anon cho cả hai quy trình để chúng xuất hiện cạnh nhau ở đầu dòng thời gian.
  2. Tìm dấu thời gian khi người dùng nhấn vào nút Màn hình chính:
    • Trong quy trình thực thi, oom_score_adj duy trì ở mức 200 và mem.rss.anon duy trì ở mức thấp và ổn định.
    • Trong quy trình giao diện người dùng, oom_score_adj tăng từ 0 lên 200, giữ nguyên ở đó trong khoảng một giây trong khi Activity dừng, sau đó tăng lên 700 (PREV). Sau khi hết thời gian chờ của ứng dụng trước đó, ứng dụng sẽ chuyển sang phạm vi được lưu vào bộ nhớ đệm, nơi hệ thống có thể thu gọn và lấy lại bộ nhớ mà không ảnh hưởng đến worker.
Perfetto theo dõi quy trình giao diện người dùng và quy trình worker. Giá trị oom_score_adj của quy trình giao diện người dùng tăng từ 0 lên 200 rồi lên 700 sau khi nhấn nút Màn hình chính, trong khi quy trình của trình chạy vẫn ở mức 200 với khoảng 19 MB RSS ẩn danh.
Hình 2. Tách các quy trình sau khi người dùng nhấn vào nút Màn hình chính.

Trong Hình 2, quy trình giao diện người dùng tạm thời ở mức 200 trong khi Activity dừng lại và sau đó tăng lên 700, trong khi worker vẫn ở mức 200 với khoảng 19 MB RSS ẩn danh. Để so sánh, Hình 3 cho thấy dấu vết 35 giây của cấu hình cùng vị trí, trong đó quy trình duy nhất vẫn ở mức 50 rồi đến 200 với tất cả bộ nhớ giao diện người dùng.

Perfetto theo dõi một quy trình được đặt cùng vị trí. oom_score_adj tăng từ 0 lên 50 sau khi về Màn hình chính và lên 200 sau khoảng 15 giây, trong khi RSS ẩn danh vẫn ở mức cao.
Hình 3. Một quy trình được đặt cùng vị trí sau khi người dùng nhấn vào nút Màn hình chính.

Để khám phá các chế độ xem này, hãy mở dấu vết phân tách và dấu vết cùng vị trí trong giao diện người dùng Perfetto.

3. Truy vấn mức sử dụng bộ nhớ cao bằng PerfettoSQL

Chạy truy vấn PerfettoSQL sau đây trong thẻ Query (SQL) (Truy vấn (SQL)) để đo lường RSS ẩn danh và RSS tệp trung bình mà mỗi quy trình của ứng dụng lưu giữ ở mỗi cấp oom_score_adj:

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;

Kết quả mẫu cho dấu vết phân tách trong Hình 2:

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

Kết quả mẫu cho dấu vết được đặt cùng vị trí trong Hình 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

Trong cấu hình phân tách, chỉ có khoảng 19 MB bộ nhớ ẩn danh ở lại oom_score_adj = 200 sau giây đầu tiên. Trong cấu hình cùng vị trí, khoảng 117 MB vẫn ở 50 và sau đó là 200 trong suốt thời gian dịch vụ trên nền trước chạy.


← Trả lại các trang bộ nhớ cho hệ điều hành | ↑ Lên trên