Bitmap và bộ nhớ

Các đối tượng bitmap thường là những thành phần đóng góp lớn nhất cho mức sử dụng bộ nhớ của một ứng dụng. Cho dù đó là biểu tượng ứng dụng, hình ảnh thông báo hay nội dung nghe nhìn, việc xử lý bitmap không hiệu quả có thể nhanh chóng dẫn đến lỗi hết bộ nhớ (OOM) và áp lực bộ nhớ trên toàn hệ thống.

Cấu hình bitmap và dữ liệu pixel

Lượng bộ nhớ mà một bitmap tiêu thụ chủ yếu được xác định bằng kích thước (chiều rộng × chiều cao) và cấu hình (Bitmap.Config).

Cấu hình này xác định số byte được dùng để biểu thị từng pixel:

Cấu hình Số byte trên mỗi pixel Mô tả
ALPHA_8 1 Chỉ kênh alpha (độ trong suốt). Hữu ích cho mặt nạ.
RGB_565 2 Đỏ (5 bit), Xanh lục (6 bit), Xanh dương (5 bit). Không có alpha. Phù hợp với những hình ảnh mờ đục mà độ trung thực về màu sắc không phải là yếu tố quan trọng.
ARGB_8888 4 Alpha, Đỏ, Lục, Lam (mỗi kênh 8 bit). Mặc định và phổ biến nhất.
RGBA_F16 8 Dấu phẩy động có độ bán chính xác. Dùng cho nội dung có gam màu rộng và HDR.
HARDWARE Không áp dụng Được lưu trữ trong bộ nhớ đồ hoạ (gralloc/DMABuf). Xem phần Bitmap phần cứng.

Công thức về bộ nhớ: Memory (Bytes) = Width × Height × Bytes Per Pixel

Ví dụ: một hình ảnh toàn màn hình trên thiết bị 1080p (1920x1080) trong ARGB_8888 sẽ có kích thước: 1920 × 1080 × 4 byte ≈ 8, 3 MB.

Bitmap trong vùng nhớ đệm so với bitmap dùng chung

Bitmap vùng nhớ khối xếp (vùng nhớ khối xếp gốc)

Trong Android hiện đại (8.0 trở lên), dữ liệu pixel bitmap được lưu trữ trong Vùng nhớ khối xếp gốc, trong khi chỉ có một đối tượng trình bao bọc nhỏ nằm trong vùng nhớ khối xếp Java.

Khi cần trình bày một hình ảnh, ứng dụng thường giải mã hình ảnh đó từ một tệp hình ảnh nén thành Bitmap và lưu trữ trong vùng nhớ heap.

Bitmap dùng chung (ashmem/memfd)

Khi một bitmap được chuyển giữa các quy trình (ví dụ: thông qua Binder đến SystemUI cho một thông báo), Android sẽ tránh sao chép dữ liệu pixel bằng cách sử dụng bộ nhớ dùng chung (ashmem hoặc memfd).

Bạn có thể sao chép một thực thể Bitmap vào bộ nhớ dùng chung một cách rõ ràng bằng cách gọi Bitmap.asShared() hoặc một cách ngầm ẩn nếu Bitmap được đặt bên trong Parcel (thường bằng cách thêm Bitmap vào Parcelable, chẳng hạn như Bundle) và gửi qua Binder IPC.

Khi một bitmap dùng chung được gửi qua Binder IPC, bản thân dữ liệu pixel sẽ không được sao chép, mà thay vào đó, một chỉ số mô tả tệp tham chiếu đến vùng nhớ dùng chung sẽ được sao chép vào quy trình nhận. Vùng nhớ cơ bản có thể được chia sẻ giữa nhiều quy trình và sẽ không được giải phóng cho đến khi tất cả các bộ mô tả tệp tham chiếu đến vùng nhớ đó đã bị đóng.

Bitmap có thể thay đổi so với bitmap không thể thay đổi

  • Bitmap có thể thay đổi: Có thể sửa đổi sau khi tạo (ví dụ: thông qua Canvas). Các bitmap này luôn yêu cầu phân bổ bộ nhớ riêng. Nếu một Bitmap có thể thay đổi được sao chép, thì bạn phải tạo một bản sao chép sâu (bản sao thứ hai của tất cả dữ liệu pixel).
  • Bitmap không thể thay đổi: Không thể thay đổi. Điều này cho phép tối ưu hoá, chẳng hạn như chia sẻ cùng một vùng đệm bộ nhớ cơ bản giữa các thực thể Bitmap khác nhau. Bitmap được tải từ tài nguyên APK (BitmapFactory) thường là bất biến.

Xử lý bitmap hiệu quả

Tập hợp và sử dụng lại bitmap

Việc phân bổ và huỷ phân bổ bitmap thường xuyên gây ra tình trạng phân bổ thay đổi liên tục, buộc GC phải chạy liên tục. Các thư viện tải hình ảnh phổ biến sử dụng một Bitmap Pool (Nhóm Bitmap).

Google đề xuất Glide làm giải pháp cho các ứng dụng dựa trên Java và Coil cho các ứng dụng dựa trên Kotlin (đặc biệt là khi sử dụng Jetpack Compose).

Khi không cần dùng bitmap nữa, thay vì để bitmap được GC'd, ứng dụng sẽ gọi bitmap.recycle() hoặc trả bitmap về một nhóm. Lần tiếp theo cần dùng một bitmap có cùng kích thước và cấu hình, nhóm sẽ cung cấp vùng đệm hiện có, tránh việc phân bổ mới.

Bitmap phần cứng

Bitmap.Config.HARDWARE cho phép bạn lưu trữ dữ liệu pixel trực tiếp trong bộ nhớ đồ hoạ (DMABuf).

  • Ưu điểm:
    • Tiết kiệm bộ nhớ: Không dùng ứng dụng hoặc vùng nhớ khối xếp gốc; dùng bộ nhớ GPU. Thường thì các Bitmap xuất hiện trong giao diện người dùng của ứng dụng vẫn cần được sao chép vào bộ nhớ GPU, vì vậy, điều này giúp tiết kiệm thao tác sao chép và chi phí bộ nhớ bổ sung.
    • Hiệu suất: Vẽ cực kỳ nhanh vì dữ liệu đã có trên GPU.
  • Nhược điểm:
    • Không thể thay đổi: Bạn không thể sửa đổi bitmap phần cứng.
    • Đọc lại chậm: Việc truy cập vào các pixel từ CPU (ví dụ: getPixel()) rất tốn kém.
    • Phân bổ: Khó theo dõi hơn trong các công cụ tiêu chuẩn như AHAT (xem bên dưới).

Bài tập thực hành: khám phá bitmap

Chúng ta sẽ dùng ứng dụng mẫu BitmapLab để khám phá những khái niệm này.

1. Đo lường bằng dumpsys meminfo

Khởi chạy BitmapLab rồi nhấn vào ALLOCATE 10MB ARGB_8888. Sau đó, hãy chạy:

adb shell dumpsys meminfo -s com.android.bitmaplab

Trên các phiên bản Android mới, hãy tìm mục Native Allocations (Phân bổ gốc). Những thông tin này cung cấp khả năng phân bổ tốt hơn nhiều cho bitmap so với Tóm tắt ứng dụng chung:

 Native Allocations
                         Count                       Total(kB)
                        ------                         ------
   Bitmap (malloced):        1                          10240  # <--- 10MB Bitmap data!
Bitmap (nonmalloced):        0                              0
  • Bitmap (malloced): Bitmap được phân bổ trong vùng nhớ heap gốc của quy trình. Đây là nơi hầu hết các bitmap tiêu chuẩn nằm trong Android 8.0 trở lên.
  • Bitmap (nonmalloced): Bitmap sử dụng bộ nhớ chuyên dụng như Bitmap phần cứng hoặc Bitmap dùng chung (thông qua ashmem hoặc memfd).

Nếu phân bổ một Bitmap dùng chung trong BitmapLab, bạn sẽ thấy bitmap đó xuất hiện trong Bitmap (nonmalloced):

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

Theo dõi bitmap dùng chung

Trên một số phiên bản Android và cấu hình kernel, dumpsys meminfo cũng cung cấp tính năng theo dõi độ phân giải cao cho các bitmap được ánh xạ vào không gian địa chỉ của quy trình thông qua các bộ mô tả tệp.

Theo mặc định, các bitmap dùng chung sẽ sử dụng một tên chung ("bitmap"). Để bật tính năng phân bổ chi tiết và theo dõi bitmap duy nhất (xác định các bitmap dùng chung trên nhiều quy trình), bạn phải bật thuộc tính hệ thống sau:

adb shell setprop debug.hwui.bitmap_ashmem_long_name true

Khi được bật, các vùng ashmem trong /proc/<pid>/smaps sẽ có tên mô tả rõ ràng hơn. meminfo sẽ tận dụng điều đó và kết quả sẽ có dạng như sau:

 Shared Bitmaps
                         Count                       Size(KB)
                        ------                         ------
              Mapped:        1                          10240
              Unique:        1                          10240
  • Đã ánh xạ: Tổng kích thước của tất cả các ánh xạ bộ nhớ liên quan đến bitmap.
  • Đơn nhất: Kích thước của bitmap chỉ xem xét các giá trị đơn nhất (tức là 2 hoặc nhiều ánh xạ của cùng một số lượng dữ liệu pixel Bitmap dùng chung cơ bản chỉ được tính một lần).

2. Bitmap trong AHAT

AHAT cung cấp khả năng trực quan hoá tuyệt vời cho Bitmap.

  1. Trong BitmapLab, hãy phân bổ một số bitmap.
  2. Chụp một tệp báo lỗi bằng cờ -b (để thêm dữ liệu bitmap gốc):

    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. Mở localhost:7100 và tìm đường liên kết Bitmaps trong thanh bên hoặc tìm kiếm lớp Bitmap.

  4. AHAT sẽ kết xuất các bitmap trong trình duyệt, giúp bạn dễ dàng xác định những hình ảnh đang chiếm dụng bộ nhớ.

AHAT hiển thị bitmap được kết xuất

3. Dấu vết bitmap trong Perfetto

