1. 程式人生 > >結合Bitmap三級快取自己做個ImageLoader 解決UI卡頓問題

結合Bitmap三級快取自己做個ImageLoader 解決UI卡頓問題

三級快取

前言

在Android開發中圖片下載和記憶體的使用是永遠繞不開的話題,頁面的載入離不開圖片的使用,圖片的使用必會佔用一定的記憶體,但是手機記憶體總是有限的,只要你一點使用不當,就會給APP造成非常差的使用體驗;所以怎麼合適的對圖片和記憶體進行一個合理的搭配就是一個重點了,圖片快取的實現能很好的解決這一個矛盾,現在圖片相關的開源框架很多,比如Glide,Fresco,Picasso等,它們都實現了很好的圖片快取策略,但是如果你自己實現怎麼做呢,今天就來實踐一下

本文所含程式碼隨時更新,可從這裡下載最新程式碼
傳送門

演示

在這裡插入圖片描述

三級快取

現在流行的一般是三級快取機制,即

  • 第一級:記憶體快取(從記憶體中載入圖片,速度最快,不浪費流量,但是會消耗手機執行記憶體)
  • 第二級:磁碟快取,或者說檔案快取(從本地載入圖片,速度快,不浪費流量,但是會消耗磁碟容量,不過可以忽略這點,畢竟現在磁碟容量都很大了)
  • 第三級:網路快取(從網路載入圖片,速度慢,浪費流量)

使用邏輯是:每次使用圖片時,先從記憶體快取中取出,如果有即使用,沒有就從磁碟快取中取出,如果有即使用並新增到記憶體快取中,沒有就從網路下載,下載完成後新增到磁碟快取和記憶體快取中

記憶體快取

LRU演算法

這裡使用Android自帶的LruCache,從這個名字可以看出使用的是LRU(Least Recently Used)演算法,即最近最少使用演算法;
它的核心是儲存最近新增最後使用的圖片,當你往裡繼續新增要快取的元素時,如果預先設定的快取容量滿了,那就剔除掉那些最久新增最少使用的快取元素

LruCache類支援泛型,內部維護了一個LinkedHashMap,可以接受多種key和value

public class LruCache<K, V> {
    private final LinkedHashMap<K, V> map;
}

這裡你會不會有疑慮,為什麼使用LinkedHashMap而不是使用HashMap

雖然LinkedHashMap和HashMap都是實現Map介面,同時LinkedHashMap還繼承自HashMap,但是它倆有個最大的區別:HashMap中新增的元素是無序存放的,但是LinkedHashMap內部維護了一個雙向連結串列,可以控制元素的迭代順序,該迭代順序可以是插入順序,也可以是訪問順序,相當於是將所有Entry節點鏈入一個雙向連結串列的HashMap

也正是因為這個特性,所以LinkedHashMap能很好的支援LRU演算法

輔助類

/**
 * @Description TODO(記憶體快取)
 * @author cxy
 * @Date 2018/11/13 9:48
 */
public class MemoryLruCache {

    private String TAG = MemoryLruCache.class.getSimpleName();

    private LruCache<String,Bitmap> mMemoryCache;

    private MemoryLruCache instance;
    private MemoryLruCache(){
        //虛擬機器能獲得的最大記憶體
        long maxMemory = Runtime.getRuntime().maxMemory();
        //記憶體快取所使用的記憶體
        int cache = (int) (maxMemory / 8);
        mMemoryCache = new LruCache<String,Bitmap>(cache){

            //重寫兩個方法

            //計算一張圖片所佔記憶體
            @Override
            protected int sizeOf(String key, Bitmap value) {

                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {    //API 19
                    int size = value.getAllocationByteCount();
                    Log.e(TAG,"sizeOf size="+size);
                    return size;
                }
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1) {//API 12
                    return value.getByteCount();
                }
                // 在低版本中使用 Bitmap所佔用的記憶體空間數等於Bitmap的每一行所佔用的空間數乘以Bitmap的行數
                return value.getRowBytes() * value.getHeight();
            }

            //回收記憶體
            @Override
            protected void entryRemoved(boolean evicted, String key, Bitmap oldValue, Bitmap newValue) {
                super.entryRemoved(evicted, key, oldValue, newValue);
                Log.e(TAG,"entryRemoved");
                if (oldValue != null && !oldValue.isRecycled()) {
                    oldValue.recycle();
                }
            }
        };

    }

    public MemoryLruCache getInstance(){
        if (instance == null) {
            instance = new MemoryLruCache();
        }
        return instance;
    }

    /**
     * 從記憶體中取出圖片
     * @param key 通常是圖片下載地址
     * @return
     */
    public Bitmap getBitmap(String key){
        if (TextUtils.isEmpty(key)) return null;
        return mMemoryCache.get(key);
    }

    /**
     * 將bitmap儲存到記憶體
     * @param key
     * @param bitmap
     */
    public void putBitmap(String key,Bitmap bitmap){
        mMemoryCache.put(key,bitmap);
    }

}

這裡有幾個注意點:

  • 這裡使用單例模式,這樣全域性都使用同一份快取
  • 在構建LruCache例項的時候需要指定快取大小,通常是JVM虛擬機器能獲得的最大記憶體的八分之一
  • 重寫sizeOf方法,返回一張圖片所佔記憶體值,為了適配不同版本,使用不同API
  • 重寫entryRemoved方法,因為這些圖片都是存在記憶體中,當儲存的圖片超過快取容量,就會從LruCache中的LinkedHashMap中去除掉,但是還佔用記憶體的,我們需要將它回收掉,釋放記憶體

這裡還有一個問題,需要圖片快取的通常是有列表這種頁面,每個item都含有圖片,沒有快取需要大量重複的網路請求;但是快取過後不再停留在這個頁面,使用者到別的頁面了,或者所在的Activity銷燬了,那這個快取就需要清除,但是LruCache沒有提供清空LinkedHashMap和回收其中的bitmap方法,且LinkedHashMap的定義又是private的,那這裡只能通過反射去清空快取了

	/**
     * 通過反射剔除快取中的bitmap
     * 回收bitmap記憶體
     * @param urls 需要清除的value對應的key 可以為null
     */
    public void cleanCache(String[] urls){

        try {
            Class classType = Class.forName("android.util.LruCache");

            Field field = classType.getDeclaredField("map");
            field.setAccessible(true);
            LinkedHashMap<String,Bitmap> map = (LinkedHashMap<String, Bitmap>) field.get(mMemoryCache);
            if (map == null) return;

            Iterator<Map.Entry<String,Bitmap>>  iterator = map.entrySet().iterator();
            while (iterator.hasNext()) {

                Map.Entry<String,Bitmap> entry = iterator.next();
                Bitmap bit = entry.getValue();

                if (urls != null && urls.length > 0) {
                    for (int i=0; i<urls.length; i++) {
                        if (TextUtils.equals(entry.getKey(),urls[i])) {
                            if (bit != null && !bit.isRecycled()) {
                                bit.recycle();
                            }
                            iterator.remove();
                            break;
                        }
                    }
                } else {
                    if (bit != null && !bit.isRecycled()) {
                        bit.recycle();
                    }
                    iterator.remove();
                }
            }

        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }

磁碟快取

這種快取通常情況下是將檔案儲存在SD卡上,不要儲存到內建儲存中,因為內建儲存記憶體空間太寶貴了,不要浪費;但是儲存到外接儲存(一般是SD卡)也不要隨便放到一個目錄,如果你允許第三方應用操作你的資料,那可以隨便找個目錄儲存;除此之外,通常將資料放在外接儲存的私有快取目錄,如:

/storage/sdcard0/Android/data/包名/cache
/storage/sdcard0/Android/data/包名/cache/imagecache

輔助類

/**
 * @Description TODO(磁碟快取)
 * @author cxy
 * @Date 2018/11/14 11:18
 */
public class DiskLruCache{

    private String TAG = DiskCache.class.getSimpleName();

    private Context mContext;
    private File cachePath;

    private static DiskCache instance;
    private DiskCache(Context context) {
        this.mContext = context;
        cachePath = FileStorageTools.getInstance(mContext).getExternalStoragePrivateCache();
    }
    public static DiskCache getInstance(Context context){
        if (instance == null) {
            instance = new DiskCache(context);
        }
        return instance;
    }

    /**
     * 設定私有快取目錄
     * @param pathName 次級目錄名稱 比如
     *                 /imagecache
     *                 /httpcache   
     */
    public void setCachePath(String pathName){
        if (TextUtils.isEmpty(pathName)) return ;
        cachePath = null;
        cachePath = new File(cachePath.getAbsolutePath()+pathName);
        cachePath.mkdirs();
    }

    public void putFileStream(String url, InputStream is){
        FileStorageTools.getInstance(mContext).putStreamToExternalStorage(cachePath,encryptUrl(url),is);
    }

    public void putBitmap(String url, Bitmap bitmap){
        FileStorageTools.getInstance(mContext).putBitmapToExternalStorage(cachePath,encryptUrl(url),bitmap);
    }

    public Bitmap getBitmap(String url,int inSamplesize){
        byte[] data = FileStorageTools.getInstance(mContext).getDataFromExternalStorage(cachePath.getAbsolutePath()+File.separator+encryptUrl(url));
        if (data == null) return null;
        return BitmapTools.byte2Bitmap(data,inSamplesize);
    }


    /**
     * 將url使用md5加密作為檔名
     * md5加密是不可逆加密,防止資源盜用
     * @param url
     * @return
     */
    private String encryptUrl(String url){

        if (TextUtils.isEmpty(url)) return null;

        String result = "";
        try {
            MessageDigest md = MessageDigest.getInstance("MD5");
            byte[] msg = md.digest(url.getBytes());
            for (byte b:msg) {
                String temp = Integer.toHexString(b & 0xff);
                if (temp.length() == 1) {
                    temp = "0" + temp;
                }
                result += temp;
            }
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
        return result;
    }
}

這裡面的FileStorageTools工具類可以在博主前面講解Android資料儲存文章中找到

這裡有的同學可能考慮到這裡只是存資料和取資料,那要不要刪資料呢;其實存放在外接儲存的私有目錄的資料在App解除安裝的時候會被刪除掉,而且現在手機SD卡記憶體容量都是64G,128G甚至更多,可以滿足快取容量需要;不過本著為使用者考慮,還是需要提供刪除的方法的

	/**
     * 清除指定目錄快取檔案
     * @param path 快取目錄
     *             如果是null,預設為cachePath.getAbsolutePath()
     */
    public void cleanAllCache(final String path){
        LocalThreadPools.THREAD_POOL_EXECUTOR.execute(new Runnable() {
            @Override
            public void run() {
                if (TextUtils.isEmpty(path)) {
                    FileStorageTools.getInstance(mContext).delFile(cachePath.getAbsolutePath());
                } else {
                    FileStorageTools.getInstance(mContext).delFile(path);
                }
                
            }
        });
    }

這是手動清除快取,或者說提供一個按鈕給使用者清除

檔案更新邏輯

有人可能會開始提需求了,上面做的只是存檔案,而且是一直存下去,那能不能對這些檔案進行一些動態管理操作,讓快取空間或者快取時間控制在一個合理的範圍呢?不用擔心,這是可以做到的

  • 按訪問順序刪除檔案:我這裡以時間進行排序,儲存檔案和修改檔案都會自動更新修改時間,但是我們需要做的是在獲取檔案的時候也去更新檔案的更新時間

    file.setLastModified(System.currentTimeMillis());
    

    這樣當快取空間達到一定值的時候,就可以刪除那些更早時間的檔案

  • 按時間刪除檔案:有時候你快取檔案是沒有達到指定快取大小,因為需要快取的檔案少呀;但是這時候可能出現的情況是:你的檔案是上個月的,甚至去年的,那這些可能就是垃圾檔案了,有必要刪除的

這時候我們就需要派LinkedHashMap上場了

	/**
     * 存放檔案路徑和時間資訊
     * 有兩種清除睡眠檔案方法:
     *                      可根據時間刪除檔案
     *                      也可根據訪問順序刪除檔案
     */
    private LinkedHashMap<String, Long> map;

	map = new LinkedHashMap<>(0, 0.75f, true);

  1. map的key是檔案路徑,因為需要唯一確定一個檔案
  2. value是檔案更新時間,後續可以通過時間刪除檔案
  3. 構造方法第三個引數傳入true,這樣map中的元素就會以訪問順序儲存,方便後續將那些訪問最少的檔案刪除掉

然後需要提前設定快取空間,快取時間

	//預設快取空間大小 100M
    private long FILE_CACHE_SIZE = 1024 * 1024 * 100;
    //檔案保留時間 預設儲存一個月內的快取檔案
    private int FARTHEST_TIME_FROM_NOW = 30 * 1;

接下來就是往map裡填充值了,這裡一定要注意先將檔案陣列按修改時間排序好,再往map裡存

private void getFileMsg(){

        List<File> files = FileStorageTools.getInstance(mContext).listFile(cachePath.getAbsolutePath());
        if (files == null) return;

        File[] fi = FileStorageTools.getInstance(mContext).sortFile(files,true);
        for (int i=0; i<fi.length; i++){
            File f = fi[i];
            map.put(f.getAbsolutePath(),f.lastModified());
        }
    }

接下來一定要注意在儲存檔案和獲取檔案的時候更新map裡面對應的值,然後就進入主題了

	/**
     * 修正快取目錄檔案
     * 超出預設快取大小 就刪除那些早期檔案
     * 早於檔案保留時間跨度 刪除
     */
    public void reviseCacheFile(){

        Calendar calendar = Calendar.getInstance();
        calendar.set(Calendar.DAY_OF_YEAR,FARTHEST_TIME_FROM_NOW);
        farthestDate = calendar.getTime().getTime();

        Iterator<Map.Entry<String, Long>> entrys = map.entrySet().iterator();
        while (entrys.hasNext()) {
            Map.Entry<String, Long> entry = entrys.next();
            String key = entry.getKey();
            long value = entry.getValue();

            /**
             * 先判斷快取空間是否超出預設值,這是最重要的
             * 因為map裡面的順序是按時間先後存放的,最先迭代出來的總是更新時間最久遠的
             */
            if (cachePath.length() > FILE_CACHE_SIZE) {
                File file = new File(key);
                file.delete();
                entrys.remove();
                continue;
            }

            /**
             * 再判斷檔案更新時間是否早於預設時間
             * 如果早於預設時間 刪除
             */
            if (value < farthestDate) {
                File file = new File(key);
                file.delete();
                entrys.remove();
                continue;
            }
        }

    }

這裡主要分為兩步

  • 先判斷快取目錄的大小有沒有超出預設值,這是最重要的,如果超出,哪個檔案時間最早就把它刪了
  • 接著判斷檔案時間是否早於預設的時間範圍,要是早於那就刪除

圖片非同步載入器

有了記憶體快取和檔案快取,那我們可以自己做一個簡單的圖片載入器
一般來說一個好的ImageLoader要做到

  • 圖片的同步載入
  • 圖片的非同步載入
  • 圖片壓縮處理
  • 圖片三級快取機制

那我們就按照這幾點來做

/**
 * @Description TODO(結合快取機制非同步載入圖片)
 * @author cxy
 * @Date 2018/11/14 11:18
 */
public class AsyncImageLoader {

    private String TAG = AsyncImageLoader.class.getSimpleName();

    private final int LOAD_IMAGE_BITMAP = 1000;
    private final int LOAD_IMAGE_ERROR = 2000;

    private WeakReference<Context> mContext;

    private MemoryLruCache mMemoryCache;
    private DiskLruCache mDiskCache;

    private int errorLoadId = -1;
    private int loadingId = -1;

    private static AsyncImageLoader imageLoader;
    private AsyncImageLoader(Context context) {
        this.mContext = new WeakReference<>(context);
        mMemoryCache = new MemoryLruCache();
        mDiskCache = new DiskLruCache(context);
    }

    public static AsyncImageLoader getInstance(Context context){
        if (imageLoader == null) {
            imageLoader = new AsyncImageLoader(context);
        }
        return imageLoader;
    }

    public AsyncImageLoader setErrorLoadView(int resourceID){
        errorLoadId = resourceID;
        return this;
    }

    public AsyncImageLoader setLoadingView(int loadingId){
        this.loadingId = loadingId;
        return this;
    }

    public AsyncImageLoader setMemoryCache(int cacheSize){
        mMemoryCache.setMemoryCache(cacheSize);
        return this;
    }

    public AsyncImageLoader setFarthestTime(int days){
        mDiskCache.setFarthestTime(days);
        return this;
    }

    public AsyncImageLoader setCacheSize(long size){
        mDiskCache.setCacheSize(size);
        return this;
    }

    public AsyncImageLoader setCachePath(String pathName){
        mDiskCache.setCachePath(pathName);
        return this;
    }

    private boolean loadMemoryBitmap(ImageView view, String imgUrl){

        if (loadingId != -1) {
            view.setBackgroundResource(loadingId);
        }

        Bitmap bitmap = mMemoryCache.getBitmap(imgUrl);
        if (bitmap != null) {
            view.setImageBitmap(bitmap);
            return true;
        }
        return false;
    }

    private boolean loadDiskBitmap(ImageView view, String imgUrl, int targetWidth, int targetHeight){
        Bitmap bitmap = mDiskCache.getBitmap(imgUrl, targetWidth,targetHeight);
        if (bitmap != null) {
            sendMessage(view,bitmap,imgUrl);
            mMemoryCache.putBitmap(imgUrl,bitmap);
            return true;
        }
        return false;
    }

    /**
     * 載入圖片
     * @param view
     * @param imageUrl
     * @param targetWidth
     * @param targetHeight
     */
    public void loadImage(final ImageView view, final String imageUrl, final int targetWidth, final int targetHeight) {

        //從記憶體快取獲取
        if (loadMemoryBitmap(view,imageUrl)) {
            return;
        }

        LocalThreadPools.THREAD_POOL_EXECUTOR.execute(new Runnable() {
            @Override
            public void run() {
                //從磁碟讀取
                if (loadDiskBitmap(view,imageUrl,targetWidth,targetHeight)) {
                    return ;
                }
                //從網路下載
                try {
                    URL url = new URL(imageUrl);
                    HttpURLConnection connection = (HttpURLConnection) url.openConnection();
                    connection.connect();
                    mDiskCache.putFileStream(imageUrl,connection.getInputStream());

                    loadDiskBitmap(view,imageUrl,targetWidth,targetHeight);
                } catch (MalformedURLException e) {
                    if (errorLoadId != -1) {
                        Message message = mHandler.obtainMessage();
                        message.what = LOAD_IMAGE_ERROR;
                        message.obj = view;
                        mHandler.sendMessage(message);
                    }
                    e.printStackTrace();
                } catch (IOException e) {
                    if (errorLoadId != -1) {
                        Message message = mHandler.obtainMessage();
                        message.what = LOAD_IMAGE_ERROR;
                        message.obj = view;
                        mHandler.sendMessage(message);
                    }
                    e.printStackTrace();
                }

            }
        });
    }

    private void sendMessage(ImageView view, Bitmap bitmap, String imageUrl){
        Message message = mHandler.obtainMessage();
        message.what = LOAD_IMAGE_BITMAP;
        message.obj = view;
        Bundle data = new Bundle();
        data.putParcelable("bitmap",bitmap);
        data.putString("url",imageUrl);
        message.setData(data);
        mHandler.sendMessage(message);
    }

    private Handler mHandler = new Handler(){

        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);

            int what = msg.what;
            switch (what) {
                case LOAD_IMAGE_BITMAP:
                    ImageView view = (ImageView) msg.obj;
                    Bundle bundle = msg.getData();
                    Bitmap bitmap = bundle.getParcelable("bitmap");
                    String imageUrl = bundle.getString("url");
                    if (bitmap == null || view.getTag() == null) return;
                    if (TextUtils.equals((String)view.getTag(),imageUrl)) {
                        view.setImageBitmap(bitmap);
                    }
                    break;
                case LOAD_IMAGE_ERROR:
                    ImageView v = (ImageView) msg.obj;
                    v.setBackgroundResource(errorLoadId);
                    break;
            }
        }
    };
    
    public void cleanCache(String[] urls){
        mMemoryCache.cleanCache(urls);
    }
}

載入圖片邏輯就是先從記憶體快取中取,如果沒有從磁碟快取中取,都沒有就從網路下載,然後再從磁碟載入,最後新增到記憶體快取中;這樣形成了一個簡單的圖片載入工具,當然了還有很多Bitmap的載入方法沒有寫,準備放到下一篇關於Bitmap的詳細解析文章中

可以看到我這裡使用的非同步載入是用執行緒池實現的(同時結合Handler達到更新UI的效果),為什麼呢?因為如果直接new Thread,資料量小還好,如果資料量大的話,那這樣大量new記憶體,手機是吃不消的;還有一個原因是如果使用AsyncTask,就沒辦法實現併發需求,顯然是不行的

在Adapter中就很方便使用了

final String imgUrl = list[position];
// 給 ImageView 設定一個 tag
holder.img.setTag(imgUrl);
// 預設一個圖片
holder.img.setImageResource(R.mipmap.ic_launcher);

if (!TextUtils.isEmpty(imgUrl)) {
	AsyncImageLoader.getInstance(context).loadImage(holder.img, imgUrl, 
											DisplayTools.dp2px(context,40),DisplayTools.dp2px(context,40));
}

優化UI卡頓

做到這裡其實基本上沒有卡頓情況了,不過還是有待優化的地方;一般情況下UI列表卡頓造成的原因包含兩個

  • 在主執行緒做了太多耗時操作,比如在adapter中直接在主執行緒載入圖片,就很容易導致卡頓,這個上面非同步載入解決了
  • 優化記憶體使用,其實這點上面還沒做的更好,因為上面沒有控制非同步任務的執行頻率;假如使用者惡意的快速滑動螢幕,在短時間內會造成大量的非同步任務在執行,且伴隨著大量的UI操作,但是UI操作是在主執行緒執行,這樣勢必會在一定情況下造成UI卡頓,解決辦法其實很簡單:在使用者滑動的時候停止非同步任務,在使用者停止滑動的時候再執行,這樣只會產生當前螢幕所包含的定量的非同步任務

不管是ListView還是RecyclerView等,都只用設定OnScrollListener監聽,然後起個標誌位判斷下即可


/**
 * Author:Mangoer
 * Time:2018/11/17 17:40
 * Version:
 * Desc:TODO()
 */
public class RecycleAdapter extends RecyclerView.Adapter<RecycleAdapter.ViewHolder> {

    private Context mContext;
    private String[] list;

    private boolean isShouldBeLoaded = true;

    public RecycleAdapter(Context mContext) {
        this.mContext = mContext;
    }

    public void setList(String[] list) {
        this.list = list;
    }

    public void setmRecyvlerView(RecyclerView mRecyvlerView) {
        mRecyvlerView.addOnScrollListener(scrollListener);
    }

    @Override
    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View content = LayoutInflater.from(mContext).inflate(R.layout.list_item,parent,false);
        ViewHolder viewHolder = new ViewHolder(content);
        return viewHolder;
    }

    @Override
    public void onBindViewHolder(ViewHolder holder, int position) {

        final String imgUrl = list[position];
        // 給 ImageView 設定一個 tag
        holder.view.setTag(imgUrl);
        // 預設一個圖片
        holder.view.setImageResource(R.mipmap.ic_launcher);
        if (!TextUtils.isEmpty(imgUrl) && isShouldBeLoaded) {
            AsyncImageLoader.getInstance(mContext).loadImage(holder.view, imgUrl, DisplayTools.dp2px(mContext,80),DisplayTools.dp2px(mContext,80));
        }
    }

    @Override
    public int getItemCount() {
        if (list == null) return 0;
        return list.length;
    }

    class ViewHolder extends RecyclerView.ViewHolder{

        ImageView view;

        public ViewHolder(View itemView) {
            super(itemView);
            view = (ImageView) itemView.findViewById(R.id.userimage);
        }
    }

    RecyclerView.OnScrollListener scrollListener = new RecyclerView.OnScrollListener() {
        @Override
        public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
            super.onScrollStateChanged(recyclerView, newState);
            if (SCROLL_STATE_IDLE == newState) {
                isShouldBeLoaded = true;
                notifyDataSetChanged();
            } else {
                isShouldBeLoaded = false;
            }
        }

        @Override
        public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
            super.onScrolled(recyclerView, dx, dy);
        }
    };
}

這樣我們就能省掉大量的非必要的非同步操作,節省記憶體開銷

到此關於圖片快取及載入告一段落,關於Bitmap的前世今生放在下一篇部落格