我的圖片四級快取框架

Android機動車發表於2018-01-30

開發App一定涉及到圖片載入、圖片處理,那就必須會用到三方的圖片框架,要麼選擇自己封裝。至於主流的三方圖片框架,就不得不說老牌的ImageLoader、如今更流行的Glide、Picasso和Fresco。但三方的框架本文不會過多介紹。

Glide等框架,畢竟是大神及團隊花費很大精力開發和維護的開源框架,他們的設計思路、效能優化、程式碼規範等等很值得我們學習,之前一段時間也研究過Glide的原始碼(不得不由衷佩服)。

今天,將自己對於圖片載入的思路想法,也借鑑了開源框架的一些好的點,封裝了一個圖片載入框架——JsLoader。(github地址:github.com/shuaijia/Js…)與大家分享。

文章目錄:

這裡寫圖片描述

前言

至於圖片的網路請求,我這裡還是使用Android原生提供的HttpUrlConnection;請求網路圖片時,開啟子執行緒進行操作,使用執行緒池對執行緒進行統一管理;執行緒間通訊還是用了Handler;提到圖片載入,大家肯定會立刻想到圖片的三級快取(記憶體—外存—網路),但我這裡提供一個新的思路——四級快取,與三級快取不同的是記憶體又分為了兩級,這些稍後會詳細介紹到。

本文目的在於和大家分享一個圖片框架的封裝思路,至於程式碼的優化,如使用OkHttp替換HttpUrlConnection,使用RxJava替換Handler等,或者有別的不足的地方,也希望大家能夠反饋給我,我們一起進步。

先看下整體流程圖:

這裡寫圖片描述

執行緒池

public class MyThreadFactory {

    //Android的執行緒池類
    private static ThreadPoolExecutor threadPoolExecutor=null;
    //獲取當前使用者的手機的CPU的核心數
    private static int num= Runtime.getRuntime().availableProcessors();
    //用於儲存提交任務的任務佇列
    private static BlockingDeque<Runnable> workQueue=new LinkedBlockingDeque<>(num*50);
    private MyThreadFactory(){
    }
    public static ThreadPoolExecutor getThreadPoolExecutor(){
        if(null==threadPoolExecutor){
            threadPoolExecutor=new ThreadPoolExecutor(num*2, num*4, 8, TimeUnit.SECONDS, workQueue, new ThreadPoolExecutor.CallerRunsPolicy());
//            threadPoolExecutor=new ThreadPoolExecutor(1, 1, 8, TimeUnit.SECONDS, workQueue, new ThreadPoolExecutor.CallerRunsPolicy());
        }
        return threadPoolExecutor;
    }

}
複製程式碼

當前類是一個執行緒池的管理類。由於當前的執行緒池,在整個專案中不需要建立多個物件,直接使用單例模式進行建立。

補充:Android中的執行緒池 在Android中使用執行緒池的類是:ThreadPoolExecutor;

ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(int corePoolSize, int maxinumPoolSize, long keepAliveTime, TimeUnit unit, BlockingDeque<Runnable> workQueue, ThreadFactory threadFactory);
複製程式碼

引數:

  • int corePoolSize : 執行緒池中的核心執行緒數
  • int maxinumPoolSize :執行緒池中允許的最大執行緒數目
  • long keepAliveTime :非核心執行緒的超時時間,超出這個時間非核心執行緒會被回收
  • TimeUnit unit :非核心執行緒的超時時間的時間單位
  • BlockingDeque workQueue : 儲存需要執行緒池執行的任務的列表
  • ThreadFactory threadFactory : 執行緒工廠,只是一個介面,只有一個方法Thread newThread(Runnable r)

在上文展示的類中,我們獲取了手機的CPU核心數num,本執行緒池的核心執行緒數為CPU數的2倍,最大執行緒數為CPU核心數的4倍。

記憶體一級快取

