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 đó:
- 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.
- Các trang được hỗ trợ tệp bẩn được ghi lại vào bộ nhớ và bị loại bỏ.
- 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ớ:
- Thu hồi và hoán đổi bộ nhớ: Giải thích cách hệ điều hành tính phí các trang được sao lưu tệp và vùng nhớ khối xếp ẩn danh riêng tư cho ứng dụng của bạn, đồng thời thu hồi các trang không hoạt động khi vượt quá ngân sách.
- Khái niệm cơ bản: Giải thích lý do bộ nhớ Zygote dùng chung bị loại trừ khỏi ngân sách của ứng dụng và mối liên hệ giữa ngân sách thường trú với RSS ẩn danh + hoán đổi trong dữ liệu đo từ xa tại trường.
- Bitmap và bộ nhớ: Giải thích cách bitmap phần cứng và mức phân bổ DMA-BUF được tính bên ngoài vùng nhớ khối xếp và ngân sách bộ nhớ của ứng dụng.
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
backgroundchặ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.
Để biết quy trình từng bước, hãy xem bài viết Đo lường tập hợp đang hoạt động và điều chỉnh ngân sách.
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áchperceptible.background: Công việc mà người dùng không 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 và bộ nhận tín hiệu truyền tin.
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_FOREGROUNDIMPORTANCE_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_SERVICEIMPORTANCE_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_PERCEPTIBLEIMPORTANCE_CANT_SAVE_STATEIMPORTANCE_SERVICE |
Tác vụ trong nền, trình nhận, đồng bộ hoá 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=4vàstate: cur=FGScho 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ạngTOP. 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ạngIMPF.- Trong bảng điều khiển OOM của quy trình,
prcpcho 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áchperceptible.
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):
- 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 (
MediaSessionServicecó dịch vụ nền trướcmediaPlayback). 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áiperceptible, trong đó ngân sách 64 MB là đủ cho công cụ phát và vùng đệm âm thanh. - 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àngstate="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ớimdpi(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ìnhxxhdpi(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 ≈ 2,59 triệu pixel), mức này sẽ thêm ≈ 41,4 MB vào ngân sách cơ sở. Trên màn hình 1440p (1440 × 3120 ≈ 4,49 triệu pixel), kích thước tăng thêm ≈ 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. |
Đo lường tập hợp đang hoạt động và hiệu chỉnh ngân sách
Ngân sách phù hợp là ngân sách cao hơn một chút so với tập hợp đang hoạt động của ứng dụng cho một trạng thái nhất định, chứ không phải ở mức cao nhất không bị ràng buộc. Để tìm điểm đó, hãy chạy một luồng người dùng thực tế, giảm dần ngân sách và theo dõi thời điểm ứng dụng của bạn bắt đầu trả phí.
Tìm hiểu về tập hợp đang hoạt động so với bộ nhớ thường trú
Nếu không có ngân sách, mức sử dụng bộ nhớ thường trú (memory.current) của ứng dụng sẽ giữ hai loại bộ nhớ:
- Tập hợp đang hoạt động: Java, Kotlin, vùng nhớ khối xếp gốc và các trang được hỗ trợ tệp nóng (mã DEX, thư viện gốc và tài sản) mà luồng người dùng hiện tại chạm vào nhiều lần
- Cold slack: bộ nhớ đệm bitmap từ các màn hình trước đó, vùng đệm khởi động một lần, vùng nhớ khối xếp còn lại sau khi phân bổ và các trang tệp đã được đọc một lần và không bao giờ được chạm vào nữa
Ngân sách ở mức cao nhất không bị ràng buộc sẽ không bao giờ lấy lại được khoảng thời gian rảnh rỗi. Ngân sách dưới tập hợp đang hoạt động khiến hạt nhân hoán đổi các trang vùng nhớ khối xếp và loại bỏ các trang mã mà ứng dụng của bạn ngay lập tức lỗi trở lại. Những lỗi trang lặp lại trong tập hợp đang hoạt động này làm tốn thời gian của CPU và pin, đồng thời khi xuất hiện trên luồng giao diện người dùng, chúng cũng làm tốn khung hình.
Điều bạn đang tìm kiếm là điểm uốn: ngân sách thấp nhất mà tại đó, việc thu hồi chỉ xoá bộ nhớ không hoạt động, vì vậy, lỗi trang, thời gian của CPU và thời gian kết xuất khung hình vẫn giữ nguyên như khi không có ngân sách.
Chuẩn bị môi trường thử nghiệm
Thiết lập một chương trình kiểm thử có thể lặp lại và hoạt động giống như môi trường thực tế:
- Hiệu chỉnh cùng một bản phát hành không thể gỡ lỗi, được tối ưu hoá cho R8 mà bạn phát hành. Bản gỡ lỗi sử dụng nhiều bộ nhớ hơn, vì vậy, ngân sách được điều chỉnh trên bản gỡ lỗi sẽ quá cao so với bản phát hành.
- Kiểm thử trên một thiết bị hoặc trình mô phỏng chạy Android 17 QPR2 (cấp độ API 37.2) trở lên. Bản dựng
userdebughoặc hình ảnh trình mô phỏng không có Google Play là lựa chọn phù hợp nhất, vì một số thiết bị chỉ cho phép bạn đọc các tệp cgroup trong những phần sau khi bạn chạyadb root. - Tạo tập lệnh cho luồng người dùng từ 30 đến 60 giây bằng Jetpack
Macrobenchmark, UI Automator hoặc
adb shell input, mỗi lần cho mỗi trạng thái mà bạn dự định lập ngân sách. Một ứng dụng rảnh không cho bạn biết gì về tập hợp đang hoạt động của ứng dụng đó.foreground: cuộn danh sách dày đặc và lưới nội dung nghe nhìn, di chuyển giữa các đích đến chính, mở trình đơn và hộp thoại, cũng như chạy ảnh độngperceptible: rời khỏi cửa sổ hiển thị trong khi công việc có thể nhận thấy vẫn tiếp tục (ví dụ: phát âm thanh hoặc tải xuống), để công việc chạy trong 10 đến 15 giây, sau đó quay lại ứng dụngbackground: kích hoạt một quá trình đồng bộ hoá, một công việcWorkManager(ví dụ: vớiadb shell cmd jobscheduler run -f <package-name> <job-id>), một dịch vụ đã bắt đầu hoặc một bộ nhận tín hiệu truyền tin trong khi ứng dụng không có giao diện người dùng hiển thị
Đo lường ngân sách background trong khi công việc trong nền đang chạy. Sau khi quy trình được lưu vào bộ nhớ đệm, nền tảng sẽ giải phóng ngân sách (xem phần Cách trạng thái ngân sách liên kết với trạng thái quy trình), vì vậy, quy trình được lưu vào bộ nhớ đệm ở trạng thái rảnh sẽ không cho bạn biết bất cứ điều gì về ngân sách background của bạn.
Đo lường đường cơ sở
Bắt đầu bằng những lần chạy không có ngân sách để biết mức bình thường là bao nhiêu.
Sử dụng một bản dựng không có nội dung khai báo <memory-budget> và bắt đầu mỗi lần chạy từ một quy trình mới (adb shell am force-stop <package-name>).
Mỗi quy trình ứng dụng đều có bộ điều khiển bộ nhớ cgroup v2 riêng. Hãy tra cứu đường dẫn của tệp này từ dòng 0:: của /proc/<PID>/cgroup thay vì tự tạo đường dẫn, vì bố cục cgroup có thể khác nhau giữa các thiết bị. Truyền từng lệnh thiết bị
đến adb shell dưới dạng một chuỗi được đặt trong dấu ngoặc đơn để vỏ thiết bị thấy
dấu ngoặc kép:
PID=$(adb shell pidof -s <package-name>)CG=/sys/fs/cgroup$(adb shell "sed -n 's/^0:://p' /proc/$PID/cgroup")adb shell "cat $CG/memory.current $CG/memory.swap.current"adb shell "grep -E '^(anon|file|workingset_refault_.*) ' $CG/memory.stat"
Việc đọc các tệp này không tốn nhiều tài nguyên và không làm gián đoạn ứng dụng, vì vậy, bạn có thể lấy mẫu các tệp này mỗi giây trong một lần chạy:
memory.current: bộ nhớ thường trú được tính cho quy trình, tính bằng byte. Đây là số lượng mà<memory-budget>giới hạn.memory.swap.current: bộ nhớ được nén vào bộ nhớ hoán đổi zRAM, tính bằng byteanonvàfiletrongmemory.stat: bộ nhớ ẩn danh thường trú và bộ nhớ được tệp hỗ trợ, tính bằng byteworkingset_refault_anonvàworkingset_refault_filetrongmemory.stat: số lượng trang bị hoán đổi hoặc bị loại bỏ mà ứng dụng của bạn cần dùng lại. Các bộ đếm này là dấu hiệu chính cho thấy ngân sách quá eo hẹp.
Nếu bạn gặp lỗi về quyền, hãy chạy adb root rồi thử lại. Nếu bạn không thể lấy quyền truy cập gốc, adb shell dumpsys meminfo <package-name> sẽ cung cấp cho bạn một chế độ xem thô hơn. Trong
App Summary, Java Heap, Native Heap và Stack chủ yếu là bộ nhớ ẩn danh, còn Code là bộ nhớ dựa trên tệp. Bitmap phần mềm được tính vào Native Heap và ngân sách của bạn, trong khi bitmap phần cứng và Graphics thì không. Đừng chạy dumpsys meminfo trong một vòng lặp khi bạn đo lường thời gian kết xuất khung hình. Công cụ này buộc thu gom rác trong ứng dụng và duyệt qua tất cả các liên kết bộ nhớ, từ đó thay đổi những gì bạn đang cố gắng đo lường.
Quét ngân sách của ứng viên
Bạn có thể thử các ngân sách đề xuất mà không cần tạo lại ứng dụng. Hãy sử dụng am memory-limiter manual để đặt ngân sách cho một quy trình đang chạy và none để xoá ngân sách đó:
adb shell am memory-limiter manual <PID> 120MBadb shell am memory-limiter manual <PID> none
Thao tác ghi đè thủ công sẽ kéo dài trong suốt thời gian hoạt động của thực thể quy trình đó, trong suốt các thay đổi về trạng thái quy trình, vì vậy, hãy xoá hoặc khởi động lại quy trình trước khi tiếp tục.
Hàm này cũng đặt giới hạn cứng của quy trình (memory.max) thành cùng một giá trị, vì vậy, ngân sách đề xuất thấp hơn tập hợp đang hoạt động của ứng dụng có thể khiến quy trình bị buộc tắt thay vì chỉ làm chậm quy trình. Coi đó là ngân sách không thành công.
Bạn cũng có thể quét từ một bản dựng điểm chuẩn bằng cách đọc giá trị đề xuất từ đối số đo lường và đặt MemoryBudgetManager.processBudgetBytes. Ngân sách thời gian chạy chỉ có thể thắt chặt ngân sách tệp kê khai, vì vậy, hãy quét bản dựng không có nội dung khai báo <memory-budget>. Chế độ này cũng duy trì hiệu lực trong suốt quá trình thay đổi trạng thái cho đến khi bạn gọi clearProcessBudget.
Bắt đầu từ memory.current cao nhất so với mức cơ sở và giảm dần từ 10 đến 15 MB mỗi lần. Đối với mỗi ngân sách đề xuất:
- Bắt đầu một quy trình mới, đưa quy trình đó vào trạng thái mục tiêu và áp dụng ngân sách.
- Chạy luồng người dùng một lần để khởi động. Việc áp dụng ngân sách sẽ loại bỏ các trang khởi động nguội một lần và bạn muốn đo lường trạng thái ổn định thay vì quá trình chuyển đổi đó.
Đặt lại số liệu thống kê về khung hình, đồng thời ghi lại bộ đếm lỗi trang bắt đầu và thời gian của CPU cho quy trình (
utimevàstime, tính theo nhịp đồng hồ):adb shell dumpsys gfxinfo <package-name> resetadb shell "grep -E '^workingset_refault_(anon|file) ' $CG/memory.stat"adb shell "cut -d ' ' -f 14,15 /proc/$PID/stat"Chạy lượt đo lường của luồng người dùng.
Ghi lại
memory.current,anon,file, bộ đếm lỗi và thời gian của CPU một lần nữa, đồng thời thu thập số liệu thống kê về khung hình.dumpsys gfxinfocó thể in cùng một số liệu thống kê nhiều lần, vì vậy, hãy lấy kết quả khớp đầu tiên, bao gồm toàn bộ ứng dụng:adb shell "dumpsys gfxinfo <package-name>" | grep -m 1 'Janky frames:'adb shell "dumpsys gfxinfo <package-name>" | grep -m 1 '95th percentile'
Lặp lại mỗi ứng viên ít nhất ba lần và xen kẽ các ứng viên thay vì chạy tất cả các lần lặp lại của một ứng viên liên tiếp. So sánh từng kết quả với mức chênh lệch của các lần chạy cơ sở, chứ không phải với một lần chạy duy nhất.
Đọc kết quả
Khi ngân sách giảm, ứng dụng của bạn sẽ trải qua 3 chế độ:
- Trên đầu gối:
memory.currenttuân theo ngân sách bằng cách loại bỏ các trangfilekhông hoạt động và vùng nhớ khối xếp rảnh.anonkhông đổi, lỗi tham chiếu vẫn ở gần mức cơ sở, còn thời gian của CPU và thời gian kết xuất khung hình không thay đổi. Đây là nơi bạn muốn đến. - Dưới đầu gối: Các trang nguội đã biến mất, vì vậy, hạt nhân bắt đầu hoán đổi các trang vùng nhớ khối xếp mà ứng dụng của bạn vẫn sử dụng. Số lỗi trang tăng lên hàng trăm hoặc hàng nghìn trang mỗi lần chạy và thời gian của CPU cũng tăng lên. Thời gian kết xuất khung hình có thể hầu như không thay đổi trên một thiết bị thử nghiệm đang ở trạng thái rảnh, nhưng ứng dụng của bạn hiện đang tiêu tốn thời gian của CPU và pin để nén và giải nén tập hợp hoạt động của chính ứng dụng đó.
- Dưới mức giới hạn: Lỗi thu hồi và hoán đổi xảy ra trên luồng giao diện người dùng và
RenderThread, đồng thời hiện tượng giật, thời gian kết xuất khung hình và thời gian tiếp tục ấm đều tăng lên.
So với mức cơ sở không bị ràng buộc, ngân sách đề xuất sẽ được thông qua nếu đáp ứng tất cả các điều kiện sau đây (đây là điểm xuất phát hợp lý):
- Quá trình này vẫn tiếp tục trong suốt quá trình chạy
- Các delta mặc định lại vẫn gần với đường cơ sở
- Thời gian của CPU của quy trình tăng không quá 5%
- Khung hình bị giật tăng không quá 1 điểm phần trăm
- Thời gian kết xuất khung hình P95 tăng không quá 3 mili giây
- Thời gian tiếp tục ấm (
TotalTimetừadb shell am start -W) tăng không quá 15% đối với ngân sáchperceptiblevàbackground
Điểm uốn là ngân sách thấp nhất của ứng viên được thông qua. Ví dụ: sau đây là một lần quét trên nền trước cho một ứng dụng giả định có mức sử dụng tối đa không bị ràng buộc là 138 MB:
| Ngân sách đề xuất | memory.current |
anon |
file |
Lỗi mặc định (trang) | Thời gian kết xuất khung hình P95 | Chế độ |
|---|---|---|---|---|---|---|
| Không có | 138 MB | 75 MB | 63 MB | 0 | 10,5 mili giây | Cơ sở |
| 125 MB | 121 MB | 74 MB | 47 MB | 10 | 10,5 mili giây | Trên đầu gối |
| 110 MB | 110 MB | 75 MB | 35 MB | 15 | 10,8 mili giây | Đầu gối |
| 95 MB | 95 MB | 74 MB | 21 MB | 176 | 11,2 mili giây | Dưới đầu gối |
| 85 MB | 85 MB | 68 MB | 17 MB | 2.050 | 12,8 mili giây | Dưới đầu gối |
| 70 MB | 70 MB | 54 MB | 16 MB | 8.771 | 16,5 mili giây | Dưới đầu gối |
Chọn ngân sách
Đặt android:maxMb cao hơn 10% đến 20% so với điểm uốn để sự khác biệt về dữ liệu người dùng, ngôn ngữ và mức phân bổ đột biến không đẩy ứng dụng của bạn xuống dưới điểm uốn. Trong ví dụ trước, điểm uốn là 110 MB, vì vậy, 125 MB là một lựa chọn phù hợp.
Sau đó, hãy cài đặt một bản phát hành có các nội dung khai báo <memory-budget> mới, chạy lại luồng người dùng và kiểm tra để đảm bảo các tiêu chí đạt vẫn được giữ nguyên. dumpsys
meminfo cho biết ngân sách đang hoạt động và nguồn của ngân sách đó trong phần Ngân sách bộ nhớ.
Xác thực ngân sách không chạy trên nền trước bằng cách trả về ấm
Ngân sách perceptible hoặc background eo hẹp có thể không có vấn đề gì khi ứng dụng của bạn ở trạng thái đó, nhưng sau đó sẽ khiến bạn tốn kém khi người dùng quay lại. Kiểm tra chuyến đi khứ hồi:
- Chạy quy trình trên nền trước, chuyển ứng dụng sang trạng thái mục tiêu và áp dụng ngân sách đề xuất bằng
am memory-limiter manual. - Để tác vụ có thể nhận biết hoặc tác vụ trong nền chạy trong 10 đến 15 giây.
- Xoá chế độ ghi đè (
adb shell am memory-limiter manual <PID> none), sau đó ngay lập tức đưa ứng dụng trở lại bằngadb shell am start -Wvà ghi lạiTotalTime. Nếu bạn đặt ứng viên thông quaMemoryBudgetManager, hãy gọiclearProcessBudgettrước khi ứng dụng trả về. - Chạy một hoạt động tương tác ngắn trên nền trước và kiểm tra lỗi trang cũng như số liệu thống kê về khung hình theo tiêu chí đạt.
Chọn ngân sách thấp nhất vượt qua cả hai khi ứng dụng ở trạng thái đó và khi quay lại ấm, đồng thời thêm khoảng trống từ 10% đến 20%.
Hiệu chỉnh tỷ lệ màn hình
Nếu bạn sử dụng android:additionalBytesPerDisplayPixel, hãy tìm điểm gãy của nền trước trên hai màn hình có độ phân giải khác nhau, chẳng hạn như 1080 × 2400 (≈ 2,59 triệu pixel) và 1440 × 3120 (≈ 4,49 triệu pixel).
Chia mức chênh lệch giữa hai điểm gãy (tính bằng byte) cho mức chênh lệch về số lượng pixel để có được android:additionalBytesPerDisplayPixel. Trừ lượng mỗi pixel đó cho màn hình nhỏ hơn từ điểm uốn của màn hình để có được cơ sở không phụ thuộc vào độ phân giải, rồi thêm khoảng không để có được android:maxMb.
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ặcclearPackageBudget()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 đặtsetProcessBudgetBytes()hoặcsetPackageBudgetBytes()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
processCurrentUsageBytesvàpackageCurrentUsageBytestì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ạiOnOverBudgetListener) 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ềnHandlerThreadLoopersangregisterProcessOverBudgetListenerhoặcregisterPackageOverBudgetListenerbấ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ànhLooper.getMainLoopercho 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ặcunregisterPackageOverBudgetListener) 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ặcAtomicBoolean. - Đă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
setProcessBudgetByteshoặcsetPackageBudgetBytes, 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ớ:
- Trình theo dõi cấp cao (
AMemoryBudgetManager_Watcher_create): Theo dõi các sự kiện trênALooperbằng tính năng tự động chống rung. - Chỉ số mô tả tệp cấp thấp:
AMemoryBudgetManager_getProcessMemoryPressureFdtrả 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ụepolltuỳ 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;
};