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

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

วัดชุดการทำงานและปรับเทียบงบประมาณ

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

ทำความเข้าใจชุดการทำงานเทียบกับหน่วยความจำที่ใช้จริง

หากไม่มีงบประมาณ ฟุตพริ้นท์ที่แอปใช้ (memory.current) จะเก็บหน่วยความจำ 2 ประเภท ได้แก่

  • ชุดการทำงาน: ฮีป Java, Kotlin และฮีปแบบเนทีฟ รวมถึงหน้าที่มีการเข้าถึงบ่อยซึ่งมีไฟล์สำรอง (โค้ด DEX, ไลบรารีแบบเนทีฟ และชิ้นงาน) ที่โฟลว์ผู้ใช้ปัจจุบันเข้าถึงซ้ำๆ
  • Cold slack: แคชบิตแมปจากหน้าจอก่อนหน้า, บัฟเฟอร์การเริ่มต้นแบบครั้งเดียว, Heap slack ที่เหลืออยู่หลังจากการจัดสรรแบบต่อเนื่อง และหน้าไฟล์ที่อ่านครั้งเดียวและไม่เคยแตะอีกเลย

งบประมาณที่จุดสูงสุดแบบไม่จำกัดจะไม่เรียกคืนส่วนที่ไม่ได้ใช้ งบประมาณที่ต่ำกว่าชุดการทำงานจะทำให้เคอร์เนลสลับหน้าฮีปออกและนำหน้าโค้ดที่แอปของคุณทำให้เกิดข้อผิดพลาดทันทีออก ข้อผิดพลาดในการเข้าถึงชุดการทำงานเหล่านี้ทำให้สูญเสียเวลา CPU และแบตเตอรี่ และเมื่อข้อผิดพลาดเหล่านี้เกิดขึ้นในเธรด UI ก็จะทำให้สูญเสียเฟรมด้วย

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

เตรียมสภาพแวดล้อมการทดสอบ

ตั้งค่าการทดสอบที่ทำซ้ำได้ซึ่งทำงานเหมือนเวอร์ชันที่ใช้งานจริง โดยทำดังนี้

  • ปรับเทียบกับบิลด์ที่เผยแพร่ซึ่งเพิ่มประสิทธิภาพด้วย R8 และแก้ไขข้อบกพร่องไม่ได้แบบเดียวกับที่คุณจัดส่ง บิลด์การแก้ไขข้อบกพร่องใช้หน่วยความจำมากกว่า ดังนั้นงบประมาณที่ปรับแต่งในบิลด์การแก้ไขข้อบกพร่องจึงสูงเกินไปสำหรับการเผยแพร่
  • ทดสอบในอุปกรณ์หรือโปรแกรมจำลองที่ใช้ Android 17 QPR2 (ระดับ API 37.2) ขึ้นไป บิลด์ userdebug หรืออิมเมจโปรแกรมจำลองที่ไม่มี Google Play จะดีที่สุด เนื่องจากอุปกรณ์บางเครื่องอนุญาตให้อ่านไฟล์ cgroup ในส่วนต่อไปนี้หลังจากเรียกใช้ adb root เท่านั้น
  • เขียนสคริปต์โฟลว์ผู้ใช้ 30 ถึง 60 วินาทีด้วย Jetpack Macrobenchmark, UI Automator หรือ adb shell input หนึ่งครั้งสำหรับแต่ละสถานะที่คุณวางแผนจะกำหนดงบประมาณ แอปที่ไม่มีการใช้งานจะไม่บอกข้อมูลใดๆ เกี่ยวกับชุดการทำงานของแอป
    • foreground: เลื่อนรายการที่หนาแน่นและตารางกริดสื่อ ย้ายไปมาระหว่างปลายทางหลัก เปิดเมนูและกล่องโต้ตอบ และเรียกใช้ภาพเคลื่อนไหว
    • perceptible: ออกจากหน้าต่างที่มองเห็นได้ขณะที่งานที่รับรู้ได้ยังคงดำเนินต่อไป (เช่น การเล่นเสียงหรือการดาวน์โหลด) ปล่อยให้ทำงานเป็นเวลา 10-15 วินาที แล้วกลับไปที่แอป
    • background: ทริกเกอร์การซิงค์, งาน WorkManager (เช่น ด้วย adb shell cmd jobscheduler run -f <package-name> <job-id>), บริการที่เริ่มต้น หรือ Broadcast Receiver ขณะที่แอปไม่มี UI ที่มองเห็นได้

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

