開發(fā)App一定涉及到圖片加載、圖片處理,那就必須會(huì)用到三方的圖片框架,要么選擇自己封裝。至于主流的三方圖片框架,就不得不說老牌的ImageLoader、如今更流行的Glide、Picasso和Fresco。但三方的框架本文不會(huì)過多介紹。
Glide等框架,畢竟是大神及團(tuán)隊(duì)花費(fèi)很大精力開發(fā)和維護(hù)的開源框架,他們的設(shè)計(jì)思路、性能優(yōu)化、代碼規(guī)范等等很值得我們學(xué)習(xí),之前一段時(shí)間也研究過Glide的源碼(不得不由衷佩服)。
今天,將自己對(duì)于圖片加載的思路想法,也借鑒了開源框架的一些好的點(diǎn),封裝了一個(gè)圖片加載框架——JsLoader。(github地址:https://github.com/shuaijia/JsImageLoader)與大家分享。
文章目錄:
前言
至于圖片的網(wǎng)絡(luò)請(qǐng)求,我這里還是使用Android原生提供的HttpUrlConnection;請(qǐng)求網(wǎng)絡(luò)圖片時(shí),開啟子線程進(jìn)行操作,使用線程池對(duì)線程進(jìn)行統(tǒng)一管理;線程間通信還是用了Handler;提到圖片加載,大家肯定會(huì)立刻想到圖片的三級(jí)緩存(內(nèi)存—外存—網(wǎng)絡(luò)),但我這里提供一個(gè)新的思路——四級(jí)緩存,與三級(jí)緩存不同的是內(nèi)存又分為了兩級(jí),這些稍后會(huì)詳細(xì)介紹到。
本文目的在于和大家分享一個(gè)圖片框架的封裝思路,至于代碼的優(yōu)化,如使用OkHttp替換HttpUrlConnection,使用RxJava替換Handler等,或者有別的不足的地方,也希望大家能夠反饋給我,我們一起進(jìn)步。
先看下整體流程圖:
線程池
public class MyThreadFactory {
//Android的線程池類
private static ThreadPoolExecutor threadPoolExecutor=null;
//獲取當(dāng)前用戶的手機(jī)的CPU的核心數(shù)
private static int num= Runtime.getRuntime().availableProcessors();
//用于存儲(chǔ)提交任務(wù)的任務(wù)隊(duì)列
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;
}
}
當(dāng)前類是一個(gè)線程池的管理類。由于當(dāng)前的線程池,在整個(gè)項(xiàng)目中不需要?jiǎng)?chuàng)建多個(gè)對(duì)象,直接使用單例模式進(jìn)行創(chuàng)建。
補(bǔ)充:Android中的線程池
在Android中使用線程池的類是:ThreadPoolExecutor;
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(int corePoolSize, int maxinumPoolSize, long keepAliveTime, TimeUnit unit, BlockingDeque<Runnable> workQueue, ThreadFactory threadFactory);
參數(shù):
- int corePoolSize : 線程池中的核心線程數(shù)
- int maxinumPoolSize :線程池中允許的最大線程數(shù)目
- long keepAliveTime :非核心線程的超時(shí)時(shí)間,超出這個(gè)時(shí)間非核心線程會(huì)被回收
- TimeUnit unit :非核心線程的超時(shí)時(shí)間的時(shí)間單位
- BlockingDeque<Runnable> workQueue : 保存需要線程池執(zhí)行的任務(wù)的列表
- ThreadFactory threadFactory : 線程工廠,只是一個(gè)接口,只有一個(gè)方法Thread newThread(Runnable r)
在上文展示的類中,我們獲取了手機(jī)的CPU核心數(shù)num,本線程池的核心線程數(shù)為CPU數(shù)的2倍,最大線程數(shù)為CPU核心數(shù)的4倍。
內(nèi)存一級(jí)緩存
private static final HashMap<String,Bitmap> mHardBitmapCache=new LinkedHashMap<String,Bitmap>(
M_LINK_SIZE/2,0.75f,true){
/**
* 這個(gè)方法是是put或putAll時(shí)調(diào)用,默認(rèn)返回false,表示添加數(shù)據(jù)時(shí)不移除最舊的數(shù)據(jù).
* @param eldest
* @return
*/
@Override
protected boolean removeEldestEntry(Entry<String, Bitmap> eldest) {
if (size() > M_LINK_SIZE) {
// 當(dāng)map的size大于30時(shí),把最近不常用的key放到mSoftBitmapCache中,從而保證mHardBitmapCache的效率
Bitmap value = eldest.getValue();
if (value != null) {
mWeakBitmapCache.put(eldest.getKey(),new SoftReference<Bitmap>(value));
}
return true;
}
return false;
}
};
定義的內(nèi)存中的一級(jí)緩存,即保存作為強(qiáng)引用的位置的HashMap。
此處HashMap使用的是LinkedHashMap。LinkedHashMap 是HashMap的一個(gè)子類,保存了記錄的插入順序,在用Iterator遍歷LinkedHashMap時(shí),先得到的記錄肯定是先插入的。也可以在構(gòu)造時(shí)用帶參數(shù),按照應(yīng)用次數(shù)排序。在遍歷的時(shí)候會(huì)比HashMap慢,不過有種情況例外,當(dāng)HashMap容量很大,實(shí)際數(shù)據(jù)較少時(shí),遍歷起來可能會(huì)比 LinkedHashMap慢,因?yàn)長(zhǎng)inkedHashMap的遍歷速度只和實(shí)際數(shù)據(jù)有關(guān),和容量無關(guān),而HashMap的遍歷速度和他的容量有關(guān)。
正是由于LinkedHashMap具有記憶功能,最近插入的最新訪問,就符合了我們的最近最多使用的原則。但由于其遍歷速度慢,我們對(duì)其容量進(jìn)行設(shè)定,最多30和元素。
重寫removeEldestEntry方法,當(dāng)map的size大于30時(shí),把最近不常用的key放到mSoftBitmapCache中(也就是內(nèi)存第二級(jí)緩存),從而保證mHardBitmapCache的效率。
這里我們?cè)贛ap中是以Url和Bitmap為Key-Value存儲(chǔ)的,由于LinkedHashMap存放少,而且插入移出快,所以這里用的是Bitmap的強(qiáng)引用。
如果LinkedHashMap中包含我們需要的圖片,則將圖片直接返回。但是注意:此時(shí)我們認(rèn)為此圖使用頻率更高,因此我們需要先將該元素移出,在加入(這是由于該map后插入的遍歷時(shí)先讀?。?。
mHardBitmapCache.remove(netUrlKey);
mHardBitmapCache.put(netUrlKey,usefulBitmap);
此為內(nèi)存的一級(jí)緩存。
內(nèi)存二級(jí)緩存
如果內(nèi)存的LinkedHashMap中未獲取到我們想要的圖片的話,在二級(jí)緩存中進(jìn)行查找。
private static Map<String, SoftReference<Bitmap>> mWeakBitmapCache = new ConcurrentHashMap<String, SoftReference<Bitmap>>(
M_LINK_SIZE / 2);
這時(shí)就用到了ConcurrentHashMap,它的最大特點(diǎn)就是線程安全、高并發(fā)、存儲(chǔ)量大。由于存儲(chǔ)量大,所以我們存放Bitmap時(shí)就需要使用其軟引用了。
如果此map中含有需要的圖片,則先取出其軟引用,在從軟引用中獲取Bitmap對(duì)象返回。再將其移至一級(jí)緩存中。
內(nèi)存的讀取整體代碼如下:
/**
* 這里定義的操作方法完成的是從內(nèi)存中的Map中獲取圖片的對(duì)象
* 既然已經(jīng)在內(nèi)存中了,默認(rèn)已經(jīng)完成了壓縮
*
* @param netUrlKey 作為圖片在Map中唯一標(biāo)志的網(wǎng)絡(luò)圖片URL
* @return
*/
public static Bitmap getBitmapFromRAM(String netUrlKey){
if(mHardBitmapCache.containsKey(netUrlKey)){
Bitmap usefulBitmap=mHardBitmapCache.get(netUrlKey);
if(null!=usefulBitmap){
//如果存在正在內(nèi)存中的Bitmap圖片,將圖片的使用級(jí)別向前提,并返回Bitmap對(duì)象
mHardBitmapCache.remove(netUrlKey);
mHardBitmapCache.put(netUrlKey,usefulBitmap);
return usefulBitmap;
}else{
//這里的情況是雖然在集合中包含對(duì)應(yīng)的Key但是通過key得不到對(duì)應(yīng)的Bitmap,此時(shí)將
//key從Map中清楚,并返回null
mHardBitmapCache.remove(netUrlKey);
return null;
}
}else{
//如果在強(qiáng)引用中不包含對(duì)應(yīng)的key,那么在軟引用中進(jìn)行查找
if(mWeakBitmapCache.containsKey(netUrlKey)){
SoftReference<Bitmap> usefulSoftBitmap=mWeakBitmapCache.get(netUrlKey);
if(null!=usefulSoftBitmap){
//從軟應(yīng)用中獲取出對(duì)應(yīng)的Bitmap對(duì)象
Bitmap usefulBitmap = usefulSoftBitmap.get();
if(null!=usefulBitmap){
//將軟引用中的低級(jí)別圖片轉(zhuǎn)移到強(qiáng)引用中
mHardBitmapCache.put(netUrlKey,usefulBitmap);
return usefulBitmap;
}else{
//軟引用中包含key但是獲取不到圖片
mWeakBitmapCache.remove(netUrlKey);
return null;
}
}else{
//軟引用中包含key但是獲取不到圖片
mWeakBitmapCache.remove(netUrlKey);
return null;
}
}else{
//軟引用中也不包括這個(gè)key,那么從判斷SD卡中是否存在這個(gè)資源圖片
return null;
}
}
}
特別聲明:在存放入內(nèi)存前,會(huì)將圖片進(jìn)行壓縮。
SD卡緩存
內(nèi)存中沒有圖片的話,就去文件中查找:
/**
* 獲取已經(jīng)保存的數(shù)據(jù)的位置的路徑
*
* @param netUrlorPath
* @return
*/
private static String getSavedPath(String netUrlorPath) {
String savedPath = null;
if (StorageUtil.isPhoneHaveSD()) {
// 創(chuàng)建以SD卡根目錄為路徑的File對(duì)象
File fileBySD = new File(StorageUtil.getPathBySD());
// 創(chuàng)建SD卡根目錄下以當(dāng)前應(yīng)用包名為文件夾的文件對(duì)象,并驗(yàn)證是否存在當(dāng)前目錄
File fileBySDSon = new File(fileBySD, PackageUtil.getAppPackageName());
// File fileBySDSon=new File(fileBySD,"AA");
if (fileBySDSon.exists()) {
String md5Url = EncryptUtil.md5(netUrlorPath);
// 以包名為文件夾的對(duì)象存在的時(shí)候,通過將文件對(duì)象和圖片的名稱的拼接構(gòu)建文件對(duì)象
File imageFile = new File(fileBySDSon, URLEncoder.encode(md5Url));
if (imageFile.exists()) {
// 圖片文件對(duì)象存在的時(shí)候獲取當(dāng)前的圖片對(duì)象對(duì)應(yīng)的路徑
savedPath = imageFile.getAbsolutePath();
} else {
return null;
}
} else {
return null;
}
} else {
// 創(chuàng)建以Cache根目錄為路徑的File對(duì)象
File fileByCache = new File(StorageUtil.getPathBycache());
// 創(chuàng)建SD卡根目錄下以當(dāng)前應(yīng)用包名為文件夾的文件對(duì)象,并驗(yàn)證是否存在當(dāng)前目錄
File fileByCacheSon = new File(fileByCache, PackageUtil.getAppPackageName());
// File fileByCacheSon=new File(fileByCache,"AA");
if (fileByCacheSon.exists()) {
String md5Url = EncryptUtil.md5(netUrlorPath);
// 以包名為文件夾的對(duì)象存在的時(shí)候,通過將文件對(duì)象和圖片的名稱的拼接構(gòu)建文件對(duì)象
File imageFile = new File(fileByCacheSon, URLEncoder.encode(md5Url));
if (imageFile.exists()) {
// 圖片文件對(duì)象存在的時(shí)候獲取當(dāng)前的圖片對(duì)象對(duì)應(yīng)的路徑
savedPath = imageFile.getAbsolutePath();
} else {
return null;
}
} else {
return null;
}
}
return savedPath;
}
上方代碼是根據(jù)圖片url獲取到圖片在文件中的路徑。
所以的緩存圖片,會(huì)保存在本包名文件夾下,以u(píng)rl的md5值為名字的文件中,判斷到有此文件的話,將文件路徑返回。
/**
* 這里完成的操作是判斷傳遞進(jìn)來的路徑是否包括Bitmap對(duì)象,如果存在將Bitmap對(duì)象返回 否則返回null
*
* @param saveTime
* 圖片的保存時(shí)間
* @param netUrl
* 網(wǎng)絡(luò)圖片的網(wǎng)絡(luò)路徑作為文件名稱
* @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("已經(jīng)存儲(chǔ)的圖片的路徑::" + 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("文件超時(shí)了!");
return null;
}
/**
* 這里的邏輯是當(dāng)文件對(duì)象存在的時(shí)候?qū)⒃撐募?duì)象獲取出來,并生成Bitmap對(duì)象并返回
*/
// Bitmap sdBitmap= BitmapFactory.decodeFile(imageSavePath);
// 從SD卡中獲取圖片的時(shí)候直接進(jìn)行圖片的壓縮處理防止OOM
//System.out.println("保存的圖片的鏈接:" + imageSavePath);
Bitmap sdBitmap = ImageUtil.getCompressBitmapBYScreen(imageSavePath);
return sdBitmap;
}
判斷到文件中有我們需要的圖片,會(huì)拿到文件路徑。但是,我們有設(shè)定文件有效時(shí)間,超過該時(shí)間則視為超時(shí),返回null,否則讀取該文件。根據(jù)圖片的路徑和當(dāng)前手機(jī)的默認(rèn)屏幕分辨率進(jìn)行圖片壓縮再返回。
文件中有該圖片,那就將該圖片移植內(nèi)存中,以提高優(yōu)先級(jí),而且內(nèi)存兩級(jí)中都放入該圖片。
網(wǎng)絡(luò)獲取
以上都沒拿到圖片的話,那只能從網(wǎng)絡(luò)來獲取啦!
對(duì)http還是https進(jìn)行判斷,分別對(duì)應(yīng)使用HttpUrlConnection和HttpsUrlConnection。他們代碼類似,就只貼其中一個(gè)了。
public static InputStream getHttpIOByGet(String netUrl) throws IOException {
// System.out.println("網(wǎng)絡(luò)的鏈接:"+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,表示請(qǐng)求成功,就將輸入流返回,否則返回null。
Bitmap bitmap= BitmapFactory.decodeStream(inputStream);
獲取輸入流后,使用上方代碼獲取Bitmap對(duì)象,原因大家懂的。
獲取到圖片后,再依次存入sd卡和內(nèi)存中,因?yàn)槭呛檬遣僮?,就在子線程中進(jìn)行了。
new Thread(){
@Override
public void run() {
//3.1、從網(wǎng)絡(luò)獲取圖片
//3.2、將圖片壓縮后的保存到SD卡或機(jī)身內(nèi)存中
FileUtil.putBitmapToSD(netUrl, finalThreeCacheBitmap);
//3.4、將圖片保存到Map中
CacheRAM.putBitmapToRAM(netUrl, finalThreeCacheBitmap);
}
}.start();
圖片壓縮
這里主要想介紹下圖片的壓縮:因?yàn)閳D片加載很容易造成OOM,所以圖片壓縮處理顯得尤為重要。
提供集中壓縮方式:
- 根據(jù)期望大小壓縮
- 根據(jù)期望尺寸壓縮
- 根據(jù)當(dāng)前手機(jī)的默認(rèn)屏幕分辨率進(jìn)行圖片的壓縮
這里就不再貼代碼了,可以去我的github中查看。https://github.com/shuaijia/JsImageLoader/blob/master/jsimageloader/src/main/java/com/jia/jsloader/utils/ImageUtil.java
使用
1、添依賴
allprojects {
repositories {
...
maven { url 'https://www.jitpack.io' }
}
}
dependencies {
compile 'com.github.shuaijia:JsImageLoader:v1.0'
}
2、添權(quán)限
<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、請(qǐng)求
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);
由于本人水平有限,不免有不對(duì)或不足的地方,希望大家能夠提出,我們共同進(jìn)步。
更多精彩內(nèi)容,請(qǐng)關(guān)注我的微信公眾號(hào)——Android機(jī)動(dòng)車