Picasso原始碼分析(三):快照功能實現和HandlerThread的使用

王世暉發表於2016-06-13

Picasso原始碼分析(一):單例模式、建造者模式、面向介面程式設計
Picasso原始碼分析(二):預設的下載器、快取、執行緒池和轉換器
Picasso原始碼分析(三):快照功能實現和HandlerThread的使用
Picasso原始碼分析(四):不變模式、建造者模式和Request的預處理
Picasso原始碼分析(五):into方法追本溯源和責任鏈模式建立BitmapHunter
Picasso原始碼分析(六):BitmapHunter與請求結果的處理

HandlerThread原理和用法

HandlerThread是一個Android系統提供的提供快速開發的工具類,功能為開啟一個具有looper的執行緒,該執行緒的looper可以和handler繫結,也就是說建立handler的時候可以在handler建構函式傳入handlerThread物件的looper,這樣此handler的訊息處理 方法handleMessage就會在handlerThread所在的執行緒執行。這樣可以在工作執行緒執行耗時的訊息迴圈。因為HandlerThread繼承自Thread因此本質上是一個執行緒,必須先呼叫start方法後再獲取其looper

public class HandlerThread extends Thread {

使用HandlerThread的優點是比較明顯的,可以避免頻繁的建立執行緒(耗費資源和效能)用於處理後臺任務,取而代之的思路是開啟一個具有訊息迴圈功能的執行緒,達到執行緒複用的目的,和執行緒池異曲同工,只不過是排隊處理多工,沒有併發。

Picasso中HandlerThread的使用

Picasso中有一個提供快照的功能類Stats,用於統計某一時間Picasso中的各項資料。這些統計的功能並不需要和UI互動,在後臺執行緒新建立一個具有可以處理訊息迴圈的執行緒最好不過了,Picasso正是這麼做了,使用Handler和HandlerThread統計維護快照。

    this.statsThread = new HandlerThread(STATS_THREAD_NAME, THREAD_PRIORITY_BACKGROUND);
    this.statsThread.start();
    Utils.flushStackLocalLeaks(statsThread.getLooper());
    this.handler = new StatsHandler(statsThread.getLooper(), this);

在Stats的建構函式中,建立了HandlerThread物件statsThread,並呼叫了其start方法用於開啟執行緒,也就是開啟了訊息迴圈,因為在HandlerThread的run方法開頭呼叫了Looper.prepare()用於繫結執行緒和looper,run方法結尾呼叫了Looper.loop()用於迴圈處理訊息。

    @Override
    public void run() {
        mTid = Process.myTid();
        Looper.prepare();
        synchronized (this) {
            mLooper = Looper.myLooper();
            notifyAll();
        }
        Process.setThreadPriority(mPriority);
        onLooperPrepared();
        Looper.loop();
        mTid = -1;
    }

接著處理了一個Android 5中HandlerThread的問題,意思是HandlerThread總是在其執行緒棧中保持最後一個訊息的引用,有可能導致最後一個訊息的記憶體洩露。解決辦法就是每秒給handlerThread的looper傳送一個訊息,這樣最後一個訊息的引用在HandlerThread棧中的引用並不會保持太久。

    Utils.flushStackLocalLeaks(statsThread.getLooper());
    /**
   * Prior to Android 5, HandlerThread always keeps a stack local reference to the last message
   * that was sent to it. This method makes sure that stack local reference never stays there
   * for too long by sending new messages to it every second.
   */
static void flushStackLocalLeaks(Looper looper) {
    Handler handler = new Handler(looper) {
      @Override public void handleMessage(Message msg) {
        sendMessageDelayed(obtainMessage(), THREAD_LEAK_CLEANING_MS);
      }
    };
    handler.sendMessageDelayed(handler.obtainMessage(), THREAD_LEAK_CLEANING_MS);
  }

handlerThread物件建立並已經啟動了,就這需要為其looper建立一個handler

    this.handler = new StatsHandler(statsThread.getLooper(), this);

可以看到handler和statsThread的looper進行了繫結,因此handler的handleMessage訊息處理方法會在statsThread的執行緒中執行。
handler是一個StatsHandler型別物件,繼承自Handler

     private static class StatsHandler extends Handler {
     ...

重寫了Handler的handleMessage方法

    @Override public void handleMessage(final Message msg) {
      switch (msg.what) {
        case CACHE_HIT:
          stats.performCacheHit();
          break;
        case CACHE_MISS:
          stats.performCacheMiss();
          break;
        case BITMAP_DECODE_FINISHED:
          stats.performBitmapDecoded(msg.arg1);
          break;
        case BITMAP_TRANSFORMED_FINISHED:
          stats.performBitmapTransformed(msg.arg1);
          break;
        case DOWNLOAD_FINISHED:
          stats.performDownloadFinished((Long) msg.obj);
          break;
        default:
          Picasso.HANDLER.post(new Runnable() {
            @Override public void run() {
              throw new AssertionError("Unhandled stats message." + msg.what);
            }
          });
      }
    }

這樣Stats訊息處理的功能有了,那麼傳送訊息的功能應該是Stats暴漏給外部的API