วัดค่าพื้นฐาน

เริ่มต้นด้วยการเรียกใช้ที่ไม่มีงบประมาณเลย เพื่อให้คุณทราบว่าการทำงานปกติเป็นอย่างไร ใช้บิลด์ที่ไม่มีการประกาศ <memory-budget> และเริ่มการเรียกใช้แต่ละครั้งจากกระบวนการใหม่ (adb shell am force-stop <package-name>)

กระบวนการของแอปแต่ละรายการมีตัวควบคุมหน่วยความจำ cgroup v2 ของตัวเอง ค้นหาเส้นทางจากบรรทัด 0:: ของ /proc/<PID>/cgroup แทนที่จะสร้างเส้นทางเอง เนื่องจากเลย์เอาต์ cgroup อาจแตกต่างกันไปในแต่ละอุปกรณ์ ส่งคำสั่งอุปกรณ์แต่ละรายการไปยัง adb shell เป็นสตริงที่ยกมาเพียงรายการเดียวเพื่อให้เชลล์ของอุปกรณ์เห็นเครื่องหมายคำพูด

PID=$(adb shell pidof -s <package-name>)
CG=/sys/fs/cgroup$(adb shell "sed -n 's/^0:://p' /proc/$PID/cgroup")
adb shell "cat $CG/memory.current $CG/memory.swap.current"
adb shell "grep -E '^(anon|file|workingset_refault_.*) ' $CG/memory.stat"

การอ่านไฟล์เหล่านี้ใช้ทรัพยากรน้อยและไม่รบกวนแอป คุณจึงสุ่มตัวอย่างไฟล์เหล่านี้ได้ทุกวินาทีระหว่างการเรียกใช้

  • memory.current: หน่วยความจำที่ใช้จริงซึ่งเรียกเก็บจากกระบวนการ (หน่วยเป็นไบต์) ซึ่งเป็นจำนวนที่ <memory-budget> จำกัดไว้
  • memory.swap.current: หน่วยความจำที่บีบอัดเป็น zRAM swap ในหน่วยไบต์
  • anon และ file ใน memory.stat: หน่วยความจำแบบไม่ระบุตัวตนและหน่วยความจำที่สำรองข้อมูลไว้ในไฟล์ในหน่วยไบต์
  • workingset_refault_anon และ workingset_refault_file ใน memory.stat: จำนวนหน้าเว็บที่สลับออกหรือถูกขับออกซึ่งแอปของคุณต้องการอีกครั้ง ตัวนับเหล่านี้เป็นสัญญาณหลักที่บ่งบอกว่างบประมาณมีจำกัดเกินไป

หากได้รับข้อผิดพลาดเกี่ยวกับสิทธิ์ ให้เรียกใช้ adb root แล้วลองอีกครั้ง หากไม่สามารถเข้าถึงรูทได้ adb shell dumpsys meminfo <package-name> จะแสดงมุมมองที่กว้างขึ้น ใน App Summary, Java Heap, Native Heap และ Stack ส่วนใหญ่เป็นหน่วยความจำแบบไม่ระบุชื่อ และ Code เป็นหน่วยความจำที่สำรองข้อมูลด้วยไฟล์ บิตแมปซอฟต์แวร์จะนับรวมใน Native Heap และงบประมาณของคุณ ส่วนบิตแมปฮาร์ดแวร์และ Graphics จะไม่นับรวม อย่าเรียกใช้ dumpsys meminfo ในลูปขณะวัดเวลาที่ใช้ในการแสดงผลเฟรม เนื่องจากจะบังคับให้มีการเก็บขยะในแอปและตรวจสอบการแมปหน่วยความจำทั้งหมด ซึ่งจะเปลี่ยนสิ่งที่คุณพยายามวัด

งบประมาณที่ใช้กวาดล้างผู้สมัครรับเลือกตั้ง

