Đặt ngân sách bộ nhớ ứng dụng

Ngân sách bộ nhớ ứng dụng cho phép các ứng dụng tự khai báo ngân sách bộ nhớ, nhờ đó cho hệ thống biết cần giảm mức sử dụng bộ nhớ khi ứng dụng sử dụng nhiều hơn ngân sách đã đặt. Điều này đặc biệt hữu ích đối với các ứng dụng hệ thống và ứng dụng đi kèm hoặc đối với các ứng dụng nhắm đến những thiết bị có bộ nhớ hạn chế, trong đó nhà phát triển biết tập hợp hoạt động bộ nhớ dự kiến (bộ nhớ mà ứng dụng cần để người dùng đang làm gì) và muốn đảm bảo ứng dụng của họ không sử dụng quá nhiều tài nguyên RAM dùng chung của hệ thống.

Ngân sách được cân bằng bằng cách sử dụng thao tác loại bỏ và hoán đổi bộ nhớ để xoá các trang bộ nhớ không được dùng gần đây, tập trung mức sử dụng bộ nhớ của ứng dụng vào nhóm hoạt động hiện tại. Khi một ứng dụng vượt quá ngân sách đã khai báo, hệ điều hành sẽ nhắm đến việc thu hồi cụ thể tại ứng dụng đó:

  1. Các trang được sao lưu bằng tệp clean (chẳng hạn như mã không hoạt động và các thành phần được liên kết) sẽ bị loại bỏ trước vì có thể đọc lại từ bộ nhớ nếu cần.
  2. Các trang được sao lưu bằng tệp có sửa đổi sẽ được ghi lại vào bộ nhớ và bị loại bỏ.
  3. Các trang bộ nhớ ẩn danh (chẳng hạn như các phân bổ vùng nhớ đệm) được nén và hoán đổi sang zRAM.

Miễn là nhóm hoạt động không vượt quá ngân sách, ứng dụng sẽ hoạt động tốt trong khi không sử dụng bộ nhớ nhiều hơn ngân sách đã đặt. Hệ điều hành sẽ loại bỏ bộ nhớ không dùng đến và nén các trang heap không hoạt động để hoán đổi, nhờ đó, việc phân bổ bộ nhớ vẫn bị giới hạn mà không làm chấm dứt quy trình.

Điều gì xảy ra khi một ứng dụng vượt quá ngân sách

Việc vượt quá hạn mức bộ nhớ không khiến hệ thống chấm dứt hoặc làm hỏng ứng dụng của bạn. Thay vào đó, khi một ứng dụng sử dụng nhiều bộ nhớ hơn hạn mức cho phép, hệ điều hành sẽ tìm bộ nhớ mà ứng dụng chưa dùng trong một khoảng thời gian và lưu trữ bộ nhớ đó một cách an toàn cho đến khi cần dùng lại. Điều này nhường chỗ cho việc phân bổ bộ nhớ ứng dụng mới và giữ cho mức sử dụng bộ nhớ tổng thể của ứng dụng nằm trong ngân sách.

Miễn là có khoảng trống giữa những gì ứng dụng cần tại một thời điểm bất kỳ (tập hợp hoạt động của ứng dụng) và ngân sách đã đặt, ứng dụng sẽ tiếp tục chạy bình thường. Nếu một ứng dụng đặt ngân sách nhỏ hơn nhóm hoạt động của ứng dụng, thì hiệu suất của ứng dụng trong thời gian chạy có thể bị chậm lại.

Để tìm hiểu cách hệ điều hành theo dõi và thu hồi bộ nhớ, hãy xem hướng dẫn về cấu trúc bộ nhớ:

Khai báo ngân sách trong tệp kê khai Android

Khai báo ngân sách bộ nhớ trong AndroidManifest.xml là phương thức chính và được đề xuất để xác định ngân sách. Công cụ này không yêu cầu mã thời gian chạy, có hiệu lực ngay lập tức khi khởi động quy trình và cung cấp một hợp đồng rõ ràng cho hệ điều hành.

Các khai báo <memory-budget> có hiệu lực trên các thiết bị chạy Android 17 QPR2 (API cấp 37.2) trở lên. Trên các phiên bản Android thấp hơn, trình phân tích cú pháp tệp kê khai nền tảng sẽ bỏ qua một cách an toàn các phần tử XML không được nhận dạng, vì vậy, bạn có thể áp dụng <memory-budget> mà không ảnh hưởng đến khả năng tương thích ngược.

Khai báo ngân sách cơ sở

Đối với hầu hết các ứng dụng, bạn chỉ cần xác định một ngân sách duy nhất cho ứng dụng. Khai báo một phần tử <memory-budget> ngay bên trong thẻ <application>:

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

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

        <!-- Baseline budget for the application -->
        <memory-budget android:maxMb="256" />

    </application>
</manifest>

Thao tác này đặt ngân sách bộ nhớ thường trú là 256 MB trên tất cả các quy trình và trạng thái cho gói. Khi mức sử dụng bộ nhớ của ứng dụng vượt quá 256 MB, hệ điều hành sẽ cắt bớt các trang bộ nhớ không hoạt động bằng cách sử dụng tính năng loại bỏ và hoán đổi.