  ...
  void dispatchCacheHit() {
    handler.sendEmptyMessage(CACHE_HIT);
  }
  void dispatchCacheMiss() {
    handler.sendEmptyMessage(CACHE_MISS);
  }
  ...

這樣外部在處理快取的時候,如果快取命中,就呼叫dispatchCacheHit()方法,快取沒命中就呼叫dispatchCacheMiss()方法,其他的解碼、轉換、下載成功等操作結果都會呼叫相應的方法,而這些方法均會給handler傳送個訊息,這些訊息對應的任務就都會在handler所繫結的looper的執行緒中排隊執行了。

Picasso快照功能的實現

快照就是維護某一時刻系統各項資料指標,方便獲取某一時刻獲取這些資料指標,在別的應用中可能還需要根據快照進行資料的恢復和容錯。
Picasso中的快照Stats主要用於統計維護Picasso的各種操作的數量,包括如下(命名很規範,顧名能思義):

  long cacheHits;
  long cacheMisses;
  long totalDownloadSize;
  long totalOriginalBitmapSize;
  long totalTransformedBitmapSize;
  long averageDownloadSize;
  long averageOriginalBitmapSize;
  long averageTransformedBitmapSize;
  int downloadCount;
  int originalBitmapCount;
  int transformedBitmapCount;

以下載成功為例進行分析。
下載成功後,會呼叫 dispatchDownloadFinished(long size)方法,該方法會給handler傳送一條Message訊息,訊息的what欄位為DOWNLOAD_FINISHED,obj欄位為下載成功的檔案大小size

  void dispatchDownloadFinished(long size) {
    handler.sendMessage(handler.obtainMessage(DOWNLOAD_FINISHED, size));
  }

在訊息處理回撥中,會處理DOWNLOAD_FINISHED型別的訊息

        case DOWNLOAD_FINISHED:
          stats.performDownloadFinished((Long) msg.obj);
          break;

也就是呼叫了performDownloadFinished方法

  void performDownloadFinished(Long size) {
    downloadCount++;
    totalDownloadSize += size;
    averageDownloadSize = getAverage(downloadCount, totalDownloadSize);
  }
  private static long getAverage(int count, long totalSize) {
    return totalSize / count;
  }

performDownloadFinished維護相應的資料變化,下載成功次數加1,已經下載的總大小加size,重新計算平均大小。
其他操作類似,一旦有操作結果的變化均會呼叫相應方法進行資料維護。
這樣快照中的各項資料均得到及時維護,如果要獲取某一時刻的快照,呼叫createSnapshot()方法即可。

  StatsSnapshot createSnapshot() {
    return new StatsSnapshot(cache.maxSize(), cache.size(), cacheHits, cacheMisses,
        totalDownloadSize, totalOriginalBitmapSize, totalTransformedBitmapSize, averageDownloadSize,
        averageOriginalBitmapSize, averageTransformedBitmapSize, downloadCount, originalBitmapCount,
        transformedBitmapCount, System.currentTimeMillis());
  }

這樣就獲取到了一個StatsSnapshot型別的物件,呼叫其dump方法即可將快照內容輸出

  /** Prints out this {@link StatsSnapshot} with the the provided {@link PrintWriter}. */
  public void dump(PrintWriter writer) {
    writer.println("===============BEGIN PICASSO STATS ===============");
    writer.println("Memory Cache Stats");
    writer.print("  Max Cache Size: ");
    writer.println(maxSize);
    writer.print("  Cache Size: ");
    writer.println(size);
    writer.print("  Cache % Full: ");
    writer.println((int) Math.ceil((float) size / maxSize * 100));
    writer.print("  Cache Hits: ");
    writer.println(cacheHits);
    writer.print("  Cache Misses: ");
    writer.println(cacheMisses);
    writer.println("Network Stats");
    writer.print("  Download Count: ");
    writer.println(downloadCount);
    writer.print("  Total Download Size: ");
    writer.println(totalDownloadSize);
    writer.print("  Average Download Size: ");
    writer.println(averageDownloadSize);
    writer.println("Bitmap Stats");
    writer.print("  Total Bitmaps Decoded: ");
    writer.println(originalBitmapCount);
    writer.print("  Total Bitmap Size: ");
    writer.println(totalOriginalBitmapSize);
    writer.print("  Total Transformed Bitmaps: ");
    writer.println(transformedBitmapCount);
    writer.print("  Total Transformed Bitmap Size: ");
    writer.println(totalTransformedBitmapSize);
    writer.print("  Average Bitmap Size: ");
    writer.println(averageOriginalBitmapSize);
    writer.print("  Average Transformed Bitmap Size: ");
    writer.println(averageTransformedBitmapSize);
    writer.println("===============END PICASSO STATS ===============");
    writer.flush();
  }

相關文章