首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

Android 圖片異步加載框架設計全解析:三級緩存、線程池與復用檢測

     編輯:Java 相關知識

前言

在 Android ListView/RecyclerView 中加載網絡圖片是高頻剛需。核心挑戰:**列表快速滑動時,Item 視圖復用導致圖片錯位、OOM、閃爍**。業界標準解法:**三級緩存 (內存/文件/網絡) + 線程池限流 + 視圖復用檢測**。本文深度解析一個經典自研圖片加載器的核心架構與關鍵代碼,助你理解 Glide/Picasso 等成熟庫的設計精髓。

整體架構流程

  1. 內存緩存 (LruCache/SoftReference Map) → 命中即返回,極快。
  2. 文件緩存 (DiskLruCache/自定義目錄) → 命中則回填內存緩存並返回。
  3. 網絡下載 (HttpURLConnection/OkHttp) → 下載、寫文件緩存、回填內存緩存、主線程更新 UI。
  4. 全程守護:線程池控制併發、Handler 切主線程、Tag 機制防錯位。

核心組件實現

1. 文件緩存 (FileCache)

public class FileCache {
    private File cacheDir;
    
    public FileCache(Context context) {
        // 優先用外部存儲 (SD卡),否則用內部緩存目錄
        if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) {
            cacheDir = new File(Environment.getExternalStorageDirectory(), "your_app_cache");
        } else {
            cacheDir = context.getCacheDir();
        }
        if (!cacheDir.exists()) cacheDir.mkdirs();
    }
    
    // 以 URL hashCode 為文件名,避免特殊字符/過長問題
    public File getFile(String url) {
        return new File(cacheDir, String.valueOf(url.hashCode()));
    }
    
    public void clear() {
        File[] files = cacheDir.listFiles();
        if (files != null) for (File f : files) f.delete();
    }
}

2. 內存緩存 (MemoryCache) — SoftReference + 同步 Map

public class MemoryCache {
    // LinkedHashMap 支持 LRU (accessOrder=true),Collections.synchronizedMap 線程安全
    private final Map> cache = 
        Collections.synchronizedMap(new LinkedHashMap<>(10, 1.5f, true));
    
    public Bitmap get(String key) {
        SoftReference ref = cache.get(key);
        return ref != null ? ref.get() : null; // SoftReference 內存緊張時可被 GC 回收
    }
    
    public void put(String key, Bitmap bitmap) {
        cache.put(key, new SoftReference<>(bitmap));
    }
    
    public void clear() { cache.clear(); }
}

為何用 SoftReference?:內存不足時 GC 可回收圖片對象,**防止 OOM**,比強引用 LruCache 更「良民」,但命中率略低。生產庫 (Glide) 常用 LruResourceCache (強引用 + 淘汰算法) 精確控制內存上限。

3. 核心加載器 (ImageLoader)

public class ImageLoader {
    private static final int THREAD_NUM = 5;          // 線程池大小,根據 CPU 核心數調整
    private static final int TIME_OUT = 30_000;       // 網絡超時
    private static final int DEFAULT_RES = R.drawable.placeholder; // 佔位圖
    
    private final MemoryCache memoryCache = new MemoryCache();
    private final FileCache fileCache;
    private final ExecutorService executor = Executors.newFixedThreadPool(THREAD_NUM);
    private final Handler handler = new Handler(Looper.getMainLooper()); // 綁定主線程
    
    // 防復用映射:ImageView -> 當前綁定的 URL
    private final Map imageViews = 
        Collections.synchronizedMap(new WeakHashMap<>());
    
    public ImageLoader(Context context) {
        fileCache = new FileCache(context);
    }
    
    // 入口方法
    public void displayImage(String url, ImageView imageView) {
        imageViews.put(imageView, url); // 記錄綁定關係
        
        Bitmap bitmap = memoryCache.get(url);
        if (bitmap != null) {
            imageView.setImageBitmap(bitmap); // 內存命中,直接設置
            return;
        }
        // 未命中,提交異步任務
        queuePhoto(url, imageView);
    }
    
    private void queuePhoto(String url, ImageView imageView) {
        executor.submit(new PhotoLoader(url, imageView));
    }
    
    // 異步加載任務
    private class PhotoLoader implements Runnable {
        private final String url;
        private final ImageView imageView;
        
        PhotoLoader(String url, ImageView imageView) {
            this.url = url; this.imageView = imageView;
        }
        
        @Override
        public void run() {
            // 1. 任務開始前檢測:ImageView 是否已綁定新 URL (列表滑動復用)
            if (isViewReused()) return;
            
            // 2. 獲取 Bitmap (文件緩存 -> 網絡)
            Bitmap bitmap = getBitmap(url);
            
            // 3. 下載完成後再次檢測防復用
            if (isViewReused()) return;
            
            // 4. 更新內存緩存
            if (bitmap != null) memoryCache.put(url, bitmap);
            
            // 5. 切主線程更新 UI
            handler.post(new BitmapDisplayer(bitmap, imageView, url));
        }
        
