Picasso源碼完全解析——學(xué)習(xí)其優(yōu)秀思想

前言

圖片加載框架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)注的分別是dispatcherThreadhandler這兩個(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钻蹬,例如ResourceRequestHandlerNetworkRequestHandler等凭需,這些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)志位抑胎,那么圖片就要保持寬高比列居中顯示燥滑。這些操作是利用MatrixBitmap.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)程帶來的性能開銷饿凛。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末狞玛,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子涧窒,更是在濱河造成了極大的恐慌心肪,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,214評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件纠吴,死亡現(xiàn)場離奇詭異硬鞍,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)戴已,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評論 2 382
  • 文/潘曉璐 我一進(jìn)店門固该,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人糖儡,你說我怎么就攤上這事伐坏。” “怎么了握联?”我有些...
    開封第一講書人閱讀 152,543評論 0 341
  • 文/不壞的土叔 我叫張陵桦沉,是天一觀的道長。 經(jīng)常有香客問我拴疤,道長永部,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,221評論 1 279
  • 正文 為了忘掉前任呐矾,我火速辦了婚禮苔埋,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘蜒犯。我一直安慰自己组橄,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,224評論 5 371
  • 文/花漫 我一把揭開白布罚随。 她就那樣靜靜地躺著玉工,像睡著了一般。 火紅的嫁衣襯著肌膚如雪淘菩。 梳的紋絲不亂的頭發(fā)上遵班,一...
    開封第一講書人閱讀 49,007評論 1 284
  • 那天屠升,我揣著相機(jī)與錄音,去河邊找鬼狭郑。 笑死腹暖,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的翰萨。 我是一名探鬼主播脏答,決...
    沈念sama閱讀 38,313評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼亩鬼!你這毒婦竟也來了殖告?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,956評論 0 259
  • 序言:老撾萬榮一對情侶失蹤雳锋,失蹤者是張志新(化名)和其女友劉穎黄绩,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體魄缚,經(jīng)...
    沈念sama閱讀 43,441評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡宝与,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,925評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了冶匹。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片习劫。...
    茶點(diǎn)故事閱讀 38,018評論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖嚼隘,靈堂內(nèi)的尸體忽然破棺而出诽里,到底是詐尸還是另有隱情,我是刑警寧澤飞蛹,帶...
    沈念sama閱讀 33,685評論 4 322
  • 正文 年R本政府宣布谤狡,位于F島的核電站,受9級特大地震影響卧檐,放射性物質(zhì)發(fā)生泄漏墓懂。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,234評論 3 307
  • 文/蒙蒙 一霉囚、第九天 我趴在偏房一處隱蔽的房頂上張望捕仔。 院中可真熱鬧,春花似錦盈罐、人聲如沸榜跌。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽钓葫。三九已至,卻和暖如春票顾,著一層夾襖步出監(jiān)牢的瞬間础浮,已是汗流浹背帆调。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評論 1 261
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留豆同,地道東北人贷帮。 一個(gè)月前我還...
    沈念sama閱讀 45,467評論 2 352
  • 正文 我出身青樓,卻偏偏與公主長得像诱告,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子民晒,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,762評論 2 345

推薦閱讀更多精彩內(nèi)容

  • Picasso精居,看的版本是v.2.5.2 使用方法,大概這么幾種加載資源的形式 還可以對圖片進(jìn)行一些操作:設(shè)置大小...
    Jinjins1129閱讀 342評論 0 3
  • 概述 Picasso是大名鼎鼎的Square公司提供的一個(gè)適用于Android的強(qiáng)大的圖片下載緩存庫潜必。 簡單使用 ...
    憨人_Vivam閱讀 209評論 0 1
  • 一. 概述 Picasso是Square出品的一個(gè)非常精簡的圖片加載及緩存庫靴姿,其主要特點(diǎn)包括: 易寫易讀的流式編程...
    SparkInLee閱讀 1,079評論 2 11
  • 目錄 Picasso加載一張圖片的流程 創(chuàng)建//通過定義一個(gè)PicassoProvider來獲取Context//...
    _Ryan閱讀 449評論 0 3
  • /oldboy和/oldboy/區(qū)別? 大部分命令一樣 /oldboy 表示oldboy目錄和下面的內(nèi)容 /old...
    看見光明才有希望閱讀 118評論 0 0