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

Ngân sách bộ nhớ ứng dụng cho phép ứng dụng khai báo ngân sách bộ nhớ cho chính mình, từ đó yêu cầu hệ thống cắt 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 cho các ứng dụng hệ thống và ứng dụng đi kèm hoặc cho các ứng dụng nhắm đến thiết bị có bộ nhớ hạn chế, trong đó nhà phát triển biết tập hợp bộ nhớ hoạt động dự kiến (bộ nhớ mà ứng dụng cần để thực hiện những việc người dùng đang làm) 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 giữ cân bằng bằng cách sử dụng hoạt động hoán đổi và loại bỏ bộ nhớ để xoá các trang bộ nhớ không được sử dụng gần đây, tập trung mức sử dụng bộ nhớ của ứng dụng vào tập hợp đang 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 mục tiêu thu hồi cụ thể vào ứng dụng đó:

  1. Các trang được hỗ trợ tệp sạch (chẳng hạn như mã không hoạt động và tài sản được liên kết) sẽ bị loại bỏ trước tiên vì có thể đọc lại từ bộ nhớ nếu cần.
  2. Các trang được hỗ trợ tệp bẩn đượ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ư phân bổ vùng nhớ khối xếp) được nén và hoán đổi sang zRAM.

Miễn là tập hợp đang 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 nhiều bộ nhớ hơn mức 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 vùng nhớ khối xếp không hoạt động để hoán đổi, nhờ đó, việc phân bổ bộ nhớ vẫn được giới hạn mà không 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á ngân sách 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 mức ngân sách cho phép, hệ điều hành sẽ tìm bộ nhớ mà ứng dụng chưa sử dụng trong một thời gian và tạm thời loại bỏ bộ nhớ đó một cách an toàn cho đến khi cần dùng lại. Điều này giúp giải phóng bộ nhớ cho các lượt phân bổ bộ nhớ mới của ứng dụng và giữ cho mức sử dụng bộ nhớ tổng thể của ứng dụng nằm trong giới hạn.

Miễn là có khoảng trống giữa những gì ứng dụng cần tại bất kỳ thời điểm nào (tập hợp đang hoạt độ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, 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 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.

Nội dung khai báo <memory-budget> có hiệu lực trên các thiết bị chạy Android 17 QPR2 (cấp độ API 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 các phần tử XML không nhận dạng được một cách an toàn, vì vậy, bạn có thể sử 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 sẽ đặt ngân sách bộ nhớ thường trú 256 MB cho tất cả các quy trình và trạng thái của 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 loại bỏ và hoán đổi.

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

Ứng dụng yêu cầu lượng bộ nhớ khác nhau tuỳ thuộc vào chế độ hiển thị của người dùng:

  • Nền trước: Quy trình đ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: Người dùng có thể nhận biết được quy trình nhưng quy trình này không lưu trữ cửa sổ hiển thị (ví dụ: lưu trữ dịch vụ trên nền trước phát nội dung nghe nhìn, tải xuống trong nền đang hoạt động, đường đi từng chặng hoặc phương thức nhập đang hoạt động).
  • Nền: Quy trình đang chạy các công việc, trình nhận hoặc đồng bộ hoá dữ liệu trong nền. Dự kiến sẽ duy trì mức sử dụng tài nguyên tối thiểu.

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

<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ệnh đề cơ sở không có android:state; nếu bạn chỉ chỉ định các mệnh đề theo trạng thái cụ thể (ví dụ: android:state="background"), thì các trạng thái khác sẽ không bị ràng buộc bởi ngân sách ứ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 của trạng thái đó thay vì bộ nhớ thường trú cao nhất (mức cao nhất của RSS):

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

Nền tảng này liên kết các trạng thái ngân sách trong tệp kê khai với các trạng thái quy trình trong thời gian chạy dựa trên RunningAppProcessInfo.importance, mà Trình quản lý hoạt động tính toán từ cả các thành phần đang hoạt động của chính quy trình đó và mọi liên kết dịch vụ hoặc truy vấn của 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 hoạt động, hiển thị của 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 nhà cung cấp nội dung được ứng dụng trên nền trước hàng đầu liên kết hoặc truy vấn một cách chủ động.
  • perceptible: Khối lượng công việc 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ư phát nội dung nghe nhìn đang hoạt động, chỉ đường từng chặng, chụp ảnh bằng camera hoặc ghi âm bằng micrô, tải xuống trong nền đang hoạt động, dịch vụ đồng bộ hoá dữ liệu trên nền trước hoặc dịch vụ do ứng dụng nền trước liên kết bằng cách sử dụng các cờ như BIND_NOT_FOREGROUND (xem phần Kiểm soát tính kế thừa bằng cờ BIND). Mặc dù các chỉ số nền tảng nội bộ có thể gắn nhãn một số khối lượng công việc này là PROCESS_STATE_IMPORTANT_FOREGROUND, 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 thể nhận thấy ngay lập tức, chẳng hạn như công việc trong nền, chuông báo, bộ nhận tín hiệu truyền tin hoặc Hoạt động trước đó sau khi người dùng rời đi.

Bảng sau đây cho biết cách các mức độ quan trọng trong thời gian chạy liên kết với 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 thông thường
foreground IMPORTANCE_FOREGROUND
IMPORTANCE_TOP_SLEEPING
Hoạt động hiển thị được tiếp tục, ứng dụng hàng đầu có phương thức khoá màn hình, dịch vụ hoặc trình cung cấp nội dung bị ràng buộc bởi ứng dụng hàng đầu
perceptible IMPORTANCE_FOREGROUND_SERVICE
IMPORTANCE_VISIBLE
Dịch vụ trên nền trước đang hoạt động để phát nội dung nghe nhìn, chỉ đường, tải xuống hoặc đồng bộ hoá; dịch vụ liên kết với dịch vụ trên nền trước hoặc cờ hiển thị
background IMPORTANCE_PERCEPTIBLE
IMPORTANCE_CANT_SAVE_STATE
IMPORTANCE_SERVICE
Công việc trong nền, trình nhận, đồng bộ hoá trong nền, Hoạt động trước đó hoặc ứng dụng Home trong nền

Để biết thông tin chi tiết về cách liên kết ứng dụng nâng cao quy trình lưu trữ của 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 phần 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 nào đang hoạt động, nền tảng sẽ giải phóng ngân sách 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 ứ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 trong ứng dụng của bạn 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ụ viết tắt sau đây cho thấy bản ghi quy trình và các mục 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 hiển thị đang hoạt động, 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ì ứng dụng sẽ xuất hiện dưới dạng 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 biết, tương ứng với ngân sách perceptible.

Cách kiểm tra ngân sách bộ nhớ hiện đượ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, kết quả này bao gồm phần Ngân sách bộ nhớ cho biết giới hạn tối đa đ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 quy trình

Nếu ứng dụng của bạn chia công việc thành 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 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 180 MB cho ứng dụng chạy trên nền trước. Khi người dùng rời khỏi ứng dụng trong khi 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à vùng đệm âm thanh.
  2. Quy trình đồng bộ hoá (:sync): Quy trình chuyên biệt chạy tính năng đồng bộ hoá siêu dữ liệu trong nền và lập chỉ mục nội dung 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"; một ngân sách duy nhất sẽ được áp dụng.
<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" />

        <processes>
            <!-- Dedicated background sync process -->
            <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>

Khi bạn khai báo một khối <processes> trong tệp kê khai, hãy chỉ liệt kê các mục <process> cho những quy trình khai báo <memory-budget>. Nếu chỉ muốn đặt ngân sách cho một số quy trình con cụ thể trong khi vẫn giữ nguyên phần còn lại của ứng dụng ở giới hạn hệ thống cho thiết bị, bạn có thể bỏ qua các phần tử <memory-budget> ngay dưới <application>.

Mức sử dụng bộ nhớ trong bất kỳ quy trình con nào cũng được tính vào cả ngân sách quy trình và ngân sách gói bao quanh. Một quy trình sẽ gặp phải áp lực 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 được 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 các ứ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 bitmap vào bộ nhớ đệm theo kích thước màn hình), Android cung cấp hai cơ chế thay thế để điều chỉnh ngân sách một cách linh động theo thông số kỹ thuật của màn hình:

  • Điều chỉnh theo nhóm mật độ hiển thị (android:additionalMbPerDensity): Thêm megabyte theo tỷ lệ bình phương của tỷ lệ mật độ của màn hình so với mdpi (1,0 lần / 160 dpi), vì các thành phần vẽ được dạng đường quét 2D và thành phần giao diện người dùng điều chỉnh theo cả chiều rộng và chiều cao:

    <!-- Baseline 180MB + 16MB multiplied by (density ratio)^2 -->
    <memory-budget
        android:maxMb="180"
        android:additionalMbPerDensity="16" />
    

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

  • Điều chỉnh tỷ lệ theo độ phân giải màn hình thực tế (android:additionalBytesPerDisplayPixel): Thêm trực tiếp byte cho mỗi pixel màn hình thực tế (Chiều rộng × Chiều cao). Điều này là lý tưởng cho các ứng dụng phân bổ bề mặt đồ hoạ toàn màn hình, vùng đệm kết xuất hoặc bộ nhớ đệm ảnh có độ phân giải đầy đủ, trong đó mức tiêu thụ bộ nhớ tăng theo tỷ lệ trực tiếp với số lượng pixel hiển thị thô thay vì 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), mứ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 tăng thêm &approx; 71,8 MB.

Hai thuộc tính này có thể thay thế cho nhau. 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 kiểu dáng thiết bị

Khi vận chuyển tệp 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ế, đồng thời giao diện người dùng và tập hợp tính năng của ứng dụng cũng đơn giản hơn nhiều. Bạn có thể khai báo 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 giải quyết: điều khoản áp dụng cuối 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ự 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 gần đây nhất sẽ được ưu tiên, nên thứ tự rất quan trọng. Luôn đặt ngân sách cơ sở chung nhất lên trước, sau đó là các cơ chế ghi đè cụ thể hơn (chẳng hạn như các mệnh đề dành riêng cho trạng thái hoặc phần cứng).

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

Tất cả thuộc tính kích thước bộ nhớ đều được biểu thị bằng megabyte (MB) và liên kết với mức sử dụng memory.current của cgroup 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 xác định.
android:additionalMbPerDensity Số nguyên (≥ 0) 0 Số megabyte bổ sung nhân với bình phương của tỷ lệ mật độ hiển thị so với mdpi (1,0 lần).
android:additionalBytesPerDisplayPixel Số nguyên (≥ 0) 0 Số byte bổ sung được phân bổ cho mỗi pixel hiển thị thực tế (Chiều rộng × Chiều cao), hữu ích cho vùng đệm bề mặt và bitmap.
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.

API thời gian chạy (lựa chọn động phụ)

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

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 linh hoạt ngân sách quy trình theo hướng giảm xuống.
  • 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 cung cấp cho các ứng dụng viết bằng Kotlin và Java kể từ Android 17 QPR2 (bản phát hành SDK nhỏ, 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ị chạy Android 17 QPR2 trở lên bằng cách kiểm tra SDK_INT_FULL. Nếu minSdk của ứng dụng thấp hơn 36, hãy kiểm tra SDK_INT trước để các Thiết bị chạy Android 15 (cấp độ API 35) trở xuống không gửi NoSuchFieldError khi đọc SDK_INT_FULL:

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA &&
    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 động

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ụ nhẹ hoặc xoá ngân sách khi tác vụ kết thúc:

// 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 đề tệp kê khai <memory-budget> (tự động chuyển đổi ngân sách bất cứ khi nào quá trình chuyển đổi giữa các trạng thái foreground, perceptible và background), ngân sách động đượ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 thử nghiệm thực địa hoặc quản lý các giới hạn theo state cụ thể, 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 lệnh gọi lại ProcessLifecycleOwner hoặc vòng đời của activity, như trong Ví dụ về trình chỉnh sửa hình ảnh thích ứng).

Hiệu suất và tính luỹ đẳng

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 bất biến: Tất cả API thay đổi ngân sách đều có tính 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 an toàn 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ấu hình lại hệ thống 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 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ớ của hệ điều hành cho quy trình. Mặc dù gọn nhẹ nhưng nó 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 riêng biệt và các mốc nhiệm 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 tiến trình nền hoặc xử lý yêu cầu lời nói) và tránh gọi các API này trong các vòng lặp hẹp, luồng xử lý âm thanh hoặ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 nên đượ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 lệnh gọi lại về áp lực vượt quá ngân sách

