個人圖片四級緩存框架

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

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

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

文章目錄: github

這裏寫圖片描述

前言

至於圖片的網絡請求,我這裏仍是使用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;
    }

}
複製代碼

當前類是一個線程池的管理類。因爲當前的線程池,在整個項目中不須要建立多個對象,直接使用單例模式進行建立。bash

補充: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.一、從網絡獲取圖片
        //3.二、將圖片壓縮後的保存到SD卡或機身內存中
        FileUtil.putBitmapToSD(netUrl, finalThreeCacheBitmap);
        //3.四、將圖片保存到Map中
        CacheRAM.putBitmapToRAM(netUrl, finalThreeCacheBitmap);
	}
}.start();
複製代碼

圖片壓縮

這裏主要想介紹下圖片的壓縮:由於圖片加載很容易形成OOM,因此圖片壓縮處理顯得尤其重要。

提供集中壓縮方式:

  • 根據指望大小壓縮
  • 根據指望尺寸壓縮
  • 根據當前手機的默認屏幕分辨率進行圖片的壓縮

這裏就再也不貼代碼了,能夠去個人github中查看。github.com/shuaijia/Js…

使用

一、添依賴

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

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

二、添權限

<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" />
複製代碼

三、繼承JsApplication

四、請求

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機動車

這裏寫圖片描述
相關文章
相關標籤/搜索