首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫

Android Bitmap OOM 解決方案:LruCache + inSampleSize + 可見區域預載/回收機制

     編輯:MySQL 資料庫相關知識

問題根因

Android 應用預設記憶體上限(Dalvik 16~24MB,ART 192~512MB),而高解析度圖片解碼後佔用 寬 × 高 × 4 bytes(ARGB_8888)。
例:3000×4000 像素 ≈ 48MB,單張即超限。
Gallery、ViewPager、RecyclerView 大量圖片滾動時,若不回收,必觸發 OutOfMemoryError。

核心三板斧

技術作用關鍵 API
採樣縮放解碼前縮小,依螢幕尺寸載入足夠解析度BitmapFactory.Options.inSampleSize
記憶體快取LRU 策略保留近期/可見圖片,超限自動淘汰LruCache<Integer, Bitmap> (API 12+)
可見區域管理只快取當前可見 + 預載鄰近項目,其餘立即回收RecyclerView.OnScrollListener / Gallery.OnItemSelectedListener

完整實作範例

1. Bitmap 解碼工具類

public class BitmapDecoder {
    private final Context context;

    public BitmapDecoder(Context context) { this.context = context; }

    public Bitmap getPhotoItem(String path, int targetWidth, int targetHeight) {
        // 1. 只解碼邊界取得原始尺寸
        BitmapFactory.Options bounds = new BitmapFactory.Options();
        bounds.inJustDecodeBounds = true;
        BitmapFactory.decodeFile(path, bounds);

        // 2. 計算 inSampleSize (2 的冪次)
        int sampleSize = 1;
        if (bounds.outHeight > targetHeight || bounds.outWidth > targetWidth) {
            sampleSize = Math.max(
                bounds.outHeight / targetHeight,
                bounds.outWidth / targetWidth
            );
            // 取最接近的 2 的冪次
            sampleSize = Integer.highestOneBit(sampleSize);
        }

        // 3. 實際解碼
        BitmapFactory.Options opts = new BitmapFactory.Options();
        opts.inSampleSize = sampleSize;
        opts.inPreferredConfig = Bitmap.Config.RGB_565; // 節省 50% 記憶體
        Bitmap bitmap = BitmapFactory.decodeFile(path, opts);

        // 4. 精確縮放至目標尺寸(避免 View 再縮放)
        if (bitmap != null && (bitmap.getWidth() != targetWidth || bitmap.getHeight() != targetHeight)) {
            Bitmap scaled = Bitmap.createScaledBitmap(bitmap, targetWidth, targetHeight, true);
            if (scaled != bitmap) bitmap.recycle();
            bitmap = scaled;
        }
        return bitmap;
    }
}

2. LruCache 快取管理器

public class BitmapLruCache extends LruCache {
    public BitmapLruCache(int maxSizeKb) {
        super(maxSizeKb);
    }

    @Override
    protected int sizeOf(Integer key, Bitmap value) {
        return value.getByteCount() / 1024; // KB
    }

    @Override
    protected void entryRemoved(boolean evicted, Integer key, Bitmap oldValue, Bitmap newValue) {
        if (oldValue != null && !oldValue.isRecycled()) {
            oldValue.recycle(); // 關鍵:釋放 native 記憶體
        }
    }
}

初始化:int cacheSize = (int) (Runtime.getRuntime().maxMemory() / 1024 / 8); // 1/8 記憶體

3. Adapter 整合(以 RecyclerView 為例)

public class PhotoAdapter extends RecyclerView.Adapter {
    private final BitmapDecoder decoder;
    private final BitmapLruCache cache;
    private final List paths;
    private final int itemWidth, itemHeight;

    @Override
    public void onBindViewHolder(VH holder, int position) {
        String path = paths.get(position);
        Bitmap bitmap = cache.get(position);
        if (bitmap != null) {
            holder.image.setImageBitmap(bitmap);
        } else {
            // 背景執行緒解碼(RxJava/Coroutines/Executor)
            executor.execute(() -> {
                Bitmap bmp = decoder.getPhotoItem(path, itemWidth, itemHeight);
                if (bmp != null) {
                    cache.put(position, bmp);
                    handler.post(() -> {
                        if (holder.getAdapterPosition() == position) // 防止位置錯位
                            holder.image.setImageBitmap(bmp);
                    });
                }
            });
        }
    }

    // 預載鄰近項目
    public void preloadAround(int centerPos, int radius) {
        for (int i = centerPos - radius; i <= centerPos + radius; i++) {
            if (i >= 0 && i < paths.size() && cache.get(i) == null) {
                executor.execute(() -> {
                    Bitmap bmp = decoder.getPhotoItem(paths.get(i), itemWidth, itemHeight);
                    if (bmp != null) cache.put(i, bmp);
                });
            }
        }
    }

    // 回收不可見項目
    public void recycleOutOfRange(int firstVisible, int lastVisible, int radius) {
        int start = firstVisible - radius;
        int end = lastVisible + radius;
        for (int i = 0; i < start; i++) removeAndRecycle(i);
        for (int i = end + 1; i < paths.size(); i++) removeAndRecycle(i);
    }

    private void removeAndRecycle(int pos) {
        Bitmap bmp = cache.get(pos);
        if (bmp != null) {
            cache.remove(pos);
            if (!bmp.isRecycled()) bmp.recycle();
        }
    }
}

4. 滾動監聽觸發預載/回收

recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
    @Override
    public void onScrolled(RecyclerView rv, int dx, int dy) {
        LinearLayoutManager lm = (LinearLayoutManager) rv.getLayoutManager();
        int first = lm.findFirstVisibleItemPosition();
        int last = lm.findLastVisibleItemPosition();
        adapter.preloadAround(first, 3);
        adapter.preloadAround(last, 3);
        adapter.recycleOutOfRange(first, last, 3);
    }
});

進階優化清單

  1. Glide / Picasso / Coil: 生產環境直接用成熟圖片庫,內建生命週期、快取、請求合併、預載、轉換管線。
  2. inBitmap (API 19+):重用現有 Bitmap 記憶體,減少 GC 抖動。
  3. Bitmap.Config.HARDWARE (API 28+):將 Bitmap 放入圖形記憶體,不佔用 Dalvik/ART 堆,適合靜態大圖。
  4. 避免在 UI 執行緒 decode: 必須用背景執行緒,否則造成卡頓(Jank)。
  5. LeakCanary 檢測 Bitmap 洩漏: 未 recycle() 或被靜態變數持有。

總結

Android Bitmap OOM 核心解法:inSampleSize 縮放解碼 + LruCache 記憶體快取 + 可見區域預載/回收。手寫版適合學習原理;實際專案請直接接入 Glide/Coil,專注業務邏輯,讓圖片庫處理邊緣案例與版本相容性。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved