1. 程式人生 > >Android經典面試問題:請你設計一套圖片非同步載入快取方案——圖片的三級快取

Android經典面試問題:請你設計一套圖片非同步載入快取方案——圖片的三級快取

友情提示:文章最後附有專案原始碼

現在,Android有很多優秀的圖片載入框架。例如:Picasso,Glide,Fresco。我們幾乎只要簡單呼叫幾句程式碼就可以很好的實現圖片的載入。很多時候也不需要我們親自去寫圖片載入方案。但是,學習圖片的三級快取策略無論是在面試時,還是對於App的其他快取框架設計都是很有必要的一件事。

今天就從頭開始設計一套圖片非同步載入快取方案。本方案用到以下技術,想了解更細緻的內容可以去以下連結檢視,在此不再贅述。

在程式碼結構的設計上參考了《Android原始碼設計模式解析與實戰》

1、何為三級快取

    所謂三級快取,指的是:記憶體快取,本地快取(或者叫檔案快取),網路快取(我個人認為把網路算在快取裡其實是不太合適的)。

    (1)記憶體快取:只有當APP執行時才會涉及到。記憶體雖然有容量限制,但是從記憶體讀取資訊是速度最快的。

    (2)本地快取:資訊以檔案的形式儲存在本地。只要不清除這些檔案,那麼資訊就一直持久化的儲存著。需要時可以通過流的方式進行讀取。本地容量大,速度次於記憶體。

    (3)網路:資訊儲存在遠端Server。通過網路獲取資訊。完全依賴網路情況,速度相對上面兩者來說要慢。

2、為什麼要用三級快取

    (1)為使用者節省流量,對相同資源減少多次重複的網路請求。

    (2)部分業務需要。例如有些業務需要在使用者斷網時也可以進行一些瀏覽或操作。

    (3)各快取讀取速度不相同,結合使用提高效率。

3、圖片非同步載入快取方案的工作流程

    

4、技術選型

    如開頭所提到的幾個技術點。這裡,記憶體快取我們選用LruCache實現。本地快取選用DiskLruCache實現。網路我們通過Retrofit進行圖片檔案的下載。當然,實現方式有很多種,可根據需要自己選擇。

5、方案實現

    (1)定義快取介面

    首先我們可以確認,無論是記憶體快取,本地快取,還是兩者的結合。都需要獲取圖片的方法和插入圖片的方法。因此我們直接定義一個快取介面,面向介面編寫快取的程式碼。

    介面如下:

public interface ImageCache {

    Bitmap getBitmap(String url);

    void putBitmap(String url, Bitmap bitmap);

}

    (2)實現記憶體快取

    記憶體快取的實現很簡單,把LruCache當成一個Map來用就好了的。程式碼如下:

public class MemoryCache implements ImageCache {

    private LruCache<String, Bitmap> mLruCache;
    private static final int MAX_LRU_CACHE_SIZE = (int) (Runtime.getRuntime().maxMemory() / 8);

    public MemoryCache() {
        //初始化LruCache
        initLruCache();
    }

    private void initLruCache() {
        mLruCache = new LruCache<String, Bitmap>(MAX_LRU_CACHE_SIZE) {
            @Override
            protected int sizeOf(String key, Bitmap bitmap) {
                return bitmap.getRowBytes() * bitmap.getHeight();
            }
        };
    }

    @Override
    public Bitmap getBitmap(String url) {
        return mLruCache.get(url);
    }

    @Override
    public void putBitmap(String url, Bitmap bitmap) {
        mLruCache.put(url, bitmap);
    }

}

   (3)實現本地快取

    DiskLruCache是Google自己寫的一個類,用來做本地快取方案十分方便。這個類的具體用法可以參看開頭的相關文章連結。

    程式碼如下:

public class DiskCache implements ImageCache {

    private DiskLruCache mDiskLruCache;
    private static final String DISK_LRU_CACHE_UNIQUE = "Image";
    private static final int MAX_DISK_LRU_CACHE_SIZE = 10 * 1024 * 1024;

    ExecutorService mExecutorsService = Executors.newFixedThreadPool(
            Runtime.getRuntime().availableProcessors()
    );

    public DiskCache(Context context) {
        //初始化DiskLruCache
        initDiskLruCache(context);
    }

    private void initDiskLruCache(Context context) {
        try {
            File cacheDir = getDiskCacheDir(
                    context,
                    DISK_LRU_CACHE_UNIQUE
            );
            if (!cacheDir.exists()) {
                cacheDir.mkdirs();
            }
            mDiskLruCache = DiskLruCache.open(
                    cacheDir,
                    getAppVersion(context),
                    1,
                    MAX_DISK_LRU_CACHE_SIZE
            );
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private File getDiskCacheDir(Context context, String uniqueName) {
        String cachePath;
        if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())
                || !Environment.isExternalStorageRemovable()) {
            cachePath = context.getExternalCacheDir().getPath();
        } else {
            cachePath = context.getCacheDir().getPath();
        }
        return new File(cachePath + File.separator + uniqueName);
    }

    private int getAppVersion(Context context) {
        try {
            PackageInfo info = context.getPackageManager().getPackageInfo(
                    context.getPackageName(),
                    0
            );
            return info.versionCode;
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        }
        return 1;
    }

