目前市場上有很多第三方圖片加載框架, 當(dāng)然,以UniversalImageLoader,Picasso,Glide為代表, 這些圖片加載庫大大方便了我們平時使用時需要圖片加載地方的代碼編寫,且其性能還高.
之前學(xué)習(xí)了Volley的源碼,生產(chǎn)者消費者模式的代碼也看了很多,就想著試著自己打造一款圖片加載框架,當(dāng)然,功能與上面的比起來肯定有很多不足,不過鍛煉一下框架思維也是不錯的.
先貼工程路徑,里面還有我準(zhǔn)備寫的數(shù)據(jù)庫lib,還有httplib,只看ImageLoaderlib即可.
https://github.com/Jerey-Jobs/Sherlock
簡單圖片加載框架需求
- 圖片請求無需用戶管理,分發(fā)器主動下載并設(shè)置圖片
- 圖片自適應(yīng)ImageView大小,不能全部加載到內(nèi)存
- TAG匹配,防止錯位
- 靈活配置,加載時顯示默認(rèn)圖片
- 自定義回調(diào)Callback,可讓用戶自己處理圖片
- 圖片加載策略選擇,是后進優(yōu)先加載還是先進優(yōu)先加載
- 圖片緩存策略配置,是否Disk緩存
- 能夠主動取消請求.
暫時先定這么多需求,至于像Glide那樣綁定住Activity或者Fragment的生命周期的功能,我們知道它是通過注冊一個空的Fragment來干的,但是先不做了.上面需求已經(jīng)夠我們做的了.
我們可以想看一下我們設(shè)計的最終結(jié)果,其實還蠻讓人期待的.
SherlockImageLoader.with(this)
.setUrl("http://img.my.csdn.net/uploads/201407/26/1406382765_7341.jpg")
.loadingImage(R.mipmap.ic_launcher_round)
.errorImage(R.drawable.blog)
.into(mImageView);
SherlockImageLoader.with(this)
.setUrl("http://img.my.csdn.net/uploads/201407/26/1406382765_7341.jpg")
.loadingImage(R.mipmap.ic_launcher_round)
.errorImage(R.drawable.blog)
.into(new SherlockImageLoader.Callback() {
@Override
public void onSuccess(Bitmap bitmap, String url) {
Log.i(TAG, "onSuccess: " + bitmap + " url" + url);
}
});
框架設(shè)計
我將該圖片框架命名為:SherlockImageLoader, 其架構(gòu)很簡單,一個入口,學(xué)習(xí)Volley的方式,那就是是分發(fā)器,加載器,緩存容器外加一個配置選項就行了.
框架如圖:

類的設(shè)計
根據(jù)上面的框架,我們需要新建SherlockImageLoader類作為主入口,提供displayImage()方法,然后呢,RequestQueue類提供添加請求的隊列,RequestDispacher負(fù)責(zé)從隊列取請求進行分發(fā),分發(fā)即是調(diào)用Loader去加載請求, 加載請求是根據(jù)Config加載的. Loader同時還依賴緩存容器,因為每次去加載時,都需要看緩存是否已經(jīng)有了.
跟著上面描述很簡單.我們先開始寫接口吧.
SherlockImageLoader我們先將其設(shè)置為單列,之后可以再改,入口的封裝留到最后.
首先SherlockImageLoader需要持有一個隊列和Config.還要提供顯示方法給外部使用.代碼如下.
public class SherlockImageLoader {
private ImageLoaderConfig mImageLoaderConfig;
private RequestQueue mRequestQueue;
/**
* 單列對象
*/
private static volatile SherlockImageLoader instance;
public SherlockImageLoader(Context context, ImageLoaderConfig imageLoaderConfig) {
if (imageLoaderConfig != null) {
mImageLoaderConfig = imageLoaderConfig;
} else {
mImageLoaderConfig = new ImageLoaderConfig.Builder()
.setBitmapCache(new DoubleCache(context))
.setLoadPolicy(new ReverseLoaderPolicy())
.build();
}
mRequestQueue = new RequestQueue(mImageLoaderConfig.getTHREAD_COUNT());
mRequestQueue.start();
}
public ImageLoaderConfig getImageLoaderConfig() {
return mImageLoaderConfig;
}
public RequestQueue getRequestQueue() {
return mRequestQueue;
}
/**
* 初始化
* @param context
*/
public static void init(Context context) {
init(context, null);
}
public static void init(Context context, ImageLoaderConfig config) {
if (instance == null) {
instance = new SherlockImageLoader(context, config);
}
}
public static SherlockImageLoader getInstance() {
if (instance == null) {
throw new RuntimeException("SherlockImageLoader must init");
}
return instance;
}
public void display(ImageView imageView, String url) {
display(imageView, url, null, null);
}
public void display(String url, Callback callback) {
display(null, url, callback, null);
}
/**
* @param imageView
* @param url
* @param callback
*/
public void display(ImageView imageView, String url, Callback callback, DisplayConfig
displayConfig) {
BitmapRequest bitmapRequest = new BitmapRequest(imageView, url, callback, displayConfig);
mRequestQueue.addRequest(bitmapRequest);
}
/**
* 供用戶自定義使用
*/
public static interface Callback {
/**
* @param bitmap
* @param url
*/
void onSuccess(Bitmap bitmap, String url);
}
}
緩存器與分發(fā)器是具體業(yè)務(wù)類,下章節(jié)設(shè)計,先寫一下Loader,Cache,Config的接口,
// Loader
public interface ILoader {
void loadImage(BitmapRequest request);
}
/**
* 加載策略
* @author xiamin
* @date 7/8/17.
*/
public interface ILoadPolicy {
/**
* 兩個加載請求進行優(yōu)先級比較
* @param request1
* @param request2
* @return
*/
int compareTo(BitmapRequest request1, BitmapRequest request2);
}
public interface BitmapCache {
/**
* 緩存bitmap
* @param bitmapRequest
* @param bitmap
*/
void put(BitmapRequest bitmapRequest, Bitmap bitmap);
/**
* 通過請求取bitmap
* @return
*/
Bitmap get(BitmapRequest request);
/**
* 移除bitmap
* @param request
*/
void remove(BitmapRequest request);
}
思路很明朗到現(xiàn)在, 加載器就一個方法,加載請求, 策略呢也就是請求間的比較,誰大誰加載, 緩存呢就是存與取還有刪除功能.
請求封裝
我們的請求就是一個Bean類,里面的變量需要:
/** 圖片軟應(yīng)用,內(nèi)存不足情況下不加載 */
private SoftReference<ImageView> mImageViewSoftReference;
/** 圖片路徑 */
private String imageURL;
/** 圖片的md5碼,做緩存唯一標(biāo)識,因為圖片名可能是非法字符,合法化一下 */
private String imageUriMD5;
/** 編號,請求的唯一標(biāo)識 */
private int serialNo;
/** 下載完監(jiān)聽 */
private SherlockImageLoader.Callback mCallback;
/** 顯示設(shè)置,要是為空,則使用全局的 */
private DisplayConfig mDisplayConfig;
/** 請求tag,供取消請求時使用 */
private Object requestTag;
/** 是否被取消 */
private boolean isCancel = false;
并設(shè)置相應(yīng)的get和set方法.
分發(fā)器設(shè)計RequestQueue]
與Volley的代碼一樣,RequestQueue就是參考Volley設(shè)計的.

