บิตแมปและหน่วยความจำ

ออบเจ็กต์บิตแมปมักเป็นปัจจัยเดียวที่ใหญ่ที่สุดที่ทำให้แอปพลิเคชันใช้หน่วยความจำที่ใช้มาก ไม่ว่าจะเป็นไอคอนแอป รูปภาพการแจ้งเตือน หรือเนื้อหาสื่อ การจัดการบิตแมปที่ไม่มีประสิทธิภาพอาจทำให้เกิดข้อผิดพลาดหน่วยความจำไม่เพียงพอ (OOM) และหน่วยความจำของระบบเต็มอย่างรวดเร็ว

การกำหนดค่าบิตแมปและข้อมูลพิกเซล

ปริมาณหน่วยความจำที่บิตแมปใช้จะขึ้นอยู่กับขนาด (กว้าง × สูง) และการกำหนดค่า (Bitmap.Config) เป็นหลัก

การกำหนดค่าจะกำหนดจำนวนไบต์ที่ใช้แสดงพิกเซลแต่ละพิกเซล

การกำหนดค่า ไบต์ต่อพิกเซล คำอธิบาย
ALPHA_8 1 มีเฉพาะช่องอัลฟ่า (ความโปร่งใส) เหมาะสำหรับมาสก์
RGB_565 2 สีแดง (5 บิต) สีเขียว (6 บิต) สีน้ำเงิน (5 บิต) ไม่มีอัลฟ่า เหมาะสำหรับรูปภาพทึบแสงที่ไม่จำเป็นต้องมีความเที่ยงตรงของสีสูง
ARGB_8888 4 อัลฟ่า สีแดง สีเขียว สีน้ำเงิน (8 บิตต่อสี) ค่าเริ่มต้นและใช้กันมากที่สุด
RGBA_F16 8 จุดลอยตัวแบบครึ่งความแม่นยำ ใช้สำหรับเนื้อหาที่มีขอบเขตสีที่กว้างและเนื้อหา HDR
HARDWARE ไม่มี จัดเก็บไว้ในหน่วยความจำกราฟิก (gralloc/DMABuf) ดูบิตแมปฮาร์ดแวร์

สูตรหน่วยความจำ: Memory (Bytes) = Width × Height × Bytes Per Pixel

เช่น รูปภาพแบบเต็มหน้าจอในอุปกรณ์ 1080p (1920x1080) ใน ARGB_8888 จะใช้พื้นที่ 1920 × 1080 × 4 ไบต์ ≈ 8.3 MB

บิตแมปฮีปเทียบกับบิตแมปที่แชร์

บิตแมปฮีป (ฮีปเนทีฟ)

ใน Android เวอร์ชันใหม่ (8.0 ขึ้นไป) ระบบจะจัดเก็บข้อมูลพิกเซลบิตแมปไว้ในฮีปเนทีฟ ส่วนออบเจ็กต์ Wrapper ขนาดเล็กจะอยู่ในฮีป Java

เมื่อแอปต้องการแสดงรูปภาพ โดยปกติแล้วระบบจะถอดรหัสรูปภาพจากไฟล์รูปภาพที่บีบอัดเป็นบิตแมปและจัดเก็บไว้ในฮีป

บิตแมปที่แชร์ (ashmem/memfd)

เมื่อมีการโอนบิตแมประหว่างกระบวนการ (เช่น ผ่าน Binder ไปยัง SystemUI สำหรับการแจ้งเตือน) Android จะหลีกเลี่ยงการคัดลอกข้อมูลพิกเซลโดยใช้หน่วยความจำที่แชร์ (ashmem หรือ memfd)

คุณสามารถคัดลอกอินสแตนซ์ Bitmap ไปยังหน่วยความจำที่แชร์ได้อย่างชัดเจนโดยเรียกใช้ Bitmap.asShared(), หรือโดยนัยหากมีการใส่ Bitmap ไว้ใน Parcel (โดยปกติจะทำโดยการเพิ่ม บิตแมปลงใน Parcelable เช่น Bundle) และส่งผ่าน Binder IPC

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

บิตแมปที่เปลี่ยนแปลงได้เทียบกับบิตแมปที่เปลี่ยนแปลงไม่ได้

  • บิตแมปที่เปลี่ยนแปลงได้: สามารถแก้ไขได้หลังจากสร้าง (เช่น ผ่าน Canvas) โดยต้องมีการจัดสรรหน่วยความจำส่วนตัวของตัวเองเสมอ หากมีการคัดลอกบิตแมปที่เปลี่ยนแปลงได้ ระบบจะต้องทำการคัดลอกแบบลึก (คัดลอกข้อมูลพิกเซลทั้งหมดเป็นครั้งที่ 2)
  • บิตแมปที่เปลี่ยนแปลงไม่ได้: เปลี่ยนแปลงไม่ได้ ซึ่งช่วยให้สามารถทำการเพิ่มประสิทธิภาพ เช่น การแชร์บัฟเฟอร์หน่วยความจำพื้นฐานเดียวกันระหว่างอินสแตนซ์ Bitmap ต่างๆ โดยปกติแล้วบิตแมปที่โหลดจากทรัพยากร APK (BitmapFactory) จะเปลี่ยนแปลงไม่ได้

