问题
直接上代码,大家一定会看的一头雾水,那我们先来引入几个问题。
我们使用图片框架的最终目的是什么?
是为了获取 Bitmap 对象 。
如何描述从不同位置加载图片 ? 例如从网络、磁盘、asset 、相册等。 如何描述图片的加载样式?(裁剪、居中、旋转等)
因此需要构造一个请求对象 - Request 。
图片加载完后,交给谁来显示 ?
大多数情况下交给 ImageView , 但有的时候我们需要直接拿到 Bitmap 对象,有的时候需要在通知栏或桌面组件上显示 。
需要一个对象来描述所需要显示的组件 - Action 。
磁盘 IO 、网络、Bitmap 处理都是耗时操作,需要放到异步线程。 如果仅仅处理这些,我们只需要线程池即可。但是有时需要取消、暂停、错误重试功能,该怎么办呢 ?
需要一个类来统一管理 ,对事件进行分发 - Dispatcher 。(OkHttp 中也有个 Dispatcher ,作用相同)。
图片从网络或磁盘加载成功后,如何处理图片(Bitmap) 为我们想要的对象呢 ?(比如图片要裁剪、旋转等。)
需要一个对象来处理图片- BitmapHunter。
Picasso 是如何处理这个过程的呢 ?
类之间的数据传递如下图 :
Picasso -> RequestCreator-> Request-> Action-> Dispatcher->BitmapHunter->Bitmp -> Action
接下来分析加载图片的流程 :
首先来看 RequestCreator 。
Picasso 支持链式调用, 当我们调用 Picasso.get().load(uri) 之后,方法的返回值类型变为 RequestCreator , 之后可以调用 placeHolder() 、centerCrop()、into() ,这些方法实际上调用的是 RequestCreator 的方法。
public RequestCreator load(@Nullable Uri uri) {
return new RequestCreator(this, uri, 0);
}
RequestCreator 这个类的作用是什么 ?
从命名上来这个类是来创造 Request 的 。 官方的定义 : 为了创建图片下载请求提供的流式 API 。
目的有两个 : (1) 链式调用。 (2) 创建 Request 。
当调用 into () 方法的时候, Picasso 完成链式调用。
public void into(ImageView target, Callback callback) {
long started = System.nanoTime();
//是否在主线程执行
checkMain();
// imageView 是否为空
if (target == null) {
throw new IllegalArgumentException("Target must not be null.");
}
// 如果没有为 ImageView 设置 Uri 或 ResourceId ,则显示占位图。
if (!data.hasImage()) {
// 如果该 ImagView 已经有请求,首先取消掉请求。
picasso.cancelRequest(target);
if (setPlaceholder) {
setPlaceholder(target, getPlaceholderDrawable());
}
return;
}
//deferred (延迟): 是否延迟执行。这个判断什么时候有效呢 ? 当且仅当设置 fit()方法 的时候,延迟加载图片。
//我们知道fit()方法要求图片填满整个 ImageView 。那就意味着必须知道 ImageView 的宽高。
//而 ImageView 的实际宽高要在 View 绘制完成后才能拿到。
//因此 Picasso为 ImageView 添加了 OnPreDrawListener 。
//当获取到 ImageView 的宽高后, Picasso 重新调用了 resize() 和 into() 方法加载图片 。
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);
// requestKey 与 Uri + 图片设置的字符串 。格式为 Uri + 换行符(\n) + 设置 +换行符(\n) ...
// 如 : http://i.imgur.com/DvpvklR.png\nrotation:90.0\nresize:3500x3500\ncenterCrop
String requestKey = createKey(request);
// 根据缓存策略,判断是否从内存缓存中获取图片。
if (shouldReadFromMemoryCache(memoryPolicy)) {
//从内存缓存中获取 Bitmap 对象
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;
}
}
// setPlaceholder 默认值为 true。如果不设置 PlaceHolder 话,我们会看到 ImageView 显示白色
// 如果你在 ListView/ RecyclerView 使用 Picasso 加载图片,请设置PlaceHolder,不然快速滑动,显示一片白色。
// 如果你把 setPlaceholder 改为 false , 在ListView / RecyclerView 中使用 Picasso 加载图片。
// 你将首先看到 View 复用前的图片。因此,千万不要改动 setPlaceholder 。
if (setPlaceholder) {
// 设置占位图
setPlaceholder(target, getPlaceholderDrawable());
}
// 构建 Action 对象
Action action =
new ImageViewAction(picasso, target, request, memoryPolicy, networkPolicy, errorResId,
errorDrawable, requestKey, tag, callback, noFade);
picasso.enqueueAndSubmit(action);
}
接着往下看 picasso.enqueueAndSubmit() 方法 :
void enqueueAndSubmit(Action action) {
// target 一般情况下是 ImageView 、 RemoteView 或 Target 对象。
// targetToAction 是 WeakHashMap() 的实例 , key 为 ImageView /target , value 为 Action 。
Object target = action.getTarget();
//下方代码做了一个判断, Target 已经存在,取消掉对应的请求。
// 这个判断很重要, 解决了 ListView/RecyclerView 快速滑动, ImageView 复用导致图片错位的问题。
if (target != null && targetToAction.get(target) != action) {
cancelExistingRequest(target);
targetToAction.put(target, action);
}
submit(action);
}
然后调用了 submit 方法 , 也就是调用了 dispatcher 的 dispatchSubmit() 方法。
void submit(Action action) {
dispatcher.dispatchSubmit(action);
}
...
// 调用 DispatcherHandler 的 sendMessage() 方法
void dispatchSubmit(Action action) {
handler.sendMessage(handler.obtainMessage(REQUEST_SUBMIT, action));
}
...
switch (msg.what) {
case REQUEST_SUBMIT: {
Action action = (Action) msg.obj;
// 在 handleMessage 中调用 performSubmit 方法。
dispatcher.performSubmit(action);
break;
}
...
我们发现最终是调用 dispatcher.performSubmit(action) 。看到这里,大家应该有疑问 ! 为什么这里要用 Handler , 直接调用 dispatcher.performSubmit() 方法不行吗 ?
这个 Handler.handleMessage() 其实是在 HandlerThread 线程执行的 , 也就是说这里进行了线程切换 , 切换到 HandlerThread 线程 。HandlerThread 继承自 Thread ,是 android 平台所特有的。同时从命名中也可以看出
在 Dispatcher 中以 perform 命名开头的方法都是在 HanderThread 中执行。
void performSubmit(Action action, boolean dismissFailed) {
// pausedTags 是 LinkedHashSet 实例
// 首选判断下载请求是否在暂停集合中。如果在暂停集合中,直接返回,不再下载。
if (pausedTags.contains(action.getTag())) {
pausedActions.put(action.getTarget(), action);
if (action.getPicasso().loggingEnabled) {
log(OWNER_DISPATCHER, VERB_PAUSED, action.request.logId(),
"because tag '" + action.getTag() + "' is paused");
}
return;
}
// BitmapHunter 是 LinkedHashMap 实例,存储的 key 为 url + 图片配置 value 为 BitmapHunter 对象 。
// 判断当前请求是否正在下载。如果正在下载, 将 action 添加到 BitmapHunter 的请求集合中。
// 目的为了避免重复下载 。
BitmapHunter hunter = hunterMap.get(action.getKey());
if (hunter != null) {
hunter.attach(action);
return;
}
// 线程池是否关闭, 这个判断大多数情况下用不到。
// 当你创建了多个 Picasso 实例, 关闭 Picasso 相应资源的时候才用到。
if (service.isShutdown()) {
if (action.getPicasso().loggingEnabled) {
log(OWNER_DISPATCHER, VERB_IGNORED, action.request.logId(), "because shut down");
}
return;
}
// 根据 Action 对象创建 BitmapHunter 对象, BitmapHunter 实现了 Runnable 接口。
hunter = forRequest(action.getPicasso(), this, cache, stats, action);
// 将任务提交到线程池。这里有个赋值操作,拿到 Feture 对象。目的是为了提供取消任务的功能。
hunter.future = service.submit(hunter);
hunterMap.put(action.getKey(), hunter);
if (dismissFailed) {
failedActions.remove(action.getTarget());
}
// 打印日志
if (action.getPicasso().loggingEnabled) {
log(OWNER_DISPATCHER, VERB_ENQUEUED, action.request.logId());
}
}
任务提交到线程池以后,最终调用了 Runnable 的 run() 方法。那么我们来看下 BimmapHunter 实现的 run ()方法。
@Override public void run() {
try {
//更新线程名称
updateThreadName(data);
//打印日志
if (picasso.loggingEnabled) {
log(OWNER_HUNTER, VERB_EXECUTING, getLogIdsForHunter(this));
}
// 重点 : 获取 Bitmap 对象。
result = hunt();
// bitmap 为空
if (result == null) {
dispatcher.dispatchFailed(this);
} else {
// bitmap 获取成功
dispatcher.dispatchComplete(this);
}
} catch (NetworkRequestHandler.ResponseException e) {
if (!NetworkPolicy.isOfflineOnly(e.networkPolicy) || e.code != 504) {
exception = e;
}
//服务器返回状态码,不在 200~300 之间。
dispatcher.dispatchFailed(this);
} catch (IOException e) {
exception = e;
// 下载重试
dispatcher.dispatchRetry(this);
} catch (OutOfMemoryError e) {
StringWriter writer = new StringWriter();
stats.createSnapshot().dump(new PrintWriter(writer));
exception = new RuntimeException(writer.toString(), e);
// 处理 Bitmap 过程中OOM
dispatcher.dispatchFailed(this);
} catch (Exception e) {
exception = e;
dispatcher.dispatchFailed(this);
} finally {
// 修改线程名称
Thread.currentThread().setName(Utils.THREAD_IDLE_NAME);
}
}
我们可以看到上述的核心逻辑有两个。
1. hunt() 方法获取 Bitmap 对象
2. 使用 Dispatcher 进行事件的分发。
首选来分析下 hunt() 方法 。代码比较多,但是逻辑是非常简单的。
Bitmap hunt() throws IOException {
Bitmap bitmap = null;
// 是否从内存缓存中获取 Bitmap
if (shouldReadFromMemoryCache(memoryPolicy)) {
bitmap = cache.get(key);
// 从内存缓存中获取到了图片,直接返回 bitmap 。
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;
//这里是重点 : 执行网络请求或者从磁盘加载 Bitmap 的地方。
RequestHandler.Result result = requestHandler.load(data, networkPolicy);
if (result != null) {
loadedFrom = result.getLoadedFrom();
exifOrientation = result.getExifOrientation();
bitmap = result.getBitmap();
// 如果 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) {
if (picasso.loggingEnabled) {
log(OWNER_HUNTER, VERB_DECODED, data.logId());
}
stats.dispatchBitmapDecoded(bitmap);
// Bitmap 是否需要进行额外处理。
if (data.needsTransformation() || exifOrientation != 0) {
// 这里比较有意思, hunt() 方法只在 run() 方法中执行。
// 为什么这里还是使用了 synchronized 呢? 翻译下 DECODE_LOCK 的注释,
// 全局锁为了保证同一时间只有一个后台线程在 decode 图片, 避免内存抖动和潜在的 OOM 。
// 有趣的是作者声称 ,这个写法是可耻的窃取自 Volley ! 哈哈 !
private static final Object DECODE_LOCK = new Object();
synchronized (DECODE_LOCK) {
// Bitmap 是否需要重新调整大小、旋转。
//如果你设置了resize()、rotate() 会执行下面方法。
if (data.needsMatrixTransform() || exifOrientation != 0) {
bitmap = transformResult(data, bitmap, exifOrientation);
if (picasso.loggingEnabled) {
log(OWNER_HUNTER, VERB_TRANSFORMED, data.logId());
}
}
// Bitmap 是否需要进行额外的转换 ,这个什么时候用呢 ?
// 我们知道 Picasso 是不支持圆角或者圆图的,我们日常开发又经常有这样的需求。
// 因此我们可以自定义 Transformation 。
// 而自定义的 Transformation 就是在 这个时候调用的。
if (data.hasCustomTransformations()) {
bitmap = applyCustomTransformations(data.transformations, bitmap);
if (picasso.loggingEnabled) {
log(OWNER_HUNTER, VERB_TRANSFORMED, data.logId(), "from custom transformations");
}
}
}
if (bitmap != null) {
stats.dispatchBitmapTransformed(bitmap);
}
}
}
return bitmap;
}
接下来我们看 Dispatcher 如何分发 Bitamp 。
void performComplete(BitmapHunter hunter) {
// 是否存储 Bitmap 到内存当中。
if (shouldWriteToMemoryCache(hunter.getMemoryPolicy())) {
cache.set(hunter.getKey(), hunter.getResult());
}
//从 hunterMap (你可以理解为下载队列,当然这个不是队列哦 !) 中移除。
hunterMap.remove(hunter.getKey());
// 批处理 hunter ,你可能对这个方法有疑惑,接着往下看。
batch(hunter);
if (hunter.getPicasso().loggingEnabled) {
log(OWNER_DISPATCHER, VERB_BATCHED, getLogIdsForHunter(hunter), "for completion");
}
}
我们注意到 performComplete() 最后又调用了 batch() 方法。 batch()方法是来做什么呢 ?
private void batch(BitmapHunter hunter) {
// 请求是否已经取消
if (hunter.isCancelled()) {
return;
}
if (hunter.result != null) {
hunter.result.prepareToDraw();
}
batch.add(hunter);
if (!handler.hasMessages(HUNTER_DELAY_NEXT_BATCH)) {
//延迟批处理
handler.sendEmptyMessageDelayed(HUNTER_DELAY_NEXT_BATCH, BATCH_DELAY);
}
}
Picasso 并不是将每个请求的回调,立即切换到主线程。而是每 200 ms 处理一次。
为什么 Picasso 这么写, 我没有找到答案 !
但是从性能方面分析,应该是为了避免同一时间大量回调,阻塞主线程 ,因此做了延迟批处理。
这样写的坏处是什么呢 ?
如果一张图片磁盘缓存中加载,那么意味着要经过 200 ms 才能显示 , 这显然不太合理,因为磁盘缓存加载也是非常快的 , Github 也有人遇到了同样问题。
怎么解决呢 ?
Github 有人解决的方法是不使用 Gradle 依赖 Picasso , 把 Picasso 下载到本地,然后手动改为 50 ms 。
如果不是老板一定要处理这个问题 , 个人建议还是不要这么改 。 我建议你提前调用 fetch() 方法 ,预加载图片到内存当中。
延迟批处理 200 ms 后, 最终调用了 mainThreadHandler 的 sendMessage() 方法 。经过这么长时间的分析,终于要在主线程显示图片了。
void performBatchComplete() {
// 200 ms 内需要分发的 BitmapHunter
List<BitmapHunter> copy = new ArrayList<>(batch);
batch.clear();
//切换到主线程显示图片
mainThreadHandler.sendMessage(mainThreadHandler.obtainMessage(HUNTER_BATCH_COMPLETE, copy));
logBatch(copy);
}
Picasso 主线程的 Handler :
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
// 遍历 200ms 内需要处理的 BitmapHunter
for (int i = 0, n = batch.size(); i < n; i++) {
BitmapHunter hunter = batch.get(i);
hunter.picasso.complete(hunter);
}
break;
最终调用 Picasso 的 complete () 方法 。
void complete(BitmapHunter hunter) {
// 初始请求
Action single = hunter.getAction();
// 重复请求
List<Action> joined = hunter.getActions();
//有重复请求
boolean hasMultiple = joined != null && !joined.isEmpty();
//是否应该分发
boolean shouldDeliver = single != null || hasMultiple;
// 如果请求都被取消了,直接 return 。
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);
}
}
接下来就不贴代码了 , 调用 Acton 的 complete 方法 ,将 Bitmap封装到 PicassoDrawable 交给Picasso 显示图片。