(1)LruCache原理分析

簡書博客文章遷移https://www.jianshu.com/u/43a04ef9d4c6


淺析LruCache原理

Android用LruCache(Least recently use Cache 意思就是最近使用次數最少的那個對象)來取代原來強引用和軟引用實現內存緩存,因爲據說自2.3以後Android將更頻繁的調用GC,導致軟引用緩存的數據極易被釋放。

LruCache使用一個LinkedHashMap簡單的實現內存的緩存,沒有軟引用,都是強引用。如果添加的數據大於設置的最大值,就刪除最先緩存的數據來調整內存。

他的主要原理在trimToSize方法中。

首先是LruCache聲明的變量


    private final LinkedHashMap<K, V> map;//LruCache關鍵的數據結構,用於存放數據

   /**劃重點,這倆個變量很關鍵*/
    private int size;//當前LruCache的內存佔用大小
    private int maxSize;//LruCache的最大容量(通過構造方法初始化的值,他表示這個緩存能緩存的最大值是多少。)

    private int putCount;//put的次數
    private int createCount;//create的次數
    private int evictionCount;//回收的次數
    private int hitCount;//命中的次數
    private int missCount;//丟失的次數

然後直接看關鍵方法trimToSize

public void trimToSize(int maxSize) {
        while (true) {
            K key;
            V value;
            synchronized (this) {
                if (size < 0 || (map.isEmpty() && size != 0)) {
                    throw new IllegalStateException(getClass().getName()
                            + ".sizeOf() is reporting inconsistent results!");
                }
                //1 判斷size是否超過maxSize
                if (size <= maxSize || map.isEmpty()) {
                    break;
                }
                //2 如果1處判斷爲不超過就取出最先插入的緩存
                Map.Entry<K, V> toEvict = map.entrySet().iterator().next();
                key = toEvict.getKey();
                value = toEvict.getValue();
                //刪除(這裏是雙向鏈表)
                map.remove(key);
                size -= safeSizeOf(key, value);//1 
                evictionCount++;
            }

            entryRemoved(true, key, value, null);
        }
    }

這個方法是一個無限循環,跳出循環的條件是,size < maxSize或者 map 爲空。主要的功能是判斷當前容量時候已經超出最大的容量,如果超出了maxSize的話,就會循環移除map中的第一個元素,直到達到跳出循環的條件。

而雙向鏈表LinkedHashMap(3個參數構造方法中accessOrder排序模式設置爲訪問模式時)中,每次get和put數據,則會將改對象移到鏈表的尾部,這樣子內存緩存達到最大值時,map中的第一個元素就是最近最少使用的那個元素。此時,把它刪除,從而達到避免OOM的出現。

註釋1處safeSizeOf中封裝了sizeOf方法,它是用來計算單個對象的大小,這裏默認返回1,一般需要重寫該方法來計算對象的大小,如果是計算bitmap的大小,這裏會重寫不返回1,而是返回bitmap的大小bitmap.getRowBytes() * bitmap.getHeight()


源碼分析

變量

在上面了

image

構造函數

public LruCache(int maxSize) {
    if (maxSize <= 0) {
        throw new IllegalArgumentException("maxSize <= 0");
    }
    this.maxSize = maxSize;
    this.map = new LinkedHashMap<K, V>(0, 0.75f, true);//1
}

這裏設置了maxSize,以及實例化了一個LinkedHashMap對象,這個LinkedHashMap對象是實現Lru算法的關鍵,註釋1處表示創建一個初始容量爲0,加載因子是0.75(容量達到75%的時候把空間增大1半),最後這個參數上面也說了,是accessOrder,意思是排序模式,這裏是true表示排序模式爲訪問模式(當map中數據有put和get時,當前操作的數據會移動到鏈表尾部)

put方法

    public final V put(K key, V value) {
         if (key == null || value == null) {
             throw new NullPointerException("key == null || value == null");
         }

         V previous;
         synchronized (this) {
             putCount++;//put的次數+1
             size += safeSizeOf(key, value);  //size加上預put對象(value)的大小
             previous = map.put(key, value);//previous爲舊的值
             if (previous != null) {
                 //如果之前存在鍵爲key的對象,則size應該減去原來對象的大小(把舊值大小刪掉)
                 size -= safeSizeOf(key, previous);
             }
         }

         if (previous != null) {
             entryRemoved(false, key, previous, value);//這個是空實現
         }
         //每次新加入對象都需要調用trimToSize方法看是否需要回收
         trimToSize(maxSize);
         return previous;
     }

看註釋應該明白了,先是增加size,然後判斷以前有沒有值,如果有就更新當前的額值,並且size要減去以前的值的大小

entryRemoved是一個空實現,如果我們使用LruCache的時候需要掌握元素移除的信息,可以重寫這個方法來獲取元素移除的信息。

get方法

    /**
    通過key獲取相應的item,或者創建返回相應的item。相應的item會移動到隊列的尾部,
    如果item的value沒有被cache或者不能被創建,則返回null。*/
    public final V get(K key) {
          if (key == null) {
              throw new NullPointerException("key == null");
          }

          V mapValue;
          synchronized (this) {
              mapValue = map.get(key);
              if (mapValue != null) {
                  //mapValue不爲空表示命中,hitCount+1並返回mapValue對象
                  hitCount++; //命中 + 1
                  return mapValue;
              }
              missCount++;  //未命中+1
          }

          /*
           * 如果未命中,則試圖創建一個對象,這裏create方法返回null,並沒有實現創建對象的方法
           *如果需要事項創建對象的方法可以重寫create方法。 假如在圖片緩存中,因爲圖片緩存時內存緩存沒有命中會去
           * 文件緩存中去取或者從網絡下載,所以並不需要創建。
           */
          V createdValue = create(key);//默認返回null(其實這裏就是創建一個空對象的意思)
          if (createdValue == null) {
              return null;
          }
          //假如創建了新的對象,則繼續往下執行
          synchronized (this) {
              createCount++;  //創建 + 1
              //將createdValue加入到map中,並且將原來鍵爲key的對象保存到mapValue
              mapValue = map.put(key, createdValue);   
              if (mapValue != null) {
                  // There was a conflict so undo that last put
                 //如果mapValue不爲空,則撤銷上一步的put操作。
                 map.put(key, mapValue);
             } else {
                 //加入新創建的對象之後需要重新計算size大小
                 size += safeSizeOf(key, createdValue);
             }
         }

         if (mapValue != null) {
             entryRemoved(false, key, createdValue, mapValue);
             return mapValue;
         } else {
             //每次新加入對象都需要調用trimToSize方法看是否需要回收
             trimToSize(maxSize);
             return createdValue;
         }
     }

remove方法

 public final V remove(K key) {
         if (key == null) {
             throw new NullPointerException("key == null");
         }

         V previous;
         synchronized (this) {
             previous = map.remove(key);
             if (previous != null) {
                 size -= safeSizeOf(key, previous);
             }
         }

         if (previous != null) {
             entryRemoved(false, key, previous, null);
         }

         return previous;
     }

從內存緩存中根據key值移除某個對象並返回該對象


實例

bb再多不如親手操作一次

LruCache的實例

這裏貼出ImageLoader類源代碼,項目源代碼PrivateTestProject

public class ImageLoader {

    private LruCache<String, Bitmap> mBitmapLruCache;
    private RecyclerView recyclerView;//傳過來的RecyclerView對象
    private Set<ImageLoaderTask> mTask = null;//存異步任務的set集合

    public ImageLoader(RecyclerView recyclerView) {
        this.recyclerView = recyclerView;
        mTask = new HashSet<>();
        int maxMemory = (int) Runtime.getRuntime().maxMemory();
        int cacheSize = maxMemory / 8;
        mBitmapLruCache = new LruCache<String, Bitmap>(cacheSize) {
            //重寫
            @Override
            protected int sizeOf(String key, Bitmap value) {
                //每次存入緩存即調用
                return value.getByteCount();
            }
        };
    }

    /**
     * 根據圖片url下載圖片
     *
     * @param imgUrl 圖片路徑
     * @return bitmap
     */
    public Bitmap getBitmapByImgUrl(String imgUrl) {
        Bitmap bitmap = null;
        HttpURLConnection httpURLConnection = null;
        try {
            URL mUrl = new URL(imgUrl);
            try {
                httpURLConnection = (HttpURLConnection) mUrl.openConnection();
                httpURLConnection.setConnectTimeout(10 * 1000);
                httpURLConnection.setReadTimeout(10 * 1000);
                bitmap = BitmapFactory.decodeStream(httpURLConnection.getInputStream());
            } catch (IOException e) {
                e.printStackTrace();
            }
        } catch (MalformedURLException e) {
            e.printStackTrace();
        } finally {
            if (httpURLConnection != null) {
                //斷開連接
                httpURLConnection.disconnect();
            }
        }
        return bitmap;
    }

    /**
     * 將bitmap加入cache
     */
    public void addBitmapToCache(String urlKey, Bitmap bitmap) {
        mBitmapLruCache.put(urlKey, bitmap);
    }

    /**
     * 從cache獲取bitmap
     */
    public Bitmap getBitmapfromCache(String urlKey) {
        return mBitmapLruCache.get(urlKey);
    }

    /**
     * 取消所有下載異步任務
     */
    public void cancelAllTask() {
        if (mTask != null) {
            for (ImageLoaderTask task :
                    mTask) {
                task.cancel(false);
            }
        }
    }

    /**
     * 按當前item的序號區間顯示圖片
     */
    public void showImages(int startIndex, int endIndex) {
        for (int i = startIndex; i < endIndex; i++) {
            String imageUrl = ImagCacheRAdapter.URLS[i];
            Bitmap bitmap = getBitmapfromCache(imageUrl);//從緩存中獲取

            if (bitmap == null) {
                //如果緩存爲空,則開啓異步線程
                ImageLoaderTask imageLoaderTask = new ImageLoaderTask(imageUrl);
                imageLoaderTask.execute();
                //加入HashSet中
                mTask.add(imageLoaderTask);
            } else {
                ImageView imageView = recyclerView.findViewWithTag(imageUrl);
                imageView.setImageBitmap(bitmap);
            }
        }
    }


    /**
     * 顯示圖片
     *
     * @param imageView
     * @param imageUrl
     */
    public void showImage(ImageView imageView, String imageUrl) {
        //從緩存中取圖片
        Bitmap bitmap = getBitmapfromCache(imageUrl);
        //如果緩存中沒有,則去下載
        if (bitmap == null) {
            imageView.setImageResource(R.mipmap.ic_launcher);
        } else {
            imageView.setImageBitmap(bitmap);
        }
    }
    private class ImageLoaderTask extends AsyncTask<Void, Void, Bitmap> {
        private String mImagerUrl;

        public ImageLoaderTask(String mImagerUrl) {
            this.mImagerUrl = mImagerUrl;
        }

        @Override
        protected Bitmap doInBackground(Void... voids) {
            //獲取圖片並且加入緩存
            Bitmap bitmap = getBitmapByImgUrl(mImagerUrl);
            if (bitmap != null) {
                addBitmapToCache(mImagerUrl, bitmap);
            }
            return bitmap;
        }

        @Override
        protected void onPostExecute(Bitmap bitmap) {
            super.onPostExecute(bitmap);
            ImageView imageView = recyclerView.findViewWithTag(mImagerUrl);
            if (null != imageView && null != bitmap) {
                imageView.setImageBitmap(bitmap);
            }
            //顯示成功後就把當前的AsyncTask從mTask中移除
            mTask.remove(this);
        }
    }
}
public class ImageLoader {

    private LruCache<String, Bitmap> mBitmapLruCache;
    private RecyclerView recyclerView;//傳過來的RecyclerView對象
    private Set<ImageLoaderTask> mTask = null;//存異步任務的set集合

    public ImageLoader(RecyclerView recyclerView) {
        this.recyclerView = recyclerView;
        mTask = new HashSet<>();
        int maxMemory = (int) Runtime.getRuntime().maxMemory();
        int cacheSize = maxMemory / 8;
        mBitmapLruCache = new LruCache<String, Bitmap>(cacheSize) {
            //重寫
            @Override
            protected int sizeOf(String key, Bitmap value) {
                //每次存入緩存即調用
                return value.getByteCount();
            }
        };
    }

    /**
     * 根據圖片url下載圖片
     *
     * @param imgUrl 圖片路徑
     * @return bitmap
     */
    public Bitmap getBitmapByImgUrl(String imgUrl) {
        Bitmap bitmap = null;
        HttpURLConnection httpURLConnection = null;
        try {
            URL mUrl = new URL(imgUrl);
            try {
                httpURLConnection = (HttpURLConnection) mUrl.openConnection();
                httpURLConnection.setConnectTimeout(10 * 1000);
                httpURLConnection.setReadTimeout(10 * 1000);
                bitmap = BitmapFactory.decodeStream(httpURLConnection.getInputStream());
            } catch (IOException e) {
                e.printStackTrace();
            }
        } catch (MalformedURLException e) {
            e.printStackTrace();
        } finally {
            if (httpURLConnection != null) {
                //斷開連接
                httpURLConnection.disconnect();
            }
        }
        return bitmap;
    }