Thay đổi ngân sách theo trạng thái quy trình

Một ứng dụng cần có lượng bộ nhớ khác nhau tuỳ thuộc vào khả năng hiển thị của người dùng:

  • Nền trước: Quy trình này đang lưu trữ một Hoạt động hiển thị tương tác với người dùng. Trạng thái này thường có mức sử dụng bộ nhớ lớn nhất do giao diện người dùng và đồ hoạ đang hoạt động.
  • Dễ nhận biết: Quy trình này người dùng có thể nhận biết nhưng không lưu trữ một cửa sổ hiển thị (ví dụ: lưu trữ một dịch vụ nền trước phát nội dung nghe nhìn, một lượt tải xuống đang hoạt động ở nền sau, đường đi từng chặng hoặc một phương thức nhập đang hoạt động).
  • Nền: Quy trình đang chạy các công việc, bộ nhận hoặc quá trình đồng bộ hoá dữ liệu ở chế độ nền. Dự kiến sẽ duy trì mức sử dụng bộ nhớ tối thiểu.

Bạn có thể khai báo nhiều mệnh đề <memory-budget> để so khớp các trạng thái này:

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

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

        <!-- Default budget for visible foreground UI -->
        <memory-budget android:maxMb="200" />

        <!-- Tighter budget when playing audio in background -->
        <memory-budget
            android:maxMb="120"
            android:state="perceptible" />

        <!-- Minimal budget when fully in background -->
        <memory-budget
            android:maxMb="48"
            android:state="background" />

    </application>
</manifest>

Bạn không bắt buộc phải có một mệnh đề cơ sở không có android:state; nếu bạn chỉ chỉ định các mệnh đề dành riêng cho tiểu bang (ví dụ: android:state="background"), thì các tiểu bang khác vẫn không bị giới hạn bởi ngân sách của ứng dụng. Khi được đưa vào, một mệnh đề không có android:state sẽ đóng vai trò là phương án dự phòng mặc định cho các trạng thái không xác định (chẳng hạn như nền trước), mà các mệnh đề hạn chế hơn sau đó sẽ ghi đè khi ứng dụng chuyển sang trạng thái perceptible hoặc background.

Khi định cỡ ngân sách cho từng trạng thái, hãy nhắm đến tập hợp hoạt động đang hoạt động của trạng thái đó thay vì bộ nhớ thường trú cao nhất (dấu hiệu bộ nhớ thường trú cao nhất):

  • Điều chỉnh kích thước ngân sách foreground để hiển thị khung hình mượt mà: Cung cấp cho giao diện người dùng nền trước đủ khoảng trống cho nhóm hoạt động đang hoạt động để quá trình thu hồi đồng bộ không làm chậm quá trình hiển thị khung hình. Nếu ứng dụng của bạn có một quy trình làm việc độc lập tiêu tốn nhiều bộ nhớ (chẳng hạn như trình chỉnh sửa video hoặc hình ảnh có độ phân giải cao), hãy cân nhắc chuyển hoạt động đó vào một tiến trình chuyên dụng (xem phần Ứng dụng nhiều tiến trình) để các hoạt động phân bổ được lập ngân sách độc lập và bị huỷ khi người dùng thoát.
  • Điều chỉnh ngân sách background một cách chặt chẽ cho hoạt động không có giao diện người dùng: RSS nền cao trong phép đo từ xa tại hiện trường thường phản ánh các hoạt động phân bổ giao diện người dùng được giữ lại sau khi rời khỏi nền trước (xem Đồ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ớ và Giải phóng bộ nhớ để phản hồi các sự kiện) hoặc các hoạt động đọc tệp một lần trong quá trình lập chỉ mục và đồng bộ hoá ở chế độ nền. Vì công việc nền không có giao diện người dùng không có thời hạn về khung hình, nên việc loại bỏ các trang bộ nhớ đệm tệp một lần và hoán đổi các trang heap không hoạt động sang zRAM khi một hoạt động đồng bộ hoá trong nền phân bổ vượt quá ngân sách trong thời gian ngắn sẽ không gây ra hiện tượng giật mà người dùng nhìn thấy được, đồng thời ngăn các hoạt động tăng đột biến trong nền loại bỏ ứng dụng trên nền trước của người dùng.

Cách trạng thái ngân sách liên kết với trạng thái xử lý

Nền tảng này ánh xạ các trạng thái ngân sách trong tệp kê khai sang các trạng thái quy trình thời gian chạy dựa trên RunningAppProcessInfo.importance. Trình quản lý hoạt động tính toán trạng thái này từ cả các thành phần đang hoạt động của quy trình và mọi hoạt động liên kết dịch vụ đến hoặc truy vấn nhà cung cấp nội dung từ các ứng dụng khác:

  • foreground: Các hoạt động tương tác đang diễn ra và hiển thị với người dùng, chẳng hạn như lưu trữ một Hoạt động đã tiếp tục, duy trì trạng thái ứng dụng hàng đầu khi màn hình tắt hoặc lưu trữ một dịch vụ hoặc trình cung cấp nội dung được ứng dụng hàng đầu trên nền trước liên kết hoặc truy vấn một cách chủ động.
  • perceptible: Những tải trọng mà người dùng có thể nhận thấy mà không có cửa sổ hiển thị, chẳng hạn như hoạt động phát nội dung nghe nhìn, chỉ đường từng chặng, hoạt động chụp bằng camera hoặc micrô, hoạt động tải xuống ở chế độ nền, dịch vụ đồng bộ hoá dữ liệu ở nền trước hoặc các dịch vụ do một ứng dụng nền trước liên kết bằng các cờ như BIND_NOT_FOREGROUND (xem phần Kiểm soát hoạt động kế thừa bằng các cờ BIND). Mặc dù các chỉ số nội bộ của nền tảng có thể gắn nhãn PROCESS_STATE_IMPORTANT_FOREGROUND cho một số khối lượng công việc này, nhưng hằng số nội bộ đó không biểu thị một cửa sổ hiển thị và chịu sự điều chỉnh của ngân sách perceptible.
  • background: Công việc mà người dùng không nhận thấy ngay, chẳng hạn như công việc trong nền, chuông báo, broadcast receiver hoặc Activity trước đó sau khi người dùng rời khỏi.

Bảng sau đây cho biết cách các cấp độ quan trọng khi chạy ánh xạ đến các trạng thái tệp kê khai:

Tệp kê khai android:state Tầm quan trọng của thời gian chạy (RunningAppProcessInfo) Các thành phần tiêu biểu
foreground IMPORTANCE_FOREGROUND
IMPORTANCE_TOP_SLEEPING
Hoạt động hiển thị đã tiếp tục, ứng dụng hàng đầu có màn hình bị khoá, dịch vụ hoặc trình cung cấp nội dung do ứng dụng hàng đầu liên kết
perceptible IMPORTANCE_FOREGROUND_SERVICE
IMPORTANCE_VISIBLE
Dịch vụ trên nền trước đang phát nội dung nghe nhìn, chỉ đường, tải xuống hoặc đồng bộ hoá; dịch vụ được liên kết với dịch vụ trên nền trước hoặc các cờ hiển thị
background IMPORTANCE_PERCEPTIBLE
IMPORTANCE_CANT_SAVE_STATE
IMPORTANCE_SERVICE
Các công việc trong nền, receiver, đồng bộ hoá ở chế độ nền, Activity trước đó hoặc ứng dụng trang chủ ở chế độ nền

Để biết thông tin chi tiết về cách các liên kết ứng dụng nâng cao quy trình lưu trữ dịch vụ hoặc nhà cung cấp và cách quy trình chuyển đổi trở lại background hoặc cached sau khi ứng dụng huỷ liên kết hoặc di chuyển ra khỏi màn hình, hãy xem bài viết Các liên kết dịch vụ và trạng thái quy trình.

Khi một quy trình chuyển sang trạng thái đã lưu vào bộ nhớ đệm (IMPORTANCE_CACHED) mà không có thành phần đang hoạt động, nền tảng sẽ giải phóng hạn mức bộ nhớ đang hoạt động và quản lý bộ nhớ thông qua quy trình thu hồi quy trình đã lưu vào bộ nhớ đệm tiêu chuẩn.

Kiểm tra trạng thái và ngân sách của quy trình trong ứng dụng

Một cách để kiểm tra trạng thái và mức độ quan trọng của quy trình đang hoạt động của ứng dụng trong quá trình phát triển là truy vấn Trình quản lý hoạt động bằng ADB:

adb shell dumpsys activity processes <package-name>

Ví dụ rút gọn sau đây cho thấy các mục nhập bản ghi quy trình và chế độ kiểm soát OOM cho một ứng dụng đang chạy dịch vụ nền trước:

ACTIVITY MANAGER RUNNING PROCESSES (dumpsys activity processes)
  All known processes:
  *APP* UID 10123 ProcessRecord{edf056c 3919:com.example.app/u0a123}
    pid=3919
    oom adj: max=1001 curRaw=200 setRaw=200 cur=200 set=200
    curProcState=4 mRepProcState=4 setProcState=4 lastStateTime=-42s360ms
    hasStartedServices=true
    mHasForegroundServices=true forcingToImportant=null
...
  Process OOM control (48 total):
    Proc #22: prcp  F/S/FGS  ---NFU-TI  t: 0 3919:com.example.app/u0a123 (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 kết quả này:

  • curProcState=4 và state: cur=FGS cho biết quy trình đang ở trạng thái dịch vụ nền trước. Nếu ứng dụng của bạn đang lưu trữ một hoạt động đang diễn ra và hiển thị, thì hoạt động này sẽ xuất hiện dưới dạng TOP. Nếu đang chạy một thành phần quan trọng trên nền trước (chẳng hạn như tải xuống hoặc đồng bộ hoá), thì biểu tượng sẽ là IMPF.
  • Trong bảng điều khiển OOM của quy trình, prcp cho biết quy trình được đánh giá theo cấp độ ưu tiên có thể nhận thấy, tương ứng với ngân sách perceptible.

Cách kiểm tra ngân sách bộ nhớ hiện đang được thực thi và mức sử dụng bộ nhớ thường trú:

adb shell dumpsys meminfo <package-name>

