Picasso使用Target無法回調的分析與解決

注:文章轉載自文章轉載自RowandJJ的博客:http://blog.csdn.net/chdjj/article/details/49964901

在加載圖片的場景中,有時需要異步拿到Bitmap做一些操作:bitmap預熱、bitmap裁剪等,當加載成功的時候通過回調的形式來獲取Bitmap,然後進行處理。Picasso提供了一種回調的方式獲取Bitmap。客戶端實現Target接口即可在加載成功的時候通過回調的方式返回bitmap。代碼如下:

Picasso.with(context).load(url).into(new Target() {
  @Override public void onBitmapLoaded(Bitmap bitmap, Picasso.LoadedFrom from) {
    //加載成功,進行處理
  }

  @Override public void onBitmapFailed(Drawable errorDrawable) {
    //加載失敗
  }

  @Override public void onPrepareLoad(Drawable placeHolderDrawable) {
    //開始加載
  }
});
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

通過上面的回調函數,我們就可以獲取Bitmap,然後進行bitmap的自定義處理。
但是有時候回調卻沒有觸發,也沒有異常,最後開啓Picasso日誌,才發現target引用被gc掉了:
這裏寫圖片描述

一、異步回調的陷阱

後面查看源碼之後才發現,由於Picasso將target引用包裝成了一個弱引用,當gc發生時target引用就很可能被回收從而無法回調。
首先,先看into(target)源碼:

public void into(Target target) {
//代碼省略....
//將target作爲參數,實例化一個targetAction,此處Action表示picasso的一個抽象行爲。
Action action = new TargetAction(picasso, target, request, memoryPolicy, networkPolicy, errorequestKey, tag, errorResId);

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

這裏我們可以看到,首先picasso會判斷是否從內存中讀取,如果不從內存中讀取,那麼就創建一個新的Action任務,將target作爲參數給TargetAction持有。重要關注TargetAction這個類,我們再看一看TargetAction類的構造有什麼內容:

final class TargetAction extends Action<Target> {

   TargetAction(Picasso picasso, Target target, Request data, int memoryPolicy,Drawable errorDrawable, String key, Object tag, int errorResId) {
      super(picasso, target, data, memoryPolicy, networkPolicy, errorResId, errorDraw,false);
   }
// 代碼省略
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

這裏可以看到,TargetAction繼承了Action類,target引用傳給了父類Action的構造函數:

abstract class Action<T> {
//picasso實現的弱引用
  static class RequestWeakReference<M> extends WeakReference<M> {
    final Action action;
    public RequestWeakReference(Action action, M referent, ReferenceQueue<? super>){
       super(referent, q);
       this.action = action;
    }
  }
  final Picasso picasso;
  final Request request;
  final WeakReference<T> target;
  final boolean noFade;

  Action(Picasso picasso, T target, Request request, int memoryPolicy, int network,int errorResId, Drawable errorDrawable, String key, Object tag, boolean ){
    this.picasso = picasso;
    this.request = request;
    //如果target不是null,那麼就將其包裹爲弱引用!同時關聯到
    //picasso的referenceQueue中。
    this.target = target == null ? null : new 
        RequestWeakReference<T>(this, target, 
               picasso.referenceQueue);
    //...省略
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

在Action的構造函數中將target包裹爲弱引用,同時關聯至picasso的referenceQueue中。這裏原因已經出來了,就是因爲target是弱引用,因此無法阻止正常的gc過程,只要回調之前發生了gc回收,那麼target很有可能就被回收掉了。一旦target被回收,那麼也就無法回調了。
將target的弱引用關聯至Picasso.referenceQueue是爲了監聽target被回收的狀態,Picasso有一個專門監聽target引用的線程CleanupThread,該線程會將監聽到的GC事件傳遞給Picasso的Handler:

private static class CleanupThread extends Thread {
  private final ReferenceQueue<Object> referenceQueue;
  private final Handler handler;

  CleanupThread(ReferenceQueue<Object> referenceQueue, 
  Handler handler) {
     this.referenceQueue = referenceQueue;
     this.handler = handler;
     setDaemon(true);
     setName(THREAD_PREFIX + "refQueue");
  }

  @Override public void run() {
     Process.setThreadPriority(THREAD_PRIORITY_BACKGROUND);
     while (true) {
       try {
       //這里開啓了一個死循環,每秒鐘從referenceQueue中拿到被
       //gc標誌的target引用
       RequestWeakReference<?> remove =
       referenceQueue.remove(THREAD_LEAK_CLEANING_M);
       Message message = handler.obtainMessage();
       //如果引用尚未爲空,說明尚未gc掉(但仍然會gc),則發出被
       //GC的通知,REQUEST_GCED通知
       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;
  }
 }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42

該線程從Picasso構造函數起執行:

Picasso(Context context, Dispatcher dispatcher, Cache cache, Listener listener,...){ 
  //省略
  //創建引用隊列,被gc標誌的引用在被gc前都會首加入其中
  this.referenceQueue = new ReferenceQueue<Object>();
  //創建並執行監聽線程
  this.cleanupThread = new 
  CleanupThread(referenceQueue, HANDLER);
  this.cleanupThread.start();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

當Picasso的Handler收到REQUEST_GCED消息時會撤銷當前請求:

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<Action>) msg.obj;
      //noinspection ForLoopReplaceableByForEach
      //發起通知
     for (int i = 0, n = batch.size(); i < n; i++) {
       BitmapHunter hunter = batch.get(i);
       hunter.picasso.complete(hunter);
     }
     break;
   }
   //GC消息
   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;
   }
   case REQUEST_BATCH_RESUME:
   @SuppressWarnings("unchecked") 
   List<Action> batch = (List<Action>) msg.obj;
   //noinspection ForLoopReplaceableByForEach
   for (int i = 0, n = batch.size(); i < n; i++) {
     Action action = batch.get(i);
     action.picasso.resumeAction(action);
   }
   break;
   default:
     throw new AssertionError("Unknown handler message 
     received: " + msg.what);
   }
 }
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41

從上面的分析我們可以得出結論:使用Target獲取bitmap並不保險,無法保證一定能夠獲得Bitmap。

二、解決方案

2.1 阻止gc(不建議)

既然是因爲弱引用造成的gc,那麼讓系統無法將target進行gc就可以了。開發者在加載圖片的週期內持有target的強引用,在獲取到bitmap之後再將其釋放即可。但是這樣違背了設計者的設計初衷,也容易引發內存泄漏的問題,原本設計者就是想讓target異步回調的形式不影響正常的gc回調。

設計者的原因很簡單:如果一個view實現了target接口,那麼view的生命週期就會被target影響,造成內存泄漏。
比如:在圖片加載期間,View可能已經離開了屏幕,將要被回收;或者Activity將要被銷燬。但是由於picasso還沒有加載完成,持有着view的引用,而view又持有Activity的引用,造成View和Activity都無法被回收。

2.2 使用get()的方式獲取Bitmap

除了使用Target來進行異步獲取,Picasso還提供了一個get()方法,進行同步的獲取:

public Bitmap get() throws IOException {
   //省略...
   Request finalData = createRequest(started);
   String key = createKey(finalData, new StringBuilder());
   Action action = new GetAction(picasso, finalData, memoryPolicy, networkPolicy, tBitmapHunter);

   //forRequest(xxx)返回的是一個BitmapHunter(繼承了
     Runnable),直接調用其中的hunt()方法獲
   return forRequest(picasso, picasso.dispatcher, picasso.cache, picasso.stats,...);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

BitmapHunter:

class BitmapHunter implements Runnable {
  //...此處省略N行代碼
  //獲取bitmap
  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;
    }
  }
  //網絡獲取
  data.networkPolicy = retryCount == 0 ? 
  NetworkPolicy.OFFLINE.index : networkPoli
  RequestHandler.Result result = 
  requestHandler.load(data, networkPolicy);
  if (result != null) {
    loadedFrom = result.getLoadedFrom();
    exifRotation = 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();
      try {
        bitmap = decodeStream(is, data);
      } finally {
        Utils.closeQuietly(is);
      }
    }
  }
  //bitmap的解碼、transform操作
  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 transformation");
           }
        }
       }
       if (bitmap != null) {
          stats.dispatchBitmapTransformed(bitmap);
       }
    }
  }
  return bitmap;
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67

我們如果想通過get來實現異步獲取,那麼就使用一個線程池進行get()方法調用就可以了:

/** 

* 同步獲取Bitmap,這種方式會在子線程當中同步去獲取Bitmap,不會採用回調的方式,也不會存在引用被 

* 要麼獲取成功;要麼獲取失敗;或者拋出異常。 

*/ 

private void fetchBySync(IFacadeBitmapCallback target) { 

    threadPoolExecutor.submit(() -> { 

       Bitmap bitmap = null; 

       try { 

         bitmap = requestCreator.get(); 

       } catch (IOException e) { 

          e.printStackTrace(); 

          target.onBitmapFailed(path, e); 

       } 

       if (bitmap == null) { 

          Log.e(getClass().getSimpleName(), "bitmap is null"); 

          target.onBitmapFailed(path, null); 

       } else { 

          Log.e(getClass().getSimpleName(), "bitmap " + bitmap.getClass().getSimpleName()); 

          target.onBitmapLoaded(path, bitmap); 

       } 

 }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章