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 |
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;
}
}
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 記憶體
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();
}
}
}
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);
}
});
inBitmap (API 19+):重用現有 Bitmap 記憶體,減少 GC 抖動。Bitmap.Config.HARDWARE (API 28+):將 Bitmap 放入圖形記憶體,不佔用 Dalvik/ART 堆,適合靜態大圖。recycle() 或被靜態變數持有。Android Bitmap OOM 核心解法:inSampleSize 縮放解碼 + LruCache 記憶體快取 + 可見區域預載/回收。手寫版適合學習原理;實際專案請直接接入 Glide/Coil,專注業務邏輯,讓圖片庫處理邊緣案例與版本相容性。