Picasso,看的版本是v.2.5.2
- 使用方法,大概這么幾種加載資源的形式
Picasso.with(context).load("http://i.imgur.com/DvpvklR.png").into(imageView);
Picasso.with(context).load(R.drawable.landing_screen).into(imageView1);
Picasso.with(context).load("file:///android_asset/DvpvklR.png").into(imageView2);
Picasso.with(context).load(newFile(...)).into(imageView3);
還可以對圖片進行一些操作:設置大小、裁剪、加載中&加載錯誤時顯示的圖片等,參考Picasso官網。
Picasso.with(context)
.load(url)
.resize(50, 50)
.centerCrop()
.into(imageView)
Picasso.with(context)
.load(url)
.placeholder(R.drawable.user_placeholder)
.error(R.drawable.user_placeholder_error)
.into(imageView);
-
類關系圖
Picasso-classes-relation - 開始捋源碼
從外部調用看,first,初始化方法:Picasso.java #with(context):單例模式 double-check + 內部靜態類Builder,在Picasso.java#Builder().build(){}方法中使用默認或自定義配置初始化Picasso,返回給外部調用。Builder 中初始化了以下配置:
public Picasso build() {
Context context = this.context;
if (downloader == null) { //1
downloader = Utils.createDefaultDownloader(context);
}
if (cache == null) { //2
cache = new LruCache(context);
}
if (service == null) { //3
service = new PicassoExecutorService();
}
if (transformer == null) { //4
transformer = RequestTransformer.IDENTITY;
}
Stats stats = new Stats(cache); //5
Dispatcher dispatcher = new Dispatcher(context, service, HANDLER, downloader, cache, stats); //6
return new Picasso(context, dispatcher, cache, listener, transformer, requestHandlers, stats,
defaultBitmapConfig, indicatorsEnabled, loggingEnabled);
}
}```
* downloader
createDefaultDownloader()里,API 9以上使用 OkHttp (最低支持API 9)、以下使用HttpURLConnection 的 Http 的本地緩存。
* cache
Picasso自定義了一個 LruCache,與 Google 的實現類似,但是是針對 value 為 Bitmap 的精簡實現。應該單獨寫一篇來回顧下[LRU緩存策略]()。
* service
創建線程池,默認是3個執行線程,會根據網絡狀況再切換線程數。
* transformer
默認配置是返回原始的 Request,Picasso 文檔對于 RequestTransformer 這個接口的解釋是
> A transformer that is called immediately before every request is submitted. This can be used to modify any information about a request.
For example, if you use a CDN you can change the hostname for the image based on the current location of the user in order to get faster download speeds.
如果使用CDN,則可以根據用戶的當前位置更改映像的主機名,以獲得更快的下載速度。
* stats
用于統計下載和緩存的狀況,比如總/平均下載數、緩存命中率/未命中率、下載圖片的總/平均大小等等
* dispatcher
用以上的1&2&3&5&new Handler(Looper.getMainLooper()),構造了dispatcher,用于調度任務,handler與主線程進行交互。
最后用 //6 dispatcher 和一些其他的參數,構造 Picasso返回給外部調用。構造函數如下:
Picasso(Context context, //1
Dispatcher dispatcher, //2
Cache cache, //3
Listener listener, //4
RequestTransformer requestTransformer, //5
List<RequestHandler> extraRequestHandlers, //6
Stats stats, //7
Bitmap.Config defaultBitmapConfig, //8
boolean indicatorsEnabled, //9
boolean loggingEnabled //10) {
...
...
int builtInHandlers = 7; // Adjust this as internal handlers are added or removed.
int extraCount = (extraRequestHandlers != null ? extraRequestHandlers.size() : 0);
List<RequestHandler> allRequestHandlers =
new ArrayList<RequestHandler>(builtInHandlers + extraCount);
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);
...
...
}
第6個參數比較重要,一個默認添加了7個RequestHandler的List,也可以自定義RequestHandler加進去,均繼承自RequestHandler,再返回一個只讀的List。類名都很見名知意,對應于開篇時的使用方法,不同的RequestHandler去處理不同資源類型的Request。除了以上10個從Builder里傳入的參數外,Picasso的構造函數里還有以下幾個:
Picasso(1,2,3,4,5,6,7,8,9,10){
...
this.targetToAction = new WeakHashMap<Object, Action>(); //11
this.targetToDeferredRequestCreator = new WeakHashMap<ImageView, DeferredRequestCreator>(); //12
this.referenceQueue = new ReferenceQueue<Object>(); //13
this.cleanupThread = new CleanupThread(referenceQueue, HANDLER); //14
this.cleanupThread.start();
}
Picasso構造好之后,調用** load(File | int | String | Uri) **傳入要加載的資源,不管傳入的什么類型,都是去構造一個RequestCreator,然后返回。好的,那就接著去看看RequestCreator。RequestCreator的構造函數很簡單,其中實例化了這樣一個變量:
private final Request.Builder data;
RequestCreator(Picasso picasso, Uri uri, int resourceId) {
...//省略
this.data = new Request.Builder(uri, resourceId, picasso.defaultBitmapConfig);
}
進而調用** load() **方法返回 Request.Builder 后可以調用一些像在前面使用方法里的那些方法,對圖像做二次處理,這些方法都是Request.java 里的方法,rotate、resize什么的,其中有一個方法靈活性很高,可以滿足自定義的一些需求,就是 ** transform(Transformation transformation) **。比如讓圖片模糊顯示(讓我想起了微信某個版本的臨時性玩法),等等。詳細用法可見 [Picasso — Image Rotation and Transformation](https://futurestud.io/tutorials/picasso-image-rotation-and-transformation)
接下來,最常用到的方法就是**into(ImageView)**方法了,一系列check以及初次調用沒有緩存時,重要的是以下幾個方法:
public void into(ImageView target, Callback callback) {
...//省略
Request request = createRequest(System.nanoTime());
String requestKey = createKey(request);
Action action = new ImageViewAction(picasso, target, request, memoryPolicy, networkPolicy, errorResId,errorDrawable, requestKey, tag, callback, noFade);
picasso.enqueueAndSubmit(action);
}
生成一個唯一標識的 requestKey 來構造 ImageViewAction,它繼承自Action,封裝了一些回調方法:**complete(),error(),cancel()**等。然后作為參數,調用**enqueueAndSubmit(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);
}
判定不為空&&取消當前target已有請求后,把當前的target和action放入targetToAction中,就是上面構造Picasso時的第11個變量,**cancelExistingRequest(target)**其實就是從這個WeakHashMap里移除。重點的方法來了**submit(action)**:
void submit(Action action) {
dispatcher.dispatchSubmit(action);
}
這個dispatcher就是前面builder里的第6個變量,再來回顧一下它的構造:
Dispatcher dispatcher = new Dispatcher(context, service, HANDLER, downloader, cache, stats);
看了dispatcher手里的牌,接下來要干什么是不是有點不言而喻了?
void dispatchSubmit(Action action) {
handler.sendMessage(handler.obtainMessage(REQUEST_SUBMIT, action));
}
@Override public void handleMessage(final Message msg) {
switch (msg.what) {
case REQUEST_SUBMIT: {
Action action = (Action) msg.obj;
dispatcher.performSubmit(action);
break;
}
...
}
簡化一下performSubmit:
void performSubmit(Action action, boolean dismissFailed) {
BitmapHunter hunter = hunterMap.get(action.getKey());
//1
if (hunter != null) {
hunter.attach(action);
return;
}
...
//2
hunter = forRequest(action.getPicasso(), this, cache, stats, action);
hunter.future = service.submit(hunter);
hunterMap.put(action.getKey(), hunter);
...
}
截了兩段我覺得比較重要的片段。
* 第1段,沒明白attach之后是要干什么,因為直接就return了,按照 **hunter.attach(action)**里的邏輯看是把相同key的action添加到了BitmapHunter內部維護的一個List<Action>中,但是目前沒有找到對于List<Action>的主動操作,只是cancel、detach用到了。
* 第2段,先是構造了BitmapHunter
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);
}
}
return new BitmapHunter(picasso, dispatcher, cache, stats, action, ERRORING_HANDLER);
}
通過責任鏈模式,匹配之前添加的那些RequestHandler誰能處理當前發送過來的Request(匹配request的資源的Uri的scheme等判斷邏輯),**requestHandler.canHandleRequest(request)**為true就用這個匹配的requestHandler作為參數構造一個BitmapHunter返回。BitmapHunter是實現了Runnable接口的,再貼一下上面的代碼:
hunter.future = service.submit(hunter);
值得注意的是這里hunter內部持有了一個future對象,是Future類型,它代表一個異步任務的計算結果。把**service.submit(Runnable)**的結果賦值給了future,這是為了拿到這個返回結果可以做后續的**cancel()**操作。如果不需要這個結果,一般調用的是**service.execute(Runnable)**。stackoverflow上有一段關于這兩個方法的選擇問答,說的還蠻清楚的。[Choose between ExecutorService's submit and ExecutorService's execute](http://stackoverflow.com/questions/3929342/choose-between-executorservices-submit-and-executorservices-execute)
然后去看看實現了Runnable接口的BitmapHunter內部的**run()**方法(簡化后的):
@Override
public void run() {
...
result = hunt();
if (result == null) {
dispatcher.dispatchFailed(this);
} else {
dispatcher.dispatchComplete(this);
}
...
}
Bitmap hunt() throws IOException {
Bitmap bitmap = null;
if (shouldReadFromMemoryCache(memoryPolicy)) {
bitmap = cache.get(key);
if (bitmap != null) {
stats.dispatchCacheHit();
loadedFrom = MEMORY;
}
return bitmap;
}
}
data.networkPolicy = retryCount == 0 ? NetworkPolicy.OFFLINE.index : networkPolicy;
RequestHandler.Result result = requestHandler.load(data, networkPolicy);
if (result != null) {
loadedFrom = result.getLoadedFrom();
exifOrientation = 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();
bitmap = decodeStream(is, data);
}
}
if (bitmap != null) {
stats.dispatchBitmapDecoded(bitmap);
if (data.needsTransformation() || exifOrientation != 0) {
synchronized (DECODE_LOCK) {
if (data.needsMatrixTransform() || exifOrientation != 0) {
bitmap = transformResult(data, bitmap, exifOrientation);
}
if (data.hasCustomTransformations()) {
bitmap = applyCustomTransformations(data.transformations, bitmap);
}
}
if (bitmap != null) {
stats.dispatchBitmapTransformed(bitmap);
}
}
}
return bitmap;
}
先根據緩存策略判斷是否允許從緩存中讀取,允許的話就從cache中get,get到的話在stats中記錄命中了緩存并返回命中的bitmap,不允許或未get到就去從之前匹配到的**RequestHandler.load()**,得到**load()**的結果再判斷是否有對結果的二次處理什么的,就不再贅述。主要分析下load()跟進去的方法,比如此次請求的是個url資源,匹配的是**NetworkRequestHandler**,去看下它的**load()**方法(簡化后的):
@Override @Nullable public Result load(Request request, int networkPolicy) throws IOException {
Response response = downloader.load(request.uri, request.networkPolicy);
Picasso.LoadedFrom loadedFrom = response.cached ? DISK : NETWORK;
InputStream is = response.getInputStream();
return new Result(is, loadedFrom);
}
先調用downloader的load方法,去獲得請求的Response,我開始看的是grep code上的源碼2.5.2版,http請求時的downloader還像前文分析的那樣分為OkHttpDownloader(API 9以上)和UrlConnectionDownloader(API 9以下),寫這篇時再看github上的源碼由于OkHttp更新到OkHttp3,只有OkHttp3Downloader這一個用于url請求時的Downloader了。無論哪種其內部邏輯都是請求網絡返回Response,然后根據Picasso的緩存策略利用的http的本地緩存。
一大圈的調用,順利地在BitmapHunter的**run()**方法中拿到result且不為空的話,就調用了**dispatcher.dispatchComplete(this)**,這個方法又調用了:
void dispatchComplete(BitmapHunter hunter) {
handler.sendMessage(handler.obtainMessage(HUNTER_COMPLETE, hunter));
}
case HUNTER_COMPLETE: {
BitmapHunter hunter = (BitmapHunter) msg.obj;
dispatcher.performComplete(hunter);
break;
}
void performComplete(BitmapHunter hunter) {
if (shouldWriteToMemoryCache(hunter.getMemoryPolicy())) {
cache.set(hunter.getKey(), hunter.getResult());
}
hunterMap.remove(hunter.getKey());
batch(hunter);
}
}
在這里,根據緩存策略,將結果里的Bitmap加入內存Cache中,也就是前面實例化的LruCache。然后發送一個HUNTER_DELAY_NEXT_BATCH,
handleMessage(Message)中處理:
case HUNTER_DELAY_NEXT_BATCH: {
dispatcher.performBatchComplete();
break;
}
void performBatchComplete() {
List<BitmapHunter> copy = new ArrayList<>(batch);
batch.clear();
mainThreadHandler.sendMessage(mainThreadHandler.obtainMessage( HUNTER_BATCH_COMPLETE, copy));
logBatch(copy);
}
這個mainThreadHandler也就是最開始Picasso的構造函數里Builder里放入去實例化Dispatcher傳入的那個HANDLER,再次回到Picasso.java里去看當時HANDLER的初始化:
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;
}
...
}
};
一切又都回到了當初入口類里的**complete(BitmapHunter)** ,按照源碼邏輯,順利的話接下來是 **deliverAction(Bitmap result, LoadedFrom from, Action action, Exception e)**,再action.complete(result, from);從上面的分析可知,最終調用的是繼承了Action的ImageViewAction的complete方法,let's go(依然是簡化版):
@Override
public void complete(Bitmap result, Picasso.LoadedFrom from) {
ImageView target = this.target.get();
Context context = picasso.context;
boolean indicatorsEnabled = picasso.indicatorsEnabled;
PicassoDrawable.setBitmap(target, context, result, from, noFade, indicatorsEnabled);
if (callback != null) {
callback.onSuccess();
}
}
這樣就完成對控件的圖片設置。
源碼分析下來,發現Picasso其實非常適合作為一個骨架去自定義重要模塊的實現,比如LruCache,比如本地緩存的Downloader,優秀的框架是易于擴展的框架。
* 小結:Picasso的源碼不多,也就30幾個類,通讀一遍也就倆小時,但是記錄成文字倒是花費了很長時間。這也是我第一次寫這么多東西。深知了那些寫文章分享技術的人的不易。同時也希望自己可以養成記錄的習慣,這對于徹底理解一件事物還是非常有幫助的。