คุณสามารถลองใช้งบประมาณที่เป็นไปได้โดยไม่ต้องสร้างแอปใหม่ ใช้ am memory-limiter manual เพื่อกำหนดงบประมาณสำหรับกระบวนการที่ทำงานอยู่ และใช้ none เพื่อนำงบประมาณออก

adb shell am memory-limiter manual <PID> 120MB
adb shell am memory-limiter manual <PID> none

การลบล้างด้วยตนเองจะคงอยู่ตลอดอายุการใช้งานของอินสแตนซ์กระบวนการนั้นๆ ในการเปลี่ยนแปลงสถานะกระบวนการ ดังนั้นให้ล้างหรือเริ่มกระบวนการใหม่ก่อนที่จะดำเนินการต่อ นอกจากนี้ยังตั้งค่าขีดจำกัดสูงสุดของกระบวนการ (memory.max) เป็นค่าเดียวกันด้วย ดังนั้นงบประมาณที่เป็นไปได้ซึ่งต่ำกว่าชุดการทำงานของแอปอาจทำให้กระบวนการถูกหยุดแทนที่จะแค่ช้าลง ให้ถือว่างบประมาณดังกล่าวไม่สำเร็จ

นอกจากนี้ คุณยังกวาดค่าจากบิลด์การเปรียบเทียบได้โดยการอ่านค่าที่เป็นไปได้จากอาร์กิวเมนต์การวัดและตั้งค่า MemoryBudgetManager.processBudgetBytes งบประมาณรันไทม์สามารถจำกัดงบประมาณไฟล์ Manifest ได้เท่านั้น ดังนั้นให้กวาดล้างบิลด์ที่ไม่มีการประกาศ <memory-budget> นอกจากนี้ยังคงมีผลบังคับใช้ตลอดการเปลี่ยนแปลงสถานะกระบวนการจนกว่าคุณจะเรียกใช้ clearProcessBudget

เริ่มที่ memory.current สูงสุดจากเกณฑ์พื้นฐาน แล้วลดลงครั้งละ 10-15 MB สำหรับงบประมาณที่เป็นไปได้แต่ละรายการ ให้ทำดังนี้

  1. เริ่มกระบวนการใหม่ นำเข้าสู่สถานะเป้าหมาย และใช้งบประมาณ
  2. เรียกใช้โฟลว์ผู้ใช้ 1 ครั้งเพื่อเป็นการเตรียมพร้อม การใช้งบประมาณจะขับหน้า Cold Startup ออก 1 ครั้ง และคุณต้องการวัดสถานะคงที่แทนการเปลี่ยนผ่านนั้น
  3. รีเซ็ตสถิติเฟรม และบันทึกตัวนับการเกิดข้อผิดพลาดซ้ำเริ่มต้นและเวลา CPU ของกระบวนการ (utime และ stime ในหน่วยขีดสัญญาณนาฬิกา)

    adb shell dumpsys gfxinfo <package-name> reset
    adb shell "grep -E '^workingset_refault_(anon|file) ' $CG/memory.stat"
    adb shell "cut -d ' ' -f 14,15 /proc/$PID/stat"
  4. เรียกใช้การส่งผ่านที่วัดได้ของโฟลว์ผู้ใช้

  5. บันทึก memory.current, anon, file, ตัวนับข้อผิดพลาดซ้ำ และเวลา CPU อีกครั้ง แล้วรวบรวมสถิติเฟรม dumpsys gfxinfo สามารถพิมพ์สถิติเดียวกันได้มากกว่า 1 ครั้ง ดังนั้นให้ใช้การจับคู่ครั้งแรกซึ่งครอบคลุมทั้งแอป ดังนี้

    adb shell "dumpsys gfxinfo <package-name>" | grep -m 1 'Janky frames:'
    adb shell "dumpsys gfxinfo <package-name>" | grep -m 1 '95th percentile'

ทำซ้ำผู้สมัครแต่ละรายอย่างน้อย 3 ครั้ง และสลับผู้สมัครแทนที่จะเรียกใช้การทำซ้ำทั้งหมดของผู้สมัครรายเดียวติดต่อกัน เปรียบเทียบผลลัพธ์แต่ละรายการกับช่วงของการเรียกใช้พื้นฐาน ไม่ใช่การเรียกใช้ครั้งเดียว

