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