    /**
     * 將bitmap加入cache
     */
    public void addBitmapToCache(String urlKey, Bitmap bitmap) {
        mBitmapLruCache.put(urlKey, bitmap);
    }

    /**
     * 從cache獲取bitmap
     */
    public Bitmap getBitmapfromCache(String urlKey) {
        return mBitmapLruCache.get(urlKey);
    }

    /**
     * 取消所有下載異步任務
     */
    public void cancelAllTask() {
        if (mTask != null) {
            for (ImageLoaderTask task :
                    mTask) {
                task.cancel(false);
            }
        }
    }

    /**
     * 按當前item的序號區間顯示圖片
     */
    public void showImages(int startIndex, int endIndex) {
        for (int i = startIndex; i < endIndex; i++) {
            String imageUrl = ImagCacheRAdapter.URLS[i];
            Bitmap bitmap = getBitmapfromCache(imageUrl);//從緩存中獲取

            if (bitmap == null) {
                //如果緩存爲空,則開啓異步線程
                ImageLoaderTask imageLoaderTask = new ImageLoaderTask(imageUrl);
                imageLoaderTask.execute();
                //加入HashSet中
                mTask.add(imageLoaderTask);
            } else {
                ImageView imageView = recyclerView.findViewWithTag(imageUrl);
                imageView.setImageBitmap(bitmap);
            }
        }
    }


    /**
     * 顯示圖片
     *
     * @param imageView
     * @param imageUrl
     */
    public void showImage(ImageView imageView, String imageUrl) {
        //從緩存中取圖片
        Bitmap bitmap = getBitmapfromCache(imageUrl);
        //如果緩存中沒有,則去下載
        if (bitmap == null) {
            imageView.setImageResource(R.mipmap.ic_launcher);
        } else {
            imageView.setImageBitmap(bitmap);
        }
    }
    private class ImageLoaderTask extends AsyncTask<Void, Void, Bitmap> {
        private String mImagerUrl;

        public ImageLoaderTask(String mImagerUrl) {
            this.mImagerUrl = mImagerUrl;
        }

        @Override
        protected Bitmap doInBackground(Void... voids) {
            //獲取圖片並且加入緩存
            Bitmap bitmap = getBitmapByImgUrl(mImagerUrl);
            if (bitmap != null) {
                addBitmapToCache(mImagerUrl, bitmap);
            }
            return bitmap;
        }

        @Override
        protected void onPostExecute(Bitmap bitmap) {
            super.onPostExecute(bitmap);
            ImageView imageView = recyclerView.findViewWithTag(mImagerUrl);
            if (null != imageView && null != bitmap) {
                imageView.setImageBitmap(bitmap);
            }
            //顯示成功後就把當前的AsyncTask從mTask中移除
            mTask.remove(this);
        }
    }
}

ImageLoaderTask這裏繼承自AsyncTask,是ImageLoader的內部類

很顯然,ImageLoader類最關鍵在LruCache對象mBitmapLruCache上,這裏用了Set集合來使異步加載線程ImageLoaderTask是唯一的

ImageLoader主要邏輯是從內存LruCache對象中加載bitmap數據,如果沒有,就網絡加載數據變爲Bitmap,然後存入LruCache對象mBitmapLruCache中。網絡加載完的時候在異步線程中ImageLoaderTask把bitmap數據顯示到ImageView

DiskLruCache實現硬盤緩存

Android照片牆完整版,完美結合LruCache和DiskLruCache

硬盤緩存通常用作三級緩存的第二層(本地持久化)

請參考 DiskLruCache基本用法

Android 緩存淺談(二) DiskLruCache

大體套路

首先初始化,創建緩存目錄以及線程池,然後加載圖片時,先從緩存中獲取(要在子線程中進行),如果緩存中有,則顯示圖片,如果沒有則去下載並加入到緩存中,然後從緩存中獲取,再顯示。

如果在列表中可能會同時加載多個圖片,如果只是一直創建線程,那麼對app的性能以及體驗都是考驗,建議使用線程池機制**


參考

淺析LruCache原理

LruCache 實現原理分析

內存緩存LruCache實現原理

Android 緩存淺談(一) LruCache

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章