การจัดการบิตแมปอย่างมีประสิทธิภาพ

การรวมกลุ่มและการใช้บิตแมปซ้ำ

การจัดสรรและยกเลิกการจัดสรรบิตแมปบ่อยๆ จะทำให้เกิดการหมุนเวียนการจัดสรร ซึ่งบังคับให้ GC ทำงานอยู่ตลอดเวลา ไลบรารีการโหลดรูปภาพทั่วไปใช้กลุ่มบิตแมป

Google แนะนำให้ใช้ Glide สำหรับแอปพลิเคชันที่ใช้ Java และ Coil สำหรับ แอปพลิเคชันที่ใช้ Kotlin (โดยเฉพาะเมื่อใช้ Jetpack Compose)

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

บิตแมปฮาร์ดแวร์

Bitmap.Config.HARDWARE ช่วยให้คุณจัดเก็บข้อมูลพิกเซลไว้ในหน่วยความจำกราฟิก (DMABuf) ได้โดยตรง

  • ข้อดี:
    • ประหยัดหน่วยความจำ: ไม่ใช้ฮีปของแอปพลิเคชันหรือฮีปเนทีฟ แต่ใช้หน่วยความจำ GPU บิตแมปที่แสดงใน UI ของแอปมักจะต้องคัดลอกไปยังหน่วยความจำ GPU อยู่แล้ว ดังนั้นการใช้บิตแมปฮาร์ดแวร์จึงช่วยประหยัดการดำเนินการคัดลอกและค่าใช้จ่ายหน่วยความจำที่เพิ่มขึ้น
    • ประสิทธิภาพ: วาดได้เร็วมากเนื่องจากข้อมูลอยู่ใน GPU อยู่แล้ว
  • ข้อเสีย:
    • เปลี่ยนแปลงไม่ได้: บิตแมปฮาร์ดแวร์แก้ไขไม่ได้
    • การอ่านกลับช้า: การเข้าถึงพิกเซลจาก CPU (เช่น getPixel()) ใช้ทรัพยากรมาก
    • การระบุแหล่งที่มา: ติดตามได้ยากกว่าในเครื่องมือมาตรฐาน เช่น AHAT (ดู ด้านล่าง)

แบบฝึกหัดภาคปฏิบัติ: การสำรวจบิตแมป

เราจะใช้แอปตัวอย่าง BitmapLab เพื่อสำรวจแนวคิดเหล่านี้

1. การวัดด้วย dumpsys meminfo

เปิด BitmapLab แล้วแตะ ALLOCATE 10MB ARGB_8888 จากนั้นเรียกใช้คำสั่งต่อไปนี้

adb shell dumpsys meminfo -s com.android.bitmaplab

ใน Android เวอร์ชันใหม่ ให้มองหาส่วนการจัดสรรเนทีฟ ซึ่ง ให้การระบุแหล่งที่มาสำหรับบิตแมปได้ดีกว่าข้อมูลสรุปของแอป ทั่วไปมาก:

 Native Allocations
                         Count                       Total(kB)
                        ------                         ------
   Bitmap (malloced):        1                          10240  # <--- 10MB Bitmap data!
Bitmap (nonmalloced):        0                              0
  • บิตแมป (malloced): บิตแมปที่จัดสรรในฮีปเนทีฟของกระบวนการ ซึ่งเป็นที่ที่บิตแมปมาตรฐานส่วนใหญ่อยู่ใน Android 8.0 ขึ้นไป
  • บิตแมป (nonmalloced): บิตแมปที่ใช้หน่วยความจำเฉพาะ เช่น บิตแมปฮาร์ดแวร์ หรือ บิตแมปที่แชร์ (ผ่าน ashmem หรือ memfd)

หากคุณจัดสรรบิตแมปที่แชร์ ใน BitmapLab คุณจะเห็นการเปลี่ยนแปลง ใน Bitmap (nonmalloced) ดังนี้

 Native Allocations
                         Count                       Total(kB)
                        ------                         ------
   Bitmap (malloced):        1                          10240
Bitmap (nonmalloced):        1                          10240  # <--- Shared Bitmap!