Kể từ Android 17 QPR2, đầu ra này bao gồm một phần Ngân sách bộ nhớ hiển thị trần giới hạn đang hoạt động, nguồn giới hạn (chẳng hạn như AndroidManifest hoặc MemoryBudgetManager) và bộ nhớ thường trú hiện tại.

Ứng dụng đa tiến trình

Nếu ứng dụng của bạn phân chia công việc trên nhiều quy trình, hãy định cấu hình ngân sách quy trình chuyên dụng bằng cách sử dụng thẻ <process> bên trong <processes>.

Ví dụ: hãy xem xét một ứng dụng phát nhạc trực tuyến (com.example.radio):

  1. Quy trình chính: Lưu trữ giao diện người dùng hiển thị và công cụ phát âm thanh (MediaSessionService có dịch vụ nền trước mediaPlayback). Khi hiển thị, quy trình này hoạt động trong phạm vi ngân sách nền trước 180 MB. Khi người dùng rời khỏi ứng dụng trong lúc nhạc vẫn tiếp tục phát, quy trình sẽ chuyển sang trạng thái perceptible, trong đó ngân sách 64 MB là đủ cho công cụ phát và bộ đệm âm thanh.
  2. Quy trình đồng bộ hoá (:sync): Quy trình chuyên dụng chạy tính năng đồng bộ hoá siêu dữ liệu ở chế độ nền và lập chỉ mục tải xuống. Vì quy trình này chỉ hoạt động ở chế độ nền, nên bạn không cần khai báo rõ ràng state="background"; chỉ áp dụng một ngân sách.
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.radio">

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

        <!-- Package baseline: main process with UI and audio playback -->
        <memory-budget android:maxMb="180" />

        <!-- Tighter budget when audio plays in the background -->
        <memory-budget
            android:maxMb="64"
            android:state="perceptible" />

        <!-- Dedicated background sync process -->
        <processes>
            <process android:process=":sync">
                <memory-budget android:maxMb="32" />
            </process>
        </processes>

        <service
            android:name=".playback.AudioPlayerService"
            android:foregroundServiceType="mediaPlayback"
            android:exported="false" />

        <service
            android:name=".sync.PlaylistSyncService"
            android:process=":sync"
            android:exported="false" />

    </application>
</manifest>

Mức sử dụng bộ nhớ trong mọi quy trình con đều được tính vào cả ngân sách quy trình và ngân sách gói kèm theo. Một quy trình gặp phải tình trạng thiếu bộ nhớ trong thời gian chạy nếu quy trình đó vi phạm ngân sách quy trình hoặc ngân sách gói, tuỳ theo ngưỡng nào đạt đến trước.

Điều chỉnh ngân sách cho màn hình có mật độ điểm ảnh cao

Đối với những ứng dụng có mức sử dụng bộ nhớ tăng đáng kể theo số lượng pixel cần vẽ trên màn hình cùng một lúc (chẳng hạn như ứng dụng thư viện ảnh lưu vào bộ nhớ đệm các bitmap có kích thước phù hợp với màn hình), Android cung cấp 2 cơ chế thay thế để điều chỉnh ngân sách một cách linh hoạt theo thông số kỹ thuật của màn hình:

  • Điều chỉnh tỷ lệ theo nhóm mật độ hiển thị (android:additionalMbPerDensity): Thêm megabyte tương ứng với tỷ lệ mật độ của màn hình so với mdpi (1,0x / 160 dpi). Điều này phù hợp khi mức sử dụng bộ nhớ tăng theo các nhóm mật độ giao diện người dùng, chẳng hạn như lưu vào bộ nhớ đệm các thành phần có thể vẽ raster độ phân giải cao hoặc thành phần giao diện người dùng:

    <!-- Baseline 180MB + 16MB per 1.0x density ratio -->
    <memory-budget
        android:maxMb="180"
        android:additionalMbPerDensity="16" />
    

    Trên màn hình mdpi (1,0x), ngân sách là 180 + 16 × 1 = 196 MB. Trên màn hình xxhdpi (3.0x), ngân sách sẽ tăng lên 180 + 16 × 3 = 228 MB.

  • Điều chỉnh tỷ lệ theo độ phân giải màn hình thực (android:additionalBytesPerDisplayPixel): Thêm trực tiếp các byte cho mỗi pixel trên màn hình thực (Chiều rộng × Chiều cao). Điều này rất phù hợp với các ứng dụng phân bổ vùng hiển thị đồ hoạ toàn màn hình, kết xuất vùng đệm hoặc bộ nhớ đệm ảnh có độ phân giải cao, trong đó mức tiêu thụ bộ nhớ tăng tỷ lệ thuận với số lượng pixel hiển thị thô thay vì các nhóm mật độ giao diện người dùng:

    <!-- Baseline 128MB + 16 bytes per physical display pixel -->
    <!-- For example, a 4-byte RGBA full-screen buffer with double or quadruple buffering -->
    <memory-budget
        android:maxMb="128"
        android:additionalBytesPerDisplayPixel="16" />
    

    Trên màn hình 1080p (1080 × 2400 &approx; 2,59 triệu pixel), thao tác này sẽ thêm &approx; 41,4 MB vào ngân sách cơ sở. Trên màn hình 1440p (1440 × 3120 &approx; 4,49 triệu pixel), kích thước này sẽ tăng thêm khoảng 71,8 MB.

Hai thuộc tính này là các thuộc tính thay thế. Chọn thuộc tính phù hợp với hệ số tỷ lệ chính của ứng dụng và tránh kết hợp cả hai trong cùng một mệnh đề.

Chuyên biệt hoá cho các hệ số hình dạng thiết bị

Khi vận chuyển một APK trên điện thoại, máy tính bảng và Wear OS, hãy sử dụng thuộc tính android:feature để điều chỉnh ngân sách cho các mục tiêu phần cứng khác nhau.

Trên đồng hồ Wear OS, RAM bị hạn chế và giao diện người dùng cũng như bộ tính năng của ứng dụng đơn giản hơn nhiều. Bạn có thể khai báo một ngân sách chặt chẽ hơn dành riêng cho tính năng watch:

<!-- General phone and tablet baseline -->
<memory-budget android:maxMb="180" />

<!-- Wear OS override: simpler UI and constrained hardware -->
<memory-budget
    android:maxMb="48"
    android:feature="watch" />

Quy tắc phân giải: điều khoản áp dụng sau cùng sẽ có hiệu lực

Khi xác định nhiều phần tử <memory-budget> cho một ứng dụng hoặc quy trình, hệ thống sẽ đánh giá các phần tử đó theo thứ tự được khai báo trong tệp kê khai. Mệnh đề ngân sách áp dụng gần đây nhất là mệnh đề được thực thi.

Vì ngân sách áp dụng sau cùng sẽ được ưu tiên, nên thứ tự là yếu tố quan trọng. Luôn đặt ngân sách cơ sở chung nhất trước, sau đó là các chế độ ghi đè cụ thể hơn (chẳng hạn như các điều khoản dành riêng cho tiểu bang hoặc phần cứng).

Tài liệu tham khảo về thuộc tính XML

Tất cả các thuộc tính kích thước bộ nhớ đều được biểu thị bằng megabyte (MB) và ánh xạ đến mức phí memory.current cgroup của Linux (không bao gồm bộ nhớ dùng chung như Zygote).

Thuộc tính Định dạng Mặc định Mô tả
android:maxMb Số nguyên (> 0) Bắt buộc Giới hạn ngân sách bộ nhớ thường trú cơ sở tính bằng MB.
android:state Enum Khán giả có Trạng thái quy trình mà ngân sách này áp dụng: foreground, perceptible hoặc background. Nếu bị bỏ qua, mệnh đề này sẽ đóng vai trò là phương án dự phòng cho mọi trạng thái không được chỉ định.
android:additionalMbPerDensity Số nguyên (≥ 0) 0 Số megabyte bổ sung cần thêm cho mỗi đơn vị tỷ lệ mật độ hiển thị so với mdpi (1,0x).
android:additionalBytesPerDisplayPixel Số nguyên (≥ 0) 0 Số byte bổ sung được phân bổ cho mỗi pixel trên màn hình thực (Chiều rộng × Chiều cao), hữu ích cho các vùng đệm và bitmap trên bề mặt.
android:feature Chuỗi Khán giả có Giới hạn mệnh đề đối với các thiết bị khai báo các tính năng phần cứng cụ thể: watch, automotive hoặc leanback.

Runtime API (lựa chọn động thứ cấp)

Giải pháp ưu tiên cho hầu hết mọi ứng dụng là khai báo ngân sách một cách tĩnh trong AndroidManifest.xml. Tuy nhiên, đối với các ứng dụng có khối lượng công việc động hoặc để thử nghiệm trong thời gian chạy, Android cung cấp API NDK và SDK thời gian chạy dưới dạng lựa chọn thứ hai.

API thời gian chạy cho phép bạn:

  • Truy vấn mức sử dụng bộ nhớ hiện tại và ngân sách hiệu quả.
  • Điều chỉnh ngân sách xử lý theo hướng giảm một cách linh hoạt.
  • Theo dõi các sự kiện vượt quá ngân sách để chủ động cắt bớt bộ nhớ đệm trước khi hệ điều hành kích hoạt tính năng thu hồi trực tiếp.

API SDK Android (MemoryBudgetManager)

Dịch vụ hệ thống MemoryBudgetManager có sẵn cho các ứng dụng được viết bằng Kotlin và Java kể từ Android 17 QPR2 (bản phát hành SDK phụ, cấp độ API 37.2 / Build.VERSION_CODES_FULL.CINNAMON_BUN_2).

Truy xuất dịch vụ

Trước khi truy cập vào MemoryBudgetManager, hãy kiểm tra để đảm bảo thiết bị không chạy phiên bản thấp hơn Android 17 QPR2 bằng cách sử dụng SDK_INT_FULL:

if (Build.VERSION.SDK_INT_FULL >= Build.VERSION_CODES_FULL.CINNAMON_BUN_2) {
    val budgetManager = context.getSystemService(MemoryBudgetManager::class.java)
}

Mức sử dụng và ngân sách truy vấn

// Query current memory charged to this process and the package UID
val processUsageBytes = budgetManager.processCurrentUsageBytes
val packageUsageBytes = budgetManager.packageCurrentUsageBytes

