Picasso使用Target無法回撥的分析與解決
在載入圖片的場景中,有時需要非同步拿到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) {
//開始載入
}
});
通過上面的回撥函式,我們就可以獲取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);
}
這裡我們可以看到,首先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);
}
// 程式碼省略
}
這裡可以看到,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);
//...省略
}
在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;
}
}
}
該執行緒從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();
}
當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);
}
}
};
從上面的分析我們可以得出結論:使用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,...);
}
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;
}
}
我們如果想通過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);
}
}