對於一個須要展現不少圖片或較多圖片的App來講,一個好的圖片加載框架是必不可少的。在我剛開始學習Android的時候,什麼都想本身寫,我之前作過一個看漫畫的App,裏面的圖片加載,緩存等都是本身寫的,可是效果並不理想,當時利用了LruCache
來作圖片的內存緩存,用DiskLruCache
作磁盤緩存,加載圖片根據Key先在內存中查詢,若是沒有就再去磁盤中查詢,若果再沒有在去網絡上加載。雖然是個邏輯上很是清楚的事,可是要真的作好仍是有必定的難度的。後來我認識到了Picasso,一個優雅、強大的圖片加載框架,我使用它製做了許多的項目。雖然Picasso不是最強大的,如今有Glide、Fresco這些更強大的圖片加載庫,但在我眼裏Picasso絕對是最優雅的,從它的API,設計方法,源碼看,Picasso集輕量、實用於一身。java
認識它的第一眼確定是它超級簡短的使用方法,只須要一行代碼,鏈式配置咱們要加載的圖片和一些加載配置和指定的加載Target,咱們的圖片就正確的加載並顯示在了界面上,不可謂不優雅。設計模式
Picasso.with(context).load(url).into(imageView);複製代碼
咱們還能夠很方便的配置各類加載選項,好比設置佔位圖,設置錯誤圖,設置是否須要對圖片進行變換,圖片顯示是否須要淡入效果,對圖片的大小進行調整等經常使用的配置,這些都只須要一行代碼就能夠解決。緩存
那麼它是怎麼作到的呢?做爲一個開發者,首先要學會使用一個庫,在學會了使用後,咱們要去了解它,瞭解它的設計,瞭解它的實現,從中學習優秀的設計思想和編碼技巧。網絡
從使用上看,Picasso
類無疑是掌握着一切的類,做爲一個全局單例類,它掌握着各類配置(內存、磁盤、BitmapConfig、線程池等),提供各類簡單有用的方法並隱藏了內部的各類實現,是咱們使用上的超級核心類。Picasso的構造方法提供包級別的訪問權限,咱們不能直接new出來,但咱們能夠很簡單的經過app
Picasso.with(context)複製代碼
來獲取這個全局單例對象,來看一下with()
方法框架
public static Picasso with(@NonNull Context context) {
if (context == null) {
throw new IllegalArgumentException("context == null");
}
if (singleton == null) {
synchronized (Picasso.class) {
if (singleton == null) {
singleton = new Builder(context).build();
}
}
}
return singleton;
}複製代碼
一個很經典的單例實現,內部使用建造者模式對Picasso對象進行構造。對於大多數簡單的應用,只須要使用默認的配置就行了。可是對於一些應用,咱們也能夠經過Picasso.Builder來進行自定義的配置。咱們能夠根據相似下面的代碼進行Picasso的配置ide
Picasso picasso = new Picasso.Builder(this)
.loggingEnabled(true)
.defaultBitmapConfig(Bitmap.Config.RGB_565)
.build();
Picasso.setSingletonInstance(picasso);複製代碼
因爲Picasso是一個很是複雜的對象,根據不一樣的配置會產生不一樣的表現,這裏經過建造者模式來構建對象很好的將構建和其表現分離。oop
經過Picasso對象,咱們能夠load各類圖片資源,這個資源能夠字符路徑、Uri、資源路徑,文件等形式提供,以後Picasso會發揮一個RequestCreator
對象,它能夠根據咱們需求以及圖片資源生成相應的Request
對象。以後Request
會生成Action*
對象,以後會分析這一系列的過程。因爲RequestCreator
的配置方法都返回自身,因而咱們能夠很方便的鏈式調用。源碼分析
RequestCreator requestCreator = Picasso.with(this)
.load("http://image.com")
.config(Bitmap.Config.RGB_565)
.centerInside()
.fit()
.memoryPolicy(MemoryPolicy.NO_CACHE)
.noPlaceholder()
.noFade();複製代碼
requestCreator.into(new ImageView(this));複製代碼
生成了RequestCreator
後,咱們能夠調用其into()方法,該方法接受能夠接收一個ImageView
或者RemoteView
或者Target
對象,固然最後他們都會變爲相應的Target
對象來進行內部的圖片加載。學習
接下來咱們就來分析一下這被隱藏起來的加載過程。這裏以最經常使用的into(imageView)
進行分析。
public void into(ImageView target, Callback callback) {
long started = System.nanoTime();
checkMain();
if (target == null) {
throw new IllegalArgumentException("Target must not be null.");
}
if (!data.hasImage()) {
picasso.cancelRequest(target);
if (setPlaceholder) {
setPlaceholder(target, getPlaceholderDrawable());
}
return;
}
if (deferred) {
if (data.hasSize()) {
throw new IllegalStateException("Fit cannot be used with resize.");
}
int width = target.getWidth();
int height = target.getHeight();
if (width == 0 || height == 0) {
if (setPlaceholder) {
setPlaceholder(target, getPlaceholderDrawable());
}
picasso.defer(target, new DeferredRequestCreator(this, target, callback));
return;
}
data.resize(width, height);
}
Request request = createRequest(started);
String requestKey = createKey(request);
if (shouldReadFromMemoryCache(memoryPolicy)) {
Bitmap bitmap = picasso.quickMemoryCacheCheck(requestKey);
if (bitmap != null) {
picasso.cancelRequest(target);
setBitmap(target, picasso.context, bitmap, MEMORY, noFade, picasso.indicatorsEnabled);
if (picasso.loggingEnabled) {
log(OWNER_MAIN, VERB_COMPLETED, request.plainId(), "from " + MEMORY);
}
if (callback != null) {
callback.onSuccess();
}
return;
}
}
if (setPlaceholder) {
setPlaceholder(target, getPlaceholderDrawable());
}
Action action =
new ImageViewAction(picasso, target, request, memoryPolicy, networkPolicy, errorResId,
errorDrawable, requestKey, tag, callback, noFade);
picasso.enqueueAndSubmit(action);
}複製代碼
這個方法有點長,咱們一點一點看,首先它檢查了當前是否爲主線程,若爲主線程就拋出異常。
static void checkMain() {
if (!isMain()) {
throw new IllegalStateException("Method call should happen from the main thread.");
}
}複製代碼
以後判斷咱們給的圖片的資源路徑是否合法,若是不合法會取消生成請求,並直接顯示佔位圖
if (!data.hasImage()) {
picasso.cancelRequest(target);
if (setPlaceholder) {
setPlaceholder(target, getPlaceholderDrawable());
}
return;
}複製代碼
若給的圖片資源路徑不爲空,會檢查一個defer
字段的真假,只有當咱們設置fit()
時,defer才爲true
,由於fit()
方法會讓加載的圖片以適應咱們ImageView
,因此只有當咱們的ImageView
laid out後纔會去進行圖片的獲取並剪裁以適應。這裏就不具體分析了,接下來往下看。
Request request = createRequest(started);
private Request createRequest(long started) {
int id = nextId.getAndIncrement();
Request request = data.build();
request.id = id;
request.started = started;
boolean loggingEnabled = picasso.loggingEnabled;
if (loggingEnabled) {
log(OWNER_MAIN, VERB_CREATED, request.plainId(), request.toString());
}
Request transformed = picasso.transformRequest(request);
if (transformed != request) {
// If the request was changed, copy over the id and timestamp from the original.
transformed.id = id;
transformed.started = started;
if (loggingEnabled) {
log(OWNER_MAIN, VERB_CHANGED, transformed.logId(), "into " + transformed);
}
}
return transformed;
}複製代碼
接下來,終於生成了咱們的Request
對象,同Http請求的Request對象相同,這個Request
對象包含了許多咱們須要的信息已得到準確的迴應。這裏注意,咱們的Request
對象是能夠經過RequestTransformer
通過變換的,默認Picasso中內置的是一個什麼也不變的RequestTransformer
。
默認的RequestTransformer
RequestTransformer IDENTITY = new RequestTransformer() {
@Override public Request transformRequest(Request request) {
return request;
}
};複製代碼
生成了Request
對象後,這裏來到了咱們熟悉的一個步驟,就是檢查內存中是否已經有圖片的緩存了,固然它還更具咱們設置的內存策略進行了是否須要進行內存檢查。好比咱們在查看大圖的時候,通常會選擇不讓大圖在內存緩存中存在,而是每次都去請求。
if (shouldReadFromMemoryCache(memoryPolicy)) {
Bitmap bitmap = picasso.quickMemoryCacheCheck(requestKey);
if (bitmap != null) {
picasso.cancelRequest(target);
setBitmap(target, picasso.context, bitmap, MEMORY, noFade, picasso.indicatorsEnabled);
if (picasso.loggingEnabled) {
log(OWNER_MAIN, VERB_COMPLETED, request.plainId(), "from " + MEMORY);
}
if (callback != null) {
callback.onSuccess();
}
return;
}
}複製代碼
找到的話就取消請求。不然會更具是否須要設置佔位設置一下展位圖,而後生成一個Action
對象並使其加入隊列發出。
if (setPlaceholder) {
setPlaceholder(target, getPlaceholderDrawable());
}
Action action =
new ImageViewAction(picasso, target, request, memoryPolicy, networkPolicy, errorResId,
errorDrawable, requestKey, tag, callback, noFade);
picasso.enqueueAndSubmit(action);複製代碼
這個Action
對象包含了咱們的目標Target
和請求數據Request
以及相應的回調Callback等信息。
到這裏咱們仍是沒有真正的進行加載,最多隻在內存中進行了緩存查詢。
經過上面的代碼調用,咱們瞭解到Picasso發出了一個Action
,就像一個導演同樣,說Action的時候就開始拍戲了,咱們的Picasso在發出Action
後就開始正式加載圖片了。
Action
最終會被Dispatcher
給分發出去。
void submit(Action action) {
dispatcher.dispatchSubmit(action);
}複製代碼
Dispatcher
隨着Picasso的初始化而生成,正如其名,它負責全部Action的分發並將收到的結果也進行分發,分發給Picasso對象。咱們來分析一下Dispatcher的工做過程。
Dispatcher
內部是經過Handler
進行工做的,這裏插下嘴,Handler真是Android中超級強大的類,幫助咱們輕鬆的實現線程之間的通訊、切換,有許多有名的開源庫都利用了Handler
來實現功能,好比Google自家的響應式框架Agera
,內部真是經過Handler
實現的。因此咱們要好好的掌握這個強大的類。
很少說了,咱們繼續看代碼
void dispatchSubmit(Action action) {
handler.sendMessage(handler.obtainMessage(REQUEST_SUBMIT, action));
}複製代碼
Dispatcher
經過內部的Handler
發送了一個消息,並將Action
對象傳遞了出去,那麼咱們就要找到這個Handler
對象的具體實現,根據收到的消息類型它作了哪些事情。
private static class DispatcherHandler extends Handler {
private final Dispatcher dispatcher;
public DispatcherHandler(Looper looper, Dispatcher dispatcher) {
super(looper);
this.dispatcher = dispatcher;
}
@Override public void handleMessage(final Message msg) {
switch (msg.what) {
case REQUEST_SUBMIT: {
Action action = (Action) msg.obj;
dispatcher.performSubmit(action);
break;
}
……
}
}
}複製代碼
很容易找到了,就是執行了performSubmit(action)
方法,繼續來看下去,這裏尚未具體的加載動做。
void performSubmit(Action action, boolean dismissFailed) {
if (pausedTags.contains(action.getTag())) {
pausedActions.put(action.getTarget(), action);
if (action.getPicasso().loggingEnabled) {
log(OWNER_DISPATCHER, VERB_PAUSED, action.request.logId(),
"because tag '" + action.getTag() + "' is paused");
}
return;
}
BitmapHunter hunter = hunterMap.get(action.getKey());
if (hunter != null) {
hunter.attach(action);
return;
}
if (service.isShutdown()) {
if (action.getPicasso().loggingEnabled) {
log(OWNER_DISPATCHER, VERB_IGNORED, action.request.logId(), "because shut down");
}
return;
}
hunter = forRequest(action.getPicasso(), this, cache, stats, action);
hunter.future = service.submit(hunter);
hunterMap.put(action.getKey(), hunter);
if (dismissFailed) {
failedActions.remove(action.getTarget());
}
if (action.getPicasso().loggingEnabled) {
log(OWNER_DISPATCHER, VERB_ENQUEUED, action.request.logId());
}
}複製代碼
又是一段比較長的代碼,可是仍是很容易看懂的。主要就是生成了BitmapHunter
對象,並將其發出。
hunter = forRequest(action.getPicasso(), this, cache, stats, action);
hunter.future = service.submit(hunter);複製代碼
經過查看代碼,咱們知道這個service
對象是一個ExecutorService
,也就咱們經常使用的線程池,這裏Picasso默認使用的是PicassoExecutorService
,在Dispatcher內部有一個監聽網絡變化廣播的Receiver,Picasso會根據咱們不一樣的網絡變化(Wifi,4G,3G,2G)智能的切換線程池中該線程的數量,以幫助咱們更好的節省流量。
既然線程池將BitmapHunter
對象發出了,說明這個BitmapHunter
對象必定是Runable
的子類,從名字上來分析,咱們也能夠知道它確定承擔了Bitmap的獲取生成。點進去一看源碼,果真是這樣的。哈哈。
線程池submit了一個Runable
對象後,必定會調用其run()
方法,咱們就來看看它是否是加載了Bitmap
吧~
@Override public void run() {
try {
updateThreadName(data);
if (picasso.loggingEnabled) {
log(OWNER_HUNTER, VERB_EXECUTING, getLogIdsForHunter(this));
}
result = hunt();
if (result == null) {
dispatcher.dispatchFailed(this);
} else {
dispatcher.dispatchComplete(this);
}
} catch (Downloader.ResponseException e) {
if (!e.localCacheOnly || e.responseCode != 504) {
exception = e;
}
dispatcher.dispatchFailed(this);
} catch (NetworkRequestHandler.ContentLengthException e) {
exception = e;
dispatcher.dispatchRetry(this);
} catch (IOException e) {
exception = e;
dispatcher.dispatchRetry(this);
} catch (OutOfMemoryError e) {
StringWriter writer = new StringWriter();
stats.createSnapshot().dump(new PrintWriter(writer));
exception = new RuntimeException(writer.toString(), e);
dispatcher.dispatchFailed(this);
} catch (Exception e) {
exception = e;
dispatcher.dispatchFailed(this);
} finally {
Thread.currentThread().setName(Utils.THREAD_IDLE_NAME);
}
}複製代碼
代碼雖長,核心只有hunt()
一句,捕獵開始!BitmapHunter
這個獵人開始狩獵Bitmap
了。
Bitmap hunt() throws IOException {
Bitmap bitmap = null;
if (shouldReadFromMemoryCache(memoryPolicy)) {
bitmap = cache.get(key);
if (bitmap != null) {
stats.dispatchCacheHit();
loadedFrom = MEMORY;
if (picasso.loggingEnabled) {
log(OWNER_HUNTER, VERB_DECODED, data.logId(), "from cache");
}
return bitmap;
}
}
data.networkPolicy = retryCount == 0 ? NetworkPolicy.OFFLINE.index : networkPolicy;
RequestHandler.Result result = requestHandler.load(data, networkPolicy);
if (result != null) {
loadedFrom = result.getLoadedFrom();
exifRotation = result.getExifOrientation();
bitmap = result.getBitmap();
// If there was no Bitmap then we need to decode it from the stream.
if (bitmap == null) {
InputStream is = result.getStream();
try {
bitmap = decodeStream(is, data);
} finally {
Utils.closeQuietly(is);
}
}
}
if (bitmap != null) {
if (picasso.loggingEnabled) {
log(OWNER_HUNTER, VERB_DECODED, data.logId());
}
stats.dispatchBitmapDecoded(bitmap);
if (data.needsTransformation() || exifRotation != 0) {
synchronized (DECODE_LOCK) {
if (data.needsMatrixTransform() || exifRotation != 0) {
bitmap = transformResult(data, bitmap, exifRotation);
if (picasso.loggingEnabled) {
log(OWNER_HUNTER, VERB_TRANSFORMED, data.logId());
}
}
if (data.hasCustomTransformations()) {
bitmap = applyCustomTransformations(data.transformations, bitmap);
if (picasso.loggingEnabled) {
log(OWNER_HUNTER, VERB_TRANSFORMED, data.logId(), "from custom transformations");
}
}
}
if (bitmap != null) {
stats.dispatchBitmapTransformed(bitmap);
}
}
}
return bitmap;
}複製代碼
這個方法很長,最終返回咱們所指望的Bitmap
對象,說明這一個方法正是真正從各個渠道獲取Bitmap
對象的方法。咱們看到,這裏又進行了一次內存緩存的檢查,避免兩次請求相同圖片的重複加載,沒有的話,利用RequestHandler
對象進行加載。RequestHandler
是一個抽象類,load()
方法抽象,由於根據不一樣圖片的加載要執行不一樣的操做,好比從網絡中獲取,從resource中獲取,從聯繫人中獲取,從MediaStore獲取等,這裏運用了模板方法的模式,將一些方法將邏輯相同的方法公用,具體的加載細節子類決定,最終都返回Result
對象。
根據不一樣的RequestHandler
實例,咱們可能能夠直接獲取一個Bitmap對象,也可能只獲取一段流InputStream,好比網絡加載圖片時。若是結果以流的形式提供,那麼Picasso會自動幫咱們將流解析成Bitmap對象。以後根據咱們以前經過RequestCreator
對Request
的配置,決定是否對Bitmap對象進行變換,具體實現都大同小異,利用強大的Matrix,最後返回Bitmap。
以後咱們再回到run()
方法,假設這裏咱們成功獲取到了Bitmap
,那麼是怎麼傳遞的呢?這裏仍是利用了Dispatcher
if (result == null) {
dispatcher.dispatchFailed(this);
} else {
dispatcher.dispatchComplete(this);
}複製代碼
和以前同樣,Dispatcher
內部經過Handler發送了一個特定的消息,而後執行。
void performComplete(BitmapHunter hunter) {
if (shouldWriteToMemoryCache(hunter.getMemoryPolicy())) {
cache.set(hunter.getKey(), hunter.getResult());
}
hunterMap.remove(hunter.getKey());
batch(hunter);
if (hunter.getPicasso().loggingEnabled) {
log(OWNER_DISPATCHER, VERB_BATCHED, getLogIdsForHunter(hunter), "for completion");
}
}複製代碼
上面就是它具體作的事,這裏着重看batch()
方法,相信這個方法必定是經過某種方式將獵人狩獵的Bitmap給上交給國家了。
private void batch(BitmapHunter hunter) {
if (hunter.isCancelled()) {
return;
}
batch.add(hunter);
if (!handler.hasMessages(HUNTER_DELAY_NEXT_BATCH)) {
handler.sendEmptyMessageDelayed(HUNTER_DELAY_NEXT_BATCH, BATCH_DELAY);
}
}複製代碼
咦?這裏仍是沒有將Bitmap
發出,可是經過handler發出了一個消息,恩恩,這都是套路了。這裏有個設計很棒的地方,這裏發送消息用了延時,爲何呢?爲何不立刻發送呢?Picasso是將圖片一批批的送出去的,每次發送的間隔爲200ms,間隔不長也不短,這樣有什麼好處呢,好處就是若是咱們看到一部分圖片是一塊兒加載出來的,而不是一張一張加載出來的。這個設計好貼心。前面根據網絡狀況決定線程池大小的作法也好貼心,Jake大大真是超棒!
找到了這個消息收到後具體執行的方法了
void performBatchComplete() {
List<BitmapHunter> copy = new ArrayList<BitmapHunter>(batch);
batch.clear();
mainThreadHandler.sendMessage(mainThreadHandler.obtainMessage(HUNTER_BATCH_COMPLETE, copy));
logBatch(copy);
}複製代碼
這裏就體現了Handler線程通訊與切換的強大之處了,這裏經過將一批Bitmap對象交給了處理主線程消息的Handler處理,這樣咱們獲取到Bitmap並加載到ImageView上就是在主線程了操做的了,咱們去看看這個Handler作了什麼。這個Handler是在Dispatcher構造時傳進來的,在Picasso類中定義。哈哈,正不虧是掌握一切權與利的對象,最苦最累的活讓別人去作,本身作最風光體面的事。
static final Handler HANDLER = new Handler(Looper.getMainLooper()) {
@Override public void handleMessage(Message msg) {
switch (msg.what) {
case HUNTER_BATCH_COMPLETE: {
@SuppressWarnings("unchecked") List<BitmapHunter> batch = (List<BitmapHunter>) msg.obj;
//noinspection ForLoopReplaceableByForEach
for (int i = 0, n = batch.size(); i < n; i++) {
BitmapHunter hunter = batch.get(i);
hunter.picasso.complete(hunter);
}
break;
}
……
}
};複製代碼
很少說了,咱們來看看代碼,恩,很清楚的針對每一個BitmapHunter執行了complete()
方法,那就去看看嘍。
void complete(BitmapHunter hunter) {
Action single = hunter.getAction();
List<Action> joined = hunter.getActions();
boolean hasMultiple = joined != null && !joined.isEmpty();
boolean shouldDeliver = single != null || hasMultiple;
if (!shouldDeliver) {
return;
}
Uri uri = hunter.getData().uri;
Exception exception = hunter.getException();
Bitmap result = hunter.getResult();
LoadedFrom from = hunter.getLoadedFrom();
if (single != null) {
deliverAction(result, from, single);
}
if (hasMultiple) {
//noinspection ForLoopReplaceableByForEach
for (int i = 0, n = joined.size(); i < n; i++) {
Action join = joined.get(i);
deliverAction(result, from, join);
}
}
if (listener != null && exception != null) {
listener.onImageLoadFailed(this, uri, exception);
}
}複製代碼
這個方法從BitmapHunter
中獲取Action
對象,若這個Action
成功完成,會執行Action的complete()
方法,並將結果傳入,很明顯這是一個回調方法。Action也是一個抽象類,根據咱們的Target
不一樣會生成不一樣的Action對象,好比into(target)
方法會生成TargetAction
對象,into(imageView)
會生成ImageViewAction
對象,調用fetch()方法會產生FetchAction
對象。這裏因爲咱們是以將圖片加載到ImageView中來分析的,因此咱們只分析一下ImageViewAction
的代碼。
每一個Action
的子類要實現兩個回調方法,一個error()
,在圖片加載失敗時觸發,一個complete()
,在圖片成功加載時調用。
@Override public void complete(Bitmap result, Picasso.LoadedFrom from) {
if (result == null) {
throw new AssertionError(
String.format("Attempted to complete action with no result!\n%s", this));
}
ImageView target = this.target.get();
if (target == null) {
return;
}
Context context = picasso.context;
boolean indicatorsEnabled = picasso.indicatorsEnabled;
PicassoDrawable.setBitmap(target, context, result, from, noFade, indicatorsEnabled);
if (callback != null) {
callback.onSuccess();
}
}
@Override public void error() {
ImageView target = this.target.get();
if (target == null) {
return;
}
Drawable placeholder = target.getDrawable();
if (placeholder instanceof AnimationDrawable) {
((AnimationDrawable) placeholder).stop();
}
if (errorResId != 0) {
target.setImageResource(errorResId);
} else if (errorDrawable != null) {
target.setImageDrawable(errorDrawable);
}
if (callback != null) {
callback.onError();
}
}複製代碼
恩,很清楚的看到成功時就正確顯示圖片,失敗時則根據是否設置了錯誤圖來選擇是否加載錯誤圖。這裏用到了一個PicassoDrawable
的類,這個類繼承自BitmapDrawable
類,經過它,能夠很方便的實現淡入淡出效果。
到這裏,咱們分析完了一次成功的Picasso圖片加載過程,固然咱們不可能每次都成功,也會發生各類錯誤,或者咱們暫停了加載又繼續加載等,這些Picasso都做出了不一樣的處理,線程之間經過Handler進行通訊。這裏就不一一詳述了。相信你們本身會去看的。
不得不說,Picasso真是一個優雅的圖片加載庫,用極少的代碼完成了了不得的事,從API的設計到代碼的編寫,無不體現了做者的深思熟慮和貼心,運用多用設計模式巧妙地提供極其簡單的調用接口,隱藏背後複雜的實現。經過對Picasso的使用與源碼分析,我學到了不少,但願我之後也能夠設計出這麼優雅實用的庫或框架。
天道酬勤,我要加油!