// Query effective budgets (returns LIMIT_IS_DISABLED if unconstrained)
val processBudgetBytes = budgetManager.processBudgetBytes
val packageBudgetBytes = budgetManager.packageBudgetBytes

Đặt hoặc xoá ngân sách một cách linh hoạt

Bạn có thể đặt ngân sách chặt chẽ hơn trong thời gian chạy để hạn chế bộ nhớ trong các tác vụ đơn giản hoặc xoá ngân sách đó khi tác vụ hoàn tất:

// Set a tighter dynamic budget on the current process (e.g., 96 MB)
try {
    budgetManager.processBudgetBytes = 96L * 1024L * 1024L
} catch (e: IllegalArgumentException) {
    // Thrown if the budget is <= 0 or exceeds the manifest ceiling or system limit
    Log.e(TAG, "Requested budget exceeds manifest or system ceiling", e)
}

// Clear the dynamic process budget to restore the manifest limit (or unconstrained baseline)
budgetManager.clearProcessBudget()

Không giống như các mệnh đề trong tệp kê khai <memory-budget>, tự động chuyển đổi ngân sách bất cứ khi nào quy trình chuyển đổi giữa các trạng thái foreground, perceptible và background, một ngân sách linh hoạt được đặt thông qua MemoryBudgetManager sẽ áp dụng một mức trần duy nhất cho đến khi mã của bạn cập nhật hoặc xoá mức trần đó. Nếu bạn sử dụng API thời gian chạy để chạy các thử nghiệm tại trường hoặc quản lý các giới hạn theo trạng thái, hãy cập nhật hoặc xoá ngân sách động trong các quá trình chuyển đổi vòng đời (ví dụ: sử dụng ProcessLifecycleOwner hoặc các lệnh gọi lại vòng đời của activity, như minh hoạ trong Ví dụ về trình chỉnh sửa hình ảnh thích ứng).

Hiệu suất và tính chất bất biến

Khi làm việc với MemoryBudgetManager trong thời gian chạy, hãy lưu ý những đặc điểm sau:

  • Tính chất bất biến: Tất cả các API biến đổi ngân sách đều có tính chất bất biến. Việc gọi clearProcessBudget() hoặc clearPackageBudget() nhiều lần hoặc khi không có ngân sách động nào đang hoạt động là một thao tác không có tác dụng. Tương tự, việc đặt setProcessBudgetBytes() hoặc setPackageBudgetBytes() thành cùng một giá trị liên tiếp sẽ không kích hoạt các cấu hình lại hệ thống dư thừa.
  • Tần suất gọi: Việc đặt, cập nhật hoặc xoá ngân sách sẽ thực hiện một lệnh gọi liên quy trình đến dịch vụ hệ thống để cập nhật bộ điều khiển bộ nhớ hệ điều hành cho quy trình. Mặc dù có dung lượng nhỏ, nhưng công cụ này không hoàn toàn miễn phí. Gọi các API này trong quá trình chuyển đổi vòng đời rời rạc và các mốc quan trọng của tác vụ (chẳng hạn như vào hoặc rời khỏi một hoạt động, bắt đầu hoặc hoàn tất quá trình xử lý ở chế độ nền hoặc xử lý các yêu cầu về lời nói) và tránh gọi các API này trong các vòng lặp chặt chẽ, các luồng xử lý âm thanh hoặc các quy trình kết xuất theo khung hình.
  • Tần suất truy vấn: Các thuộc tính truy vấn processCurrentUsageBytes và packageCurrentUsageBytes tìm nạp mức sử dụng bộ nhớ trực tiếp từ dịch vụ hệ thống. Mặc dù nhanh, nhưng các truy vấn phải được gọi theo yêu cầu (chẳng hạn như trong quá trình chuyển đổi tác vụ hoặc bên trong lệnh gọi lại OnOverBudgetListener) thay vì được thăm dò trong các vòng lặp liên tục.

Theo dõi các lệnh gọi lại áp lực vượt quá ngân sách

Các ứng dụng có thể đăng ký một trình nghe để nhận thông báo khi mức sử dụng bộ nhớ vượt quá ngưỡng ngân sách. Điều này cho phép ứng dụng thực hiện quy trình dọn dẹp chủ động ở cấp ứng dụng (chẳng hạn như xoá bộ nhớ đệm bitmap trong bộ nhớ) trước khi hệ điều hành kích hoạt độ trễ thu hồi trực tiếp:

val listener = MemoryBudgetManager.OnOverBudgetListener { budgetBytes ->
    Log.w(TAG, "Process exceeded memory budget of $budgetBytes bytes")
    // Proactively evict caches to release memory
    imageTileCache.evictAll()
}

// Register on the main Looper
budgetManager.registerProcessOverBudgetListener(mainLooper, listener)

// When done (e.g., in onStop)
budgetManager.unregisterProcessOverBudgetListener(listener)