        private boolean isViewReused() {
            String tag = imageViews.get(imageView);
            return tag == null || !tag.equals(url);
        }
        
        // 讀取文件或下載
        private Bitmap getBitmap(String url) {
            File f = fileCache.getFile(url);
            Bitmap bmp = decodeFile(f); // 嘗試讀文件緩存
            if (bmp != null) return bmp;
            
            // 網絡下載
            try {
                URL imageUrl = new URL(url);
                HttpURLConnection conn = (HttpURLConnection) imageUrl.openConnection();
                conn.setConnectTimeout(TIME_OUT);
                conn.setReadTimeout(TIME_OUT);
                conn.setInstanceFollowRedirects(true);
                
                try (InputStream is = conn.getInputStream();
                     OutputStream os = new FileOutputStream(f)) {
                    copyStream(is, os); // 流拷貝存文件
                }
                conn.disconnect();
                return decodeFile(f); // 解碼文件
            } catch (Throwable ex) {
                if (ex instanceof OutOfMemoryError) clearCaches();
                return null;
            }
        }
        
        private Bitmap decodeFile(File f) {
            try (FileInputStream fis = new FileInputStream(f)) {
                return BitmapFactory.decodeStream(fis);
                // 生產環境必加:inSampleSize 壓縮大圖、inBitmap 復用內存
            } catch (FileNotFoundException e) { return null; }
        }
        
        private void copyStream(InputStream is, OutputStream os) throws IOException {
            byte[] buf = new byte[1024];
            int len;
            while ((len = is.read(buf)) != -1) os.write(buf, 0, len);
        }
    }
    
    // 主線程顯示任務
    private class BitmapDisplayer implements Runnable {
        private final Bitmap bitmap;
        private final ImageView imageView;
        private final String url;
        
        BitmapDisplayer(Bitmap bitmap, ImageView imageView, String url) {
            this.bitmap = bitmap; this.imageView = imageView; this.url = url;
        }
        
        @Override
        public void run() {
            // 最後一道防線:設置前再次驗證綁定關係
            String tag = imageViews.get(imageView);
            if (tag == null || !tag.equals(url)) return;
            
            if (bitmap != null) imageView.setImageBitmap(bitmap);
            else imageView.setImageResource(DEFAULT_RES);
        }
    }
    
    public void clearCaches() {
        memoryCache.clear();
        fileCache.clear();
    }
}

關鍵設計模式深度解析

1. 三級緩存分層策略

層級存儲介質讀取速度容量生命週期作用
內存RAM (SoftRef Map)極快 (ns)小 (MB 級)進程存活高頻熱點圖片秒現
文件ROM (SD卡/內部存儲)快 (ms)大 (GB 級)持久/手動清理離線可用、節省流量、二次啟動快
網絡遠程服務器慢 (不可控)無限服務器決定源頭數據、首次加載、緩存失效回源

2. 線程池併發控制

  • Executors.newFixedThreadPool(5):固定 5 線程,**防止滑動時無限創建線程導致 OOM/CPU 飆升**。
  • 核心數建議:Runtime.getRuntime().availableProcessors() * 2 或固定 3-5 (IO 密集型)。
  • 生產庫使用自定義 ThreadPoolExecutor + 優先級隊列 (可見圖優先加載)。

3. 視圖復用檢測 (Tag 機制) — 防錯位核心

問題:ListView 滑動時,Item View 被回收復用。任務 A 為 View1 加載 url1,任務未完成時 View1 被復用顯示 url2。任務 A 完成後若直接 setImageBitmap,View1 會錯誤顯示 url1 圖片 (閃爍/錯位)。

解法:

  1. 提交任務時:imageViews.put(imageView, url) 記錄「View 當前綁定 URL」。
  2. 任務關鍵節點 (開始/下載後/主線程前) 檢查:imageViews.get(imageView).equals(url)。
  3. 不一致 → **立即放棄任務**,不更新 UI。

此機制配合 WeakHashMap (View 被 GC 時自動移除條目),完美解決復用錯位。

4. Handler 切換主線程

  • new Handler(Looper.getMainLooper()) 確保 UI 更新在主線程。
  • 若在子線程直接操作 View → CalledFromWrongThreadException。

生產級升級建議 (別造輪子,懂原理即可)

  • Glide:Google 推薦、生命周期感知、自動縮放、GIF 支持、模組化組件化。
  • Picasso:Square 出品、API 極簡、自動處理復用/取消、指示器調試。
  • Coil:Kotlin 協程優先、輕量、現代 API。
  • Fresco:Facebook、共享內存 (Ashmem)、WebP/動圖強、低端機優勢。

總結

圖片異步加載器的核心邏輯可濃縮為:**「三級緩存分層 + 固定線程池限流 + Tag 防復用 + Handler 主線程更新」**。理解這套機制,不僅能應對面試,更能在定制化需求 (如預加載、圖片處理管道、進度監聽) 時靈活擴展,或在排查 Glide 疑難雜症時直擊本質。**造輪子是為了懂輪子,上線請用成熟庫。**

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