前言
圖片加載框架Picasso相信大家都已經(jīng)用過很多次了屠尊,對它們的使用方法也早就熟稔于心了,那么本文就Picasso的源碼進(jìn)行剖析耕拷,學(xué)習(xí)設(shè)計(jì)者的優(yōu)秀的代碼設(shè)計(jì)理念和方法讼昆。
幾個(gè)重要的類
在源碼解析開始之前,筆者認(rèn)為有必要對Picasso的幾個(gè)重要的類進(jìn)行簡單梳理骚烧,以便于后面遇到這些組件的時(shí)候可以馬上知道它的作用是什么浸赫。
1、OkHttp3Downloader
Picasso借助該類來下載圖片赃绊,并把圖片緩存在磁盤空間上既峡。實(shí)際上,它用的是OkHttp3
這個(gè)網(wǎng)絡(luò)通信庫來完成下載任務(wù)碧查。我們看看它的構(gòu)造方法:
public OkHttp3Downloader(final Context context) {
this(Utils.createDefaultCacheDir(context));
}
public OkHttp3Downloader(final File cacheDir) {
this(cacheDir, Utils.calculateDiskCacheSize(cacheDir));
}
public OkHttp3Downloader(final File cacheDir, final long maxSize) {
this(new OkHttpClient.Builder().cache(new Cache(cacheDir, maxSize)).build());
sharedClient = false;
}
public OkHttp3Downloader(OkHttpClient client) {
this.client = client;
this.cache = client.cache();
}
通過Utils.createDefaultCacheDir(context)
方法來創(chuàng)建緩存文件夾运敢,通過Utils.calculateDiskCacheSize(cacheDir)
來確定磁盤緩存空間的大小校仑。由此我們可以知道,Picasso利用了OkHttp3
的下載機(jī)制來緩存圖片传惠,并且磁盤緩存的大小也是可以配置的迄沫,默認(rèn)實(shí)現(xiàn)是可用空間的2%且不少于5MB.
2、LruCache
如果說OkHttp3Downloader
實(shí)現(xiàn)了磁盤緩存卦方,那么LruCache
則是實(shí)現(xiàn)了內(nèi)存緩存羊瘩。內(nèi)存緩存的意義在于避免圖片過多地堆積在內(nèi)存中而導(dǎo)致OOM。這里使用的是Lru算法(Least recently used,最近最少使用算法)愿汰,該算法可以使得經(jīng)常使用的圖片駐留于內(nèi)存中困后,避免了反復(fù)從磁盤加載圖片而導(dǎo)致內(nèi)存抖動(dòng)的問題。
3衬廷、PicassoExecutorService
這個(gè)實(shí)際上是一個(gè)線程池,它的主要作用就在于把下載任務(wù)分配到各個(gè)子線程中去執(zhí)行汽绢。
class PicassoExecutorService extends ThreadPoolExecutor {
private static final int DEFAULT_THREAD_COUNT = 3;
PicassoExecutorService() {
super(DEFAULT_THREAD_COUNT, DEFAULT_THREAD_COUNT, 0, TimeUnit.MILLISECONDS,
new PriorityBlockingQueue<Runnable>(), new Utils.PicassoThreadFactory());
}
}
從構(gòu)造方法可以看出吗跋,該線程池的默認(rèn)實(shí)現(xiàn)是3個(gè)核心線程且最大線程數(shù)不超過3條。也就是說宁昭,默認(rèn)情況下Picasso在下載圖片的時(shí)候跌宛,最大的同時(shí)下載數(shù)量是3。但實(shí)際上积仗,核心線程和最大線程數(shù)是會隨著設(shè)備的網(wǎng)絡(luò)狀態(tài)而改變的疆拘,比如WIFI狀態(tài)下是4條核心線程,而4G狀態(tài)下是3條核心線程寂曹,以此類推哎迄。
4、Dispatcher
顧名思義隆圆,該類是一個(gè)調(diào)度器漱挚,負(fù)責(zé)分發(fā)、調(diào)度和處理Picasso產(chǎn)生的各種事件渺氧。在這個(gè)調(diào)度器內(nèi)旨涝,需要關(guān)注的分別是dispatcherThread
和handler
這兩個(gè)成員變量÷卤常可以先看一下Dispatcher的構(gòu)造方法:
Dispatcher(Context context, ExecutorService service, Handler mainThreadHandler,
Downloader downloader, Cache cache, Stats stats) {
this.dispatcherThread = new DispatcherThread();
this.dispatcherThread.start();
this.handler = new DispatcherHandler(dispatcherThread.getLooper(), this);
//省略...
}
其中白华,DispatcherThread
繼承自Thread,是一條子線程贩耐;而DispatcherHandler
則繼承自Handler弧腥,熟悉Handler的同學(xué),肯定知道這是用于處理線程間通信的常見方法憔杨。由此可知鸟赫,Dispatcher這個(gè)調(diào)度器的主要工作都是在DispatcherThread
這條線程內(nèi)完成,而線程切換的任務(wù)則是DispatcherHandler
來完成。
5抛蚤、Request
Request封裝了有關(guān)一次圖片請求的所有信息台谢,比如圖片的url、圖片的變換策略等岁经,這些都是不可更改的信息朋沮。舉個(gè)例子來說,Picasso.get().load(url).centerCrop().rotate(15).into(imageview);
上面的調(diào)用鏈缀壤,Request會封裝centerCrop樊拓、rotate等信息。與Request相關(guān)的是RequestCreator塘慕,它可以看作是一個(gè)建造器筋夏,配置了圖片請求的信息。
6图呢、RequestHandler
上面說到Picasso將圖片請求封裝成了一個(gè)Request条篷,而處理Request的組件則是RequestHandler
,因?yàn)閳D片的請求是多種多樣的蛤织,有的是提供了一個(gè)URL從網(wǎng)絡(luò)獲取圖片赴叹;有的則是提供了一個(gè)resourceId,從本地加載圖片指蚜,不同的請求會有不同的加載方式乞巧。因此Picasso提供了多個(gè)RequestHandler
來應(yīng)對不同的情況,用戶也可以自定義RequestHandler
來實(shí)現(xiàn)自己的需求摊鸡,只需要重寫canHandleRequest
方法和load
方法绽媒,如下所示:
public abstract class RequestHandler {
public abstract boolean canHandleRequest(Request data);
@Nullable public abstract Result load(Request request, int networkPolicy) throws IOException;
}
由此看出,Picasso在處理不同的圖片請求的時(shí)候柱宦,將不同請求的實(shí)現(xiàn)方式放在了所對應(yīng)的handler去實(shí)現(xiàn)些椒,這樣便實(shí)現(xiàn)了圖片請求和處理請求的解耦合,這樣用戶自行拓展以適應(yīng)不同場景下的圖片加載需求掸刊。
加載圖片流程的詳細(xì)分析
1免糕、Picasso.get()
該方法的調(diào)用是一切流程的起點(diǎn),通過該方法我們可以獲取一個(gè)Picasso的實(shí)例忧侧。在Picasso以前的版本石窑,我們是通過Picasso.with(context)
的方式來獲取實(shí)例的,這限制了我們只能在有上下文context
的環(huán)境下使用Picasso蚓炬。我們來看看這個(gè)方法的實(shí)現(xiàn)以及探究下為什么新版本的Picasso不用context
這個(gè)參數(shù)了松逊。
//代碼清單:Picasso#get()
public static Picasso get() {
if (singleton == null) { //第一次判空
synchronized (Picasso.class) {
if (singleton == null) { //上鎖后的第二次判空
if (PicassoProvider.context == null) { //確保有context
throw new IllegalStateException("context == null");
}
singleton = new Builder(PicassoProvider.context).build();
}
}
}
return singleton;
}
從上面的代碼,我們可以看出Picasso使用了DCL(double check lock)形式的單例模式肯夏,確保全局只有一個(gè)Picasso對象经宏。同時(shí)我們注意到context
對象是由PicassoProvider.context
來提供的犀暑,顯然PicassoProvider
是一個(gè)ContentProvider,是Android的四大組件之一烁兰,通過它也是可以獲取到我們應(yīng)用的上下文環(huán)境的耐亏。Picasso通過這樣形式的改動(dòng),使得Picasso可以適應(yīng)更多不同的環(huán)境沪斟,比如在沒有context的條件下僅僅利用Picasso進(jìn)行圖片的預(yù)下載广辰。
1-1、Picasso實(shí)例的構(gòu)造
Picasso實(shí)例的構(gòu)造是通過構(gòu)造器模式來進(jìn)行創(chuàng)建的主之,Picasso.get()
方法獲取的是默認(rèn)配置的Picasso實(shí)例择吊,我們也可以通過Picasso.Builder
來靈活配置適合我們需求的Picasso實(shí)例。我們來看看Picasso.Builder.build()
方法槽奕,看它是怎樣創(chuàng)建一個(gè)實(shí)例的:
//代碼清單1-1:Picasso.Builder#build()
public Picasso build() {
Context context = this.context;
if (downloader == null) {
downloader = new OkHttp3Downloader(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);
}
顯然几睛,這里實(shí)例化了上面提到的幾個(gè)重要組件,如downloader史翘、cache枉长、service、dispatcher等琼讽,它們都在Picasso的工作過程中起著重要作用。在Picasso構(gòu)造方法的內(nèi)部洪唐,還初始化了一系列的RequestHandler
钻蹬,例如ResourceRequestHandler
、NetworkRequestHandler
等凭需,這些Handler根據(jù)不同形式的圖片請求來執(zhí)行相應(yīng)的邏輯问欠。
2、Picasso#load(String)
通過Picasso.get()
獲取到Picasso對象后粒蜈,我們接下來就會通過load
的一系列重載方法來確定圖片的來源顺献,可以是uri、file或者string等枯怖。我們選取其中一個(gè)load
方法來看看源碼:
//代碼清單2:Picasso#load(string)
public RequestCreator load(@Nullable Uri uri) {
return new RequestCreator(this, uri, 0);
}
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);
}
這里創(chuàng)建了RequestCreator
注整,前面提到過Picasso將圖片的來源url、圖片的placeholder度硝、圖片的變換操作等一系列信息封裝到了Request
內(nèi)肿轨,而RequestCreator
就相當(dāng)于是Request
的一個(gè)構(gòu)造器。
2-1蕊程、RequestCreator的相關(guān)方法
以上獲取到了RequestCreator實(shí)例椒袍,通常我們接下來的做法是對這一次的圖片請求配置各種功能,舉個(gè)例子來說:Picasso.get().load(url).placeholder(resId)
是我們常見的一種調(diào)用藻茂,這樣給圖片設(shè)置了占位圖驹暑。實(shí)際上玫恳,RequestCreator
的一系列方法都是將用戶的操作暫存在RequestCreator
的成員變量內(nèi)部,等到用戶調(diào)用into(imageview)
方法時(shí)优俘,再把所有參數(shù)填充到Request
京办。
3、PicassoCreator#into(imageview)
在確定圖片的相關(guān)操作后兼吓,我們最后會調(diào)用into
方法臂港,也即是:Picasso.get().load(url).placeholder(placeholderResid).into(target);
前面的一切工作都是準(zhǔn)備工作(獲取Picasso實(shí)例,設(shè)置圖片來源以及設(shè)置圖片的操作)视搏,接下來就是Picasso將這些操作完成并顯示在target上的過程审孽,我們來詳細(xì)分析這個(gè)過程。先來看into
方法的源碼:
//代碼清單3:PicassoCreator#into()
public void into(ImageView target, Callback callback) {
long started = System.nanoTime();
checkMain(); //確保在主線程調(diào)用該方法
if (target == null) {
throw new IllegalArgumentException("Target must not be null.");
}
//如果圖片的uri為null浑娜,則取消請求佑力,然后設(shè)置占位圖
if (!data.hasImage()) {
picasso.cancelRequest(target);
if (setPlaceholder) {
setPlaceholder(target, getPlaceholderDrawable());
}
return;
}
//如果調(diào)用了RequestCreator#fit()方法,那么deferred會被設(shè)置為true
//這是因?yàn)閒it()方法需要適應(yīng)ImageView的大小筋遭,必須等到ImageView的layout過程完畢才能fit()
//因此打颤,這里實(shí)際上是推遲了圖片的加載過程,即Picasso#defer()
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); //根據(jù)RequestCreator的參數(shù)來創(chuàng)建一個(gè)Request
String requestKey = createKey(request); //創(chuàng)建與該Request對應(yīng)的一個(gè)Key
//如果內(nèi)存緩存可用漓滔,那么直接從內(nèi)存緩存獲取Request對應(yīng)的Bitmap编饺,并取消請求
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 action =
new ImageViewAction(picasso, target, request, memoryPolicy, networkPolicy, errorResId,
errorDrawable, requestKey, tag, callback, noFade);
picasso.enqueueAndSubmit(action); //排隊(duì),等待調(diào)度
}
簡單來說响驴,into
方法所做的工作主要是生成一個(gè)Request
透且,并且封裝成一個(gè)Action
,最后通過picasso.enqueueAndSubmit(action)
把該動(dòng)作排隊(duì)等待執(zhí)行豁鲤。最后會調(diào)用到Dispatcher#submit
方法秽誊。
4、Dispatcher#submit(Action)
void dispatchSubmit(Action action) {
handler.sendMessage(handler.obtainMessage(REQUEST_SUBMIT, action));
}
顯然琳骡,這里通過DispatcherHandler
發(fā)送了一個(gè)submit消息锅论,那么根據(jù)前面所述,這個(gè)消息將會被投遞到DispatcherThread
線程楣号。根據(jù)Handler的相關(guān)知識最易,該消息會在handler的handleMessage
方法得到處理,即:
//Dispatcher.DispatcherHandler#handleMessage
private static class DispatcherHandler extends Handler {
//...
@Override
public void handleMessage(final Message msg) {
switch (msg.what) {
case REQUEST_SUBMIT: {
Action action = (Action) msg.obj;
dispatcher.performSubmit(action);
break;
}
//...
}
}
4-1竖席、Dispatcher#performSubmit(action)
此時(shí)耘纱,線程已經(jīng)被切換到了DispatcherThread,接著調(diào)用了performSubmit方法毕荐。因此我們可以知道束析,Dispatcher起到了排隊(duì)、分發(fā)請求憎亚、處理請求結(jié)果的作用员寇。但實(shí)際上請求的處理過程弄慰,比如從url上下載圖片等都是放到線程池去實(shí)現(xiàn)的。我們先來看performSubmit
方法如下:
//代碼清單4-1:Dispatcher#performSubmit(action)
void performSubmit(Action action, boolean dismissFailed) {
//省略...
hunter = forRequest(action.getPicasso(), this, cache, stats, action);
hunter.future = service.submit(hunter);
hunterMap.put(action.getKey(), hunter);
}
//BitmapHunter#forRequest
static BitmapHunter forRequest(Picasso picasso, Dispatcher dispatcher, Cache cache, Stats stats,
Action action) {
Request request = action.getRequest();
List<RequestHandler> requestHandlers = picasso.getRequestHandlers();
//找到一個(gè)可以處理該Request的RequestHandler
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);
}
從上面的代碼可以看出蝶锋,先是生成了一個(gè)BitmapHunter
陆爽,這個(gè)類的作用顧名思義,就是獲取Bitmap扳缕,它是一個(gè)Runnable
慌闭,它內(nèi)部根據(jù)Request
的不同類型來確定不同的獲取方法(實(shí)際上是RequestHandler在起作用)。
緊接著躯舔,調(diào)用了service.submit(hunter)
方法驴剔,這里的service實(shí)際上就是PicassoExecutorService
線程池,將BitmapHunter
這個(gè)runnable投遞進(jìn)了線程池粥庄,如果線程池有空閑的線程那么就會執(zhí)行這個(gè)runnable丧失,否則阻塞等待。最終惜互,如果runnable獲得執(zhí)行的機(jī)會布讹,它的run()方法會被調(diào)用。
5训堆、BitmapHunter#run()
那么代碼運(yùn)行到這里描验,線程又切換到了PicassoExecutorService內(nèi)的某一條線程。也就是說坑鱼,加載圖片的工作是在這些子線程內(nèi)執(zhí)行的挠乳。我們來看看run()
:
//代碼清單5:BitmapHunter#run
@Override public void run() {
try {
updateThreadName(data);
//獲取result
result = hunt();
if (result == null) {
//如果加載失敗,則分發(fā)失敗事件
dispatcher.dispatchFailed(this);
} else {
//如果加載成功姑躲,則分發(fā)成功事件
dispatcher.dispatchComplete(this);
}
}
//省略異常狀態(tài)的處理...
}
代碼的邏輯很簡單,hunt()
是加載細(xì)節(jié)盟蚣,如果加載失敗就由Dispatcher分發(fā)失敗事件黍析,反之分發(fā)成功事件。
5-1屎开、BitmapHunter#hunt()
接著阐枣,我們來探索一下hunt
方法的實(shí)現(xiàn)方式,首先查看源碼如下:
//代碼清單5-1:BitmapHunter#hunt()
Bitmap hunt() throws IOException {
Bitmap bitmap = null;
//從內(nèi)存緩存讀取bitmap奄抽,如果命中則添加計(jì)數(shù)
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;
}
}
networkPolicy = retryCount == 0 ? NetworkPolicy.OFFLINE.index : networkPolicy;
//利用requestHandler解析圖片請求
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.
// 如果bitmap為空蔼两,那么從stream讀取字節(jié)流解析成bitmap
if (bitmap == null) {
Source source = result.getSource();
try {
bitmap = decodeStream(source, data);
} finally {
try {
//noinspection ConstantConditions If bitmap is null then source is guranteed non-null.
source.close();
} catch (IOException ignored) {
}
}
}
}
if (bitmap != null) {
//省略部分代碼...
//對Bitmap進(jìn)行轉(zhuǎn)換操作,Transformation是一個(gè)自定義的轉(zhuǎn)換操作
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);
}
}
}
}
return bitmap;
}
總的流程可以概括為:先從內(nèi)存緩存獲取逞度,如果沒有則交給對應(yīng)的RequestHandler
來進(jìn)行圖片的加載额划,不同的請求對應(yīng)了不同的加載方式,這里暫不深究档泽。在獲得一個(gè)Bitmap對象后俊戳,便對這個(gè)位圖進(jìn)行了一系列的轉(zhuǎn)換操作揖赴,比如圖片自身的寬高和目標(biāo)寬高不一致時(shí)要進(jìn)行縮放,或者用戶設(shè)置了centerCrop
的標(biāo)志位抑胎,那么圖片就要保持寬高比列居中顯示燥滑。這些操作是利用Matrix
和Bitmap.createBitmap
來完成的。同時(shí)Picasso允許用戶自定義轉(zhuǎn)換器Transformation
來對圖片進(jìn)行個(gè)性化的修改阿逃,例如添加水印铭拧。具體的加載過程和轉(zhuǎn)換過程,本文暫不進(jìn)行深究恃锉。
那么搀菩,當(dāng)hunt()
方法執(zhí)行完畢之后,會返回bitmap對象淡喜,我們順著代碼清單5往下走秕磷,下面就調(diào)用了dispatcher.dispatchComplete(this)
方法,與上面出現(xiàn)過的dispatcher.dispatchSubmit
一樣炼团,把這個(gè)事件交給了Dispatcher
去分發(fā)和處理澎嚣。我們自然而然就會想到,這里會進(jìn)行線程的切換瘟芝,從PicassoExecutorService
線程池的某一條線程切換到了DispatcherThread
易桃。最終,在DispatcherHandler#handleMessage
處理這個(gè)完成事件锌俱。
6晤郑、Dispatcher#performComplete(BitmapHunter)
代碼會運(yùn)行到這個(gè)方法,我們直接來看源碼:
void performComplete(BitmapHunter hunter) {
//如果條件允許贸宏,那么把該bitmap緩存到內(nèi)存
if (shouldWriteToMemoryCache(hunter.getMemoryPolicy())) {
cache.set(hunter.getKey(), hunter.getResult());
}
hunterMap.remove(hunter.getKey()); //從map移除這個(gè)已完成的hunter
batch(hunter); //進(jìn)行批處理
}
private void batch(BitmapHunter hunter) {
if (hunter.isCancelled()) {
return;
}
if (hunter.result != null) {
hunter.result.prepareToDraw();
}
batch.add(hunter); //添加到batch列表內(nèi)
if (!handler.hasMessages(HUNTER_DELAY_NEXT_BATCH)) {
handler.sendEmptyMessageDelayed(HUNTER_DELAY_NEXT_BATCH, BATCH_DELAY);
}
}
從上面源碼可以看出造寝,該hunter會被添加到一個(gè)batch
的列表內(nèi),同時(shí)延遲發(fā)送一個(gè)HUNTER_DELAY_NEXT_BATCH
消息吭练,這意味著诫龙,第一個(gè)hunter完成后,會被添加到batch
列表鲫咽,然后延遲200ms發(fā)送batch消息签赃。此時(shí)如果有別的hunter到達(dá),也會被一一添加到batch
列表分尸,直到一開始的batch消息得到處理锦聊。這里利用了批處理的思想,在200ms的等待時(shí)間內(nèi)箩绍,會暫存多個(gè)hunter請求孔庭,時(shí)間到了之后便切換到主線程進(jìn)行UI的顯示,這樣就不用頻繁地進(jìn)行線程切換伶选,可以提升UI顯示的流暢性史飞。
7尖昏、Dispatcher#performBatchComplete
最后,在DispatcherThread
會處理HUNTER_DELAY_NEXT_BATCH
消息构资,我們來看該代碼:
void performBatchComplete() {
List<BitmapHunter> copy = new ArrayList<>(batch);
batch.clear();
mainThreadHandler.sendMessage(mainThreadHandler.obtainMessage(HUNTER_BATCH_COMPLETE, copy));
logBatch(copy);
}
這里的mainThreadHandler
是持有主線程Looper的handler抽诉,它發(fā)送的消息都會在主線程得到處理虐块。實(shí)際上盈滴,它是在Dispatcher
實(shí)例化的時(shí)候由Picasso傳遞進(jìn)來的,那么它的源碼可以在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;
}
}
};
此時(shí)湘今,線程環(huán)境已經(jīng)由DispatcherThread
切換到了UI Thread.
在主線程內(nèi)己单,逐個(gè)遍歷batch列表唉窃,對里面的每一個(gè)hunter進(jìn)行最后的收尾工作,把bitmap填充到imageview上纹笼。
8-1纹份、Picasso#complete
下面就是收尾工作,我們直接看源碼:
//代碼清單8-1:Picasso#complete
void complete(BitmapHunter hunter) {
//獲取hunter所含有的Action
Action single = hunter.getAction();
//hunter可能對應(yīng)多個(gè)Action廷痘,對同一圖片的同一操作的多個(gè)請求會保存在一個(gè)hunter內(nèi)
//避免不必要的重復(fù)加載步驟蔓涧。
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, exception);
}
if (hasMultiple) {
//noinspection ForLoopReplaceableByForEach
for (int i = 0, n = joined.size(); i < n; i++) {
Action join = joined.get(i);
deliverAction(result, from, join, exception);
}
}
if (listener != null && exception != null) {
listener.onImageLoadFailed(this, uri, exception);
}
}
這里對hunter內(nèi)的所有Action進(jìn)行遍歷操作,每一個(gè)Action都有自己要設(shè)置的imageview對象笋额。對每一個(gè)Action元暴,進(jìn)一步調(diào)用了deliverAction
方法。
8-2兄猩、Picasso#deliverAction
//代碼清單8-2:Picasso#deliverAction
private void deliverAction(Bitmap result, LoadedFrom from, Action action, Exception e) {
//省略...
if (result != null) {
action.complete(result, from);
} else {
action.error(e);
}
這里的Action是ImageViewAction
實(shí)例茉盏,因此最后會調(diào)用ImageViewAction#complete()
方法。實(shí)際上枢冤,用戶完全可以繼承Action
來實(shí)現(xiàn)不同的需求鸠姨。默認(rèn)實(shí)現(xiàn)的ImageViewAction
是為了把圖片填充到ImageView.
8-3、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();
}
}
代碼出現(xiàn)了新的一個(gè)類PicassoDrawable
淹真,它繼承自BitmapDrawable
享怀,那么到現(xiàn)在就很清楚了,Picasso是以Drawable的形式把圖片設(shè)置進(jìn)ImageView的趟咆,通過這樣的形式,Picasso可以最后在圖片上添加一些信息梅屉。比如值纱,開啟了Debug模式后,所加載的圖片的右下角會有不同顏色的角標(biāo)來表示圖片的來源(網(wǎng)絡(luò)坯汤、內(nèi)存或磁盤)虐唠,這個(gè)功能的實(shí)現(xiàn)就是借助于BitmapDrawable.draw
方法在畫布上添加額外的信息。
8-4惰聂、PicassoDrawable#setBitmap
static void setBitmap(ImageView target, Context context, Bitmap bitmap,
Picasso.LoadedFrom loadedFrom, boolean noFade, boolean debugging) {
Drawable placeholder = target.getDrawable();
if (placeholder instanceof Animatable) {
((Animatable) placeholder).stop();
}
PicassoDrawable drawable =
new PicassoDrawable(context, bitmap, placeholder, loadedFrom, noFade, debugging);
target.setImageDrawable(drawable); //最后的最后疆偿,把drawable設(shè)置進(jìn)imageview上
}
上面生成了PicassoDrawable
咱筛,這里不但傳遞了bitmap,也轉(zhuǎn)遞了loadedFrom
信息杆故,用于debug模式下判斷圖片來源迅箩。最后,調(diào)用了ImageView的方法設(shè)置了圖片处铛。
至此饲趋,Picasso加載圖片的一次完整流程便完成了。
小結(jié)
縱觀整個(gè)Picasso的加載圖片的流程撤蟆,其中涉及了多次的線程切換以及多個(gè)組件的協(xié)同工作奕塑,為了方便讀者的理解,筆者繪制了整體的流程圖并標(biāo)注出了線程切換的時(shí)機(jī)家肯,讀者可以結(jié)合流程圖來梳理一下上面的源碼解析龄砰。
思路借鑒
經(jīng)過上面的源碼學(xué)習(xí),我們可以發(fā)現(xiàn)Picasso有很多優(yōu)秀的設(shè)計(jì)思想值得我們?nèi)W(xué)習(xí)讨衣。
1换棚、單例模式
對于全局只需要一個(gè)實(shí)例的庫來說,應(yīng)該設(shè)計(jì)為單例模式值依。這種庫往往承擔(dān)了繁重的工作圃泡,并且開銷不小,如果系統(tǒng)內(nèi)存在多個(gè)實(shí)例愿险,那么就會造成額外的開銷颇蜡。比如Picasso的線程池默認(rèn)有多條子線程,如果Picasso不是單例模式的辆亏,那么就會有頻繁創(chuàng)建線程池风秤、回收線程池的操作,這完全是沒有必要的扮叨。只要設(shè)計(jì)為單例模式缤弦,全局提供統(tǒng)一的入口方法,這樣不但節(jié)省了內(nèi)存的消耗彻磁,同時(shí)也有利于馬上定位到問題所在碍沐。
2、模塊化設(shè)計(jì)
當(dāng)一個(gè)系統(tǒng)實(shí)現(xiàn)的功能比較復(fù)雜的時(shí)候衷蜓,我們可以利用模塊化的思想抽離出一個(gè)個(gè)子模塊累提,這些子模塊可以獨(dú)立成一個(gè)單獨(dú)的子系統(tǒng),也可以相互配合以實(shí)現(xiàn)整個(gè)系統(tǒng)的功能磁浇。Picasso也是這樣設(shè)計(jì)的斋陪,它的子模塊包括downloader、cache和stats等子模塊,每一個(gè)子模塊負(fù)責(zé)不同的功能无虚,相互之間沒有聯(lián)系缔赠,極大程度上消除了類之間的耦合關(guān)系,修改一個(gè)模塊也不會影響到另一模塊的正常工作友题。
因此嗤堰,我們在設(shè)計(jì)一個(gè)庫或者系統(tǒng)的時(shí)候,可以考慮將庫的功能分成不同的子模塊咆爽,各個(gè)模塊各司其職梁棠,盡量降低代碼的耦合度。
3斗埂、多態(tài)和可拓展性
多態(tài)是面向?qū)ο缶幊痰娜筇匦灾环鄳B(tài)簡單地說就是父類類型的變量可以引用子類的實(shí)例,這樣的好處在于子類可以靈活多變適應(yīng)不同的場景的同時(shí)也遵循了一定規(guī)則的約束呛凶。Picasso的Downloader
就是一個(gè)例子男娄,它是一個(gè)接口,默認(rèn)的實(shí)現(xiàn)是OkHttp3Downloader
漾稀,它允許用戶自己實(shí)現(xiàn)Downloader
接口模闲,并在Picasso的構(gòu)造器中添加這樣一個(gè)自定義的下載器。這是模塊化和多態(tài)的結(jié)合崭捍,下載器是一個(gè)可以替換的模塊尸折,用戶只需要遵循某些約定即可。除此之外殷蛇,還有RequestHandler
实夹,它作為一個(gè)抽象類,僅定義了需要實(shí)現(xiàn)的幾個(gè)方法粒梦,具體不同圖像的加載方式由不同的RequestHandler
去實(shí)現(xiàn)亮航。
在代碼中運(yùn)用了多態(tài)的思想后,這意味著我們的代碼是可拓展的匀们,可以適應(yīng)未來可能出現(xiàn)的不同需求缴淋。我們可以將庫的某一模塊定義為抽象類或者接口,具體的實(shí)現(xiàn)可以根據(jù)具體需求而定泄朴。
4重抖、批處理思想
批處理思想是指等請求聚集到一定數(shù)量或者經(jīng)過一段時(shí)間后再一起處理的思想。如果請求涉及到了跨線程處理甚至跨進(jìn)程處理祖灰,并且請求的數(shù)量在短時(shí)間內(nèi)是密集的仇哆,那么如果對于每一個(gè)單一請求都進(jìn)行一次線程間/進(jìn)程間通信,顯然這會頻繁地切換線程造成很大的開銷夫植。如果利用了批處理思想,那么每隔一段時(shí)間處理一批請求,只需要切換一次線程详民。Picasso在處理完BitmapHunter
后就是利用了批處理思想延欠,等待200ms
后再切換到UI線程進(jìn)行UI的顯示。
在實(shí)時(shí)性要求不高的場景下(延遲200ms后展示圖片沈跨,完全可以接受)由捎,我們可以善于利用批處理思想,降低切換線程/進(jìn)程帶來的性能開銷饿凛。