อ่านผลลัพธ์

เมื่อบัดเจ็ตลดลง แอปจะเข้าสู่ 3 ระบอบการทำงานดังนี้

  • เหนือขีดจำกัด: memory.current จะลดการใช้หน่วยความจำตามงบประมาณโดยการกำจัดหน้า file ที่ไม่ได้ใช้งานและฮีปที่ไม่มีการใช้งาน anon คงที่ ข้อผิดพลาดในการดึงข้อมูลหน้าเว็บซ้ำยังคงอยู่ใกล้กับเส้นฐาน และเวลา CPU และเวลาที่ใช้ในการแสดงผลเฟรมไม่เปลี่ยนแปลง คุณมาถูกที่แล้ว
  • ต่ำกว่าระดับเข่า: หน้าเว็บที่ไม่ได้ใช้งานหายไป เคอร์เนลจึงเริ่มสลับหน้าฮีปที่แอปยังคงใช้อยู่ออก ข้อผิดพลาดในการอ้างอิงจะเพิ่มขึ้นหลายร้อยหรือหลายพันหน้าต่อการเรียกใช้ และเวลา CPU จะเพิ่มขึ้น เวลาที่ใช้ในการแสดงผลเฟรมอาจแทบไม่เปลี่ยนแปลงในอุปกรณ์ทดสอบที่ไม่มีการใช้งาน แต่ตอนนี้แอปของคุณใช้เวลา CPU และแบตเตอรี่ในการบีบอัดและคลายการบีบอัดชุดการทำงานของตัวเอง
  • ต่ำกว่าระดับที่เหมาะสมมาก: ข้อผิดพลาดในการเรียกคืนและสลับเข้าจะเกิดขึ้นในเธรด UI และ RenderThread รวมถึงอาการกระตุก เวลาที่ใช้ในการแสดงผลเฟรม และเวลาในการกลับมาทำงานแบบอุ่นเครื่องจะเพิ่มขึ้นทั้งหมด

เมื่อเทียบกับเกณฑ์พื้นฐานที่ไม่มีข้อจำกัด งบประมาณที่เป็นไปได้จะผ่านเกณฑ์หากเป็นไปตามเงื่อนไขต่อไปนี้ทั้งหมด ซึ่งเป็นจุดเริ่มต้นที่สมเหตุสมผล

  • กระบวนการจะยังคงอยู่หลังจากการเรียกใช้
  • เดลต้าที่เกิดจากข้อผิดพลาดซ้ำยังคงใกล้เคียงกับค่าพื้นฐาน
  • เวลา CPU ของกระบวนการเพิ่มขึ้นไม่เกิน 5%
  • เฟรมที่กระตุกเพิ่มขึ้นไม่เกิน 1 เปอร์เซ็นต์
  • เวลาที่ใช้ในการแสดงผลเฟรม P95 เพิ่มขึ้นไม่เกิน 3 มิลลิวินาที
  • เวลาในการกลับมาทำงานแบบ Warm (TotalTime จาก adb shell am start -W) เพิ่มขึ้นไม่เกิน 15% สำหรับงบประมาณ perceptible และ background

จุดหักเหคืองบประมาณที่เป็นไปได้ต่ำสุดที่ผ่านเกณฑ์ ตัวอย่างเช่น ต่อไปนี้คือการกวาดข้อมูลในเบื้องหน้าสำหรับแอปสมมติที่มีจุดสูงสุดแบบไม่จำกัดที่ 138 MB

งบประมาณที่เสนอ memory.current anon file การผิดนัดชำระหนี้ซ้ำ (หน้า) เวลาที่ใช้ในการแสดงผลเฟรม P95 Regime
ไม่มี 138 MB 75 MB 63 MB 0 10.5 มิลลิวินาที เกณฑ์พื้นฐาน
125 MB 121 MB 74 MB 47 MB 10 10.5 มิลลิวินาที เหนือเข่า
110 MB 110 MB 75 MB 35 MB 15 10.8 มิลลิวินาที เข่า
95 MB 95 MB 74 MB 21 MB 176 11.2 มิลลิวินาที ยาวเลยเข่า
85 MB 85 MB 68 MB 17 MB 2,050 12.8 มิลลิวินาที ยาวเลยเข่า
70 MB 70 MB 54 MB 16 MB 8,771 16.5 มิลลิวินาที ยาวเลยเข่ามาก