private static final HashMap<String,Bitmap> mHardBitmapCache=new LinkedHashMap<String,Bitmap>(
            M_LINK_SIZE/2,0.75f,true){

	/**
	 * 這個方法是是put或putAll時呼叫,預設返回false,表示新增資料時不移除最舊的資料.
	 * @param eldest
	 * @return
	 */
	@Override
	protected boolean removeEldestEntry(Entry<String, Bitmap> eldest) {
		if (size() > M_LINK_SIZE) {
			// 當map的size大於30時,把最近不常用的key放到mSoftBitmapCache中,從而保證mHardBitmapCache的效率
			Bitmap value = eldest.getValue();
			if (value != null) {
				mWeakBitmapCache.put(eldest.getKey(),new SoftReference<Bitmap>(value));
			}
			return true;
		}
		return false;
	}
};
複製程式碼

定義的記憶體中的一級快取,即儲存作為強引用的位置的HashMap。

此處HashMap使用的是LinkedHashMap。LinkedHashMap 是HashMap的一個子類,儲存了記錄的插入順序,在用Iterator遍歷LinkedHashMap時,先得到的記錄肯定是先插入的。也可以在構造時用帶引數,按照應用次數排序。在遍歷的時候會比HashMap慢,不過有種情況例外,當HashMap容量很大,實際資料較少時,遍歷起來可能會比 LinkedHashMap慢,因為LinkedHashMap的遍歷速度只和實際資料有關,和容量無關,而HashMap的遍歷速度和他的容量有關。

正是由於LinkedHashMap具有記憶功能,最近插入的最新訪問,就符合了我們的最近最多使用的原則。但由於其遍歷速度慢,我們對其容量進行設定,最多30和元素。

重寫removeEldestEntry方法,當map的size大於30時,把最近不常用的key放到mSoftBitmapCache中(也就是記憶體第二級快取),從而保證mHardBitmapCache的效率。

這裡我們在Map中是以Url和Bitmap為Key-Value儲存的,由於LinkedHashMap存放少,而且插入移出快,所以這裡用的是Bitmap的強引用。

如果LinkedHashMap中包含我們需要的圖片,則將圖片直接返回。但是注意:此時我們認為此圖使用頻率更高,因此我們需要先將該元素移出,在加入(這是由於該map後插入的遍歷時先讀取)。

mHardBitmapCache.remove(netUrlKey);
mHardBitmapCache.put(netUrlKey,usefulBitmap);
複製程式碼

此為記憶體的一級快取。

記憶體二級快取

如果記憶體的LinkedHashMap中未獲取到我們想要的圖片的話,在二級快取中進行查詢。

private static Map<String, SoftReference<Bitmap>> mWeakBitmapCache = new ConcurrentHashMap<String, SoftReference<Bitmap>>(
            M_LINK_SIZE / 2);
複製程式碼

這時就用到了ConcurrentHashMap,它的最大特點就是執行緒安全、高併發、儲存量大。由於儲存量大,所以我們存放Bitmap時就需要使用其軟引用了。

如果此map中含有需要的圖片,則先取出其軟引用,在從軟引用中獲取Bitmap物件返回。再將其移至一級快取中。

記憶體的讀取整體程式碼如下:

    /**
     * 這裡定義的操作方法完成的是從記憶體中的Map中獲取圖片的物件
     * 既然已經在記憶體中了,預設已經完成了壓縮
     *
     * @param netUrlKey  作為圖片在Map中唯一標誌的網路圖片URL
     * @return
     */
    public static Bitmap getBitmapFromRAM(String netUrlKey){

        if(mHardBitmapCache.containsKey(netUrlKey)){

            Bitmap usefulBitmap=mHardBitmapCache.get(netUrlKey);
            if(null!=usefulBitmap){
                //如果存在正在記憶體中的Bitmap圖片,將圖片的使用級別向前提,並返回Bitmap物件
                mHardBitmapCache.remove(netUrlKey);
                mHardBitmapCache.put(netUrlKey,usefulBitmap);
                return usefulBitmap;

            }else{
                //這裡的情況是雖然在集合中包含對應的Key但是通過key得不到對應的Bitmap,此時將
                //key從Map中清楚,並返回null
                mHardBitmapCache.remove(netUrlKey);
                return null;
            }
        }else{
            //如果在強引用中不包含對應的key,那麼在軟引用中進行查詢
            if(mWeakBitmapCache.containsKey(netUrlKey)){
                SoftReference<Bitmap> usefulSoftBitmap=mWeakBitmapCache.get(netUrlKey);
                if(null!=usefulSoftBitmap){
                    //從軟應用中獲取出對應的Bitmap物件
                    Bitmap usefulBitmap = usefulSoftBitmap.get();
                    if(null!=usefulBitmap){
                        //將軟引用中的低階別圖片轉移到強引用中
                        mHardBitmapCache.put(netUrlKey,usefulBitmap);
                        return usefulBitmap;
                    }else{
                        //軟引用中包含key但是獲取不到圖片
                        mWeakBitmapCache.remove(netUrlKey);
                        return null;
                    }

                }else{
                    //軟引用中包含key但是獲取不到圖片
                    mWeakBitmapCache.remove(netUrlKey);
                    return null;
                }
            }else{
                //軟引用中也不包括這個key,那麼從判斷SD卡中是否存在這個資源圖片
                return null;
            }
        }
    }
