Picasso學習筆記
下面是我在學習Picasso
過程中做的筆記,思路可能有些凌亂,如果閱讀過程中有發現不準確或者錯誤的地方歡迎交流(微博@楚奕RJ)
內容主要分三部分,第一部分是picasso的基本使用,第二部分是picasso的原始碼閱讀,最後一部分是關於原始碼中一些細節的梳理.
關於圖片載入庫的思考
我們為什麼要用圖片庫呢?
圖片庫通常會對圖片載入邏輯進行封裝、優化,比如多級快取/非同步執行緒排程/圖片壓縮變換,有了這些特性,開發者不再需要關注記憶體佔用、
OOM、網路請求等問題,而只需關注業務本身的邏輯,這對提高生產效率有很大幫助。
我工作半年以來也研究了一些主流的圖片庫,發現雖然每個圖片的架構和實現細節不太一樣,但是通常都會有以下模組:
- 請求分發模組。負責封裝請求,對請求進行優先順序排序,並按照型別進行分發。
- 快取模組。通常包括一個二級的快取,記憶體快取、磁碟快取。並預置多種快取策略。
- 下載模組。負責下載網路圖片。
- 監控模組。負責監控快取命中率、記憶體佔用、載入圖片平均耗時等。
- 圖片處理模組。負責對圖片進行壓縮、變換等處理。
- 本地資源載入模組。負責載入本地資源,如assert、drawable、sdcard等。
- 顯示模組。負責將圖片輸出顯示。
Android平臺圖片載入庫現狀
目前社群主流的圖片載入庫有Universal Image Loader,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);
注意哦,如果你以以下面這種方式加上圖形變換preload的話:
Picasso.with(this).load(URL).rotate(20).fetch();
再下面這種方式是取不到preload的圖片的,因為預快取的是經過變換後的圖片,它的cachekey會有rotation
標識
Picasso.with(this).load(URL).into(imageView);
當然我說的是preload到記憶體中的那份經過旋轉的圖片,http會快取旋轉前的圖片到磁碟(支援快取的情況下),所以最終還是可以從磁碟快取
拿到圖片的。
- 替換預設的
Picasso
Picasso p = new Picasso.Builder(this).executor().downloader(downloader).memoryCache(cache).build();
Picasso.setSingletonInstance(p);
- 同步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();
注意,必須在非同步執行緒呼叫,否則crash,另外,這個結果並不會快取到記憶體裡面,所以慎用。
- 自適應
Picasso.with(TestImageActivity.this).load(url).fit().into(imageview);
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);
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);
最終顯示結果如下:
可以很明顯看到下面的圖模糊許多,這是因為圖片被壓縮了,但是顯示的時候又被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);
各種圖形變換: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.
}
}
Picasso原始碼分析
其實看原始碼分析什麼的最無聊了,最好的方式是自己跟一遍原始碼,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
下面是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;
}
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);
}
此方法做了如下基本配置:
- 使用預設的快取策略,記憶體快取基於
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);
從命名就可以看出來,可以從網路、file、assert、contactsphoto等地方載入圖片.
另,Picasso支援增加自己的處理器.
load()
方法用於從不同地方載入圖片,比如網路、resource、File等,該方法內部邏輯很簡單,只是建立了一個RequestCreator
Picasso#load()
public RequestCreator load(Uri uri) {
return new RequestCreator(this, uri, 0);
}
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);
}
那麼可想而知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);
}
邏輯註釋寫的很清楚了,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;
再看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;
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);
}
它會先從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);
每一個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);
而dispatcherThread
則是一個HandlerThread
:
Dispatcher內部類
static class DispatcherThread extends HandlerThread {
DispatcherThread() {
super(Utils.THREAD_PREFIX + DISPATCHER_THREAD_NAME, THREAD_PRIORITY_BACKGROUND);
}
}
也就是說,這個handler的訊息處理是在子執行緒進行的!這樣就可以避免阻塞主執行緒的訊息佇列啦!
好的,再回到剛才的問題,來看下dispatchSubmit
方法(不知道大家有沒有看暈。。。。):
Dispatcher#dispatchSubmit
void dispatchSubmit(Action action) {
handler.sendMessage(handler.obtainMessage(REQUEST_SUBMIT, action));
}
不用看都知道會發訊息給handler。而handler收到這個訊息之後呼叫了這個方法:
dispatcher.performSubmit(action);
果斷跟進去:
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());
}
}
首先建立了一個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);
}
還記得大明湖畔的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);
}
}
核心邏輯是由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;
}
這裡假設是一個網路請求,那麼最終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);
}
現在我們關注下這個downloader
的前世今生,如果使用者沒有自定義的話,那將使用預設downloader
:
Picasso#Builder#build()
downloader = Utils.createDefaultDownloader(context);
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);
}
首先反射下,看有沒有依賴okhttp
,如果依賴的話,那就使用OkHttpClient
嘍,否則就使用預設的HttpUrlConnection
了。
注:其實從4.4開始,okhttp
已經作為HttpUrlConnection
的實現引擎了。
可以從picasso
的pom檔案裡面看到,okhttp是optional的:
<dependency>
<groupId>com.squareup.okhttp</groupId>
<artifactId>okhttp</artifactId>
<optional>true</optional>
</dependency>
以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);
}
注意哦,Disk Cache
功能是在這裡做掉的,它基於Http
語義來判斷是否快取.
另,返回的是inputStream
流,而不是Bitmap
物件.
好的,現在我們回到BitmapHunter#run()
,它在拿到結果後會將結果交給dispatcher
BitmapHunter#run()
if (result == null) {
dispatcher.dispatchFailed(this);
} else {
dispatcher.dispatchComplete(this);
}
我們看dispatcher.dispatchComplete(this)
,它會把訊息發給自己內部的handler,也就是剛才說的Looper在子執行緒
,handler將做如下處理:
的handler
BitmapHunter hunter = (BitmapHunter) msg.obj;
dispatcher.performComplete(hunter);
注意哦,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");
}
}
首先會根據事先設定的快取策略決定是否將結果加到記憶體快取。然後呼叫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);
}
主執行緒收到訊息後會進行處理:
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);
}
對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();
}
}
注意看這一句,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上.
其他需要關注的點
關於快取策略
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; } }
寫快取時機
:圖片從網路或者其他地方載入成功後,即在BitmapHunter
的run方法執行結束的時候.
Dispatcher#performCompleteif (shouldWriteToMemoryCache(hunter.getMemoryPolicy())) { cache.set(hunter.getKey(), hunter.getResult()); }
注意哦,快取的是經過壓縮之後的圖片(如果你使用了
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(); } }
可以看到它會不斷輪詢
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; }
那麼這個
ReferenceQueue
又是如何關聯Action
的呢?這個可以從Action
的構造器中拿到答案:this.target =//RequestWeakReference是WeakReference的子類 target == null ? null : new RequestWeakReference<T>(this/*即Action本身*/, target, picasso.referenceQueue);
可以看到兩點:
- 每個
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); }
注意到它會判斷
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
進行壓縮。
相關文章
- numpy的學習筆記\pandas學習筆記筆記
- IT學習筆記筆記
- 學習筆記筆記
- 【學習筆記】數學筆記
- 《JAVA學習指南》學習筆記Java筆記
- Elasticsearch學習筆記Elasticsearch筆記
- Scala學習筆記筆記
- MySql學習筆記MySql筆記
- jQuery 學習筆記jQuery筆記
- react學習筆記React筆記
- 學習筆記(4.3)筆記
- 學習筆記(4.4)筆記
- 學習筆記(3.29)筆記
- 學習筆記(4.1)筆記
- AOP學習筆記筆記
- AspectJ學習筆記筆記
- 學習筆記(3.27)筆記
- 學習筆記(4.2)筆記
- golang 學習筆記Golang筆記
- Zookeeper學習筆記筆記
- 學習筆記(3.24)筆記
- 學習筆記(3.25)筆記
- 學習筆記(3.21)筆記
- GitHub學習筆記Github筆記
- jest 學習筆記筆記
- typescript 學習筆記TypeScript筆記
- Echarts學習筆記Echarts筆記
- js學習筆記JS筆記
- shell學習筆記筆記
- Dubbo 學習筆記筆記
- SVN 學習筆記筆記
- 笨笨學習筆記筆記
- vue學習筆記Vue筆記
- wepack學習筆記筆記
- redis學習筆記Redis筆記
- PureMVC學習筆記REMMVC筆記
- gitee 學習筆記Gitee筆記
- 機器學習學習筆記機器學習筆記