注:文章轉載自文章轉載自RowandJJ的博客:http://blog.csdn.net/chdjj/article/details/49964901
下面是我在學習Picasso
過程中做的筆記.
關於圖片加載庫的思考
我們爲什麼要用圖片庫呢?
圖片庫通常會對圖片加載邏輯進行封裝、優化,比如多級緩存/異步線程調度/圖片壓縮變換,有了這些特性,開發者不再需要關注內存佔用、
OOM、網絡請求等問題,而只需關注業務本身的邏輯,這對提高生產效率還是很有幫助的。
我通過調研一些圖片庫,發現一個圖片加載庫通常有以下模塊:
- 請求分發模塊。負責封裝請求,對請求進行優先級排序,並按照類型進行分發。
- 緩存模塊。通常包括一個二級的緩存,內存緩存、磁盤緩存。並預置多種緩存策略。
- 下載模塊。負責下載網絡圖片。
- 監控模塊。負責監控緩存命中率、內存佔用、加載圖片平均耗時等。
- 圖片處理模塊。負責對圖片進行壓縮、變換等處理。
- 本地資源加載模塊。負責加載本地資源,如assert、drawable、sdcard等。
- 顯示模塊。負責將圖片輸出顯示。
Android平臺圖片加載庫現狀
目前社區主流的圖片加載庫有Universal ImageLoader,Picasso,Volley,Fresco,Glide.
Picasso簡介
A powerful image downloading and caching library for Android,developed by Square
Picasso的特性
- 絕對是最輕量的圖片加載庫,120kb.
- 自帶監控功能,可以檢測cache hit/內存大小等等數據
- 圖片預加載
- 線程併發數依網絡狀態變化而變化、優先級調度
- 圖片變換
- 圖片壓縮、自適應
- 易擴展
Picasso的使用
- 加載一張網絡圖片到
ImageView
上
Picasso.with(context)
.load(url)
.placeholder(R.drawable.user_placeholder)
.error(R.drawable.user_placeholder_error)
.into(imageView);//此種策略並不會壓縮圖片
- 預加載一張圖片
Picasso.with(this).load(URL).fetch();
Picasso.with(this).load(URL).fetch(Callback);
- 1
- 2
注意哦,如果你以以下面這種方式加上圖形變換preload的話:
Picasso.with(this).load(URL).rotate(20).fetch();
- 1
再下面這種方式是取不到preload的圖片的,因爲預緩存的是經過變換後的圖片,它的cachekey會有rotation
標識
Picasso.with(this).load(URL).into(imageView);
- 1
當然我說的是preload到內存中的那份經過旋轉的圖片,http會緩存旋轉前的圖片到磁盤(支持緩存的情況下),所以最終還是可以從磁盤緩存
拿到圖片的。
- 替換默認的
Picasso
Picasso p = new Picasso.Builder(this).executor().downloader(downloader).memoryCache(cache).build();
Picasso.setSingletonInstance(p);
- 1
- 2
- 同步call
new AsyncTask<Void,Void,Bitmap>(){
@Override
protected Bitmap doInBackground(Void... params) {
try {
return Picasso.with(PicassoTestActivity.this).load(URL).get();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
@Override
protected void onPostExecute(Bitmap bitmap) {
if(bitmap != null){
imageView.setImageBitmap(bitmap);
}
}
}.execute();
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
注意,必須在異步線程調用,否則crash,另外,這個結果並不會緩存到內存裏面,所以慎用。
- 自適應
Picasso.with(TestImageActivity.this).load(url).fit().into(imageview);
- 1
fit
方法的意思是,讓圖片的寬高恰好等於imageView
的寬高.前提是你的imageView控件不能設置成wrap_content
,也就是必須
有大小才行。另外,如果使用了fit
方法,那麼就不能調用resize
.
- 壓縮到指定尺寸
Picasso.with(TestImageActivity.this).load(url).resize(widthPixel,heightPixel).centerInside().into(imageView);
Picasso.with(TestImageActivity.this).load(URL).resizeDimen(R.dimen.width,R.dimen.height).centerCrop().into(iv);
- 1
- 2
resize
後面通常接centerInside
或者centerCrop
。注意這跟ImageView的scaleTyoe
沒有關係,僅僅指的是圖片的縮放方式。
比如如下代碼,iv不壓縮,iv_2寬高壓縮到40dp並且指定爲centerInside
.
<ImageView
android:id="@+id/iv"
android:background="#000"
android:layout_width="100dp"
android:layout_height="100dp"/>
<ImageView
android:layout_below="@id/iv"
android:id="@+id/iv_2"
android:background="#000"
android:layout_width="100dp"
android:layout_height="100dp"/>
Picasso.with(TestImageActivity.this).load(URL).into(iv);
Picasso.with(TestImageActivity.this).load(URL).resizeDimen(R.dimen.width/*40dp*/, R.dimen.height/*40dp*/).centerInside().into(iv_2);
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
最終顯示結果如下:
可以很明顯看到下面的圖模糊許多,這是因爲圖片被壓縮了,但是顯示的時候又被ImageView拉伸了(默認scaleType是fitCenter),
要想不顯示拉伸的圖,可以給iv_2增加scaleType="centerInside"
,效果如下:
- 圖形變換
Picasso.with(TestImageActivity.this).load(URL).rotate(20).into(iv);
//自定義變換
Picasso.with(TestImageActivity.this).load(URL).transform(new Transformation() {
@Override
public Bitmap transform(Bitmap source) {//從原圖中間裁剪一個正方形
int size = Math.min(source.getWidth(), source.getHeight());
int x = (source.getWidth() - size) / 2;
int y = (source.getHeight() - size) / 2;
Bitmap result = Bitmap.createBitmap(source, x, y, size, size);
if (result != source) {
source.recycle();
}
return result;
}
@Override
public String key() {
return "square()";
}
}).into(iv);
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
各種圖形變換:https://github.com/wasabeef/picasso-transformations
- 暫停/重啓請求任務
通常在滑動列表的時候需要暫停請求
Picasso.with(context).load(URL).tag(context);
public class SampleScrollListener implements AbsListView.OnScrollListener {
private final Context context;
public SampleScrollListener(Context context) {
this.context = context;
}
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
final Picasso picasso = Picasso.with(context);
if (scrollState == SCROLL_STATE_IDLE || scrollState == SCROLL_STATE_TOUCH_SCROLL) {
picasso.resumeTag(context);
} else {
picasso.pauseTag(context);
}
}
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
int totalItemCount) {
// Do nothing.
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
Picasso源碼分析
整體架構
盜用下Trinea的圖:
Picasso中的核心類包括
Picasso
、Dispatcher
、BitmapHunter
、RequestHandler
、Request
、Action
、Cache
等.Picasso
類是一個負責圖片下載、變換、緩存的管理器,當它收到一個圖片下載請求的時候,它會創建Request
並提交給Dispatcher
,
Dispatcher
會尋找對應的處理器RequestHandler
,並將請求與該處理器一起提交給線程池執行,圖片獲取成功後,最終會交給
PicassoDrawable
顯示到Target
上。
它將一張圖片的加載過程分爲八步,依次爲:
創建->入隊->執行->解碼->變換->批處理->完成->分發->顯示(可選)
也可以從日誌中看到這個過程:
11-05 10:39:00.942 2952-2952/com.taobao.paimainews D/Picasso: Main created [R0] Request{http://ww3.sinaimg.cn/mw600/006g34NHgw1exj5c4hmfvj30hs0qoqff.jpg resize(90,300) centerInside rotation(30.0) ARGB_8888}
11-05 10:39:00.981 2952-3109/com.taobao.paimainews D/Picasso: Dispatcher enqueued [R0]+40ms
11-05 10:39:00.993 2952-3193/com.taobao.paimainews D/Picasso: Hunter executing [R0]+50ms
11-05 10:39:01.038 2952-3193/com.taobao.paimainews D/Picasso: Hunter decoded [R0]+97ms
11-05 10:39:01.041 2952-3193/com.taobao.paimainews D/Picasso: Hunter transformed [R0]+100ms
11-05 10:39:01.042 2952-3109/com.taobao.paimainews D/Picasso: Dispatcher batched [R0]+101ms for completion
11-05 10:39:01.279 2952-2952/com.taobao.paimainews D/Picasso: Main completed [R0]+338ms from DISK
11-05 10:39:01.280 2952-3109/com.taobao.paimainews D/Picasso: Dispatcher delivered [R0]+338ms
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
下面是Picasso
的類圖:
代碼分析
版本:2.5.2
Picasso
類是整個圖片加載器的入口,負責初始化各個模塊,配置相關參數等等。
Picasso.with()
方法用於創建全局唯一的Picasso實例,爲了確保唯一,使用了單例模式
。
Picasso#with()
static volatile Picasso singleton = null;
public static Picasso with(Context context) {
if (singleton == null) {
synchronized (Picasso.class) {
if (singleton == null) {
singleton = new Builder(context).build();
}
}
}
return singleton;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
with
方法內部通過Builder
模式創建Picasso實例,這樣做的好處是簡潔清晰,通常在構造器參數很多的時候使用。
build
方法會最終創建Picasso
實例:
Picasso#Builder#build()
public Picasso build() {
Context context = this.context;
if (downloader == null) {
downloader = Utils.createDefaultDownloader(context);
}
if (cache == null) {
cache = new LruCache(context);
}
if (service == null) {
service = new PicassoExecutorService();
}
if (transformer == null) {
transformer = RequestTransformer.IDENTITY;
}
Stats stats = new Stats(cache);
Dispatcher dispatcher = new Dispatcher(context, service, HANDLER, downloader, cache, stats);
return new Picasso(context, dispatcher, cache, listener, transformer, requestHandlers, stats,
defaultBitmapConfig, indicatorsEnabled, loggingEnabled);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
此方法做了如下基本配置:
- 使用默認的緩存策略,內存緩存基於
LruCache
,磁盤緩存基於http緩存,HttpResponseCache
- 創建默認的下載器
- 創建默認的線程池(3個worker線程)
- 創建默認的
Transformer
,這個Transformer
什麼事情也不幹,只負責轉發請求 - 創建默認的監控器(
Stats
),用於統計緩存命中率、下載時長等等 - 創建默認的處理器集合,即
RequestHandlers
.它們分別會處理不同的加載請求
處理器集合的初始化在Picasso的構造器中:
Picasso構造器
allRequestHandlers.add(new ResourceRequestHandler(context));
if (extraRequestHandlers != null) {
allRequestHandlers.addAll(extraRequestHandlers);
}
allRequestHandlers.add(new ContactsPhotoRequestHandler(context));
allRequestHandlers.add(new MediaStoreRequestHandler(context));
allRequestHandlers.add(new ContentStreamRequestHandler(context));
allRequestHandlers.add(new AssetRequestHandler(context));
allRequestHandlers.add(new FileRequestHandler(context));
allRequestHandlers.add(new NetworkRequestHandler(dispatcher.downloader, stats));
requestHandlers = Collections.unmodifiableList(allRequestHandlers);
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
從命名就可以看出來,可以從網絡、file、assert、contactsphoto等地方加載圖片.
另,Picasso支持增加自己的處理器.
load()
方法用於從不同地方加載圖片,比如網絡、resource、File等,該方法內部邏輯很簡單,只是創建了一個RequestCreator
Picasso#load()
public RequestCreator load(Uri uri) {
return new RequestCreator(this, uri, 0);
}
- 1
- 2
- 3
RequestCreator
從名字就可以知道這是一個封裝請求的類,請求在Picasso中被抽象成Request
。RequestCreator
類提供了
諸如placeholder
、tag
、error
、memoryPolicy
、networkPolicy
等方法.
由於可配置項太多,所以Request
也使用了Builder
模式:
RequestCreator構造器
RequestCreator(Picasso picasso, Uri uri, int resourceId) {
if (picasso.shutdown) {
throw new IllegalStateException(
"Picasso instance already shut down. Cannot submit new requests.");
}
this.picasso = picasso;
this.data = new Request.Builder(uri, resourceId, picasso.defaultBitmapConfig);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
那麼可想而知into
方法一定會去將Request創建,並丟到線程池或者分發器中執行。into
方法有多種重載,因爲Picasso不僅僅可以
將圖片加載到ImageView
上,還可以加載到Target
或者RemoteView
上.
這裏選取imageView
作爲分析對象,該方法代碼如下:
RequestCreator#into()
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()) {//檢查是否設置uri或者resID
//如果沒有設置當然取消請求
picasso.cancelRequest(target);
if (setPlaceholder) {
setPlaceholder(target, getPlaceholderDrawable());
}
return;
}
if (deferred) {//是否調用了fit(),如果是,代表需要將image調整爲ImageView的大小
if (data.hasSize()) {//不能與resize一起用
throw new IllegalStateException("Fit cannot be used with resize.");
}
//既然要適應ImageView,肯定需要拿到ImageView大小
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 request = createRequest(started);
String requestKey = createKey(request);
if (shouldReadFromMemoryCache(memoryPolicy)) {//是否需要在緩存裏面先查找
Bitmap bitmap = picasso.quickMemoryCacheCheck(requestKey);
if (bitmap != null) {//cache hit
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,將任務交給dispatcher
if (setPlaceholder) {
setPlaceholder(target, getPlaceholderDrawable());
}
Action action =
new ImageViewAction(picasso, target, request, memoryPolicy, networkPolicy, errorResId,
errorDrawable, requestKey, tag, callback, noFade);
picasso.enqueueAndSubmit(action);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
邏輯註釋寫的很清楚了,into
方法會先從緩存裏面查找圖片,如果找不到的話,則會創建Action
即一個加載任務,交給Dispatcher執行。
那我們就來看看picasso.enqueueAndSubmit
方法做了什麼.
在這之前,先來看下Action
是什麼鬼,爲什麼有了Request
還要Action
.
先看Request
有哪些屬性:
int id;
long started;
int networkPolicy;
public final Uri uri;
public final int resourceId;
public final String stableKey;
public final List<Transformation> transformations;
public final int targetWidth;
public final int targetHeight;
public final boolean centerCrop;
public final boolean centerInside;
public final boolean onlyScaleDown;
public final float rotationDegrees;
public final float rotationPivotX;
public final float rotationPivotY;
public final boolean hasRotationPivot;
public final Bitmap.Config config;
public final Priority priority;
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
再看Action
的屬性:
final Picasso picasso;
final Request request;
final WeakReference<T> target;
final boolean noFade;
final int memoryPolicy;
final int networkPolicy;
final int errorResId;
final Drawable errorDrawable;
final String key;
final Object tag;
boolean willReplay;
boolean cancelled;
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
Request
關注的是請求本身,比如請求的源、id、開始時間、圖片變換配置、優先級等等,而Action
則代表的是一個加載任務,所以不僅需要
Request
對象的引用,還需要Picasso
實例,是否重試加載等等
Action
有個需要關注的點,那就是WeakReference<T> target
,它持有的是Target(比如ImageView..)的弱引用,這樣可以保證加載時間很長的情況下
也不會影響到Target的回收了.
好的,那回到剛纔的思路,我們開始分析picasso.enqueueAndSubmit
方法:
picasso#enqueueAndSubmit()
final Map<Object, Action> targetToAction;
...
this.targetToAction = new WeakHashMap<Object, Action>();
...
void enqueueAndSubmit(Action action) {
Object target = action.getTarget();
if (target != null && targetToAction.get(target) != action) {
// This will also check we are on the main thread.
cancelExistingRequest(target);
targetToAction.put(target, action);
}
submit(action);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
它會先從action任務上拿到對應target,也就是imageView,然後從weakHashMap中通過這個imageView索引到對應的action,如果
發現這個action跟傳進來的action不一樣的話,那就取消掉之前的加載任務。最後將當前加載任務提交
.
跟進submit
發現最終調用的是Dispatcher
的dispatchSubmit(action)
方法.這個Dispatcher
即任務分發器,它是在
Picasso
實例創建的時候初始化的.。
那我們在看dispatchSubmit
方法之前,必然得了解下Dispatcher
.
Picasso.Builder.build()
Dispatcher dispatcher = new Dispatcher(context, service, HANDLER, downloader, cache, stats);
- 1
每一個Dispatcher
都需要關聯線程池(service)、下載器(downloader)、主線程的Handler(HANDLER)、緩存(cache)、
監控器(stats).
這裏先看線程池,Picasso
默認的線程池叫PicassoExecutorService
,它繼承自ThreadPoolExecutor
,默認線程數量爲
3.但是PicassoExecutorService
的特性是可以根據網絡情況調整線程數量,wifi下是4個線程,而2g網只有一個線程。具體是
通過在Dispatcher
中註冊了監聽網絡變化的廣播接收者。
另外,PicassoExecutorService
中還有一個很重要的方法叫submit
,它會去執行一個runnable
.
好的,我們回到Dispatcher
,這裏還需要關注的是Dispatcher
中有個內部類叫DispatcherHandler
,注意哦,
這個handler是Dispatcher
自己的,而不是構造器傳進來的。而且,這個handler綁定的是子線程的Looper
,爲什麼?請看:
Dispatcher#構造器
this.handler = new DispatcherHandler(dispatcherThread.getLooper(), this);
- 1
而dispatcherThread
則是一個HandlerThread
:
Dispatcher內部類
static class DispatcherThread extends HandlerThread {
DispatcherThread() {
super(Utils.THREAD_PREFIX + DISPATCHER_THREAD_NAME, THREAD_PRIORITY_BACKGROUND);
}
}
- 1
- 2
- 3
- 4
- 5
也就是說,這個handler的消息處理是在子線程進行的!這樣就可以避免阻塞主線程的消息隊列啦!
好的,再回到剛纔的問題,來看下dispatchSubmit
方法(不知道大家有沒有看暈。。。。):
Dispatcher#dispatchSubmit
void dispatchSubmit(Action action) {
handler.sendMessage(handler.obtainMessage(REQUEST_SUBMIT, action));
}
- 1
- 2
- 3
不用看都知道會發消息給handler。而handler收到這個消息之後調用了這個方法:
dispatcher.performSubmit(action);
- 1
果斷跟進去:
Dispatcher#performSubmit
void performSubmit(Action action, boolean dismissFailed) {//注意哦,這裏已經不在主線程了,而是在dispatcher線程(HandlerThread)
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
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());
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
首先創建了一個BitmapHunter
,它繼承自Runnable
,可以被線程池調用。然後判斷線程池有沒有關閉,如果沒有的話,
就會將這個bitmapHunter
丟到線程池裏面,即調用剛纔說的submit
方法。
我們先看下forRequest
方法裏面幹了什麼:
BitmapHunter#forRequest
static BitmapHunter forRequest(Picasso picasso, Dispatcher dispatcher, Cache cache, Stats stats,
Action action) {
Request request = action.getRequest();
List<RequestHandler> requestHandlers = picasso.getRequestHandlers();
// Index-based loop to avoid allocating an iterator.
//noinspection ForLoopReplaceableByForEach
for (int i = 0, count = requestHandlers.size(); i < count; i++) {
RequestHandler requestHandler = requestHandlers.get(i);
if (requestHandler.canHandleRequest(request)) {
return new BitmapHunter(picasso, dispatcher, cache, stats, action, requestHandler);
}
}
//沒有人能處理這個請求,那麼交給ERRORING_HANDLER,它會直接拋異常
return new BitmapHunter(picasso, dispatcher, cache, stats, action, ERRORING_HANDLER);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
還記得大明湖畔的Picasso
麼?在它的構造器中創建了若干RequestHandler
,用於處理不同的加載請求,在這裏,它會遍歷
這些requestHandler
,看誰可以處理當前請求,如果發現了,那就創建BitmapHandler
,並把這個requestHandler
傳進去,
線程池在收到BitmapHunter
之後,會調用其run
方法,那麼我們就來看下:
BitmapHunter#run
@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);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
核心邏輯是由hunt
方法完成的,下面一堆catch語句分別捕捉不同的異常然後上報給dispatcher進行處理。
而hunt方法裏面肯定會調用RequestHandler的load
方法:
BitmapHunter#hunt
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;
}
}
//緩存沒有命中的話,再調用requestHandler.load
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
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;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
這裏假設是一個網絡請求,那麼最終NetworkRequestHandler
會處理請求:
NetworkRequestHandler#load
@Override public Result load(Request request, int networkPolicy) throws IOException {
//這個downloader也是Dispatcher創建的時候傳進來的
Response response = downloader.load(request.uri, request.networkPolicy);
if (response == null) {
return null;
}
//判斷是從緩存還是網絡拿的
Picasso.LoadedFrom loadedFrom = response.cached ? DISK : NETWORK;
//從響應中拿到bitmap
Bitmap bitmap = response.getBitmap();
if (bitmap != null) {
return new Result(bitmap, loadedFrom);
}
//如果是從網絡返回的,那麼拿到的是流對象
InputStream is = response.getInputStream();
if (is == null) {
return null;
}
// Sometimes response content length is zero when requests are being replayed. Haven't found
// root cause to this but retrying the request seems safe to do so.
if (loadedFrom == DISK && response.getContentLength() == 0) {
Utils.closeQuietly(is);
throw new ContentLengthException("Received response with 0 content-length header.");
}
if (loadedFrom == NETWORK && response.getContentLength() > 0) {
stats.dispatchDownloadFinished(response.getContentLength());
}
//將結果封裝返回
return new Result(is, loadedFrom);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
現在我們關注下這個downloader
的前世今生,如果用戶沒有自定義的話,那將使用默認downloader
:
Picasso#Builder#build()
downloader = Utils.createDefaultDownloader(context);
- 1
Utils#createDefaultDownloader
static Downloader createDefaultDownloader(Context context) {
try {
Class.forName("com.squareup.okhttp.OkHttpClient");
return OkHttpLoaderCreator.create(context);
} catch (ClassNotFoundException ignored) {
}
return new UrlConnectionDownloader(context);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
首先反射下,看有沒有依賴okhttp
,如果依賴的話,那就使用OkHttpClient
嘍,否則就使用默認的HttpUrlConnection
了。
注:其實從4.4開始,okhttp
已經作爲HttpUrlConnection
的實現引擎了。
可以從picasso
的pom文件裏面看到,okhttp是optional的:
<dependency>
<groupId>com.squareup.okhttp</groupId>
<artifactId>okhttp</artifactId>
<optional>true</optional>
</dependency>
- 1
- 2
- 3
- 4
- 5
以UrlConnectionDownloader
爲例,看下它的load
方法:
@Override public Response load(Uri uri, int networkPolicy) throws IOException {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
installCacheIfNeeded(context);
}
HttpURLConnection connection = openConnection(uri);
connection.setUseCaches(true);
if (networkPolicy != 0) {
String headerValue;
if (NetworkPolicy.isOfflineOnly(networkPolicy)) {
headerValue = FORCE_CACHE;
} else {
StringBuilder builder = CACHE_HEADER_BUILDER.get();
builder.setLength(0);
if (!NetworkPolicy.shouldReadFromDiskCache(networkPolicy)) {
builder.append("no-cache");
}
if (!NetworkPolicy.shouldWriteToDiskCache(networkPolicy)) {
if (builder.length() > 0) {
builder.append(',');
}
builder.append("no-store");
}
headerValue = builder.toString();
}
connection.setRequestProperty("Cache-Control", headerValue);
}
int responseCode = connection.getResponseCode();
if (responseCode >= 300) {
connection.disconnect();
throw new ResponseException(responseCode + " " + connection.getResponseMessage(),
networkPolicy, responseCode);
}
long contentLength = connection.getHeaderFieldInt("Content-Length", -1);
boolean fromCache = parseResponseSourceHeader(connection.getHeaderField(RESPONSE_SOURCE));
return new Response(connection.getInputStream(), fromCache, contentLength);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
注意哦,Disk Cache
功能是在這裏做掉的,它基於Http
語義來判斷是否緩存.
另,返回的是inputStream
流,而不是Bitmap
對象.
好的,現在我們回到BitmapHunter#run()
,它在拿到結果後會將結果交給dispatcher
BitmapHunter#run()
if (result == null) {
dispatcher.dispatchFailed(this);
} else {
dispatcher.dispatchComplete(this);
}
- 1
- 2
- 3
- 4
- 5
我們看dispatcher.dispatchComplete(this)
,它會把消息發給自己內部的handler,也就是剛纔說的Looper在子線程
,handler將做如下處理:
的handler
BitmapHunter hunter = (BitmapHunter) msg.obj;
dispatcher.performComplete(hunter);
- 1
- 2
注意哦,BitmapHunter會持有網絡請求回來的Bitmap
引用.來看下performComplete
:
Dispatcher#performComplete
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");
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
首先會根據事先設置的緩存策略決定是否將結果加到內存緩存。然後調用batch方法,從名字就可以知道,這個方法會把結果暫存,
然後批量處理(等待200ms),這樣做也是爲了防止短時間大量任務阻塞消息隊列。到時間後,就會執行performBatchComplete
,
此方法會將這個批次的所有結果一次性發給主線程的Handler,也就是Picasso
中定義的Handler:
Dispatcher#performBatchComplete
void performBatchComplete() {
List<BitmapHunter> copy = new ArrayList<BitmapHunter>(batch);
batch.clear();
mainThreadHandler.sendMessage(mainThreadHandler.obtainMessage(HUNTER_BATCH_COMPLETE, copy));
logBatch(copy);
}
- 1
- 2
- 3
- 4
- 5
- 6
主線程收到消息後會進行處理:
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);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
對batch中每個BitmapHunter調用complete
方法,而complete
方法會調用deliverAction
方法,最終其實調用的是具體
action的complete
方法,如果是ImageView
的話,那就是ImageViewAction
的complete
方法:
ImageViewAction#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();
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
注意看這一句,ImageView target = this.target.get()
,因爲target
是ImageView
的弱引用,在下載過程中,
ImageView
可能已經被銷燬了,所以這裏要做下判斷。
如果沒有被回收,那麼圖片最終通過PicassoDrawable.setBitmap()
方法被設置到ImageView
上.
這個PicassoDrawable
提供了fade
動畫.
好了,分析基本完畢,下面是整個流程的時序圖.
簡單總結下當我們執行
Picasso.with(context).load(url).into(imageview)
時,首先會構造Picasso實例,然後會
根據url創建請求,然後請求會被交給Dispatcher
,Dispatcher
將在子線程對請求任務進行調度,將請求任務交給線程池
執行,執行完畢後,將結果傳給主線程的handler,最後在主線程中將圖片設置到ImageView上.
下面是我在測試的時候截的圖,注意每張圖所在線程都不一樣哦:
1.主線程中調用dispatchSubmit
2.Dispatcher線程對請求任務進行調度
3.線程池中執行請求任務
其他需要關注的點
關於緩存策略
Picasso的緩存是內存緩存+磁盤緩存,內存緩存基於
LruCache
類,可配置替換。磁盤緩存依賴於http緩存,不可配置。
先看內存緩存.內存緩存比較簡單,是通過LinkedHashMap
實現.
讀緩存時機
:生成了請求Request
對象,準備創建Action
加載任務之前,會先去緩存裏面查找下.RequestCreator#into
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; } }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
寫緩存時機
:圖片從網絡或者其他地方加載成功後,即在BitmapHunter
的run方法執行結束的時候.
Dispatcher#performCompleteif (shouldWriteToMemoryCache(hunter.getMemoryPolicy())) { cache.set(hunter.getKey(), hunter.getResult()); }
- 1
- 2
- 3
- 4
- 5
注意哦,緩存的是經過壓縮之後的圖片(如果你使用了
fit
或者resize
方法的話),
再看磁盤緩存。
如果你是使用UrlConnectionDownloader
的話,那很不幸,緩存只在Api>14上生效,因爲緩存依賴於HttpResponseCache
.
如果你依賴了okhttp
,那麼緩存策略始終是有效的。另外需要說明的是,既然是http緩存,那麼緩存的可用性依賴於http響應是
否允許緩存,也就是說得看響應中是否攜帶Cache-Control
、Expires
等字段.對於這塊不瞭解的話,可以參考我的這篇文章:
HttpCache in android
還有一點,緩存的路徑是 應用cache目錄/picasso-cache 文件夾.具體代碼參考Utils.createDefaultCacheDir
方法關於預加載
首先要注意的是
Callback
是一個強引用,如果你使用帶Callback
的重載形式的話,只有當Request
結束的時候纔會釋放
引用,在此期間你的Activity
/Fragment
等組件引用不會被釋放.因此你需要注意內存泄露的情形.怎麼實現?很簡單拉,調
fetch
的時候創建了FetchAction
,然後其他流程上面描述的一樣,最終在Dispatcher.performComplete
的時候將結果寫入內存緩存,結果回傳到主線程的時候,調用了FetchAction
的complete
方法,這裏面不對Bitmap
做
任何處理就行拉:FetchAction#complete
``` @Override void complete(Bitmap result, Picasso.LoadedFrom from) { if (callback != null) { callback.onSuccess(); } } ```
關於圖形變換
圖形變換在
Picasso
中被抽象成Transformation
接口,具體的變換操作由transform
方法實現.Request
維護一個
圖形變換的列表List<Transformation>
,當圖片加載成功後,BitmapHunter
中將會遍歷這個變換集合,依次進行變換,
最後返回變換後的bitmap
.恩,其實是一個回調的思想,將操作封裝到接口中交給系統,系統在某個特定時機調用你的接口。具體代碼:
BitmapHunter#applyCustomTransformations
``` static Bitmap applyCustomTransformations(List<Transformation> transformations, Bitmap result) { for (int i = 0, count = transformations.size(); i < count; i++) { final Transformation transformation = transformations.get(i); Bitmap newResult; try { newResult = transformation.transform(result); } catch (final RuntimeException e) { Picasso.HANDLER.post(new Runnable() { @Override public void run() { throw new RuntimeException( "Transformation " + transformation.key() + " crashed with exception.", e); } }); return null; } .... result = newResult; } return result; } ```
關於CleanupThread
Picasso
類中有一個內部線程叫CleanupThread
,這是一個daemon線程,它的工作是找到那些Target
(比如說ImageView
)已經被回收
但是所對應的Request
請求還在繼續的任務(Action
),找到之後,會取消對應的請求,避免資源浪費.看下代碼:
Picasso#CleanupThread
private static class CleanupThread extends Thread { private final ReferenceQueue<Object> referenceQueue; private final Handler handler; CleanupThread(ReferenceQueue<Object> referenceQueue, Handler handler) {//關聯主線程的handler,refreenceQueue this.referenceQueue = referenceQueue; this.handler = handler; setDaemon(true); setName(THREAD_PREFIX + "refQueue"); } @Override public void run() { Process.setThreadPriority(THREAD_PRIORITY_BACKGROUND); while (true) { try { // Prior to Android 5.0, even when there is no local variable, the result from // remove() & obtainMessage() is kept as a stack local variable. // We're forcing this reference to be cleared and replaced by looping every second // when there is nothing to do. // This behavior has been tested and reproduced with heap dumps. RequestWeakReference<?> remove = (RequestWeakReference<?>) referenceQueue.remove(THREAD_LEAK_CLEANING_MS); Message message = handler.obtainMessage(); if (remove != null) { message.what = REQUEST_GCED; message.obj = remove.action; handler.sendMessage(message); } else { message.recycle(); } } catch (InterruptedException e) { break; } catch (final Exception e) { handler.post(new Runnable() { @Override public void run() { throw new RuntimeException(e); } }); break; } } } void shutdown() { interrupt(); } }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
可以看到它會不斷輪詢
ReferenceQueue
,找到這樣的reference
,就交給handler
,handler
會從reference
中拿到action
,
並取消請求.case REQUEST_GCED: { Action action = (Action) msg.obj; if (action.getPicasso().loggingEnabled) { log(OWNER_MAIN, VERB_CANCELED, action.request.logId(), "target got garbage collected"); } action.picasso.cancelExistingRequest(action.getTarget()); break; }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
那麼這個
ReferenceQueue
又是如何關聯Action
的呢?這個可以從Action
的構造器中拿到答案:this.target =//RequestWeakReference是WeakReference的子類 target == null ? null : new RequestWeakReference<T>(this/*即Action本身*/, target, picasso.referenceQueue);
- 1
- 2
可以看到兩點:
- 每個
Action
都會關聯Picasso
中唯一的referenceQueue
實例; - 每個
RequestWeakReference
都會同時關聯Target
和Action
.
resume/pause
pause
流程如下。
可能會有疑問的地方在於
Dispatcher#performPauseTag
中遍歷所有的hunter,都會調一次cancel
,這似乎會取消所有
的請求。但其實不是這樣的,可以看下BitmapHunter#cancel
方法的代碼:
boolean cancel() { return action == null && (actions == null || actions.isEmpty()) && future != null && future.cancel(false); }
- 1
- 2
- 3
- 4
- 5
- 6
注意到它會判斷
action
是否爲空,如果不爲空就不會取消了。而在Dispatcher#performPauseTag
中會把tag
匹配的
action
與對應的BitmapHunter
解綁(detach
),讓BitmapHunter
的action爲空.所以這並不影響其他任務的執行。resume 流程如下。
其實就是遍歷
pausedActions
,挨個重新交給dispatcher
分發。
作者的提交記錄: https://github.com/square/picasso/pull/665/files#diff-f11286bbae6959a7a5dd74bf99276f1aR229
圖片壓縮
圖片壓縮的原理通常都是利用
BitmapFactory#Options
類,先將injustDecodeBounds
設置爲true,對Bitmap
進行一次
解碼,拿到outWidth
/outHeight
,即實際寬高,然後根據期望壓縮到的寬和高算出inSampleSize
,最後將injustDecodeBounds
設置爲false,
再對Bitmap
進行一次解碼即可。另一種壓縮的方法是設置圖片的顯示效果,比如ARGB_8888
等等.Picasso
綜合了利用這兩種方案.詳細代碼參考
BitmapHunter#decodeStream
、RequestHandler#createBitmapOptions
、RequestHandler#calculateInSampleSize
這三個方法,有個需要注意的地方,只有當設置圖片的寬高時(調用了fit
或者resize
)纔會計算smpleSize
進行壓縮。