複製程式碼

特別宣告:在存放入記憶體前,會將圖片進行壓縮。

SD卡快取

記憶體中沒有圖片的話,就去檔案中查詢:

	/**
	 * 獲取已經儲存的資料的位置的路徑
	 *
	 * @param netUrlorPath
	 * @return
	 */
	private static String getSavedPath(String netUrlorPath) {

		String savedPath = null;
		if (StorageUtil.isPhoneHaveSD()) {
			// 建立以SD卡根目錄為路徑的File物件
			File fileBySD = new File(StorageUtil.getPathBySD());
			// 建立SD卡根目錄下以當前應用包名為資料夾的檔案物件,並驗證是否存在當前目錄
			File fileBySDSon = new File(fileBySD, PackageUtil.getAppPackageName());
			// File fileBySDSon=new File(fileBySD,"AA");
			if (fileBySDSon.exists()) {
				String md5Url = EncryptUtil.md5(netUrlorPath);
				// 以包名為資料夾的物件存在的時候,通過將檔案物件和圖片的名稱的拼接構建檔案物件
				File imageFile = new File(fileBySDSon, URLEncoder.encode(md5Url));
				if (imageFile.exists()) {
					// 圖片檔案物件存在的時候獲取當前的圖片物件對應的路徑
					savedPath = imageFile.getAbsolutePath();
				} else {
					return null;
				}
			} else {
				return null;
			}
		} else {
			// 建立以Cache根目錄為路徑的File物件
			File fileByCache = new File(StorageUtil.getPathBycache());
			// 建立SD卡根目錄下以當前應用包名為資料夾的檔案物件,並驗證是否存在當前目錄
			File fileByCacheSon = new File(fileByCache, PackageUtil.getAppPackageName());
			// File fileByCacheSon=new File(fileByCache,"AA");
			if (fileByCacheSon.exists()) {
				String md5Url = EncryptUtil.md5(netUrlorPath);
				// 以包名為資料夾的物件存在的時候,通過將檔案物件和圖片的名稱的拼接構建檔案物件
				File imageFile = new File(fileByCacheSon, URLEncoder.encode(md5Url));
				if (imageFile.exists()) {
					// 圖片檔案物件存在的時候獲取當前的圖片物件對應的路徑
					savedPath = imageFile.getAbsolutePath();
				} else {
					return null;
				}
			} else {
				return null;
			}
		}
		return savedPath;

	}
複製程式碼

上方程式碼是根據圖片url獲取到圖片在檔案中的路徑。