    @Override
    public Bitmap getBitmap(String url) {
        String bitmapUrlMD5 = Md5Util.getMD5String(url);
        Bitmap bitmap = null;
        DiskLruCache.Snapshot snapshot = null;
        try {
            snapshot = mDiskLruCache.get(bitmapUrlMD5);
        } catch (IOException e) {
            e.printStackTrace();
        }
        if (snapshot != null) {
            InputStream inputStream = snapshot.getInputStream(0);
            bitmap = BitmapFactory.decodeStream(inputStream);
        }
        return bitmap;
    }

    @Override
    public void putBitmap(String url, final Bitmap bitmap) {
        final String bitmapUrlMD5 = Md5Util.getMD5String(url);
        mExecutorsService.submit(
                new Runnable() {
                    @Override
                    public void run() {
                        writeFileToDisk(mDiskLruCache, bitmap, bitmapUrlMD5);
                    }
                }
        );
    }

    private static void writeFileToDisk(
            DiskLruCache diskLruCache,
            Bitmap bitmap,
            String bitmapUrlMD5
    ) {
        DiskLruCache.Editor editor = null;
        OutputStream outputStream = null;
        try {
            editor = diskLruCache.edit(bitmapUrlMD5);
            if (editor != null) {
                outputStream = editor.newOutputStream(0);
                if (bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream)) {
                    editor.commit();
                }
            }
        } catch (Exception e) {
            try {
                if (editor != null) {
                    editor.abort();
                }
            } catch (Exception e1) {
                e1.printStackTrace();
            }
            e.printStackTrace();
        } finally {
            try {
                diskLruCache.flush();
            } catch (Exception e) {

            }
        }
    }

}

    可以看到本地快取的時候對url做了一次MD5加密。這是為了從安全考慮。畢竟直接把url暴露在檔案上實在不太雅觀。

    (4)完成記憶體快取加本地快取的雙快取邏輯實現

    這一塊很簡單。參看之前的三級快取工作流程圖。

    對於圖片的獲取:先從記憶體快取獲取圖片。如果不為空直接返回。如果為空,再從本地快取獲取圖片。

    對於圖片的儲存:就是往記憶體快取和本地快取分別新增圖片。

    程式碼如下:

public class MemoryAndDiskCache implements ImageCache {

    private MemoryCache mMemoryCache;
    private DiskCache mDiskCache;

    public MemoryAndDiskCache(Context context) {
        mMemoryCache = new MemoryCache();
        mDiskCache = new DiskCache(context);
    }

    @Override
    public Bitmap getBitmap(String url) {
        Bitmap bitmap = mMemoryCache.getBitmap(url);
        if (bitmap != null) {
            return bitmap;
        } else {
            bitmap = mDiskCache.getBitmap(url);
            return bitmap;
        }
    }

    @Override
    public void putBitmap(String url, Bitmap bitmap) {
        mMemoryCache.putBitmap(url, bitmap);
        mDiskCache.putBitmap(url, bitmap);
    }

}

    (5)實現ImageLoader類

    這個類中我們會在建構函式中傳入ImageCache的例項。那麼在獲取和儲存圖片時,只需要呼叫介面中定義的兩個方法即可,無需關注細節。實現細節完全交由建構函式中傳入的ImageCache例項。當要獲取圖片時,先呼叫ImageCache介面例項的getBitmap方法,如果為空。那麼需要我們從網路下載圖片。下載完成後我們只要呼叫ImageCache介面示例的putBitmap方法,即可完成整個圖片快取方案。

    程式碼如下:

public class ImageLoader {

    private ImageCache mImageCache;

    public ImageLoader(ImageCache imageCache) {
        mImageCache = imageCache;
    }

    public void displayImage(String url, ImageView imageView, int defaultImageRes) {
        imageView.setImageResource(defaultImageRes);
        imageView.setTag(url);

        Bitmap bitmap = mImageCache.getBitmap(url);
        if (bitmap != null) {
            imageView.setImageBitmap(bitmap);
        } else {
            downloadImage(imageView, url);
        }
    }

    private void downloadImage(final ImageView imageView, final String url) {
        Call<ResponseBody> resultCall = ServiceFactory.getServices().downloadImage(url);
        resultCall.enqueue(new Callback<ResponseBody>() {
            @Override
            public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
                if (response != null && response.body() != null) {
                    InputStream inputStream = response.body().byteStream();
                    Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
                    if (TextUtils.equals((String) imageView.getTag(), url)) {
                        imageView.setImageBitmap(bitmap);
                    }
                    mImageCache.putBitmap(url, bitmap);
                }
            }

            @Override
            public void onFailure(Call<ResponseBody> call, Throwable t) {
            }
        });
    }

}

    (6)實際使用

    這塊只需要new一個ImageLoader物件。並在建構函式中傳入你希望使用的快取策略。之後呼叫它的displayImage方法即可。

    程式碼如下:

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ImageView iv = (ImageView) findViewById(R.id.iv);
        String url = "圖片的url地址";
        ImageLoader imageLoader = new ImageLoader(
                new MemoryAndDiskCache(getApplicationContext())
        );
        imageLoader.displayImage(url, iv, R.mipmap.ic_launcher);
    }

}

6、演示(動圖較大,載入略慢,有興趣的同學請直接跳到7,去下載原始碼吧)

    好了,折騰這麼一通後我們來找個圖片試一下吧。

    首先看一下,在有網的時候,載入一張網路圖片:


    之後,我們殺掉程式,並且關閉網路。再將程式開啟,可以看到之前的圖片仍然能正常顯示:


7、原始碼下載(覺得這篇文章對你有幫助的同學們,歡迎Star一下!):

    原始碼下載