งบประมาณหน่วยความจำของแอปช่วยให้แอปประกาศงบประมาณหน่วยความจำของตัวเองได้ ซึ่งจะบอกให้ระบบลดการใช้หน่วยความจำของแอปเมื่อแอปใช้หน่วยความจำมากกว่างบประมาณที่ตั้งไว้ ซึ่งจะเป็นประโยชน์อย่างยิ่งสำหรับแอปของระบบและแอปที่มาพร้อมเครื่อง หรือสำหรับแอปที่กำหนดเป้าหมายเป็นอุปกรณ์ที่มีข้อจำกัดด้านหน่วยความจำ ซึ่งนักพัฒนาแอปทราบชุดการทำงานของหน่วยความจำที่คาดไว้และต้องการให้มั่นใจว่าแอปจะไม่ใช้ทรัพยากร RAM ที่แชร์ของระบบมากเกินไป
ระบบจะรักษาสมดุลของงบประมาณโดยใช้การนำออกจากหน่วยความจำและการสลับเพื่อ นำหน้าหน่วยความจำที่ไม่ได้ใช้เมื่อเร็วๆ นี้ออก เพื่อให้แอปมุ่งเน้นการใช้หน่วยความจำ ในชุดการทำงานปัจจุบัน เมื่อแอปใช้งบประมาณเกินที่ประกาศไว้ ระบบปฏิบัติการจะกำหนดเป้าหมายการเรียกคืนที่แอปนั้นโดยเฉพาะ
- ระบบจะล้างหน้าเว็บที่สำรองข้อมูลไว้ในไฟล์ (เช่น โค้ดที่ไม่ได้ใช้งานและชิ้นงานที่แมป) ก่อน เนื่องจากสามารถอ่านซ้ำจากพื้นที่เก็บข้อมูลได้หากจำเป็น
- ระบบจะเขียนหน้าที่มีการสำรองข้อมูลไฟล์ที่มีการแก้ไขกลับไปยังพื้นที่เก็บข้อมูลและนำออก
- ระบบจะบีบอัดและ สลับหน้าหน่วยความจำที่ไม่ระบุตัวบุคคล (เช่น การจัดสรรฮีป) ไปยัง zRAM
ตราบใดที่งบประมาณไม่เกินชุดการทำงาน แอปจะทำงานได้ดี โดยใช้หน่วยความจำไม่เกินงบประมาณที่ตั้งไว้ ระบบปฏิบัติการ จะนำหน่วยความจำที่ไม่ได้ใช้ออกและบีบอัดหน้าฮีปที่ไม่ได้ใช้งานเพื่อสลับเพื่อให้การจัดสรรหน่วยความจำ ยังคงมีขอบเขตโดยไม่สิ้นสุดกระบวนการ
ประกาศงบประมาณในไฟล์ Manifest ของ Android
การประกาศงบประมาณหน่วยความจำใน AndroidManifest.xml เป็นวิธีหลักและ
แนะนำในการกำหนดงบประมาณ โดยไม่ต้องใช้โค้ดรันไทม์ มีผลทันทีเมื่อเริ่มต้นกระบวนการ และมีสัญญาที่ชัดเจนสำหรับระบบปฏิบัติการ
<memory-budget> จะมีผลในอุปกรณ์ที่ใช้ Android 17 QPR2
(API ระดับ 37.2) ขึ้นไป ใน Android เวอร์ชันที่ต่ำกว่า ตัวแยกวิเคราะห์ไฟล์ Manifest ของแพลตฟอร์ม
จะละเว้นองค์ประกอบ XML ที่ไม่รู้จักอย่างปลอดภัย คุณจึงใช้
<memory-budget> ได้โดยไม่ส่งผลต่อความเข้ากันได้แบบย้อนหลัง
ประกาศงบประมาณพื้นฐาน
สำหรับแอปส่วนใหญ่ การกำหนดงบประมาณเดียวสำหรับแอปพลิเคชันก็เพียงพอแล้ว
ประกาศองค์ประกอบ <memory-budget> โดยตรงภายในแท็ก <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>
ซึ่งจะกำหนดงบประมาณหน่วยความจำที่ใช้งานอยู่ 256 MB ในกระบวนการและสถานะทั้งหมดสำหรับ แพ็กเกจ เมื่อหน่วยความจำที่ใช้ของแอปเกิน 256MB ระบบปฏิบัติการจะตัดแต่งหน้าหน่วยความจำที่ไม่ได้ใช้งานโดยใช้การขับออกและการสลับ
ปรับงบประมาณตามสถานะกระบวนการ
แอปต้องใช้หน่วยความจำในปริมาณที่แตกต่างกันไปตามระดับการมองเห็นของผู้ใช้ ดังนี้
- เบื้องหน้า: กระบวนการนี้โฮสต์กิจกรรมที่มองเห็นได้ซึ่งโต้ตอบกับผู้ใช้ โดยปกติแล้ว สถานะนี้จะมีร่องรอยที่ใหญ่ที่สุดเนื่องจาก UI และกราฟิกที่ใช้งานอยู่
- รับรู้ได้: ผู้ใช้รับรู้กระบวนการได้ แต่ไม่ได้โฮสต์ หน้าต่างที่มองเห็นได้ (เช่น การโฮสต์บริการที่ทำงานอยู่เบื้องหน้าสำหรับการเล่นสื่อ การนำทางแบบเลี้ยวต่อเลี้ยว หรือวิธีการป้อนข้อมูลที่ใช้งานอยู่)
- เบื้องหลัง: กระบวนการกำลังเรียกใช้ งานในเบื้องหลัง ตัวรับ หรือ การซิงค์ข้อมูล โดยคาดว่าจะมีการใช้พื้นที่น้อยที่สุด
คุณสามารถประกาศคําสั่ง <memory-budget> หลายรายการเพื่อให้ตรงกับสถานะต่อไปนี้
<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>
ไม่จำเป็นต้องมีข้อความพื้นฐานที่ไม่มี android:state หากคุณระบุเฉพาะเงื่อนไขที่เจาะจงรัฐ (เช่น android:state="background") รัฐอื่นๆ จะไม่ถูกจำกัดด้วยงบประมาณของแอป เมื่อรวมไว้แล้ว ข้อความที่ไม่มี
android:state จะทำหน้าที่เป็นค่าเริ่มต้นสำรองสำหรับสถานะที่ไม่ได้ระบุ (เช่น
เบื้องหน้า) ซึ่งข้อความที่จำกัดมากกว่าในภายหลังจะลบล้างเมื่อแอปเปลี่ยนไปเป็นสถานะ perceptible หรือ background
แอปแบบหลายกระบวนการ
หากแอปพลิเคชันแบ่งงานออกเป็นหลายกระบวนการ ให้กําหนดค่า
งบประมาณกระบวนการเฉพาะโดยใช้แท็ก <process> ภายใน <processes>
ตัวอย่างเช่น ลองพิจารณาแอปสตรีมมิงเพลง (com.example.radio)
- กระบวนการหลัก: โฮสต์ UI ที่มองเห็นได้และเครื่องมือเล่นเสียง
(
MediaSessionServiceพร้อมmediaPlaybackบริการที่ทำงานอยู่เบื้องหน้า) เมื่อมองเห็นได้ กระบวนการจะทำงานภายใต้งบประมาณเบื้องหน้า 180 MB เมื่อผู้ใช้ออกจากแอปขณะที่เพลงยังเล่นอยู่ กระบวนการจะเข้าสู่สถานะperceptibleซึ่งงบประมาณ 64 MB เพียงพอสำหรับเครื่องมือการเล่นและบัฟเฟอร์เสียง - กระบวนการซิงค์ (
:sync): กระบวนการเฉพาะที่เรียกใช้การซิงโครไนซ์ข้อมูลเมตาในเบื้องหลัง และการจัดทำดัชนีการดาวน์โหลด เนื่องจากกระบวนการนี้จะทำงานในเบื้องหลังเท่านั้น คุณจึงไม่จำเป็นต้องประกาศstate="background"อย่างชัดเจน โดยจะใช้งบประมาณเดียว
<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>
การใช้หน่วยความจำในกระบวนการย่อยจะนับรวมทั้งงบประมาณของกระบวนการและงบประมาณของแพ็กเกจที่ครอบคลุม กระบวนการจะประสบปัญหาหน่วยความจำไม่เพียงพอในรันไทม์หาก ละเมิดงบประมาณของกระบวนการหรืองบประมาณของแพ็กเกจ ไม่ว่าเกณฑ์ใด จะถึงก่อน
ปรับงบประมาณสำหรับจอแสดงผลความหนาแน่นสูง
สำหรับแอปพลิเคชันที่มีการใช้หน่วยความจำเพิ่มขึ้นอย่างมากตามจำนวนพิกเซลที่ต้องวาดบนจอแสดงผลพร้อมกัน เช่น แอปแกลเลอรีรูปภาพที่แคชบิตแมปซึ่งมีขนาดเท่าหน้าจอ Android มีกลไกทางเลือก 2 อย่างเพื่อปรับขนาดงบประมาณแบบไดนามิกตามข้อกำหนดของจอแสดงผล
ปรับขนาดตามกลุ่มความหนาแน่นของการแสดงผล (
android:additionalMbPerDensity): เพิ่ม เมกะไบต์ตามสัดส่วนของอัตราส่วนความหนาแน่นของจอแสดงผล เทียบกับmdpi(1.0x / 160 dpi) วิธีนี้เหมาะสำหรับกรณีที่การใช้หน่วยความจำ ปรับขนาดตามกลุ่มความหนาแน่นของ UI เช่น การแคชภาพวาดแรสเตอร์ความละเอียดสูงหรือชิ้นส่วน UI<!-- Baseline 180MB + 16MB per 1.0x density ratio --> <memory-budget android:maxMb="180" android:additionalMbPerDensity="16" />ใน
mdpiจอแสดงผล (1.0x) งบประมาณคือ 180 + 16 × 1 = 196 MB ในxxhdpiจอแสดงผล (3.0x) งบประมาณจะปรับขนาดเป็น 180 + 16 × 3 = 228 MBปรับขนาดตามความละเอียดของจอแสดงผลจริง (
android:additionalBytesPerDisplayPixel): เพิ่มไบต์โดยตรงต่อ พิกเซลของจอแสดงผลจริง (ความกว้าง × ความสูง) ซึ่งเหมาะสำหรับ แอปพลิเคชันที่จัดสรรพื้นผิวกราฟิกแบบเต็มหน้าจอ บัฟเฟอร์การแสดงผล หรือ แคชรูปภาพแบบความละเอียดเต็มที่การใช้หน่วยความจำจะปรับขนาดตาม จำนวนพิกเซลของจอแสดงผลดิบโดยตรง แทนที่จะเป็นกลุ่มความหนาแน่นของ UI<!-- 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" />ในจอแสดงผล 1080p (1080 × 2400 ≈ 2.59 ล้านพิกเซล) การดำเนินการนี้ จะเพิ่มงบประมาณพื้นฐานอีกประมาณ 41.4 MB ในจอแสดงผล 1440p (1440 × 3120 ≈ 4.49 ล้านพิกเซล) จะเพิ่ม ≈ 71.8 MB
แอตทริบิวต์ทั้ง 2 นี้เป็นทางเลือก เลือกแอตทริบิวต์ที่ตรงกับค่าตัวคูณมาตราส่วนหลักของแอป และหลีกเลี่ยงการรวมทั้ง 2 อย่างไว้ในเงื่อนไขเดียวกัน
ปรับแต่งให้เหมาะกับรูปแบบของอุปกรณ์
เมื่อจัดส่ง APK ในโทรศัพท์ แท็บเล็ต และ Wear OS ให้ใช้แอตทริบิวต์
android:feature เพื่อปรับงบประมาณสำหรับเป้าหมายฮาร์ดแวร์ที่แตกต่างกัน
ในนาฬิกา Wear OS นั้น RAM มีข้อจำกัด และ UI และชุดฟีเจอร์ของแอปจะ
เรียบง่ายกว่ามาก คุณสามารถประกาศงบประมาณที่เข้มงวดมากขึ้นซึ่งออกแบบมาสำหรับฟีเจอร์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" />
กฎการแก้ปัญหา: ข้อที่เกี่ยวข้องล่าสุดจะมีผลบังคับใช้
เมื่อกำหนดองค์ประกอบ <memory-budget> หลายรายการสำหรับแอปพลิเคชันหรือกระบวนการ ระบบจะประเมินองค์ประกอบเหล่านั้นตามลำดับที่ประกาศไว้ในไฟล์ Manifest โดยเราจะบังคับใช้เงื่อนไขงบประมาณสุดท้ายที่เกี่ยวข้อง
เนื่องจากงบประมาณสุดท้ายที่เกี่ยวข้องจะเป็นงบประมาณที่ชนะ การจัดลำดับจึงมีความสำคัญ วางงบประมาณพื้นฐานที่ทั่วไปที่สุดไว้ก่อนเสมอ ตามด้วยการลบล้างที่เฉพาะเจาะจงมากขึ้น (เช่น ข้อกำหนดเฉพาะรัฐหรือข้อกำหนดเฉพาะฮาร์ดแวร์)
การอ้างอิงแอตทริบิวต์ XML
แอตทริบิวต์ขนาดหน่วยความจำทั้งหมดแสดงในหน่วยเมกะไบต์ (MB) และแมปกับค่าใช้จ่ายของ cgroup memory.current ใน Linux (ซึ่งไม่รวมหน่วยความจำที่ใช้ร่วมกัน เช่น Zygote)
| แอตทริบิวต์ | รูปแบบ | ค่าเริ่มต้น | คำอธิบาย |
|---|---|---|---|
android:maxMb |
จำนวนเต็ม (> 0) | ต้องระบุ | ขีดจำกัดงบประมาณหน่วยความจำที่ใช้งานอยู่พื้นฐานในหน่วย MB |
android:state |
ค่าแจกแจง | ช่วง | สถานะกระบวนการที่งบประมาณนี้ใช้: foreground, perceptible หรือ background หากละเว้นไว้ เงื่อนไขนี้จะทำหน้าที่เป็นข้อความสำรองสำหรับสถานะที่ไม่ได้ระบุ |
android:additionalMbPerDensity |
จำนวนเต็ม (≥ 0) | 0 |
เมกะไบต์เพิ่มเติมที่จะเพิ่มต่อหน่วยของอัตราส่วนความหนาแน่นของจอแสดงผลเทียบกับ mdpi (1.0x) |
android:additionalBytesPerDisplayPixel |
จำนวนเต็ม (≥ 0) | 0 |
ไบต์เพิ่มเติมที่จัดสรรต่อพิกเซลของจอแสดงผลจริง (ความกว้าง × ความสูง) ซึ่งมีประโยชน์สำหรับบัฟเฟอร์พื้นผิวและบิตแมป |
android:feature |
สตริง | ช่วง | จำกัดข้อความเฉพาะอุปกรณ์ที่ประกาศฟีเจอร์ฮาร์ดแวร์ที่เฉพาะเจาะจง: watch, automotive หรือ leanback |
API รันไทม์ (ตัวเลือกแบบไดนามิกรอง)
การประกาศงบประมาณแบบคงที่ใน AndroidManifest.xml เป็นวิธีที่แนะนำ
สำหรับแอปเกือบทั้งหมด อย่างไรก็ตาม สำหรับแอปพลิเคชันที่มีภาระงานแบบไดนามิกหรือสำหรับการทดลองรันไทม์ Android มี API ของ SDK และ NDK รันไทม์เป็นตัวเลือกสำรอง
API รันไทม์ช่วยให้คุณทำสิ่งต่อไปนี้ได้
- ค้นหาการใช้งานหน่วยความจำปัจจุบันและงบประมาณที่มีผล
- ปรับงบประมาณกระบวนการลงโดยอัตโนมัติ
- รอเหตุการณ์ที่ใช้จ่ายเกินงบประมาณเพื่อล้างแคชเชิงรุกก่อนที่ระบบปฏิบัติการจะเรียกใช้การกู้คืนโดยตรง
Android SDK API (MemoryBudgetManager)
บริการของระบบ MemoryBudgetManager พร้อมให้บริการแก่แอปที่เขียนด้วย
Kotlin และ Java ตั้งแต่ Android 17 QPR2 (SDK รุ่นย่อย ระดับ API
37.2 / Build.VERSION_CODES_FULL.CINNAMON_BUN_2)
เรียกข้อมูลบริการ
ก่อนเข้าถึง MemoryBudgetManager โปรดตรวจสอบว่าอุปกรณ์ไม่ได้ใช้ Android เวอร์ชันต่ำกว่า 17 QPR2 โดยใช้ SDK_INT_FULL
if (Build.VERSION.SDK_INT_FULL >= Build.VERSION_CODES_FULL.CINNAMON_BUN_2) {
val budgetManager = context.getSystemService(MemoryBudgetManager::class.java)
}
การใช้งานคำค้นหาและงบประมาณ
// 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
ตั้งค่าหรือล้างงบประมาณแบบไดนามิก
คุณสามารถตั้งงบประมาณที่เข้มงวดมากขึ้นในรันไทม์เพื่อจำกัดหน่วยความจำในระหว่างงานที่มีน้ำหนักเบา หรือล้างงบประมาณเมื่องานเสร็จสิ้นได้
// 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()
ฟังการเรียกกลับเมื่อมีการแจ้งเตือนว่าใช้งบประมาณเกิน
แอปสามารถลงทะเบียน Listener เพื่อรับการแจ้งเตือนเมื่อการใช้งานหน่วยความจำเกินเกณฑ์งบประมาณ ซึ่งช่วยให้แอปทำการล้างข้อมูลระดับแอปพลิเคชันเชิงรุก (เช่น ล้างแคชบิตแมปในหน่วยความจำ) ก่อนที่ระบบปฏิบัติการจะทริกเกอร์เวลาในการตอบสนองของการเรียกคืนโดยตรง
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)
แนวทางปฏิบัติแนะนำสำหรับการเรียกกลับที่ใช้งบประมาณเกิน
- รวดเร็ว: การดำเนินการขอคืนต้องให้ความช่วยเหลือทันที การคำนวณที่ซับซ้อน ระหว่างแรงกดดันทำให้ประสิทธิภาพแย่ลง
- หลีกเลี่ยงการจัดสรร: อย่าจัดสรรออบเจ็กต์ใหม่หรือเริ่มเธรดใหม่ ภายในโค้ดเรียกกลับ เนื่องจากอาจทําให้ระบบปฏิบัติการ เรียกคืนโดยตรงทันที
- มุ่งเน้นเป้าหมายที่ให้ผลลัพธ์สูง: การนำ Bitmap ขนาดใหญ่ บัฟเฟอร์การแสดงผล หรือ การปิดไฟล์ที่แมปหน่วยความจำออกจะมีประสิทธิภาพมากกว่าการปล่อยออบเจ็กต์ขนาดเล็ก จำนวนมาก
Native NDK API (<android/memory_budget_manager.h>)
แอปที่มาพร้อมเครื่องสามารถใช้ C NDK API ที่แสดงโดย libandroid.so ตั้งแต่ Android 17 QPR2 (ระดับ API 37.2) เป็นต้นไป
การกำหนดค่า CMake
find_library(android-lib android)
target_link_libraries(my_native_engine PRIVATE ${android-lib})
รวมการใช้งานส่วนหัวและการค้นหา
#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
}
กำหนดค่างบประมาณดั้งเดิมแบบไดนามิก
// 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();
ตรวจสอบเหตุการณ์แรงดันหน่วยความจำ
NDK มีวิธีตรวจสอบเหตุการณ์เกี่ยวกับหน่วยความจำ 2 วิธี ได้แก่
- High-Level Watcher (
AMemoryBudgetManager_Watcher_create): ตรวจสอบเหตุการณ์ในALooperโดยมีการดีเบานซ์อัตโนมัติ - ตัวอธิบายไฟล์ระดับต่ำ:
AMemoryBudgetManager_getProcessMemoryPressureFdจะแสดงตัวอธิบายไฟล์ดั้งเดิม ที่ผสานรวมเข้ากับลูปของเครื่องมือepollที่กำหนดเองได้โดยตรง
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);
ตัวอย่าง Runtime API
ตัวอย่างต่อไปนี้แสดงวิธีใช้ API รันไทม์โดยใช้ Android SDK API (เขียนด้วย Kotlin) และ Native NDK API (เขียนด้วย C++)
ตัวอย่าง Android SDK: โปรแกรมแก้ไขรูปภาพแบบปรับอัตโนมัติ
ตัวอย่างนี้แสดงแอปแก้ไขรูปภาพ (com.example.imageeditor) ซึ่งไฟล์
Manifest ประกาศขีดจำกัดสูงสุด 256 MB เพื่อรองรับ Canvas แบบหลายเลเยอร์
<manifest ... >
<application ... >
<!-- Manifest ceiling accommodates the heaviest editing workload -->
<memory-budget android:maxMb="256" />
</application>
</manifest>
เมื่อผู้ใช้เรียกดูแกลเลอรีภาพขนาดย่อแบบเบา แอปจะใช้
Android SDK API ใน Kotlin เพื่อปรับงบประมาณกระบวนการแบบไดนามิกให้เหลือ 96 MB
เมื่อผู้ใช้เปิด Canvas การแก้ไขแบบหลายเลเยอร์ แอปจะล้างงบประมาณแบบไดนามิก
เพื่อคืนค่าเพดาน Manifest เต็ม 256 MB นอกจากนี้ ยังลงทะเบียน
OnOverBudgetListenerเพื่อนำบิตแมปตัวอย่างที่แคชไว้ออกเมื่อมีแรงกดดันด้วย
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"
}
}
ตัวอย่าง NDK C++: เครื่องมือ 3 มิติแบบเนทีฟ
ตัวอย่างนี้แสดงเอ็นจินเกม C++ แบบเนทีฟที่จัดการงบประมาณหน่วยความจำตาม
ระดับคุณภาพกราฟิกที่ใช้งานอยู่ โดยสมมติว่าไฟล์ Manifest ของแอปพลิเคชันประกาศ
ขีดจำกัดสูงสุด 512 MB เพื่อรองรับกราฟิกคุณภาพสูง (android:maxMb="512") เอ็นจินจะ
ปรับงบประมาณแบบไดนามิกสำหรับค่าที่กำหนดไว้ล่วงหน้าที่มีคุณภาพต่ำกว่า และใช้
AMemoryBudgetManager_Watcher_create ใน ALooper เพื่อยกเลิกการโหลด MIPMAP ของเท็กซ์เจอร์
เมื่อใช้งบประมาณเกิน
#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;
};