RequestQueue代碼設(shè)計
RequestQueue作為分發(fā)的源頭,其一定持有一個Queue,然后持有Dispatchers來處理隊列里面的請求.
既然需要隊列,我們這種生產(chǎn)者消費者模式對隊列的要求是:
- 線程安全
- 能夠阻塞
- 能夠優(yōu)先級控制
這樣我們的數(shù)據(jù)結(jié)構(gòu)只能是: PriorityBlockingQueue
我們還需要分發(fā)器,從PriorityBlockingQueue中不斷的take(), 然后進行處理, 因此我們需要一個分發(fā)器列表.這里參考Volley源碼,使用一個數(shù)組來存儲,畢竟數(shù)組夠完成任務(wù)了.
private RequestDispacher[] mRequestDispachers;
我們還需要提供方法去讓主程序start自己.stop自己.
因此有了UML里面的start方法,stop方法.在這兩個方法里面我們就開啟分發(fā)器,關(guān)閉分發(fā)器就行了.
分發(fā)器RequestDispacher設(shè)計
分發(fā)器從PriorityBlockingQueue中取請求, 我大Java可沒有那種什么全局變量的玩意兒,把PriorityBlockingQueue的引用傳給RequestDispacher就行了,然后里面開一個線程不斷的跑,從隊列里取東西,進行消費就行了.
那么我們的RequestDispacher就可以寫成繼承于Thread類, 正好也有start/stop方法可以供調(diào)用.
代碼編寫
在往RequesQueue添加請求的時候,我們可以直接給ImageView設(shè)置加載中的圖片,雖然這個請求還未被處理,也就是還未被加載中,但是宏觀來說,進入了我們的ImageLoader就已經(jīng)是加載中了.
public class RequestQueue {
BlockingQueue<BitmapRequest> mRequestQueue = new PriorityBlockingQueue<>();
private AtomicInteger mNo = new AtomicInteger(0);
private int mThreadCount;
private RequestDispacher[] mRequestDispachers;
public RequestQueue(int threadCount) {
mThreadCount = threadCount;
}
/**
* start各個分發(fā)器
*/
public void start() {
mRequestDispachers = new RequestDispacher[mThreadCount];
for (int i = 0; i < mRequestDispachers.length; i++) {
RequestDispacher dispacher = new RequestDispacher(mRequestQueue);
mRequestDispachers[i] = dispacher;
dispacher.start();
}
}
/**
* 停止所有請求,以interrupt異常的方式
* 分發(fā)器也會停止運行
*/
public void stop() {
for (int i = 0; i < mRequestDispachers.length; i++) {
if (mRequestDispachers[i] != null) {
mRequestDispachers[i].quit();
}
}
}
/**
* 取消所有請求,但不停止分發(fā)器的運行
*/
public void cancel() {
for (BitmapRequest request : mRequestQueue) {
request.setCancel(true);
}
}
/**
* 根據(jù)tag取消請求
* @param tag
*/
public void cancel(Object tag) {
if (tag == null) {
return;
}
for (BitmapRequest request : mRequestQueue) {
if (tag.equals(request.getRequestTag())) {
request.setCancel(true);
}
}
}
public void addRequest(BitmapRequest request) {
if (!mRequestQueue.contains(request)) {
/** 設(shè)置唯一標(biāo)識 */
request.setSerialNo(mNo.incrementAndGet());
mRequestQueue.add(request);
L.w("請求添加成功, 編號為:" + request.getSerialNo());
} else {
L.w("請求已經(jīng)存在, 編號為:" + request.getSerialNo());
}
if (request.getDisplayConfig() != null
&& request.getDisplayConfig().loadingImage != -1) {
ImageView imageView = request.getImageView();
if (imageView != null && imageView.getTag().equals(request.getImageURL())) {
imageView.setImageResource(request.getDisplayConfig().loadingImage);
}
}
}
}
分發(fā)器的代碼
/**
* 請求轉(zhuǎn)發(fā)線程
* 依賴于BitmapRequest
* 依賴于Loader
* Created by xiamin on 7/8/17.
*/
public class RequestDispacher extends Thread {
private BlockingQueue<BitmapRequest> mBitmapRequests;
/** Used for telling us to die. */
private volatile boolean mQuit = false;
public RequestDispacher(BlockingQueue<BitmapRequest> requests) {
mBitmapRequests = requests;
}
/**
* Forces this dispatcher to quit immediately. If any requests are still in
* the queue, they are not guaranteed to be processed.
*/
public void quit() {
mQuit = true;
interrupt();
}
@Override
public void run() {
/**不斷獲取請求,處理請求*/
while (!isInterrupted()) {
try {
BitmapRequest bitmapRequest = mBitmapRequests.take();
if (bitmapRequest.isCancel()) {
continue;
}
L.d("開始處理" + bitmapRequest.getSerialNo() + "號請求,線程號:" + Thread.currentThread()
.getId());
/**
* 處理請求對象
*/
String type = parseURL(bitmapRequest.getImageURL());
ILoader l = LoaderManager.getInstance().getLoaderByType(type);
l.loadImage(bitmapRequest);
} catch (InterruptedException e) {
// We may have been interrupted because it was time to quit.
e.printStackTrace();
if (mQuit) {
return;
}
continue;
}
}
}
/**
* 解析圖片來源
* @param imageURL
* @return
*/
private String parseURL(String imageURL) {
if (imageURL.contains("://")) {
return imageURL.split("://")[0];
}
L.e("不支持的URL:" + imageURL);
return " ";
}
}
加載器緩存器的設(shè)計
加載器與緩存器的接口都在一開始定好了,這邊只需要完成其接口.我們看一下UML圖

