Picasso使用Target无法回调的分析与解决
Posted David-Kuper
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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);
以上是关于Picasso使用Target无法回调的分析与解决的主要内容,如果未能解决你的问题,请参考以下文章