所有的快取圖片,會儲存在本包名資料夾下,以url的md5值為名字的檔案中,判斷到有此檔案的話,將檔案路徑返回。

	/**
	 * 這裡完成的操作是判斷傳遞進來的路徑是否包括Bitmap物件,如果存在將Bitmap物件返回 否則返回null
	 *
	 * @param saveTime
	 *            圖片的儲存時間
	 * @param netUrl
	 *            網路圖片的網路路徑作為檔名稱
	 * @return
	 */
	public static Bitmap getBitmapFromSD(long saveTime, String netUrl) {

		long nativeSaveTime = saveTime > 0 ? saveTime : DATA_DEFAULT_SAVETIME;
		long actualSaveTime = 0L;
		if (null == netUrl) {
			return null;
		}
		String imageSavePath = getSavedPath(netUrl);
	//	System.out.println("已經儲存的圖片的路徑::" + imageSavePath);
		if (null == imageSavePath) {
			return null;
		}
		File imageFile = new File(imageSavePath);
		if (!imageFile.exists()) {
			// throw new StructException("需要的檔案不存在!");
			return null;
		}
		actualSaveTime = System.currentTimeMillis() - imageFile.lastModified();
		if (actualSaveTime > nativeSaveTime) {
			imageFile.delete();
			//System.out.println("檔案超時了!");
			return null;

		}
		/**
		 * 這裡的邏輯是當檔案物件存在的時候將該檔案物件獲取出來,並生成Bitmap物件並返回
		 */
		// Bitmap sdBitmap= BitmapFactory.decodeFile(imageSavePath);
		// 從SD卡中獲取圖片的時候直接進行圖片的壓縮處理防止OOM

		//System.out.println("儲存的圖片的連結:" + imageSavePath);
		Bitmap sdBitmap = ImageUtil.getCompressBitmapBYScreen(imageSavePath);
		return sdBitmap;

	}
複製程式碼

判斷到檔案中有我們需要的圖片,會拿到檔案路徑。但是,我們有設定檔案有效時間,超過該時間則視為超時,返回null,否則讀取該檔案。根據圖片的路徑和當前手機的預設螢幕解析度進行圖片壓縮再返回。

檔案中有該圖片,那就將該圖片移植記憶體中,以提高優先順序,而且記憶體兩級中都放入該圖片。

網路獲取

以上都沒拿到圖片的話,那隻能從網路來獲取啦!

對http還是https進行判斷,分別對應使用HttpUrlConnection和HttpsUrlConnection。他們程式碼類似,就只貼其中一個了。

    public static InputStream getHttpIOByGet(String netUrl) throws IOException {

//        System.out.println("網路的連結:"+netUrl);

        URL url = new URL(netUrl);
        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
        conn.setRequestMethod("GET");
        conn.setConnectTimeout(5000);
        int code = conn.getResponseCode();
//        System.out.println("返回碼::"+code);
        if (code == 200) {
            InputStream is = conn.getInputStream();
            return is;
        }else{
            return null;
        }

    }
複製程式碼

返回碼200,表示請求成功,就將輸入流返回,否則返回null。

Bitmap bitmap= BitmapFactory.decodeStream(inputStream);
複製程式碼

獲取輸入流後,使用上方程式碼獲取Bitmap物件,原因大家懂的。

獲取到圖片後,再依次存入sd卡和記憶體中,因為是好是操作,就在子執行緒中進行了。

new Thread(){
    @Override
    public void run() {
        //3.1、從網路獲取圖片
        //3.2、將圖片壓縮後的儲存到SD卡或機身記憶體中
        FileUtil.putBitmapToSD(netUrl, finalThreeCacheBitmap);
        //3.4、將圖片儲存到Map中
        CacheRAM.putBitmapToRAM(netUrl, finalThreeCacheBitmap);
	}
}.start();
複製程式碼

圖片壓縮

這裡主要想介紹下圖片的壓縮:因為圖片載入很容易造成OOM,所以圖片壓縮處理顯得尤為重要。

提供集中壓縮方式:

  • 根據期望大小壓縮
  • 根據期望尺寸壓縮
  • 根據當前手機的預設螢幕解析度進行圖片的壓縮

這裡就不再貼程式碼了,可以去我的github中檢視。github.com/shuaijia/Js…

使用

1、添依賴

allprojects {
  repositories {
    ...
    maven { url 'https://www.jitpack.io' }
  }
}

dependencies {
  compile 'com.github.shuaijia:JsImageLoader:v1.0'
}
複製程式碼

2、添許可權

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
複製程式碼

3、繼承JsApplication

4、請求

JsLoader.with(this)
    .load("https://ss2.bdstatic.com/70cFvnSh_Q1YnxGkpoWK1HF6hhy/it/u=699359866,1092793192&fm=27&gp=0.jpg")
    .defaultImg(R.mipmap.default)
    .errorImg(R.mipmap.error)
    .into(imageView);
複製程式碼

由於本人水平有限,不免有不對或不足的地方,希望大家能夠提出,我們共同進步。

更多精彩內容,請關注我的微信公眾號——Android機動車

這裡寫圖片描述

相關文章