Các phương pháp hay nhất cho lệnh gọi lại vượt quá ngân sách:

  • Nhanh chóng: Các hoạt động khôi phục phải mang lại hiệu quả ngay lập tức. Các phép tính phức tạp trong quá trình chịu áp lực sẽ làm giảm hiệu suất.
  • Tránh phân bổ: Đừng phân bổ các đối tượng mới hoặc bắt đầu các luồng mới bên trong lệnh gọi lại, vì việc này có thể kích hoạt hoạt động thu hồi trực tiếp ngay lập tức của hệ điều hành.
  • Tập trung vào các mục tiêu có năng suất cao: Việc loại bỏ các Bitmap lớn, vùng đệm kết xuất hoặc đóng các tệp được ánh xạ trong bộ nhớ sẽ hiệu quả hơn nhiều so với việc phát hành nhiều đối tượng nhỏ.

API NDK gốc (<android/memory_budget_manager.h>)

Các ứng dụng gốc có thể sử dụng C NDK API do libandroid.so cung cấp kể từ Android 17 QPR2 (cấp độ API 37.2).

Cấu hình CMake

find_library(android-lib android)
target_link_libraries(my_native_engine PRIVATE ${android-lib})

Bao gồm tiêu đề và mức sử dụng truy vấn

#include <android/memory_budget_manager.h>

// Query current memory usage
int64_t process_usage = AMemoryBudgetManager_getProcessCurrentUsageBytes();
int64_t package_usage = AMemoryBudgetManager_getPackageCurrentUsageBytes();

// Query current budget
int64_t process_budget = 0;
AMemoryBudgetResult result = AMemoryBudgetManager_getProcessBudget(&process_budget);
if (result == AMEMORY_BUDGET_RESULT_SUCCESS) {
    // Current budget available in process_budget
} else if (result == AMEMORY_BUDGET_RESULT_LIMIT_IS_DISABLED) {
    // No budget is currently active
}

Định cấu hình ngân sách gốc một cách linh hoạt

// Set a tighter process budget (e.g. 160MB)
AMemoryBudgetResult result = AMemoryBudgetManager_setProcessBudget(160LL * 1024 * 1024);
if (result != AMEMORY_BUDGET_RESULT_SUCCESS) {
    const char* error_msg = AMemoryBudgetManager_resultToString(result);
    // Handle error (e.g. AMEMORY_BUDGET_RESULT_ERROR_EXCEEDS_MANIFEST_LIMIT)
}

// Clear the dynamic budget to resume manifest limits (or unconstrained baseline)
AMemoryBudgetManager_clearProcessBudget();

Theo dõi các sự kiện áp lực bộ nhớ

NDK cung cấp 2 cách để theo dõi các sự kiện liên quan đến bộ nhớ:

  1. High-Level Watcher (AMemoryBudgetManager_Watcher_create): Giám sát các sự kiện trên ALooper bằng tính năng loại bỏ trùng lặp tự động.
  2. Chỉ số mô tả tệp cấp thấp: AMemoryBudgetManager_getProcessMemoryPressureFd trả về chỉ số mô tả tệp gốc có thể được tích hợp trực tiếp vào một vòng lặp công cụ epoll tuỳ chỉnh.
void onMemoryPressure(int32_t event_mask, const AMemoryBudgetEvents* events, void* userdata) {
    // High-yield eviction of unused native textures or geometry caches
    purgeNativeTextureCaches();
}

// Register watcher on an ALooper with a 1000ms debounce interval
AMemoryBudgetManagerWatcher* watcher = AMemoryBudgetManager_Watcher_create(
    looper,
    AMEMORY_BUDGET_MANAGER_EVENT_PROCESS,
    1000 /* debounce_ms */,
    &onMemoryPressure,
    NULL /* userdata */
);

// When done:
AMemoryBudgetManager_Watcher_destroy(watcher);

Ví dụ về Runtime API

Các ví dụ sau đây minh hoạ cách triển khai API thời gian chạy bằng API SDK Android (viết bằng Kotlin) và API NDK gốc (viết bằng C++).

Ví dụ về SDK Android: Trình chỉnh sửa hình ảnh thích ứng

Ví dụ này cho thấy một ứng dụng chỉnh sửa hình ảnh (com.example.imageeditor) có tệp kê khai khai báo mức trần 256 MB để phù hợp với canvas nhiều lớp:

<manifest ... >
    <application ... >
        <!-- Manifest ceiling accommodates the heaviest editing workload -->
        <memory-budget android:maxMb="256" />
    </application>
</manifest>

Khi người dùng duyệt xem thư viện hình thu nhỏ có kích thước nhỏ, ứng dụng sẽ sử dụng API SDK Android bằng Kotlin để tự động điều chỉnh ngân sách xử lý xuống 96 MB. Khi người dùng mở canvas chỉnh sửa nhiều lớp, ứng dụng sẽ xoá ngân sách động để khôi phục hạn mức tệp kê khai đầy đủ là 256 MB. Nó cũng đăng ký một OnOverBudgetListener để loại bỏ các bitmap xem trước được lưu vào bộ nhớ đệm khi chịu áp lực.

package com.example.imageeditor.ui

import android.app.Activity
import android.app.MemoryBudgetManager
import android.graphics.Bitmap
import android.os.Bundle
import android.util.Log
import android.util.LruCache

class ImageEditorActivity : Activity() {

    private lateinit var budgetManager: MemoryBudgetManager

