งบประมาณหน่วยความจำของแอปช่วยให้แอปประกาศงบประมาณหน่วยความจำสำหรับตัวเอง ซึ่งจะบอกให้ระบบตัดการใช้หน่วยความจำเมื่อแอปใช้งบประมาณเกินที่ตั้งไว้ ซึ่งมีประโยชน์อย่างยิ่งสำหรับแอประบบและแอปที่รวมมากับระบบ หรือสำหรับแอปที่กำหนดเป้าหมายเป็นอุปกรณ์ที่มีหน่วยความจำจำกัด ซึ่งนักพัฒนาแอปทราบชุดการทำงานของหน่วยความจำที่คาดไว้ (หน่วยความจำที่แอปจำเป็นต้องใช้สำหรับสิ่งที่ผู้ใช้กำลังทำอยู่) และต้องการตรวจสอบว่าแอปไม่ได้ใช้ทรัพยากร RAM ที่ใช้ร่วมกันของระบบมากเกินไป
ระบบจะรักษาสมดุลของงบประมาณโดยใช้การขับไล่และการสลับหน่วยความจำเพื่อนำหน้าหน่วยความจำที่ไม่ได้ใช้เมื่อเร็วๆ นี้ออก โดยมุ่งเน้นที่หน่วยความจำที่ใช้ของแอปในชุดการทำงานปัจจุบัน เมื่อแอปใช้หน่วยความจำเกินงบประมาณที่ประกาศไว้ ระบบปฏิบัติการจะกำหนดเป้าหมายการเรียกคืนหน่วยความจำไปที่แอปนั้นโดยเฉพาะ
- ระบบจะลบหน้าเว็บที่สำรองข้อมูลไฟล์ที่สะอาด (เช่น โค้ดที่ไม่ได้ใช้งานและเนื้อหาที่แมป) ออกก่อน เนื่องจากสามารถอ่านซ้ำจากพื้นที่เก็บข้อมูลได้หากจำเป็น
- ระบบจะเขียนหน้าที่มีไฟล์สำรองซึ่งมีการแก้ไขกลับไปยังพื้นที่เก็บข้อมูลและนำออก
- หน้าหน่วยความจำที่ไม่ระบุชื่อ (เช่น การจัดสรรฮีป) จะถูกบีบอัดและสลับไปยัง zRAM
ตราบใดที่ชุดการทำงานไม่เกินงบประมาณ แอปจะทำงานได้ดีโดยใช้หน่วยความจำไม่เกินงบประมาณที่ตั้งไว้ ระบบปฏิบัติการจะขับหน่วยความจำที่ไม่ได้ใช้ออกและบีบอัดหน้าฮีปที่ไม่ได้ใช้งานเพื่อสลับ เพื่อให้การจัดสรรหน่วยความจำยังคงถูกจำกัดโดยไม่สิ้นสุดกระบวนการ
สิ่งที่จะเกิดขึ้นเมื่อแอปใช้เกินงบประมาณ
การใช้หน่วยความจำเกินงบประมาณไม่ได้ทำให้ระบบสิ้นสุดหรือทำให้แอปขัดข้อง แต่เมื่อแอปใช้หน่วยความจำเกินงบประมาณที่อนุญาต ระบบปฏิบัติการจะค้นหาหน่วยความจำที่แอปไม่ได้ใช้มาระยะหนึ่งแล้วและเก็บไว้อย่างปลอดภัยจนกว่าจะจำเป็นต้องใช้อีกครั้ง ซึ่งจะช่วยให้มีการจัดสรรหน่วยความจำของแอปใหม่และทำให้การใช้หน่วยความจำโดยรวมของแอปอยู่ในงบประมาณ
ตราบใดที่มีพื้นที่ว่างระหว่างสิ่งที่แอปต้องการ ณ เวลาใดเวลาหนึ่ง (ชุดการทำงาน) กับงบประมาณที่ตั้งไว้ แอปจะยังคงทำงานตามปกติ หากแอปตั้งค่างบประมาณที่น้อยกว่าชุดการทำงาน ประสิทธิภาพของแอปที่รันไทม์อาจช้าลงได้
ดูวิธีที่ระบบปฏิบัติการติดตามและเรียกคืนหน่วยความจำได้ที่คู่มือสถาปัตยกรรมหน่วยความจำ
- การเรียกคืนหน่วยความจำและการสลับ: อธิบายวิธีที่ระบบปฏิบัติการเรียกเก็บเงินจากฮีปแบบไม่ระบุตัวตนส่วนตัวและหน้าเว็บที่ใช้ไฟล์เป็นแบ็กอัปไปยังแอปของคุณ และเรียกคืนหน้าเว็บที่ไม่มีการใช้งานเมื่อเกินงบประมาณ
- แนวคิดพื้นฐาน: อธิบายสาเหตุที่ระบบไม่รวมหน่วยความจำ Zygote ที่ใช้ร่วมกันไว้ในงบประมาณของแอป และความสัมพันธ์ระหว่างงบประมาณที่ใช้กับ RSS แบบไม่ระบุชื่อ + สลับในการตรวจวัดระยะไกลภาคสนาม
- บิตแมปและหน่วยความจำ: อธิบายวิธีพิจารณาบิตแมปฮาร์ดแวร์ที่สนับสนุนโดย GPU และการจัดสรร DMA-BUF นอกฮีปและงบประมาณหน่วยความจำของแอป
ประกาศงบประมาณในไฟล์ 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 ในกระบวนการและสถานะทั้งหมดสำหรับแพ็กเกจ เมื่อหน่วยความจำที่แอปใช้เกิน 256 MB ระบบปฏิบัติการจะตัดหน้าหน่วยความจำที่ไม่ได้ใช้งานออกโดยใช้การขับออกและการสลับ
ปรับงบประมาณตามสถานะกระบวนการ
แอปต้องใช้หน่วยความจำในปริมาณที่แตกต่างกัน โดยขึ้นอยู่กับระดับการมองเห็นของผู้ใช้ ดังนี้
- เบื้องหน้า: กระบวนการกำลังโฮสต์กิจกรรมที่มองเห็นได้ซึ่งโต้ตอบกับผู้ใช้ โดยปกติแล้ว สถานะนี้จะมีขนาดใหญ่ที่สุดเนื่องจาก 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
เมื่อกำหนดขนาดงบประมาณสำหรับแต่ละสถานะ ให้กำหนดเป้าหมายชุดการทำงานที่ใช้งานอยู่ของสถานะนั้นแทนที่จะเป็นหน่วยความจำที่ใช้สูงสุด (RSS High-Water Mark) ดังนี้
- กำหนดขนาดงบประมาณ
foregroundสำหรับการแสดงผลเฟรมที่ราบรื่น: ให้ UI เบื้องหน้ามีพื้นที่ว่างเพียงพอสำหรับชุดการทำงานที่ใช้งานอยู่ เพื่อให้การเรียกคืนแบบซิงโครนัสไม่ทำให้การแสดงผลเฟรมล่าช้า หากแอปของคุณมีเวิร์กโฟลว์แบบสแตนด์อโลนที่ใช้หน่วยความจำมาก (เช่น โปรแกรมแก้ไขรูปภาพหรือวิดีโอความละเอียดสูง) ให้พิจารณาย้ายกิจกรรมนั้นไปยังกระบวนการเฉพาะ (ดูแอปแบบหลายกระบวนการ) เพื่อให้มีการจัดสรรงบประมาณอย่างอิสระและถูกทำลายเมื่อผู้ใช้ออก - กำหนดงบประมาณ
backgroundอย่างเข้มงวดสำหรับงานแบบไม่มีส่วนหัว: RSS พื้นหลังสูงในการตรวจวัดระยะไกลภาคสนามมักสะท้อนถึงการจัดสรร UI ที่คงไว้หลังจากออกจากเบื้องหน้า (ดูการจัดวางบริการที่มีผลผูกพันร่วมกับ UI ที่ใช้หน่วยความจำมากและการปล่อยหน่วยความจำเพื่อตอบสนองต่อเหตุการณ์) หรือการอ่านไฟล์แบบครั้งเดียวระหว่างการจัดทำดัชนีและการซิงค์ในเบื้องหลัง เนื่องจากการทำงานเบื้องหลังแบบไม่มีส่วนหัวไม่มีกำหนดเวลาเฟรม การขับไล่หน้าแคชไฟล์แบบครั้งเดียวและการสลับหน้าฮีปที่ไม่ได้ใช้งานไปยัง zRAM เมื่อการซิงค์เบื้องหลังจัดสรรเกินงบประมาณชั่วคราวจะไม่ทำให้เกิดความล่าช้าที่ผู้ใช้มองเห็นได้ ขณะเดียวกันก็ป้องกันไม่ให้การเพิ่มขึ้นอย่างรวดเร็วในเบื้องหลังขับไล่แอปที่ทำงานอยู่เบื้องหน้าของผู้ใช้
วิธีจับคู่สถานะงบประมาณกับสถานะกระบวนการ
แพลตฟอร์มจะแมปสถานะงบประมาณที่ระบุไว้กับสถานะกระบวนการรันไทม์ตาม RunningAppProcessInfo.importance ซึ่ง Activity Manager จะคำนวณจากทั้งคอมโพเนนต์ที่ใช้งานอยู่ของกระบวนการเองและการผูกบริการหรือการค้นหา Content Provider ที่เข้ามาจากแอปอื่นๆ ดังนี้
foreground: การโต้ตอบของผู้ใช้ที่ทำงานอยู่และมองเห็นได้ เช่น การโฮสต์กิจกรรมที่กลับมาทำงานอีกครั้ง การรักษาสถานะแอปที่อยู่ด้านบนสุดเมื่อหน้าจอปิดลง หรือการโฮสต์บริการหรือ Content Provider ที่แอปที่ทำงานอยู่เบื้องหน้าด้านบนสุดเชื่อมโยงหรือค้นหาอยู่perceptible: ภาระงานที่ผู้ใช้รับรู้ได้โดยไม่มีหน้าต่างที่มองเห็นได้ เช่น การเล่นสื่อที่ใช้งานอยู่ การนำทางแบบเลี้ยวต่อเลี้ยว การจับภาพด้วยกล้องหรือไมโครโฟน การดาวน์โหลดที่ทำงานอยู่เบื้องหลัง บริการที่ทำงานอยู่เบื้องหน้าสำหรับการซิงค์ข้อมูล หรือบริการที่เชื่อมโยงโดยไคลเอ็นต์ที่ทำงานอยู่เบื้องหน้าโดยใช้แฟล็ก เช่นBIND_NOT_FOREGROUND(ดูการควบคุมการรับค่าด้วยแฟล็ก BIND) แม้ว่าเมตริกแพลตฟอร์มภายในอาจติดป้ายกำกับเวิร์กโหลดเหล่านี้บางรายการเป็นPROCESS_STATE_IMPORTANT_FOREGROUNDแต่ค่าคงที่ภายในนั้นไม่ได้หมายถึงกรอบเวลาที่มองเห็นได้ และอยู่ภายใต้งบประมาณperceptiblebackground: งานที่ผู้ใช้ไม่สามารถรับรู้ได้ทันที เช่น งานที่ทำงานอยู่เบื้องหลัง การปลุก Broadcast Receiver หรือกิจกรรมก่อนหน้าหลังจากที่ผู้ใช้ออกจากแอป
ตารางต่อไปนี้แสดงวิธีที่ระดับความสำคัญของรันไทม์แมปกับสถานะไฟล์ Manifest
ไฟล์ Manifest android:state |
ความสำคัญของรันไทม์ (RunningAppProcessInfo) |
องค์ประกอบทั่วไป |
|---|---|---|
foreground |
IMPORTANCE_FOREGROUNDIMPORTANCE_TOP_SLEEPING |
กิจกรรมที่มองเห็นได้ซึ่งกลับมาทำงานอีกครั้ง, แอปยอดนิยมที่มีหน้าจอล็อกอยู่, บริการหรือ Content Provider ที่เชื่อมโยงกับแอปยอดนิยม |
perceptible |
IMPORTANCE_FOREGROUND_SERVICEIMPORTANCE_VISIBLE |
การเล่นสื่อที่ทำงานอยู่ การนำทาง การดาวน์โหลด หรือการซิงค์บริการที่ทำงานอยู่เบื้องหน้า บริการที่เชื่อมโยงกับบริการที่ทำงานอยู่เบื้องหน้าหรือแฟล็กที่มองเห็นได้ |
background |
IMPORTANCE_PERCEPTIBLEIMPORTANCE_CANT_SAVE_STATEIMPORTANCE_SERVICE |
งานที่ทำอยู่เบื้องหลัง, ตัวรับ, การซิงค์ในเบื้องหลัง, กิจกรรมก่อนหน้า หรือแอปหน้าแรกในเบื้องหลัง |
ดูรายละเอียดเกี่ยวกับวิธีที่การผูกไคลเอ็นต์ยกระดับกระบวนการโฮสต์บริการหรือผู้ให้บริการ และวิธีที่กระบวนการเปลี่ยนกลับไปเป็น background หรือ cached หลังจากที่ไคลเอ็นต์ยกเลิกการเชื่อมโยงหรือย้ายออกจากหน้าจอได้ที่การผูกบริการและสถานะกระบวนการ
เมื่อกระบวนการเข้าสู่สถานะแคช (IMPORTANCE_CACHED) โดยไม่มีคอมโพเนนต์ที่ทำงานอยู่ แพลตฟอร์มจะปล่อยงบประมาณหน่วยความจำที่ใช้งานอยู่และจัดการหน่วยความจำผ่านการเรียกคืนกระบวนการแคชมาตรฐาน
ตรวจสอบสถานะกระบวนการและงบประมาณของแอป
วิธีหนึ่งในการตรวจสอบสถานะกระบวนการที่ทำงานอยู่ของแอปและความสำคัญในระหว่างการพัฒนาคือการค้นหา Activity Manager โดยใช้ ADB ดังนี้
adb shell dumpsys activity processes <package-name>
ตัวอย่างแบบย่อต่อไปนี้แสดงรายการบันทึกกระบวนการและรายการควบคุม OOM สำหรับแอปที่เรียกใช้บริการที่ทำงานอยู่เบื้องหน้า
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
ในเอาต์พุตนี้
curProcState=4และstate: cur=FGSแสดงให้เห็นว่ากระบวนการอยู่ในสถานะบริการที่ทำงานอยู่เบื้องหน้า หากแอปโฮสต์กิจกรรมที่มองเห็นได้และทำงานอยู่ ระบบจะแสดงเป็นTOPหากกำลังเรียกใช้คอมโพเนนต์ที่ทำงานอยู่เบื้องหน้าที่สำคัญ (เช่น การดาวน์โหลดหรือการซิงค์) จะปรากฏเป็นIMPF- ในตารางควบคุม OOM ของกระบวนการ
prcpบ่งชี้ว่ากระบวนการได้รับการประเมินภายใต้ระดับความสำคัญที่รับรู้ได้ ซึ่งสอดคล้องกับงบประมาณperceptible
วิธีตรวจสอบงบประมาณหน่วยความจำที่บังคับใช้อยู่ในปัจจุบันและการใช้งานหน่วยความจำที่คงอยู่
adb shell dumpsys meminfo <package-name>
ตั้งแต่ Android 17 QPR2 เป็นต้นไป เอาต์พุตนี้จะมีส่วนงบประมาณหน่วยความจำที่แสดงเพดานขีดจำกัดที่ใช้งานอยู่ แหล่งที่มาของขีดจำกัด (เช่น AndroidManifest หรือ MemoryBudgetManager) และหน่วยความจำที่ใช้ในปัจจุบัน
แอปแบบหลายกระบวนการ
หากแอปพลิเคชันแบ่งงานออกเป็นหลายกระบวนการ ให้กำหนดค่างบประมาณกระบวนการเฉพาะโดยใช้แท็ก <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" />
<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>
เมื่อคุณประกาศบล็อก <processes> ในไฟล์ Manifest ให้แสดงรายการ <process> สำหรับกระบวนการที่ประกาศ <memory-budget> เท่านั้น หากต้องการกำหนดงบประมาณเฉพาะกระบวนการย่อยบางรายการเท่านั้น โดยปล่อยให้ส่วนที่เหลือของแอปอยู่ที่ขีดจำกัดของระบบสำหรับอุปกรณ์ คุณสามารถละเว้นองค์ประกอบ <memory-budget> ที่อยู่ใต้ <application> โดยตรงได้
การใช้หน่วยความจำในกระบวนการย่อยจะนับรวมทั้งงบประมาณกระบวนการและงบประมาณแพ็กเกจที่ล้อมรอบ กระบวนการจะพบกับแรงกดดันด้านหน่วยความจำที่รันไทม์หากละเมิดงบประมาณกระบวนการหรืองบประมาณแพ็กเกจ ขึ้นอยู่กับว่าถึงเกณฑ์ใดก่อน
ปรับงบประมาณสำหรับจอแสดงผลที่มีความหนาแน่นสูง
สำหรับแอปพลิเคชันที่มีหน่วยความจำที่ใช้เพิ่มขึ้นอย่างมากตามจำนวนพิกเซลที่ต้องวาดบนจอแสดงผลพร้อมกัน เช่น แอปแกลเลอรีรูปภาพที่แคชบิตแมปขนาดเท่าหน้าจอ Android มีกลไกทางเลือก 2 อย่างในการปรับขนาดงบประมาณแบบไดนามิกตามข้อกำหนดของจอแสดงผล ดังนี้
ปรับขนาดตามกลุ่มความหนาแน่นของการแสดงผล (
android:additionalMbPerDensity): เพิ่มเมกะไบต์ตามสัดส่วนของอัตราส่วนความหนาแน่นของการแสดงผลยกกำลังสองเทียบกับmdpi(1.0 เท่า / 160 dpi) เนื่องจากทรัพยากร UI และทรัพยากรที่ถอดแบบได้แบบแรสเตอร์ 2 มิติจะปรับขนาดทั้งความกว้างและความสูง ดังนี้<!-- Baseline 180MB + 16MB multiplied by (density ratio)^2 --> <memory-budget android:maxMb="180" android:additionalMbPerDensity="16" />ในจอแสดงผล
mdpi(1.0 เท่า) งบประมาณคือ 180 + 16 × 12 = 196 MB ในจอแสดงผลxxhdpi(3.0x) งบประมาณจะปรับเป็น 180 + 16 × 32 = 324 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) และแมปกับค่าใช้จ่าย memory.current ของ Linux cgroup (ซึ่งไม่รวมหน่วยความจำที่ใช้ร่วมกัน เช่น Zygote)
| แอตทริบิวต์ | รูปแบบ | ค่าเริ่มต้น | คำอธิบาย |
|---|---|---|---|
android:maxMb |
จำนวนเต็ม (> 0) | ต้องระบุ | ขีดจำกัดงบประมาณหน่วยความจำที่ใช้จริงพื้นฐานในหน่วย MB |
android:state |
ค่าแจกแจง | ช่วง | สถานะกระบวนการที่งบประมาณนี้มีผลบังคับใช้: foreground, perceptible หรือ background หากละเว้น ส่วนคำสั่งจะทำหน้าที่เป็นตัวสำรองสำหรับสถานะที่ไม่ได้ระบุ |
android:additionalMbPerDensity |
จำนวนเต็ม (≥ 0) | 0 |
เมกะไบต์เพิ่มเติมคูณด้วยกำลังสองของอัตราส่วนความหนาแน่นของการแสดงผลที่สัมพันธ์กับ mdpi (1.0 เท่า) |
android:additionalBytesPerDisplayPixel |
จำนวนเต็ม (≥ 0) | 0 |
ไบต์เพิ่มเติมที่จัดสรรต่อพิกเซลการแสดงผลจริง (ความกว้าง × ความสูง) ซึ่งมีประโยชน์สำหรับบัฟเฟอร์พื้นผิวและบิตแมป |
android:feature |
สตริง | ช่วง | จำกัดส่วนคำสั่งไว้เฉพาะอุปกรณ์ที่ประกาศฟีเจอร์ฮาร์ดแวร์ที่เฉพาะเจาะจง ได้แก่ watch, automotive หรือ leanback |
Runtime API (ตัวเลือกแบบไดนามิกรอง)
การประกาศงบประมาณแบบคงที่ใน AndroidManifest.xml เป็นวิธีที่แนะนำสำหรับแอปเกือบทั้งหมด อย่างไรก็ตาม สำหรับแอปพลิเคชันที่มีภาระงานแบบไดนามิกหรือสำหรับการทดสอบรันไทม์ Android มี SDK รันไทม์และ NDK API เป็นตัวเลือกรอง
รันไทม์ 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 หาก
minSdk ของแอปต่ำกว่า 36 ให้ตรวจสอบ SDK_INT ก่อนเพื่อไม่ให้อุปกรณ์ที่ใช้ Android 15 (ระดับ API 35) หรือต่ำกว่าแสดง NoSuchFieldError เมื่ออ่าน 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)
}
การใช้งานการค้นหาและงบประมาณ
// 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()
งบประมาณแบบไดนามิกที่ตั้งค่าผ่าน MemoryBudgetManager จะใช้เพดานเดียวจนกว่าโค้ดจะอัปเดตหรือล้างค่า ซึ่งต่างจากคำสั่งไฟล์ Manifest ของ <memory-budget> ที่จะเปลี่ยนงบประมาณโดยอัตโนมัติเมื่อใดก็ตามที่กระบวนการเปลี่ยนสถานะระหว่าง foreground, perceptible และ background หากคุณใช้ Runtime API เพื่อทำการทดสอบภาคสนามหรือจัดการขีดจำกัดเฉพาะสถานะ ให้อัปเดตหรือล้างงบประมาณแบบไดนามิกในการเปลี่ยนวงจร (เช่น การใช้ ProcessLifecycleOwner หรือ Callback ของวงจรกิจกรรม ดังที่แสดงในตัวอย่างเครื่องมือแก้ไขรูปภาพแบบปรับอัตโนมัติ)
ประสิทธิภาพและความสามารถในการทำงานซ้ำ
เมื่อทำงานกับ MemoryBudgetManager ขณะรันไทม์ โปรดคำนึงถึงลักษณะต่อไปนี้
- ความสามารถในการทำงานซ้ำ: API การเปลี่ยนแปลงงบประมาณทั้งหมดมีความสามารถในการทำงานซ้ำ การเรียกใช้
clearProcessBudget()หรือclearPackageBudget()ซ้ำๆ หรือเมื่อไม่มีงบประมาณแบบไดนามิกที่ใช้งานอยู่เป็นการดำเนินการที่ปลอดภัยและไม่มีผลใดๆ ในทำนองเดียวกัน การตั้งค่าsetProcessBudgetBytes()หรือsetPackageBudgetBytes()เป็นค่าเดียวกันติดต่อกันจะไม่ทริกเกอร์การกำหนดค่าระบบใหม่ที่ซ้ำซ้อน - ความถี่ในการเรียกใช้: การตั้งค่า การอัปเดต หรือการล้างงบประมาณจะทำการเรียกใช้ข้ามกระบวนการไปยังบริการของระบบเพื่ออัปเดตตัวควบคุมหน่วยความจำของระบบปฏิบัติการสำหรับกระบวนการนั้น แม้ว่าจะมีขนาดเล็ก แต่ก็ไม่ได้ไม่มีค่าใช้จ่ายเลย เรียกใช้ API เหล่านี้ในระหว่างการเปลี่ยนวงจรและเหตุการณ์สำคัญของงาน (เช่น การเข้าหรือออกจากกิจกรรม การเริ่มหรือเสร็จสิ้นกระบวนการเบื้องหลัง หรือการจัดการคำขอเสียงพูด) และหลีกเลี่ยงการเรียกใช้ API เหล่านี้ในลูปที่จำกัด เทรดการประมวลผลเสียง หรือรูทีนการแสดงผลต่อเฟรม
- ความถี่ในการค้นหา: พร็อพเพอร์ตี้การค้นหา
processCurrentUsageBytesและpackageCurrentUsageBytesจะดึงข้อมูลการใช้งานหน่วยความจำแบบเรียลไทม์จากบริการของระบบ แม้ว่าจะรวดเร็ว แต่ควรเรียกใช้การค้นหาตามต้องการ (เช่น ระหว่างการเปลี่ยนงานหรือภายใน Callback ของOnOverBudgetListener) แทนที่จะทำการสำรวจในลูปต่อเนื่อง
คอยฟัง Callback ของแรงกดที่เกินงบประมาณ
แอปสามารถลงทะเบียน Listener เพื่อรับการแจ้งเตือนเมื่อการใช้งานหน่วยความจำเกินเกณฑ์งบประมาณ ซึ่งจะช่วยให้แอปทำการล้างข้อมูลเชิงรุกในระดับแอปได้ (เช่น การล้างแคชบิตแมปในหน่วยความจำ) ขณะที่ระบบปฏิบัติการเรียกคืนหน่วยความจำเพื่อรักษากระบวนการหรือแพ็กเกจให้อยู่ภายในงบประมาณ
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)
แนวทางปฏิบัติแนะนำสำหรับ Callback ที่เกินงบประมาณ
- ใช้
Looperพื้นหลัง: ส่งLooperพื้นหลังHandlerThreadไปยังregisterProcessOverBudgetListenerหรือregisterPackageOverBudgetListenerเมื่อเป็นไปได้ การเรียกใช้การลบแคชหรือการล้างไฟล์ในเธรดเบื้องหลังจะช่วยหลีกเลี่ยงการแย่งทรัพยากรกับการแสดงผลเฟรมในเธรด UI ขณะที่กระบวนการอยู่ภายใต้แรงกดดันด้านหน่วยความจำ สำรองLooper.getMainLooperไว้สำหรับ Listener ที่แตะเฉพาะสถานะ UI ที่เชื่อมโยงกับเทรดหลัก - คาดว่าจะมีการเรียกกลับซ้ำๆ: การเรียกกลับเมื่อเกินงบประมาณไม่ใช่การแจ้งเตือนแบบครั้งเดียว เมื่อการใช้งานเกินงบประมาณ ระบบปฏิบัติการจะเรียกคืนหน้าเว็บที่ไม่มีการใช้งานก่อน และหากยังคงมีแรงกดดัน ระบบจะเรียกคืนหน่วยความจำและเรียกใช้ Listener ของคุณอย่างน้อย 1 ครั้งต่อวินาที ทำให้ Callback เป็นแบบ Idempotent หากแอปของคุณต้องการการแจ้งเตือนเพียงครั้งเดียวหรือต้องการระยะเวลาพักที่นานขึ้นระหว่างการล้างข้อมูล ให้เรียกใช้
unregisterProcessOverBudgetListener(หรือunregisterPackageOverBudgetListener) ภายใน Callback หรือป้องกันการล้างข้อมูลด้วยการประทับเวลาของระยะเวลาพักหรือAtomicBoolean - ลงทะเบียนก่อนจำกัดงบประมาณ: หากการใช้งานหน่วยความจำปัจจุบันเกินงบประมาณใหม่แล้วเมื่อคุณเรียกใช้
setProcessBudgetBytesหรือsetPackageBudgetBytesระบบจะทริกเกอร์เหตุการณ์งบประมาณเกินทันทีที่มีการใช้งบประมาณใหม่ ลงทะเบียน Listener ก่อนตั้งค่างบประมาณเพื่อให้แอปไม่พลาดการแจ้งเตือนงบประมาณเกินครั้งแรก - ดำเนินการอย่างรวดเร็ว: การดำเนินการเพื่อเรียกคืนต้องให้ความช่วยเหลือทันที การคำนวณที่ซับซ้อนระหว่างที่เกิดแรงกดดันจะทำให้ประสิทธิภาพแย่ลง
- หลีกเลี่ยงการจัดสรร: อย่าจัดสรรออบเจ็กต์ใหม่หรือเริ่มชุดข้อความใหม่ภายใน Callback เนื่องจากอาจทำให้ระบบปฏิบัติการเรียกคืนโดยตรงทันที
- มุ่งเน้นที่เป้าหมายที่ให้ผลตอบแทนสูง: การลบ 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 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);
เชื่อมโยง Watcher กับ ALooper ของเทรดผู้ปฏิบัติงานในเบื้องหลัง แทนที่จะเป็น UI หลักหรือเทรดการแสดงผล เพื่อไม่ให้การลบแคชขัดแย้งกับการแสดงผลเฟรม สร้าง Watcher ก่อนที่จะจำกัดงบประมาณด้วย AMemoryBudgetManager_setProcessBudget หรือ AMemoryBudgetManager_setPackageBudget เพื่อไม่ให้แอปพลาดเหตุการณ์งบประมาณเกินครั้งแรก
ขณะที่กระบวนการหรือแพ็กเกจยังคงใช้หน่วยความจำเกินงบประมาณและระบบปฏิบัติการยังคงเรียกคืนหน่วยความจำต่อไป ผู้เฝ้าติดตามจะเรียกใช้ Callback ซ้ำๆ (อย่างมากที่สุด 1 ครั้งต่อช่วงเวลา debounce_ms) ทำให้ Callback เป็นแบบ Idempotent หรือเรียกใช้ AMemoryBudgetManager_Watcher_destroy (ซึ่งเรียกใช้จากภายใน Callback ได้อย่างปลอดภัย) หากแอปต้องการการแจ้งเตือนแบบครั้งเดียวเท่านั้น
ตัวอย่าง Runtime API
ตัวอย่างต่อไปนี้แสดงวิธีใช้ API รันไทม์โดยใช้ Android SDK API (เขียนด้วย Kotlin) และ Native NDK API (เขียนด้วย C++)
ตัวอย่าง Android SDK: เครื่องมือแก้ไขรูปภาพแบบปรับเปลี่ยนได้
ตัวอย่างนี้แสดงแอปแต่งรูป (com.example.imageeditor) ที่ไฟล์ Manifest ประกาศเพดาน 256 MB เพื่อรองรับแคนวาสแบบหลายเลเยอร์
<manifest ... >
<application ... >
<!-- Manifest ceiling accommodates the heaviest editing workload -->
<memory-budget android:maxMb="256" />
</application>
</manifest>
เมื่อผู้ใช้เรียกดูแกลเลอรีภาพขนาดย่อที่มีน้ำหนักเบา แอปจะใช้ Android SDK API ใน Kotlin เพื่อลดงบประมาณกระบวนการลงเหลือ 96 MB แบบไดนามิก
เมื่อผู้ใช้เปิดพื้นที่ทำงานสำหรับการแก้ไขแบบหลายเลเยอร์ แอปจะล้างงบประมาณแบบไดนามิกเพื่อคืนค่าเพดานไฟล์ Manifest ขนาด 256 MB เต็ม นอกจากนี้ยังลงทะเบียน
OnOverBudgetListener ใน HandlerThread
Looper เบื้องหลังก่อนที่จะใช้บัดเจ็ตแกลเลอรีเพื่อลบบิตแมปตัวอย่างที่แคชไว้
ภายใต้แรงกดดันโดยไม่บล็อกเธรด UI
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"
}
}
ตัวอย่าง 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;
}
// 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;
};