Loader
Loader我們實現(xiàn)一個實現(xiàn)ILoader的BaseLoader類,這邊用到了策略模式,其下有加載本地圖片的Loader,加載網(wǎng)絡(luò)請求的Loader.
BaseLoader類中,需要先去緩存中獲取Bitmap,獲取到了直接設(shè)置,獲取不到,就去調(diào)用子類的加載方法去加載.
public abstract class BaseLoader implements ILoader {
public static BitmapCache mBitmapCache = SherlockImageLoader.getInstance()
.getImageLoaderConfig()
.getmBitmapCache();
public static Handler handler = new Handler(Looper.getMainLooper());
@Override
public void loadImage(BitmapRequest request) {
/** 從緩存中取bitmap */
Bitmap bitmap = mBitmapCache.get(request);
if (bitmap == null) {
L.i("獲取緩存失敗,先顯示Loading圖");
showLoadingImage(request);
bitmap = onLoad(request);
cacheBitmap(request, bitmap);
}
deliveryToUIThread(request, bitmap);
}
/**
* 緩存圖片
* @param request
* @param bitmap
*/
private void cacheBitmap(BitmapRequest request, Bitmap bitmap) {
if (request != null && bitmap != null) {
synchronized (BaseLoader.class) {
mBitmapCache.put(request, bitmap);
}
}
}
protected abstract Bitmap onLoad(BitmapRequest request);
/**
* 顯示加載中圖片
* @param request
*/
private void showLoadingImage(final BitmapRequest request) {
if (request.getDisplayConfig() != null) {
final ImageView imageview = request.getImageView();
if (imageview == null) {
return;
}
handler.post(new Runnable() {
@Override
public void run() {
imageview.setImageResource(request.getDisplayConfig().loadingImage);
}
});
}
}
/**
* 交給主線程顯示
* @param request
* @param bitmap
*/
protected void deliveryToUIThread(final BitmapRequest request, final Bitmap bitmap) {
ImageView imageView = request.getImageView();
L.d("deliveryToUIThread imageView = " + imageView);
if (imageView == null) {
return;
}
handler.post(new Runnable() {
@Override
public void run() {
updateImageView(request, bitmap);
}
});
}
private void updateImageView(final BitmapRequest request, final Bitmap bitmap) {
ImageView imageView = request.getImageView();
L.d("更新UI");
if (imageView == null) {
L.d("為空.返回");
return;
}
//加載正常 防止圖片錯位
if (bitmap != null && imageView.getTag().equals(request.getImageURL())) {
L.d("加載正常");
imageView.setImageBitmap(bitmap);
} else {
L.d("加載失敗,TAG不對");
}
//有可能加載失敗
if (bitmap == null
&& request.getDisplayConfig() != null
&& request.getDisplayConfig().failedImage != -1) {
L.d("加載失敗,顯示默認(rèn)");
imageView.setImageResource(request.getDisplayConfig().failedImage);
}
//監(jiān)聽
//回調(diào) 給圓角圖片 特殊圖片進行擴展
if (request.getCallback() != null) {
request.getCallback().onSuccess(bitmap, request.getImageURL());
}
}
}
緩存策略
緩存直接寫了幾個實現(xiàn)BitmapCache接口的類就行了.
- RAMCache內(nèi)存緩存
使用 LruCache<String, Bitmap> mLruCache; - RomCache磁盤緩存
使用 DiskLruCache - DoubleCache
即先從內(nèi)存取,取不到從磁盤取.
舉個列子RAM緩存代碼
public class RAMCache implements BitmapCache {
private LruCache<String, Bitmap> mLruCache;
public RAMCache() {
int maxSize = (int) (Runtime.getRuntime().maxMemory() / 4);
L.d("RAMCache maxSize: " + maxSize);
mLruCache = new LruCache<String, Bitmap>(maxSize) {
@Override
protected int sizeOf(String key, Bitmap value) {
int size = value.getRowBytes() * value.getHeight();
L.d("size = " + size);
return size;
}
};
}
@Override
public void put(BitmapRequest bitmapRequest, Bitmap bitmap) {
mLruCache.put(bitmapRequest.getImageUriMD5(), bitmap);
}
@Override
public Bitmap get(BitmapRequest request) {
return mLruCache.get(request.getImageUriMD5());
}
@Override
public void remove(BitmapRequest request) {
mLruCache.remove(request.getImageUriMD5());
}
}
封裝
幾個關(guān)鍵的類都設(shè)計完了,該我們封裝打通的時候呢,我一直喜歡RESTful風(fēng)格,況且現(xiàn)在是2017年代碼調(diào)用還不鏈?zhǔn)讲荒苋?
看一下我們最終希望調(diào)用的方式.
SherlockImageLoader.with(this)
.setUrl("http://img.my.csdn.net/uploads/201407/26/1406382765_7341.jpg")
.loadingImage(R.mipmap.ic_launcher_round)
.errorImage(R.drawable.blog)
.into(mImageView);
SherlockImageLoader.with(this)
.setUrl("http://img.my.csdn.net/uploads/201407/26/1406382765_7341.jpg")
.loadingImage(R.mipmap.ic_launcher_round)
.errorImage(R.drawable.blog)
.into(new SherlockImageLoader.Callback() {
@Override
public void onSuccess(Bitmap bitmap, String url) {
Log.i(TAG, "onSuccess: " + bitmap + " url" + url);
}
});
這是一個當(dāng)前流行的調(diào)用風(fēng)格,我們這個是簡單的圖片加載框架,沒有Glide的功能那么多,只提供了上面顯示的這些接口.
with的不是綁定生命周期,而是用來tag,即強制將每次申請都綁定tag,這樣我們可以在onDestroy的時候可以取消請求.
ps: Glide的with是使用的注冊一個空的Fragment來綁定調(diào)用方的生命周期,從而做到不需要請求方主動去取消請求的功能,我們這個框架那樣寫就復(fù)雜多了.with的對象有好多種的.
好了,開始封裝
Builder的編寫
with(Obj tag)方法肯定是SherlockImageLoader的一個靜態(tài)方法, 返回一個Builder就行了.
public static RequestBuilder with(Object tag) {
return new RequestBuilder(tag);
}
這個Builder里面有into方法, 相當(dāng)于exec or build,
代碼如下:
/**
* @author xiamin
* @date 7/18/17.
*/
public class RequestBuilder {
private String url;
/** 顯示設(shè)置,要是為空,則使用全局的 */
private DisplayConfig mDisplayConfig;
/** 請求tag,供取消請求時使用 */
private Object requestTag;
public RequestBuilder(Object requestTag) {
this.requestTag = requestTag;
}
public RequestBuilder withDisplayConfig(DisplayConfig displayConfig) {
mDisplayConfig = displayConfig;
return this;
}
public RequestBuilder loadingImage(int loadingImage) {
if (mDisplayConfig == null) {
mDisplayConfig = new DisplayConfig();
}
mDisplayConfig.loadingImage = loadingImage;
return this;
}
public RequestBuilder errorImage(int errorImage) {
if (mDisplayConfig == null) {
mDisplayConfig = new DisplayConfig();
}
mDisplayConfig.failedImage = errorImage;
return this;
}
public RequestBuilder setUrl(String url) {
this.url = url;
return this;
}
public void into(ImageView imageView) {
if (imageView == null) {
throw new IllegalArgumentException("imageview can not be null");
}
BitmapRequest bitmapRequest = new BitmapRequest(imageView, url, null, mDisplayConfig);
bitmapRequest.setRequestTag(requestTag);
SherlockImageLoader.getInstance().display(bitmapRequest);
}
public void into(SherlockImageLoader.Callback callback) {
if (callback == null) {
throw new IllegalArgumentException("callback can not be null");
}
BitmapRequest bitmapRequest = new BitmapRequest(null, url, callback, mDisplayConfig);
bitmapRequest.setRequestTag(requestTag);
SherlockImageLoader.getInstance().display(bitmapRequest);
}
}
很簡單, 我們提供了上述接口, 然后在在調(diào)用方就能像上面那樣調(diào)用了.
完善
對于Cancel請求, 一個請求可能在走到任何地方的時候被Cancel掉,因此其實我們應(yīng)該盡可能的在多處關(guān)鍵點進行請求是否已經(jīng)被Cancel的判斷.被Cancel就結(jié)束.
對于生命周期的綁定,上面說過了,需要向調(diào)用方注冊一個空Fragment來進行監(jiān)聽生命周期,此功能暫時未做.
對于錯誤回調(diào),因為是圖片加載,錯誤我們都'catch'了,沒有傳遞到回調(diào)里面
各類型的支持,我們這邊只進行了Bitmap的編寫,各種Drawable均未支持,這種級別的支持工程量就大了
總結(jié)
這個圖片框架只是一個簡單的圖片框架,不過是為了鍛煉框架思維所編寫,真實情況下我們還是用Picasso,Glide比較好,畢竟JakeWharton的確強啊。
工程路徑:
里面還有我準(zhǔn)備寫的數(shù)據(jù)庫lib,還有httplib,只看ImageLoaderlib即可.
https://github.com/Jerey-Jobs/Sherlock
本文作者:Anderson/Jerey_Jobs
博客地址 : http://jerey.cn/
簡書地址 : Anderson大碼渣
github地址 : https://github.com/Jerey-Jobs