การติดตามบิตแมปที่แชร์

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

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

adb shell setprop debug.hwui.bitmap_ashmem_long_name true

เมื่อเปิดใช้แล้ว พื้นที่ ashmem ใน /proc/<pid>/smaps จะมีชื่อที่อธิบายได้ชัดเจนมากขึ้น meminfo จะใช้ประโยชน์จากชื่อเหล่านั้น และผลลัพธ์จะมีลักษณะดังนี้

 Shared Bitmaps
                         Count                       Size(KB)
                        ------                         ------
              Mapped:        1                          10240
              Unique:        1                          10240
  • Mapped: ขนาดรวมของการแมปหน่วยความจำทั้งหมดที่เกี่ยวข้องกับบิตแมป
  • Unique: ขนาดของบิตแมปโดยพิจารณาเฉพาะบิตแมปที่ไม่ซ้ำกัน (เช่น การแมปข้อมูลพิกเซลบิตแมปที่แชร์พื้นฐานเดียวกัน 2 รายการขึ้นไปจะนับเพียงครั้งเดียว)

2. บิตแมปใน AHAT

AHAT มีการแสดงภาพที่ยอดเยี่ยมสำหรับบิตแมป

  1. จัดสรรบิตแมป 2-3 รายการใน BitmapLab
  2. บันทึกฮีปดัมป์ด้วยแฟล็ก -b (เพื่อรวมข้อมูลบิตแมปเนทีฟ)

    adb shell am dumpheap -b png com.android.bitmaplab /data/local/tmp/bitmaps.hprof
    adb pull /data/local/tmp/bitmaps.hprof .
    ahat bitmaps.hprof
    
  3. เปิด localhost:7100 แล้วมองหาลิงก์บิตแมป ในแถบด้านข้าง หรือค้นหาคลาส Bitmap

  4. AHAT จะแสดงบิตแมปในเบราว์เซอร์ ซึ่งช่วยให้ระบุได้ง่ายว่ารูปภาพใดใช้หน่วยความจำมาก

AHAT แสดงบิตแมปที่เรนเดอร์แล้ว

3. แทร็กบิตแมปใน Perfetto

Perfetto สามารถติดตามการจัดสรรและการนับบิตแมปเมื่อเวลาผ่านไป เฟรมเวิร์ก Android จะปล่อยตัวนับเหล่านี้เมื่อเปิดใช้หมวดหมู่ gfx atrace สำหรับแอปพลิเคชันที่เฉพาะเจาะจง

  1. เริ่มการติดตาม คุณต้องรวมหมวดหมู่ gfx และกำหนดเป้าหมายแพ็กเกจแอปที่เฉพาะเจาะจงโดยใช้แฟล็ก -a ดังนี้

    external/perfetto/tools/record_android_trace -o bitmaps.perfetto-trace \
        -t 15s -b 64mb view gfx dalvik am res memory -a com.android.bitmaplab
    
  2. ใน BitmapLab ให้แตะปุ่ม จัดสรร และ ล้าง ซ้ำๆ

  3. นอกจากนี้ ให้แตะบิตแมป Parcel/Unparcel

  4. วิเคราะห์การติดตามใน ui.perfetto.dev

ในส่วนกระบวนการสำหรับ com.android.bitmaplab คุณจะเห็นข้อมูลต่อไปนี้ * จำนวนบิตแมป: ตัวนับที่แสดงจำนวนบิตแมปที่ใช้งานอยู่ * หน่วยความจำบิตแมป: ตัวนับที่แสดงจำนวนไบต์ทั้งหมดที่บิตแมปใช้

สไลซ์ระดับสูง (Perfetto SDK)

BitmapLab ยังใช้ Perfetto SDK เพื่อปล่อยสไลซ์ระดับสูงสำหรับการดำเนินการบิตแมปด้วย ค้นหา BitmapLab_ ในการติดตามเพื่อดูข้อมูลต่อไปนี้ * BitmapLab_parcelUnparcel: สไลซ์ที่ครอบคลุมตรรกะการจัดกลุ่มและการยกเลิกการจัดกลุ่ม * BitmapLab_postNotification: สไลซ์ที่ครอบคลุมขั้นตอนการโพสต์การแจ้งเตือน

การติดตามขั้นตอนการแจ้งเตือน

เมื่อคุณแตะโพสต์การแจ้งเตือน แอปจะสร้างการแจ้งเตือนที่มี บิตแมปปัจจุบันและส่งไปยังระบบ โค้ดเฟรมเวิร์กที่รับผิดชอบการดำเนินการนี้จะปล่อยสไลซ์ Perfetto พร้อมเหตุการณ์โฟลว์ที่เชื่อมต่อการจัดกลุ่ม (การเขียนบิตแมปลงใน Parcel เพื่อส่งผ่าน Binder IPC) และการยกเลิกการจัดกลุ่ม (การอ่านบิตแมปจาก Parcel ในฝั่งผู้รับ)