Ứng dụng có thể đăng ký 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 chủ động dọn dẹp ở cấp ứng dụng (chẳng hạn như xoá bộ nhớ đệm bitmap trong bộ nhớ) trong khi hệ điều hành thu hồi bộ nhớ để giữ cho quy trình hoặc gói nằm trong ngân sách:

val cleanupThread = HandlerThread("MemoryBudgetCleanup").apply { start() }

val listener = MemoryBudgetManager.OnOverBudgetListener { budgetBytes ->
    Log.w(TAG, "Process exceeded memory budget of $budgetBytes bytes")
    // Proactively evict thread-safe caches on the background Looper
    imageTileCache.evictAll()
}

// Register on a background Looper before tightening a dynamic budget
budgetManager.registerProcessOverBudgetListener(cleanupThread.looper, listener)

// When done (for example, 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:

  • Sử dụng nền Looper: Chuyển nền HandlerThread Looper sang registerProcessOverBudgetListener hoặc registerPackageOverBudgetListener bất cứ khi nào có thể. Việc chạy quy trình loại bỏ bộ nhớ đệm hoặc dọn dẹp tệp trên một luồng nền sẽ giúp tránh xung đột với quá trình kết xuất khung hình trên luồng giao diện người dùng khi quy trình này chịu áp lực về bộ nhớ. Hãy dành Looper.getMainLooper cho những trình nghe chỉ chạm vào trạng thái giao diện người dùng liên kết với luồng chính.
  • Dự kiến các lệnh gọi lại lặp lại: Lệnh gọi lại vượt quá ngân sách không phải là thông báo một lần. Khi mức sử dụng vượt quá ngân sách, hệ điều hành sẽ thu hồi các trang không hoạt động trước tiên. Nếu áp lực vẫn tiếp diễn, hệ thống sẽ tiếp tục thu hồi bộ nhớ và gọi trình nghe của bạn tối đa một lần mỗi giây. Hãy đảm bảo lệnh gọi lại của bạn là bất biến. Nếu ứng dụng của bạn chỉ cần một thông báo hoặc muốn có thời gian hồi chiêu lâu hơn giữa các lần dọn dẹp, hãy gọi unregisterProcessOverBudgetListener (hoặc unregisterPackageOverBudgetListener) bên trong lệnh gọi lại hoặc bảo vệ quá trình dọn dẹp bằng dấu thời gian hồi chiêu hoặc AtomicBoolean.
  • Đăng ký trước khi thắt chặt ngân sách: Nếu mức sử dụng bộ nhớ hiện tại đã vượt quá ngân sách mới khi bạn gọi setProcessBudgetBytes hoặc setPackageBudgetBytes, thì sự kiện vượt quá ngân sách sẽ được kích hoạt ngay khi ngân sách mới được áp dụng. Đăng ký trình nghe trước khi đặt ngân sách để ứng dụng của bạn không bỏ lỡ thông báo vượt ngân sách ban đầu đó.
  • Nhanh chóng: Các thao tác thu hồi phải mang lại hiệu quả ngay lập tức. Các phép tính phức tạp trong thời gian áp lực sẽ làm giảm hiệu suất.
  • Tránh phân bổ: Đừng phân bổ đối tượng mới hoặc bắt đầu luồng mới bên trong lệnh gọi lại, vì làm như vậy có thể kích hoạt ngay lập tức việc thu hồi trực tiếp của hệ điều hành.
  • Tập trung vào các mục tiêu có hiệu 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 giải phóng nhiều đối tượng nhỏ.

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

Ứng dụng gốc có thể sử dụng API C NDK do libandroid.so hiển thị 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 mức sử dụng tiêu đề và 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 động