    // In-memory cache for rendered preview tiles (32MB limit)
    private val previewCache = object : LruCache<String, Bitmap>(32 * 1024 * 1024) {
        override fun sizeOf(key: String, value: Bitmap): Int = value.byteCount
    }

    private val overBudgetListener = MemoryBudgetManager.OnOverBudgetListener { budgetBytes ->
        Log.w(TAG, "Process memory pressure detected (budget: ${budgetBytes / 1048576}MB). Evicting preview cache.")
        previewCache.evictAll()
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        budgetManager = getSystemService(MemoryBudgetManager::class.java)
    }

    override fun onStart() {
        super.onStart()
        // Register listener for process-level memory breaches
        budgetManager.registerProcessOverBudgetListener(mainLooper, overBudgetListener)
        // Constrain memory during lightweight gallery browsing
        applyGalleryBudget()
    }

    override fun onStop() {
        super.onStop()
        budgetManager.unregisterProcessOverBudgetListener(overBudgetListener)
    }

    /**
     * Called when the user enters the high-resolution editing canvas.
     * Clears the dynamic budget, restoring the full 256MB manifest ceiling.
     */
    fun enterEditingCanvas() {
        // Clear the tighter dynamic budget to restore the full manifest ceiling (256MB)
        budgetManager.clearProcessBudget()
        Log.i(TAG, "Restored manifest budget ceiling (256MB) for editing canvas")
    }

    /**
     * Called when the user exits the editor back to the thumbnail gallery.
     * Re-applies the tighter dynamic budget.
     */
    fun exitToGallery() {
        previewCache.trimToSize(8 * 1024 * 1024)
        applyGalleryBudget()
    }

    private fun applyGalleryBudget() {
        try {
            // Dynamically tighten budget to 96MB for the lightweight gallery view
            budgetManager.processBudgetBytes = 96L * 1024L * 1024L
            Log.i(TAG, "Tighter dynamic budget applied for gallery: 96MB")
        } catch (e: IllegalArgumentException) {
            Log.e(TAG, "Could not apply dynamic budget", e)
        }
    }

    companion object {
        private const val TAG = "ImageEditor"
    }
}

Ví dụ về C++ trong NDK: Công cụ 3D gốc

Ví dụ này cho thấy một công cụ phát triển trò chơi C++ gốc đang quản lý ngân sách bộ nhớ dựa trên cấp chất lượng đồ hoạ đang hoạt động, giả sử tệp kê khai của ứng dụng khai báo trần 512 MB để đáp ứng đồ hoạ Chất lượng cao (android:maxMb="512"). Công cụ này sẽ tự động thắt chặt ngân sách cho các chế độ cài đặt sẵn có chất lượng thấp hơn và sử dụng AMemoryBudgetManager_Watcher_create trên ALooper để huỷ tải mipmap kết cấu khi vượt quá ngân sách.

#include <android/memory_budget_manager.h>
#include <android/looper.h>
#include <android/log.h>

#define LOG_TAG "Native3DEngineMemory"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__)

class MemoryGovernor {
public:
    MemoryGovernor() : mWatcher(nullptr) {}

    ~MemoryGovernor() {
        stopMonitoring();
    }

    // Configures process budget based on user graphics quality settings
    bool setQualityBudget(int qualityLevel) {
        int64_t targetBytes = 0;
        switch (qualityLevel) {
            case 0: // Low (budget: 128MB)
                targetBytes = 128LL * 1024 * 1024;
                break;
            case 1: // Medium (budget: 256MB)
                targetBytes = 256LL * 1024 * 1024;
                break;
            case 2: // High (budget: 512MB)
                targetBytes = 512LL * 1024 * 1024;
                break;
            default:
                // Clear dynamic override and restore manifest limit
                AMemoryBudgetManager_clearProcessBudget();
                return true;
        }

        AMemoryBudgetResult result = AMemoryBudgetManager_setProcessBudget(targetBytes);
        if (result != AMEMORY_BUDGET_RESULT_SUCCESS) {
            LOGW("Could not set quality budget: %s", AMemoryBudgetManager_resultToString(result));
            return false;
        }
        return true;
    }

    bool startMonitoring(ALooper* looper) {
        if (!looper) return false;

        // Monitor process budget events, debounced to at most once every 1000ms
        mWatcher = AMemoryBudgetManager_Watcher_create(
            looper,
            AMEMORY_BUDGET_MANAGER_EVENT_PROCESS,
            1000,
            &MemoryGovernor::onPressureEvent,
            this
        );
        return mWatcher != nullptr;
    }

    void stopMonitoring() {
        if (mWatcher) {
            AMemoryBudgetManager_Watcher_destroy(mWatcher);
            mWatcher = nullptr;
        }
    }

    void unloadUnusedTextures() {
        LOGW("Memory pressure callback triggered. Purging cached texture mipmaps...");
        // Fast, high-yield eviction without allocating memory
    }

private:
    static void onPressureEvent(
        int32_t event_mask,
        const AMemoryBudgetEvents* events,
        void* userdata
    ) {
        auto* governor = static_cast<MemoryGovernor*>(userdata);
        governor->unloadUnusedTextures();
    }

    AMemoryBudgetManagerWatcher* mWatcher;
};