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

งบประมาณหน่วยความจำของแอปช่วยให้แอปประกาศงบประมาณหน่วยความจำของตัวเองได้ ซึ่งจะบอกให้ระบบลดการใช้หน่วยความจำของแอปเมื่อแอปใช้หน่วยความจำมากกว่างบประมาณที่ตั้งไว้ ซึ่งมีประโยชน์อย่างยิ่งสำหรับแอปของระบบและแอปที่มาพร้อมเครื่อง หรือสำหรับแอปที่กำหนดเป้าหมายเป็นอุปกรณ์ที่มีข้อจำกัดด้านหน่วยความจำ ซึ่งนักพัฒนาแอปทราบชุดการทำงานของหน่วยความจำที่คาดไว้และต้องการให้มั่นใจว่าแอปจะไม่ใช้ทรัพยากร 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 ในกระบวนการและสถานะทั้งหมดสำหรับ แพ็กเกจ เมื่อหน่วยความจำที่ใช้ของแอปเกิน 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="foreground" ในอนุประโยคแรก เงื่อนไขที่ไม่มี android:state จะทำหน้าที่เป็นข้อความสำรองเริ่มต้นสำหรับทุกสถานะ ข้อความที่จำกัดมากกว่าด้านล่างจะลบล้างงบประมาณเมื่อแอปเปลี่ยน เป็นสถานะ perceptible หรือ background

แอปแบบหลายกระบวนการ

หากแอปพลิเคชันแบ่งงานออกเป็นหลายกระบวนการ ให้กําหนดค่า งบประมาณกระบวนการเฉพาะโดยใช้แท็ก <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" />

        <!-- 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 &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) และแมปกับค่าใช้จ่ายของ 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-declared ceiling
    Log.e(TAG, "Requested budget exceeds manifest or system ceiling", e)
}

// Clear the dynamic process budget to restore the manifest limit
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
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 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) ที่ใช้ Android SDK API ใน Kotlin เพื่อเพิ่มงบประมาณหน่วยความจำแบบไดนามิกเมื่อผู้ใช้ เปิด Canvas การแก้ไขแบบหลายเลเยอร์ และล้างงบประมาณแบบไดนามิกเมื่อกลับไป ที่มุมมองแกลเลอรีภาพขนาดย่อ นอกจากนี้ ยังลงทะเบียน 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)
    }

    override fun onStop() {
        super.onStop()
        budgetManager.unregisterProcessOverBudgetListener(overBudgetListener)
    }

    /**
     * Called when the user enters the high-resolution editing canvas.
     */
    fun enterEditingCanvas() {
        try {
            // Dynamically set budget to 256MB for the editing canvas
            budgetManager.processBudgetBytes = 256L * 1024L * 1024L
            Log.i(TAG, "Dynamic budget applied: 256MB")
        } catch (e: IllegalArgumentException) {
            Log.e(TAG, "Could not apply dynamic budget", e)
        }
    }

    /**
     * Called when the user exits the editor back to the thumbnail gallery.
     */
    fun exitToGallery() {
        previewCache.trimToSize(8 * 1024 * 1024)
        // Clear dynamic budget; restores the baseline manifest budget
        budgetManager.clearProcessBudget()
    }

    companion object {
        private const val TAG = "ImageEditor"
    }
}

ตัวอย่าง NDK C++: เครื่องมือ 3 มิติแบบเนทีฟ

ตัวอย่างนี้แสดงเกมเอนจิน C++ แบบเนทีฟที่จัดการงบประมาณหน่วยความจำตามระดับคุณภาพกราฟิกที่ใช้งานอยู่ โดยใช้ 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;
};