ในภาพหน้าจอด้านล่าง คุณจะเห็นแอปจัดกลุ่มบิตแมปขนาดใหญ่เพื่อใช้ในการทำธุรกรรม Binder เพื่อโพสต์การแจ้งเตือน และการยกเลิกการจัดกลุ่มที่เกี่ยวข้องในกระบวนการ system_server

Perfetto แสดงโฟลว์จาก BitmapLab ไปยัง system_server ผ่านการแจ้งเตือน

การใช้ Perfetto ช่วยให้คุณติดตามบิตแมปการแจ้งเตือนเดียวกันได้แม้ว่าบิตแมปจะแพร่กระจายไปยังเธรดและกระบวนการต่างๆ เช่น จากเธรด Binder ใน system_server (ซึ่งใช้เซิร์ฟเวอร์ Binder INotificationManager) ไปยังเธรดงาน system_server ซึ่งอาจส่งต่อบิตแมปเดียวกันไปยัง com.android.systemui เพื่อแสดงในแผงการแจ้งเตือน

ความท้าทายของแอประบบ

แอประบบ เช่น SystemUI (การแจ้งเตือน) และตัวเรียกใช้ เผชิญกับความท้าทายที่ไม่เหมือนใครดังนี้

  1. เนื้อหาไม่จำกัด: การแจ้งเตือนและวิดเจ็ตอาจมีจำนวนมาก หากแต่ละรายการมีบิตแมปขนาดใหญ่ ระบบอาจใช้หน่วยความจำจนหมดอย่างรวดเร็ว
  2. การทำซ้ำ: ไอคอนแอปเดียวกันอาจอยู่ในแคชของตัวเรียกใช้ พื้นที่การแจ้งเตือนของ SystemUI และแอปการตั้งค่า
  3. การแชร์ผ่านบัฟเฟอร์ฮาร์ดแวร์: เพื่อลดปัญหานี้ คอมโพเนนต์ของระบบกำลังเปลี่ยนไปใช้บริการ "การลดภาระรูปภาพ" แบบรวมศูนย์ที่แชร์HardwareBuffer อินสแตนซ์ในกระบวนการต่างๆ
  4. การระบุแหล่งที่มาของ DMABuf: บิตแมปฮาร์ดแวร์ช่วยประหยัดพื้นที่ฮีป แต่ใช้หน่วยความจำ DMABuf ซึ่งระบุแหล่งที่มาไปยังกระบวนการที่เฉพาะเจาะจงได้ยากกว่าในเครื่องมือหน่วยความจำ มาตรฐาน

    ใช้ adb shell dmabuf_dump เพื่อดูการจัดสรร DMABuf ทั่วทั้งระบบ เครื่องมือนี้จะแสดงรายละเอียดบัฟเฟอร์ตามกระบวนการดังนี้

     droid.bitmaplab:19562
                      Name              Rss              Pss         nr_procs            Inode               Exporter
                 <unknown>          3840 kB          1280 kB                3             3397              virtio_gpu
                    system            12 kB             4 kB                3             3398                  system
                 <unknown>          3840 kB          1920 kB                2             3399              virtio_gpu
                    system            12 kB             6 kB                2             3400                  system
             PROCESS TOTAL         11556 kB          5136 kB
    
    • RSS: ขนาดรวมของบัฟเฟอร์หากมีการแมปในกระบวนการ
    • Pss: ขนาดตามสัดส่วน (RSS หารด้วยจำนวนกระบวนการที่ แชร์บัฟเฟอร์) ซึ่งเป็นเมตริกที่ดีที่สุดสำหรับการบัญชี
    • nr_procs: จำนวนกระบวนการที่อ้างอิงถึง บัฟเฟอร์นี้ในปัจจุบัน
    • Exporter: ไดรเวอร์ที่จัดสรรบัฟเฟอร์ (เช่น virtio_gpu ใน Cuttlefish หรือฮีป Ion/DMA-BUF ที่เฉพาะเจาะจงของผู้ให้บริการในฮาร์ดแวร์)

    นอกจากนี้ คุณยังใช้ adb shell dmabuf_dump -b เพื่อดูข้อมูลสรุปของบัฟเฟอร์ทั้งหมดและการใช้งาน DMA-BUF ทั้งหมดทั่วทั้งระบบได้ด้วย


← Java | ↑ ขึ้น | เนทีฟ →