กำหนดงบประมาณหน่วยความจำของแอป

งบประมาณหน่วยความจำของแอปช่วยให้แอปประกาศงบประมาณหน่วยความจำสำหรับตัวเอง ซึ่งจะบอกให้ระบบตัดการใช้หน่วยความจำเมื่อแอปใช้งบประมาณเกินที่ตั้งไว้ ซึ่งมีประโยชน์อย่างยิ่งสำหรับแอประบบและแอปที่รวมมากับระบบ หรือสำหรับแอปที่กำหนดเป้าหมายเป็นอุปกรณ์ที่มีหน่วยความจำจำกัด ซึ่งนักพัฒนาแอปทราบชุดการทำงานของหน่วยความจำที่คาดไว้ (หน่วยความจำที่แอปจำเป็นต้องใช้สำหรับสิ่งที่ผู้ใช้กำลังทำอยู่) และต้องการตรวจสอบว่าแอปไม่ได้ใช้ทรัพยากร RAM ที่ใช้ร่วมกันของระบบมากเกินไป

ระบบจะรักษาสมดุลของงบประมาณโดยใช้การขับไล่และการสลับหน่วยความจำเพื่อนำหน้าหน่วยความจำที่ไม่ได้ใช้เมื่อเร็วๆ นี้ออก โดยมุ่งเน้นที่หน่วยความจำที่ใช้ของแอปในชุดการทำงานปัจจุบัน เมื่อแอปใช้หน่วยความจำเกินงบประมาณที่ประกาศไว้ ระบบปฏิบัติการจะกำหนดเป้าหมายการเรียกคืนหน่วยความจำไปที่แอปนั้นโดยเฉพาะ

  1. ระบบจะลบหน้าเว็บที่สำรองข้อมูลไฟล์ที่สะอาด (เช่น โค้ดที่ไม่ได้ใช้งานและเนื้อหาที่แมป) ออกก่อน เนื่องจากสามารถอ่านซ้ำจากพื้นที่เก็บข้อมูลได้หากจำเป็น
  2. ระบบจะเขียนหน้าที่มีไฟล์สำรองซึ่งมีการแก้ไขกลับไปยังพื้นที่เก็บข้อมูลและนำออก
  3. หน้าหน่วยความจำที่ไม่ระบุชื่อ (เช่น การจัดสรรฮีป) จะถูกบีบอัดและสลับไปยัง 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

เมื่อกำหนดขนาดงบประมาณสำหรับแต่ละสถานะ ให้กำหนดเป้าหมายชุดการทำงานที่ใช้งานอยู่ของสถานะนั้นแทนที่จะเป็นหน่วยความจำที่ใช้สูงสุด (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 แต่ค่าคงที่ภายในนั้นไม่ได้หมายถึงกรอบเวลาที่มองเห็นได้ และอยู่ภายใต้งบประมาณ perceptible
  • background: งานที่ผู้ใช้ไม่สามารถรับรู้ได้ทันที เช่น งานที่ทำงานอยู่เบื้องหลัง การปลุก Broadcast Receiver หรือกิจกรรมก่อนหน้าหลังจากที่ผู้ใช้ออกจากแอป

ตารางต่อไปนี้แสดงวิธีที่ระดับความสำคัญของรันไทม์แมปกับสถานะไฟล์ Manifest

ไฟล์ Manifest android:state ความสำคัญของรันไทม์ (RunningAppProcessInfo) องค์ประกอบทั่วไป
foreground IMPORTANCE_FOREGROUND
IMPORTANCE_TOP_SLEEPING
กิจกรรมที่มองเห็นได้ซึ่งกลับมาทำงานอีกครั้ง, แอปยอดนิยมที่มีหน้าจอล็อกอยู่, บริการหรือ Content Provider ที่เชื่อมโยงกับแอปยอดนิยม
perceptible IMPORTANCE_FOREGROUND_SERVICE
IMPORTANCE_VISIBLE
การเล่นสื่อที่ทำงานอยู่ การนำทาง การดาวน์โหลด หรือการซิงค์บริการที่ทำงานอยู่เบื้องหน้า บริการที่เชื่อมโยงกับบริการที่ทำงานอยู่เบื้องหน้าหรือแฟล็กที่มองเห็นได้
background IMPORTANCE_PERCEPTIBLE
IMPORTANCE_CANT_SAVE_STATE
IMPORTANCE_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) ดังนี้

  1. กระบวนการหลัก: โฮสต์ UI ที่มองเห็นได้และเครื่องมือเล่นเสียง (MediaSessionService พร้อมบริการที่ทำงานอยู่เบื้องหน้า mediaPlayback) เมื่อมองเห็นได้ กระบวนการจะทำงานภายใต้งบประมาณเบื้องหน้า 180 MB เมื่อผู้ใช้ออกจากแอปขณะที่เพลงยังเล่นอยู่ กระบวนการจะเข้าสู่สถานะ perceptible ซึ่งงบประมาณ 64 MB เพียงพอสำหรับเครื่องมือการเล่นและบัฟเฟอร์เสียง
  2. กระบวนการซิงค์ (: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 &approx; 2.59 ล้านพิกเซล) การดำเนินการนี้จะเพิ่มงบประมาณพื้นฐานประมาณ 41.4 MB ในจอแสดงผล 1440p (1440 × 3120 &approx; 4.49 ล้านพิกเซล) จะเพิ่มขึ้น &approx; 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 วิธีในการตรวจสอบเหตุการณ์หน่วยความจำ ดังนี้

  1. High-Level Watcher (AMemoryBudgetManager_Watcher_create): ตรวจสอบเหตุการณ์ใน ALooper ด้วยการลดการตีกลับโดยอัตโนมัติ
  2. ตัวบอกไฟล์ระดับต่ำ: 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;
};