Perfetto có thể theo dõi số lượng và mức phân bổ bitmap theo thời gian. Khung Android sẽ phát ra các bộ đếm này khi danh mục gfx atrace được bật cho một ứng dụng cụ thể.

  1. Bắt đầu theo dõi. Bạn phải thêm danh mục gfx và nhắm đến gói ứng dụng cụ thể bằng cờ -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. Trong BitmapLab, hãy nhấn vào các nút Allocate (Phân bổ) và Clear (Xoá) nhiều lần.

  3. Ngoài ra, hãy nhấn vào Parcel/Unparcel Bitmap (Gói/Huỷ gói Bitmap).

  4. Phân tích dấu vết trong ui.perfetto.dev.

Trong phần quy trình cho com.android.bitmaplab, bạn sẽ thấy: * Số lượng bitmap: Bộ đếm cho biết số lượng bitmap đang hoạt động. * Bộ nhớ bitmap: Bộ đếm cho biết tổng số byte mà các bitmap sử dụng.

Lát cắt cấp cao (Perfetto SDK)

BitmapLab cũng sử dụng Perfetto SDK để phát ra các lát cắt cấp cao cho các thao tác với bitmap. Tìm BitmapLab_ trong dấu vết để tìm: * BitmapLab_parcelUnparcel: Các lát cắt bao gồm logic phân tích cú pháp và giải phân tích cú pháp. * BitmapLab_postNotification: Các lát cắt bao gồm quy trình đăng thông báo.

Theo dõi quy trình thông báo

Khi bạn nhấn vào Đăng thông báo, ứng dụng sẽ tạo một thông báo chứa bitmap hiện tại và gửi thông báo đó đến hệ thống. Mã khung chịu trách nhiệm cho việc này sẽ phát ra các lát Perfetto có sự kiện luồng kết nối việc phân tích cú pháp (ghi bitmap vào một Parcel để gửi qua Binder IPC) và việc huỷ phân tích cú pháp (đọc bitmap từ một Parcel ở đầu nhận).

Trong ảnh chụp màn hình bên dưới, bạn có thể thấy ứng dụng phân chia bitmap lớn sẽ được dùng trong một giao dịch Binder để đăng thông báo và quá trình phân chia tương ứng trong quy trình system_server.

Perfetto cho thấy một luồng từ BitmapLab đến system_server thông qua Thông báo

Khi dùng Perfetto, bạn thậm chí có thể theo dõi cùng một bitmap thông báo khi bitmap này tiếp tục truyền qua các luồng và quy trình, chẳng hạn như từ một luồng trình liên kết trong system_server (triển khai máy chủ trình liên kết INotificationManager) đến các luồng worker system_server. Sau đó, các luồng này có thể chuyển tiếp cùng một bitmap đến com.android.systemui để hiển thị trong ngăn thông báo.

Thách thức đối với ứng dụng hệ thống

Các ứng dụng hệ thống như SystemUI (Thông báo) và Trình chạy phải đối mặt với những thách thức riêng:

  1. Nội dung không giới hạn: Có thể có nhiều Dịch vụ thông báo và Tiện ích. Nếu mỗi thành phần chứa một bitmap lớn, hệ thống có thể nhanh chóng hết bộ nhớ.
  2. Sao chép: Cùng một biểu tượng ứng dụng có thể được lưu trong bộ nhớ đệm của Trình chạy, vùng thông báo của SystemUI và ứng dụng Cài đặt.
  3. Chia sẻ qua vùng đệm phần cứng: Để giảm thiểu vấn đề này, các thành phần hệ thống đang chuyển sang một dịch vụ "tải hình ảnh xuống" tập trung, chia sẻ các thực thể HardwareBuffer trên các quy trình.
  4. Phân bổ DMABuf: Bitmap phần cứng giúp tiết kiệm không gian heap nhưng sử dụng bộ nhớ DMABuf. Việc phân bổ bộ nhớ này cho một quy trình cụ thể trong các công cụ bộ nhớ tiêu chuẩn sẽ khó khăn hơn.

    Dùng adb shell dmabuf_dump để xem các hoạt động phân bổ DMABuf trên toàn hệ thống. Công cụ này cung cấp thông tin chi tiết về các vùng đệm theo từng quy trình:

     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: Tổng kích thước của vùng đệm nếu vùng đệm được ánh xạ trong quy trình.
    • Pss: Kích thước theo tỷ lệ (RSS chia cho số lượng quy trình dùng chung bộ nhớ đệm). Đây là chỉ số phù hợp nhất cho việc kế toán.
    • nr_procs: Số lượng quy trình hiện đang giữ một tham chiếu đến vùng đệm này.
    • Trình xuất: Trình điều khiển đã phân bổ vùng đệm (ví dụ: virtio_gpu trên Cuttlefish hoặc một heap Ion/DMA-BUF dành riêng cho nhà cung cấp trên phần cứng).

    Bạn cũng có thể dùng adb shell dmabuf_dump -b để xem thông tin tóm tắt về tất cả các vùng đệm và tổng mức sử dụng DMA-BUF trên toàn hệ thống.


← Java | ↑ Lên | Gốc →