งบประมาณหน่วยความจำของแอปช่วยให้แอปประกาศงบประมาณหน่วยความจำของตัวเองได้ ซึ่งจะบอกให้ระบบลดการใช้หน่วยความจำของแอปเมื่อแอปใช้หน่วยความจำมากกว่างบประมาณที่ตั้งไว้ ซึ่งมีประโยชน์อย่างยิ่งสำหรับแอปของระบบและแอปที่มาพร้อมเครื่อง หรือสำหรับแอปที่กำหนดเป้าหมายไปยังอุปกรณ์ที่มีข้อจำกัดด้านหน่วยความจำ ซึ่งนักพัฒนาแอปทราบชุดการทำงานของหน่วยความจำที่คาดไว้ (หน่วยความจำที่แอปต้องการอย่างยิ่งสำหรับสิ่งที่ผู้ใช้กำลังทำอยู่) และต้องการให้มั่นใจว่าแอปจะไม่ใช้ทรัพยากร 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 ในกระบวนการและสถานะทั้งหมดสำหรับ แพ็กเกจ เมื่อหน่วยความจำที่ใช้ของแอปเกิน 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
วิธีที่สถานะงบประมาณเชื่อมโยงกับสถานะกระบวนการ
แพลตฟอร์มจะแมปสถานะงบประมาณของไฟล์ Manifest กับสถานะกระบวนการรันไทม์ตาม
RunningAppProcessInfo.importance
foreground: การโต้ตอบของผู้ใช้ที่ใช้งานอยู่และมองเห็นได้ เช่น การโฮสต์กิจกรรมที่กลับมาทำงานต่อ หรือการรักษาสถานะแอปที่ด้านบนเมื่อหน้าจอปิดperceptible: ภาระงานที่ผู้ใช้รับรู้ได้โดยไม่มีหน้าต่างที่มองเห็นได้ เช่น การเล่นสื่อที่ใช้งานอยู่ การนำทางแบบเลี้ยวต่อเลี้ยว การจับภาพจากกล้องหรือไมโครโฟน การดาวน์โหลดในพื้นหลังที่ใช้งานอยู่ หรือบริการซิงค์ข้อมูลในเบื้องหน้า แม้ว่าเมตริกแพลตฟอร์มภายในอาจระบุภาระงานบางอย่างเหล่านี้PROCESS_STATE_IMPORTANT_FOREGROUNDแต่ค่าคงที่ภายในดังกล่าวไม่ได้หมายถึงหน้าต่างที่มองเห็นได้และอยู่ในบังคับของงบประมาณperceptiblebackground: งานที่ผู้ใช้ไม่รับรู้ได้ทันที เช่น งานที่ทำงานอยู่เบื้องหลัง การปลุก ตัวรับสัญญาณออกอากาศ หรือกระบวนการที่แคชไว้
ตารางต่อไปนี้แสดงวิธีจับคู่ระดับความสําคัญของรันไทม์กับสถานะของไฟล์ Manifest
ไฟล์ Manifest android:state |
ความสำคัญของรันไทม์ (RunningAppProcessInfo) |
คอมโพเนนต์ทั่วไป |
|---|---|---|
foreground |
IMPORTANCE_FOREGROUNDIMPORTANCE_TOP_SLEEPING |
กิจกรรมที่มองเห็นได้ซึ่งกลับมาทำงานต่อ แอปยอดนิยมขณะล็อกหน้าจอ |
perceptible |
IMPORTANCE_FOREGROUND_SERVICEIMPORTANCE_VISIBLE |
บริการที่ทำงานอยู่เบื้องหน้าซึ่งมีการเล่นสื่อ การนำทาง การดาวน์โหลด หรือการซิงค์ที่ใช้งานอยู่ |
background |
IMPORTANCE_PERCEPTIBLEIMPORTANCE_CANT_SAVE_STATEIMPORTANCE_SERVICEIMPORTANCE_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" />
<!-- 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;
};