เลือกงบประมาณ

ตั้งค่า android:maxMb ให้สูงกว่าจุดเปลี่ยน 10% ถึง 20% เพื่อให้ความแตกต่างในข้อมูลผู้ใช้ ภาษา และการจัดสรรที่เพิ่มขึ้นอย่างรวดเร็วไม่ทำให้แอปของคุณต่ำกว่าจุดเปลี่ยน ในตัวอย่างก่อนหน้านี้ จุดหักเหอยู่ที่ 110 MB ดังนั้น 125 MB จึงเป็นตัวเลือกที่ดี

จากนั้นติดตั้งบิลด์ที่เผยแพร่ซึ่งมีการประกาศ <memory-budget> ใหม่ เรียกใช้โฟลว์ผู้ใช้อีกครั้ง และตรวจสอบว่ายังคงเป็นไปตามเกณฑ์การผ่าน dumpsys meminfo จะแสดงงบประมาณที่ใช้งานอยู่และแหล่งที่มาในส่วนงบประมาณหน่วยความจำ

ตรวจสอบงบประมาณที่ไม่ใช่เบื้องหน้าด้วยการส่งคืนแบบวอร์ม

งบประมาณ perceptible หรือ background ที่จำกัดอาจดูเหมือนไม่มีปัญหาขณะที่แอปอยู่ในสถานะนั้น แต่จะทำให้คุณเสียค่าใช้จ่ายเมื่อผู้ใช้กลับมา ทดสอบการเดินทางขากลับด้วย

  1. เรียกใช้โฟลว์เบื้องหน้า ย้ายแอปไปยังสถานะเป้าหมาย และใช้งบประมาณที่เป็นไปได้ด้วย am memory-limiter manual
  2. ปล่อยให้งานที่รับรู้ได้หรือที่ทำงานอยู่เบื้องหลังทำงานเป็นเวลา 10-15 วินาที
  3. ล้างการลบล้าง (adb shell am memory-limiter manual <PID> none) แล้วนำแอปกลับมาทันทีด้วย adb shell am start -W และบันทึก TotalTime หากคุณตั้งค่าผู้สมัครผ่าน MemoryBudgetManager แทน ให้เรียกใช้ clearProcessBudget ก่อนที่แอปจะแสดงผล
  4. เรียกใช้การโต้ตอบในเบื้องหน้าสั้นๆ และตรวจสอบข้อผิดพลาดซ้ำและสถิติเฟรมเทียบกับเกณฑ์การผ่าน

เลือกงบประมาณต่ำสุดที่ผ่านทั้ง 2 เกณฑ์ขณะที่แอปอยู่ในสถานะนั้นและเมื่อกลับมาทำงานแบบวอร์ม แล้วเพิ่มส่วนเผื่อ 10% ถึง 20% เช่นเดียวกัน

ปรับเทียบการปรับขนาดการแสดงผล

หากคุณใช้ android:additionalBytesPerDisplayPixel ให้ค้นหาจุดเปลี่ยนของโฟร์กราวด์บนจอแสดงผล 2 จอที่มีความละเอียดต่างกัน เช่น 1080 × 2400 (ประมาณ 2.59 ล้านพิกเซล) และ 1440 × 3120 (ประมาณ 4.49 ล้านพิกเซล) หารความแตกต่างระหว่างจุดหักเหทั้ง 2 จุด (หน่วยเป็นไบต์) ด้วยความแตกต่างของจำนวนพิกเซลเพื่อให้ได้ android:additionalBytesPerDisplayPixel ลบจำนวนต่อพิกเซลสำหรับจอแสดงผลขนาดเล็กออกจากจุดหักเหเพื่อให้ได้ฐานที่ไม่ขึ้นอยู่กับความละเอียด และเพิ่มเฮดรูมเพื่อให้ได้ android:maxMb

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;
};