// 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 hai cách để theo dõi các sự kiện bộ nhớ:

  1. Trình theo dõi cấp cao (AMemoryBudgetManager_Watcher_create): Theo dõi các sự kiện trên ALooper bằng tính năng tự động chống rung.
  2. Chỉ số mô tả tệp cấp thấp: AMemoryBudgetManager_getProcessMemoryPressureFd trả về một chỉ số mô tả tệp gốc có thể được tích hợp trực tiếp vào 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 a background worker ALooper with a 1000ms debounce interval
AMemoryBudgetManagerWatcher* watcher = AMemoryBudgetManager_Watcher_create(
    background_looper,
    AMEMORY_BUDGET_MANAGER_EVENT_PROCESS,
    1000 /* debounce_ms */,
    &onMemoryPressure,
    NULL /* userdata */
);

// When done:
AMemoryBudgetManager_Watcher_destroy(watcher);

Liên kết trình theo dõi với ALooper của luồng worker trong nền, thay vì giao diện người dùng chính hoặc luồng kết xuất, để việc loại bỏ bộ nhớ đệm không ảnh hưởng đến quá trình kết xuất khung hình. Tạo trình theo dõi trước khi thắt chặt ngân sách bằng AMemoryBudgetManager_setProcessBudget hoặc AMemoryBudgetManager_setPackageBudget để ứng dụng của bạn không bỏ lỡ sự kiện vượt quá ngân sách ban đầu. Trong khi quy trình hoặc gói vẫn vượt quá ngân sách và hệ điều hành tiếp tục thu hồi bộ nhớ, trình theo dõi sẽ gọi lệnh gọi lại nhiều lần (tối đa một lần cho mỗi khoảng thời gian debounce_ms). Hãy đặt lệnh gọi lại thành idempotent hoặc gọi AMemoryBudgetManager_Watcher_destroy (an toàn để gọi từ bên trong lệnh gọi lại) nếu ứng dụng của bạn chỉ cần thông báo một lần.

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ỏ gọn nhẹ, ứng dụng sẽ sử dụng API SDK Android trong Kotlin để thắt chặt ngân sách quy trình một cách linh động xuống còn 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 toàn bộ giới hạn tệp kê khai 256 MB. Lớp này cũng đăng ký một OnOverBudgetListener trên HandlerThread nền Looper trước khi áp dụng ngân sách thư viện để loại bỏ các bitmap xem trước được lưu vào bộ nhớ đệm dưới áp lực mà không chặn luồng giao diện người dùng.

package com.example.imageeditor.ui

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

class ImageEditorActivity : Activity() {

    private lateinit var budgetManager: MemoryBudgetManager
    private lateinit var cleanupThread: HandlerThread

    // Thread-safe 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)
        cleanupThread = HandlerThread("MemoryBudgetCleanup").apply { start() }
    }

    override fun onStart() {
        super.onStart()
        // Register listener on a background Looper before tightening the budget
        budgetManager.registerProcessOverBudgetListener(cleanupThread.looper, overBudgetListener)
        // Constrain memory during lightweight gallery browsing
        applyGalleryBudget()
    }

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

    override fun onDestroy() {
        cleanupThread.quitSafely()
        super.onDestroy()
    }

    /**
     * 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ề NDK C++: 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 quản lý ngân sách bộ nhớ dựa trên mức chất lượng đồ hoạ đang hoạt động, giả sử tệp kê khai của ứng dụng khai báo mức trần 512 MB để phù hợp với đồ hoạ Chất lượng cao (android:maxMb="512"). Công cụ này bắt đầu AMemoryBudgetManager_Watcher_create trên ALooper của luồng worker trong nền trước khi thắt chặt ngân sách cho các giá trị đặt trước có chất lượng thấp hơn và 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;
    }

    // Call on a background worker ALooper before tightening the budget with setQualityBudget
    bool startMonitoring(ALooper* backgroundLooper) {
        if (!backgroundLooper) return false;

        // Monitor process budget events, debounced to at most once every 1000ms
        mWatcher = AMemoryBudgetManager_Watcher_create(
            backgroundLooper,
            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, idempotent, 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;
};