• Android 框架学习1:EventBus 3.0 的特点与如何使用
  • Android 框架学习2:源码分析 EventBus 3.0 如何实现事件总线
  • Android 框架学习3:我从 EventBus 中学到的精华

通过本篇文章你将学到如下内容:

  • 假如我们来写一个框架
  • 走近 Picasso
    • 认识核心 API

      • 请求信息相关的类
      • 加载行为相关的类
      • 图片获取处理相关的类
      • 调度器 Dispatcher
      • 最核心的 图片猎手 BitmapHunter
      • 最终的门面 Picasso
    • 常见功能实现分析
      • 发起图片请求后的整体流程
      • 取消、暂停、恢复加载如何实现
      • 动态调整线程池数量的实现
      • 缓存策略
  • 总结一下从 Picasso 中我们能学到什么
  • Thanks

分析过源码的朋友可以直接滑到文章底部看最后的总结,欢迎一起讨论。

假如我们来写一个框架

在学习一个框架之前,我们最好先设想一下,如果让自己来写这样一个框架,会如何写呢?

就拿本篇文章要研究的图片加载框架来说,我们知道一个图片框架的核心功能就是:将图片显示到界面上

具体点说,图片显示到界面上这个过程中可能会遇到这些情况:

  • 加载的图片可能有网络、本地等多种来源;
  • 如果是网络的话,就得先下载下来;
  • 下载过程中可能需要暂停、恢复或者取消;
  • 下载后需要解码、对图片进行一些额外操作(比如裁剪、转变等);
  • 最好还有个缓存系统,避免每次都去网络请求;
  • 为了实现性能监控,最好再有个数据统计功能…

有了以上需求,根据职责分离的原则,我们可以定义一些核心类来完成上述功能:

  1. 请求信息类:其中包含了所有可以配置的选项,比如图片地址、要进行的操作等
  2. 图片获取类:根据不同的来源去不同地方获取,比如网络、本地、内存等
  3. 调度器类:实现图片获取的入队、执行、完成、取消、暂停等
  4. 图片处理类:图片拿到后进行解码、反转、裁剪等
  5. 缓存类:图片的内存、磁盘缓存控制
  6. 监控类:统计核心数据,比如当前内存、磁盘缓存的大小、某个图片的加载时间等

OK,有了这些核心类,我们就可以画一个简单的图片加载框架流程图了:

画出图后逻辑就清晰多了,接着我们来看看 Picasso 的核心 API 以及它们如何实现的图片加载,和我们设想的有什么区别吧。

走近 Picasso

  • 本文分析代码基于 Picasso v2.71828
  • 下载地址:https://github.com/square/picasso/releases/tag/2.71828

认识核心 API

首先我们来认识下 Picasso 的核心 API。

下图是 Picasso 的项目结构(吐槽一下,怎么都不分几个文件夹,可能是为了少写些 public 吧哈哈):

我给 Picasso 文件夹结构进行了调整,变成了这样:

主要分为几个关键部分:

  1. request 文件夹中的:请求信息相关的类
  2. action 文件夹中的:加载行为相关的类
  3. handler 文件夹中的:图片获取具体处理的类
  4. Dispatcher:调度器
  5. BitmapHunter:耗时任务执行者
  6. Picasso:暴露给用户的类

请求信息相关的类

上图中的 request 文件夹里放的是 Picasso 中构建图片请求信息相关的类,总共有五个,我们来分别了解下它们。

首先看 Request.java的成员变量(直接看它的 Builder ):

/** Builder for creating {@link Request} instances. */
public static final class Builder {private Uri uri;private int resourceId;private String stableKey;private int targetWidth;private int targetHeight;private boolean centerCrop;private int centerCropGravity;private boolean centerInside;private boolean onlyScaleDown;private float rotationDegrees;private float rotationPivotX;private float rotationPivotY;private boolean hasRotationPivot;private boolean purgeable;private List<Transformation> transformations;private Bitmap.Config config;private Priority priority;//...
}

可以看到,Request 中放的是一个图片的本地信息、要进行的转换操作信息、图片配置信息以及优先级等。

这里我们可以学习到的是:如果一个请求参数很多,我们最好用一个类给它封装起来,避免在传递时传递多个参数;如果经常使用的话,还可以创建一个对象池,节省开销

接着看第二个类 RequestCreator:

public class RequestCreator {private static final AtomicInteger nextId = new AtomicInteger();private final Picasso picasso;private final Request.Builder data;private boolean noFade;private boolean deferred;private boolean setPlaceholder = true;private int placeholderResId;private int errorResId;private int memoryPolicy;private int networkPolicy;private Drawable placeholderDrawable;private Drawable errorDrawable;private Object tag;//...
}

可以看到, RequestCreator 中包含了 Request.Builder,此外还有了些额外的信息,比如是否设置占位图、是否有渐变动画、是否延迟处理、以及占位图错误图资源 ID、内存使用策略、网络请求策略等。

RequestCreator 是相当重要的一个类,我们后面会进一步介绍它。

接着看第三个类 DeferredRequestCreator:

public class DeferredRequestCreator implements OnPreDrawListener, OnAttachStateChangeListener {private final RequestCreator creator;public @VisibleForTesting final WeakReference<ImageView> target;@VisibleForTestingpublic Callback callback;//...
}

可以看到, DeferredRequestCreator 中引用了 RequestCreator,此外还有一个要加载的 ImageView 弱引用对象,还有一个 Callback,它实现了 OnPreDrawListeneronAttachStateChangeListener 接口,这两个接口的作用如下:

  • OnPreDrawListener:当布局树将要绘制前,会回调这个借口的 onPreDraw() 方法
  • onAttachStateChangeListener:当布局绑定到一个 window 或者解除绑定和一个 window 时会调用

DeferredRequestCreator 中比较重要的就是这个 onPreDraw() 方法:


@Override public boolean onPreDraw() {ImageView target = this.target.get();if (target == null) {return true;}ViewTreeObserver vto = target.getViewTreeObserver();if (!vto.isAlive()) {return true;}int width = target.getWidth();int height = target.getHeight();if (width <= 0 || height <= 0) {return true;}target.removeOnAttachStateChangeListener(this);vto.removeOnPreDrawListener(this);this.target.clear();this.creator.unfit().resize(width, height).into(target, callback);return true;
}

在加载网络图片后需要让图片的尺寸和目标 ImageView 一样大时(即调用 RequestCreator.fit() 方法),会使用到 DeferredRequestCreator

剩下的两个枚举 MemoryPolicyNetworkPolicy 就简单多了。

MemoryPolicy 指定了两种内存缓存策略:不去内存缓存里查找和不写入内存缓存。

public enum MemoryPolicy {//当请求图片时不去内存缓存里找NO_CACHE(1 << 0),//拿到图片后不写到内存缓存里,一般用于一次性请求NO_STORE(1 << 1);public static boolean shouldReadFromMemoryCache(int memoryPolicy) {return (memoryPolicy & MemoryPolicy.NO_CACHE.index) == 0;}public static boolean shouldWriteToMemoryCache(int memoryPolicy) {return (memoryPolicy & MemoryPolicy.NO_STORE.index) == 0;}
}

NetworkPolicy 指定了三种网络请求策略:

  1. NO_CACHE: 跳过检查磁盘缓存,强制请求网络
  2. NO_STORE: 拿到结果不写入磁盘缓存中
  3. OFFLINE: 不请求网络,只能去磁盘缓存里查找
public enum NetworkPolicy {NO_CACHE(1 << 0),NO_STORE(1 << 1),OFFLINE(1 << 2);public static boolean shouldReadFromDiskCache(int networkPolicy) {return (networkPolicy & NetworkPolicy.NO_CACHE.index) == 0;}public static boolean shouldWriteToDiskCache(int networkPolicy) {return (networkPolicy & NetworkPolicy.NO_STORE.index) == 0;}public static boolean isOfflineOnly(int networkPolicy) {return (networkPolicy & NetworkPolicy.OFFLINE.index) != 0;}
}

上面介绍了 Picasso 中关于请求信息的五个类,小结一下,它们的作用如下:

  1. Request:保存一个图片的本地信息、要进行的转换操作信息、图片配置信息以及优先级
  2. RequestCreator:保存了一个图片加载请求的完整信息,包括图片信息、是否设置占位图、是否有渐变动画、是否延迟处理、以及占位图错误图资源 ID、内存使用策略、网络请求策略等
  3. MemoryPolicy:定义了加载图片时的两种图片缓存策略
  4. NetworkPolicy:定义了加载图片时的三种网络请求策略

加载行为相关的类

了解完请求信息相关的类后,我们再看看 action 文件夹下,关于加载行为的类(这里的 “加载行为” 是我临时起的名,可能不是很容易理解,稍后我再解释一下)。

这六个类里 Action 是基类,我们先看它。

public abstract class Action<T> {public final Picasso picasso;public final Request request;public final WeakReference<T> target;public final boolean noFade;public final int memoryPolicy;public final int networkPolicy;public final int errorResId;public final Drawable errorDrawable;public final String key;public final Object tag;public boolean willReplay;public boolean cancelled;/*** 图片获取到后要调用的方法* @param result* @param from*/public abstract void complete(Bitmap result, Picasso.LoadedFrom from);/*** 图片获取失败后要调用的方法* @param e*/public  abstract void error(Exception e);
}

可以看到, Action 的成员变量里包含了一个图片的请求信息和加载策略、错误占位图,同时定义了两个抽象方法,这两个方法的作用是当图片加载成功后会调用 complete()(参数是拿到的图片和加载来源),加载失败后会调用 eror(),子类继承后可以实现自己特定的操作。

前面提到这些 action 表示的是加载行为,所谓“加载行为”简单点说就是“拿到图片要干啥”。

发起一个图片加载请求的目的可能有多种,最常见的就是加载到图片上,对应 Picasso 里的 ImageViewAction(加载完成时它会把图片设置给 ImageView):

public class ImageViewAction extends Action<ImageView> {Callback callback;//加载成功,将图片设置给 ImageView@Override public void complete(Bitmap result, Picasso.LoadedFrom from) {if (result == null) {throw new AssertionError(String.format("Attempted to complete action with no result!\n%s", this));}ImageView target = this.target.get();if (target == null) {return;}Context context = picasso.context;boolean indicatorsEnabled = picasso.indicatorsEnabled;PicassoDrawable.setBitmap(target, context, result, from, noFade, indicatorsEnabled);  //设置图片if (callback != null) {callback.onSuccess();}}//失败时给 ImageView 设置错误图片@Override public void error(Exception e) {ImageView target = this.target.get();if (target == null) {return;}Drawable placeholder = target.getDrawable();if (placeholder instanceof Animatable) {((Animatable) placeholder).stop();}if (errorResId != 0) {target.setImageResource(errorResId);} else if (errorDrawable != null) {target.setImageDrawable(errorDrawable);}if (callback != null) {callback.onError(e);}}
}

除此外,Picasso 还提供了四种其他用途的加载行为类,源码比较容易理解,这里就直接贴出作用:

  1. FetchAction: 拿到图片后会有个回调,除此外不会将图片显示到界面上

    • Picasso.fetch() 方法会使用到它,这个方法在后台线程异步加载图片,只会将图片保存到硬盘或者内存上,不会显示到界面上。如果你不久之后就用这个图片,或者想要减少加载时间,你可以提前将图片下载缓存起来。
  2. GetAction:拿到图片后不会有任何操作,不知道干啥的
    • Picasso.get() 方法会使用到它,这个方法会同步加载图片并返回 Bitmap 对象,请确保你没有在Ui线程里面使用.get() 方法。这将会阻塞UI!
  3. RemoteViewsAction: 拿到图片后设置给 RemoteView,有两个实现类 AppWidgetActionNotificationAction,分别对应桌面插件和提醒栏
  4. TargetAction:首先 Target 是 Picasso 中定义的一个接口,表示对图片加载的监听;TargetAction 在拿到图片后会调用 Target 接口的方法

上面的 1 2 两点部分摘自:
http://blog.csdn.net/u011337574/article/details/51588785

图片获取处理相关的类

接着介绍 handler 文件夹下的类,这个文件夹中类的功能就是:处理去不同渠道加载图片的请求

其中 RequestHandler 是基类,我们先来看看它。

public abstract class RequestHandler {/*** Whether or not this {@link RequestHandler} can handle a request with the given {@link Request}.*/public abstract boolean canHandleRequest(Request data);/*** Loads an image for the given {@link Request}.** @param request the data from which the image should be resolved.* @param networkPolicy the {@link NetworkPolicy} for this request.*/@Nullable public abstract Result load(Request request, int networkPolicy) throws IOException;

RequestHandler 代码也比较简单,除了几个计算图片尺寸的方法外,最关键的就是上述的两个抽象方法:

  1. boolean canHandleRequest(Request data) 表示当前获取类能否处理这个请求,一般子类会根据请求的 URI 来判断
  2. Result load(Request request, int networkPolicy) 表示根据网络策略加载某个请求,返回加载结果

加载结果 Result 也比较简单:

  public static final class Result {private final Picasso.LoadedFrom loadedFrom;    //从哪儿加载的(网络、内存、磁盘)private final Bitmap bitmap;private final Source source;    //okio 中定义的数据流类private final int exifOrientation;    //图片的旋转方向public Result(@NonNull Bitmap bitmap, @NonNull Picasso.LoadedFrom loadedFrom) {this(checkNotNull(bitmap, "bitmap == null"), null, loadedFrom, 0);}public Result(@NonNull Source source, @NonNull Picasso.LoadedFrom loadedFrom) {this(null, checkNotNull(source, "source == null"), loadedFrom, 0);}Result(@Nullable Bitmap bitmap,@Nullable Source source,@NonNull Picasso.LoadedFrom loadedFrom,int exifOrientation) {if ((bitmap != null) == (source != null)) {throw new AssertionError();}this.bitmap = bitmap;this.source = source;this.loadedFrom = checkNotNull(loadedFrom, "loadedFrom == null");this.exifOrientation = exifOrientation;}}

RequestHandler 的子类实现都比较简单,这里我们就选常见的处理网络和文件请求的获取类来看看。

从名字就可以看出的从网络获取图片处理类 NetworkRequestHandler:

public class NetworkRequestHandler extends RequestHandler {private static final String SCHEME_HTTP = "http";private static final String SCHEME_HTTPS = "https";private final Downloader downloader;private final Stats stats;public NetworkRequestHandler(Downloader downloader, Stats stats) {this.downloader = downloader;this.stats = stats;}//根据请求 uri 的 scheme 判断是否为 http/https 请求@Override public boolean canHandleRequest(Request data) {String scheme = data.uri.getScheme();return (SCHEME_HTTP.equals(scheme) || SCHEME_HTTPS.equals(scheme));}//去网络加载一个图片@Override public Result load(Request request, int networkPolicy) throws IOException {okhttp3.Request downloaderRequest = createRequest(request, networkPolicy);Response response = downloader.load(downloaderRequest);ResponseBody body = response.body();if (!response.isSuccessful()) {body.close();throw new ResponseException(response.code(), request.networkPolicy);}// Cache response is only null when the response comes fully from the network. Both completely// cached and conditionally cached responses will have a non-null cache response.Picasso.LoadedFrom loadedFrom = response.cacheResponse() == null ? NETWORK : DISK;// Sometimes response content length is zero when requests are being replayed. Haven't found// root cause to this but retrying the request seems safe to do so.if (loadedFrom == DISK && body.contentLength() == 0) {body.close();throw new ContentLengthException("Received response with 0 content-length header.");}return new Result(body.source(), loadedFrom);}
}

从上面的代码我们可以看到,Picasso 使用 okhttp3 来完成下载的功能,其中的下载器 downloader 就是通过构造一个 okhttp.Call 来完成同步下载文件:

@NonNull @Override public Response load(@NonNull Request request) throws IOException {return client.newCall(request).execute();
}

从文件获取图片的请求处理类 FileRequestHandler

public class FileRequestHandler extends ContentStreamRequestHandler {public FileRequestHandler(Context context) {super(context);}@Override public boolean canHandleRequest(Request data) {return SCHEME_FILE.equals(data.uri.getScheme());}@Override public Result load(Request request, int networkPolicy) throws IOException {Source source = Okio.source(getInputStream(request));return new Result(null, source, DISK, getFileExifRotation(request.uri));}InputStream getInputStream(Request request) throws FileNotFoundException {ContentResolver contentResolver = context.getContentResolver();return contentResolver.openInputStream(request.uri);
}static int getFileExifRotation(Uri uri) throws IOException {ExifInterface exifInterface = new ExifInterface(uri.getPath());return exifInterface.getAttributeInt(TAG_ORIENTATION, ORIENTATION_NORMAL);}
}

也很简单是吧,根据 URI 获取输入流通过 ContentResolver.openInputStream( Uri uri) 可以实现,这个可以记一下以后可能会用到,拿到 IO 流后,接下来的的操作直接通过 Okio 实现了。

通过这几个图片请求处理类我们可以看到 Picasso 的设计多么精巧,每个类即精简又功能独立,我们在开发时最好可以参考这样的代码,先定义接口和基类,然后再考虑不同的实现。

分析完这些“大家族”后,剩下的就是一些单独的类了。

调度器 Dispatcher

调度器的角色在许多框架里可以看到,实际上在稍微复杂一点的业务逻辑,都需要这么一个调度器类,它负责业务逻辑在不同线程的切换、执行、取消。

我们来看看 Picasso 中的调度器,首先看它的成员变量:

public class Dispatcher {private static final String DISPATCHER_THREAD_NAME = "Dispatcher";private static final int BATCH_DELAY = 200; // msfinal DispatcherThread dispatcherThread;    //HandlerThread,用于为子线程 Handler 准备 Looperfinal Context context;final ExecutorService service;    //线程池final Downloader downloader;    //下载器final Map<String, BitmapHunter> hunterMap;    //Action's key 和 图片猎人 的关联关系final Map<Object, Action> failedActions;  //失败的操作 mapfinal Map<Object, Action> pausedActions;  //暂停的操作 mapfinal Set<Object> pausedTags;    //暂停的 tagfinal Handler handler;    //子线程的 Handlerfinal Handler mainThreadHandler;    //ui 线程的 Handlerfinal Cache cache;    //缓存final Stats stats;    //数据统计final List<BitmapHunter> batch;    //后面介绍,获取图片最核心的类final NetworkBroadcastReceiver receiver;final boolean scansNetworkChanges;boolean airplaneMode;
}

可以看到,Dispatcher 的成员变量有 HandlerThread,两个 Handler、线程池,下载器、BitmapHunter(我叫它“图片猎手”,后面介绍它)、缓存、数据统计等等。

从 Picasso 的 Dispatcher 中,我们可以学到如何创建一个复杂业务的调度器。

复杂业务往往需要在子线程中进行,于是需要用到线程池;线程之间切换需要用到 Handler,为了省去创建 Looper 的功夫,就需要使用 HandlerThread;此外还需要持有几个列表、Map,来保存操作数据。

作为调度器,最重要的功能就是给外界提供各种功能的接口,一般我们都使用不同的常量来标识不同的逻辑,在开始写业务之前,最好先定好功能、确定常量。

我们来看看 Dispatcher 中定义的常量都代表着什么功能:

  private static final int RETRY_DELAY = 500;    //重试的延迟时间private static final int AIRPLANE_MODE_ON = 1;private static final int AIRPLANE_MODE_OFF = 0;public static final int REQUEST_SUBMIT = 1;    //提交请求public static final int REQUEST_CANCEL = 2;    //取消请求public static final int REQUEST_GCED = 3;    //请求被回收public static final int HUNTER_COMPLETE = 4;    //图片获取完成public static final int HUNTER_RETRY = 5;        //重试图片获取public static final int HUNTER_DECODE_FAILED = 6;    //图片解码失败public static final int HUNTER_DELAY_NEXT_BATCH = 7;   public static final int HUNTER_BATCH_COMPLETE = 8;    //图片批量获取成功public static final int NETWORK_STATE_CHANGE = 9;    //网络状态改变public static final int AIRPLANE_MODE_CHANGE = 10;    //飞行模式改变public static final int TAG_PAUSE = 11;public static final int TAG_RESUME = 12;public static final int REQUEST_BATCH_RESUME = 13;

上图中对大多数操作的功能做了注释。确定好功能后,就可以创建 Handler 了,它负责接收不同线程发出的请求。

Dispatcher 的内部类 DispatcherHandler 是在子线程中执行的 Handler:

private static class DispatcherHandler extends Handler {private final Dispatcher dispatcher;DispatcherHandler(Looper looper, Dispatcher dispatcher) {super(looper);this.dispatcher = dispatcher;}@Override public void handleMessage(final Message msg) {switch (msg.what) {case REQUEST_SUBMIT: {Action action = (Action) msg.obj;dispatcher.performSubmit(action);break;}case REQUEST_CANCEL: {Action action = (Action) msg.obj;dispatcher.performCancel(action);break;}case TAG_PAUSE: {Object tag = msg.obj;dispatcher.performPauseTag(tag);break;}case TAG_RESUME: {Object tag = msg.obj;dispatcher.performResumeTag(tag);break;}case HUNTER_COMPLETE: {BitmapHunter hunter = (BitmapHunter) msg.obj;dispatcher.performComplete(hunter);break;}//...}}
}

然后在 Dispatcher 中创建接受请求的方法:

public void dispatchSubmit(Action action) {handler.sendMessage(handler.obtainMessage(REQUEST_SUBMIT, action));
}public void dispatchCancel(Action action) {handler.sendMessage(handler.obtainMessage(REQUEST_CANCEL, action));
}public void dispatchPauseTag(Object tag) {handler.sendMessage(handler.obtainMessage(TAG_PAUSE, tag));
}public void dispatchResumeTag(Object tag) {handler.sendMessage(handler.obtainMessage(TAG_RESUME, tag));
}

最后就是创建处理请求的方法了,比如提交图片获取操作的方法:

public void performSubmit(Action action, boolean dismissFailed) {if (pausedTags.contains(action.getTag())) {pausedActions.put(action.getTarget(), action);return;}BitmapHunter hunter = hunterMap.get(action.getKey());if (hunter != null) {hunter.attach(action);return;}if (service.isShutdown()) {return;}hunter = forRequest(action.getPicasso(), this, cache, stats, action);hunter.future = service.submit(hunter);hunterMap.put(action.getKey(), hunter);if (dismissFailed) {failedActions.remove(action.getTarget());}
}

具体一些方法如何实现的,我们后面再看。这里了解调度器的基本信息,掌握如何写一个调度器的流程即可。

最核心的 图片猎手 BitmapHunter

前面介绍了那么多 API,它们基本是各自实现一个单独的模块功能,Picasso 中的 BitmapHunter 是把这些组合起来,具体实现图片的获取、解码、转换操作的类。

public class BitmapHunter implements Runnable {//解码 bitmap 使用的全局锁,确保一次只解码一个,避免内存溢出private static final Object DECODE_LOCK = new Object();private static final AtomicInteger SEQUENCE_GENERATOR = new AtomicInteger();final int sequence;final Picasso picasso;final Dispatcher dispatcher;final Cache cache;final Stats stats;final String key;final Request data;final int memoryPolicy;int networkPolicy;final RequestHandler requestHandler;Action action;List<Action> actions;   //要执行的操作列表Bitmap result;Future<?> future;Picasso.LoadedFrom loadedFrom;Exception exception;int exifOrientation; // Determined during decoding of original resource.int retryCount;Priority priority;
}

可以看到, BitmapHunter 的成员变量有我们前面介绍的那些关键类。同时它实现了 Runnable 接口,在 run() 方法中执行耗时任务:

@Override public void run() {try {updateThreadName(data);if (picasso.loggingEnabled) {log(OWNER_HUNTER, VERB_EXECUTING, getLogIdsForHunter(this));}result = hunt();    //获取图片if (result == null) {dispatcher.dispatchFailed(this);} else {dispatcher.dispatchComplete(this);}} catch (NetworkRequestHandler.ResponseException e) {if (!NetworkPolicy.isOfflineOnly(e.networkPolicy) || e.code != 504) {exception = e;}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);dispatcher.dispatchFailed(this);    } catch (Exception e) {exception = e;dispatcher.dispatchFailed(this);} finally {Thread.currentThread().setName(Utils.THREAD_IDLE_NAME);}
}

run() 方法非常简单,调用 hunt() 方法后就是一长串异常捕获和调度,这里可以看出自定义异常的重要性,在复杂的 IO、网络操作中,有很多产生异常的可能,在不同操作里抛出不同类型的异常,有助于最后的排查、处理。

我们来看看完成主要任务的 hunt() 方法:

public Bitmap hunt() throws IOException {Bitmap bitmap = null;if (shouldReadFromMemoryCache(memoryPolicy)) {    //1.根据请求的缓存策略,判断是否要读取缓存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;}}//2.调用适当的 requestHandler 来处理图片加载请求networkPolicy = retryCount == 0 ? NetworkPolicy.OFFLINE.index : networkPolicy;RequestHandler.Result result = requestHandler.load(data, networkPolicy);if (result != null) {   //加载成功loadedFrom = result.getLoadedFrom();exifOrientation = result.getExifOrientation();bitmap = result.getBitmap();//拿到图片加载结果时,有可能这个数据还没有解码,因此需要进行解码if (bitmap == null) {Source source = result.getSource();try {bitmap = decodeStream(source, data);  //解码操作} finally {try {source.close();} catch (IOException ignored) {}}}}//3.拿到图片加载结果后有解码好的 bitmap,进入下一步,转换if (bitmap != null) {   if (picasso.loggingEnabled) {log(OWNER_HUNTER, VERB_DECODED, data.logId());}stats.dispatchBitmapDecoded(bitmap);if (data.needsTransformation() || exifOrientation != 0) {synchronized (DECODE_LOCK) {if (data.needsMatrixTransform() || exifOrientation != 0) {bitmap = transformResult(data, bitmap, exifOrientation);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 transformations");}}}if (bitmap != null) {stats.dispatchBitmapTransformed(bitmap);}}}return bitmap;
}

可以看到,BitmapHunter 中获取图片的 hunt() 方法的逻辑如下:

  1. 如果缓存策略允许去内存缓存读取,就去缓存里找,找到就返回
  2. 否则调用适当的 RequestHandler 去处理图片加载请求
  3. 如果 RequestHandler 加载成功但是这个图片数据还没有解码,就去解码
  4. 拿到解码后的图片就进入下一步,转换
  5. 转换有 Picasso 支持的转换(比如裁剪什么的),也有自定义的
  6. 最后返回转换后的图片

最终的门面 Picasso

终于该介绍我们的门面类 Picasso 了。

Picasso 类的存在就是“外观模式”(也成门面模式)的完美体现,它集成了前面提到的复杂的类,然后为我们提供了许多配置的方法,这样我们在使用时只需要调用 Picasso 的方法即实现目的,不用和更多类打交道:

Picasso.get() //获得 Picasso 单例.load(url) //.placeholder(R.drawable.placeholder) //.error(R.drawable.error) //.fit() //.tag(context) //.into(view);

我们看看 Picasso 的成员属性:

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;for (int i = 0, n = batch.size(); i < n; i++) {BitmapHunter hunter = batch.get(i);hunter.picasso.complete(hunter);}break;}case REQUEST_GCED: {    //请求被回收了,取消Action action = (Action) msg.obj;action.picasso.cancelExistingRequest(action.getTarget());break;}case REQUEST_BATCH_RESUME:    //回复批量获取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);}}
};@SuppressLint("StaticFieldLeak") static volatile Picasso singleton = null;private final Listener listener;
private final RequestTransformer requestTransformer;
private final CleanupThread cleanupThread;        //清理线程
private final List<RequestHandler> requestHandlers;    //请求处理器列表public final Context context;
public final Dispatcher dispatcher;    //调度器
public final Cache cache;
public final Stats stats;
public final Map<Object, Action> targetToAction;   //ImageView 和对应的请求
public final Map<ImageView, DeferredRequestCreator> targetToDeferredRequestCreator;  //ImageView 和对应的延迟请求
public final ReferenceQueue<Object> referenceQueue;    //引用队列
public final Bitmap.Config defaultBitmapConfig;public boolean indicatorsEnabled;
public volatile boolean loggingEnabled;public boolean shutdown;

可以看到它集成了前面介绍的关键类,同时也持有了请求处理器列表、ImageView 和对应的 Action 哈希表等数据。

在它的构造方法中,这些成员变量进行了赋值和初始化:

Picasso(Context context, Dispatcher dispatcher, Cache cache, Listener listener,RequestTransformer requestTransformer, List<RequestHandler> extraRequestHandlers, Stats stats,Bitmap.Config defaultBitmapConfig, boolean indicatorsEnabled, boolean loggingEnabled) {int builtInHandlers = 7; // Adjust this as internal handlers are added or removed.int extraCount = (extraRequestHandlers != null ? extraRequestHandlers.size() : 0);List<RequestHandler> allRequestHandlers = new ArrayList<>(builtInHandlers + extraCount);// ResourceRequestHandler needs to be the first in the list to avoid// forcing other RequestHandlers to perform null checks on request.uri// to cover the (request.resourceId != 0) case.allRequestHandlers.add(new ResourceRequestHandler(context));if (extraRequestHandlers != null) {allRequestHandlers.addAll(extraRequestHandlers);}allRequestHandlers.add(new ContactsPhotoRequestHandler(context));allRequestHandlers.add(new MediaStoreRequestHandler(context));allRequestHandlers.add(new ContentStreamRequestHandler(context));allRequestHandlers.add(new AssetRequestHandler(context));allRequestHandlers.add(new FileRequestHandler(context));allRequestHandlers.add(new NetworkRequestHandler(dispatcher.downloader, stats));requestHandlers = Collections.unmodifiableList(allRequestHandlers);
}

上面的构造方法省去了其他元素,单独露出了 RequestHandler 列表的初始化,可以看到在 Picasso 构造时将我们见过的所有 RequestHandler 子类进行了实例化,后面在获取图片时,会遍历这个列表来找到能够处理请求的处理器类。

Picasso 中暴露了很多方法,我们在后面的具体业务逻辑时查看。

除了 Picasso ,其他比较知名的框架都会用到“外观模式” ,我们在编写复杂逻辑或者 SDK 时应该在完成各个子模块以后,在它们的上面增加一层,由这一层来和各个模块交互,给使用者提供统一、简单的调用接口,避免暴露太多内部模块。

常见功能实现分析

经过前面对核心 API 的介绍,我们已经对 Picasso 有个大概的了解了,接下来通过不同的业务逻辑,来整体上掌握 Picasso 的实现流程。

主要看四个功能的实现:

  1. 发起图片请求后的整体流程
  2. 取消、暂停、恢复加载如何实现
  3. 动态调整线程池数量的实现
  4. 缓存策略

发起图片请求后的整体流程

经典的调用:

Picasso.get() //1.获得 Picasso 单例.load(url) //2.创建 RequestCreator.placeholder(R.drawable.placeholder) .error(R.drawable.error) .fit() .tag(context) .into(view);    //3.发起请求

第一步 Picasso.get() 方法返回的是 Picasso 的单例,它通过 Picasso.Builder 构造:

public static Picasso get() {if (singleton == null) {synchronized (Picasso.class) {if (singleton == null) {if (PicassoProvider.context == null) {throw new IllegalStateException("context == null");}singleton = new Builder(PicassoProvider.context).build();}}}return singleton;
}

我们看看 Picasso.Builder.build() 方法:

public Picasso build() {Context context = this.context;if (downloader == null) {downloader = new OkHttp3Downloader(context);    //下载}if (cache == null) {cache = new LruCache(context);    //缓存}if (service == null) {service = new PicassoExecutorService();    //线程池}if (transformer == null) {transformer = RequestTransformer.IDENTITY;    //请求转换,可以用作 CDN}Stats stats = new Stats(cache);    //统计数据Dispatcher dispatcher = new Dispatcher(context, service, HANDLER, downloader, cache, stats);return new Picasso(context, dispatcher, cache, listener, transformer, requestHandlers, stats,defaultBitmapConfig, indicatorsEnabled, loggingEnabled);
}

从中可以看出的是:

  1. Picasso 的下载是使用 OkHttp3 实现的
  2. 缓存使用的 LruCache,底层实现是 LinkedHashMap()
  3. 线程池是自定义的,我们后面介绍
  4. 默认的请求转换为不转换
Picasso.get() //1.获得 Picasso 单例.load(url) /

请求的第二步调用了 load(url) 方法:

public RequestCreator load(@Nullable Uri uri) {return new RequestCreator(this, uri, 0);
}

可以看到创建了一个 RequestCreator,后面的配置都是调用它的方法。

Picasso.get() //1.获得 Picasso 单例.load(url) //2.创建 RequestCreator.placeholder(R.drawable.placeholder) .error(R.drawable.error) .fit() .tag(context) .into(view);    //3.发起请求

这些配置方法也很简单,就是修改属性:

public RequestCreator placeholder(@DrawableRes int placeholderResId) {//去掉检查方法this.placeholderResId = placeholderResId;return this;
}
public RequestCreator error(@DrawableRes int errorResId) {//去掉检查方法this.errorResId = errorResId;return this;
}
public RequestCreator fit() {deferred = true;return this;
}

配置好后调用 into(ImageView) 发起请求:

public void into(ImageView target) {into(target, null);
}public void into(ImageView target, Callback callback) {long started = System.nanoTime();//1.延迟操作if (deferred) {   //延迟执行,配置 fit() 等操作后会进入这一步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);String requestKey = createKey(request);//2.缓存获取if (shouldReadFromMemoryCache(memoryPolicy)) {    //先去内存缓存中获取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;}}if (setPlaceholder) {setPlaceholder(target, getPlaceholderDrawable());}//3.构造一个 action 去请求Action action =new ImageViewAction(picasso, target, request, memoryPolicy, networkPolicy, errorResId,errorDrawable, requestKey, tag, callback, noFade);picasso.enqueueAndSubmit(action);
}

有缓存时会去缓存取,否则就构造一个 action 调用 picasso.enqueueAndSubmit(action) 方法提交请求:

void enqueueAndSubmit(Action action) {Object target = action.getTarget();if (target != null && targetToAction.get(target) != action) { //不重复// This will also check we are on the main thread.cancelExistingRequest(target);targetToAction.put(target, action);}submit(action);
}void submit(Action action) {dispatcher.dispatchSubmit(action);
}

这个提交方法就是把要执行的操作和对象(这里是要显示的 ImageView)保存到一个 map 里,如果之前有这个 ImageView 的请求,就取消掉,避免重复加载。

最后调用了 dispatcher.dispatchSubmit(action),然后又调用到了 performSubmit(action) 方法:

public void dispatchSubmit(Action action) {handler.sendMessage(handler.obtainMessage(REQUEST_SUBMIT, action));
}
public void performSubmit(Action action) {performSubmit(action, true);
}/*** 提交获取请求* @param action* @param dismissFailed*/
public void performSubmit(Action action, boolean dismissFailed) {if (pausedTags.contains(action.getTag())) {   //如果暂停集合里有这个 action 的 tag,这次就先不请求,返回pausedActions.put(action.getTarget(), action);return;}//如果已经创建了这个 action 对应的 BitmapHunter,就把数据添加到待操作列表,不重复创建了BitmapHunter hunter = hunterMap.get(action.getKey());    if (hunter != null) {hunter.attach(action);return;}if (service.isShutdown()) {    //如果线程池退出,就直接结束return;}//这一步是遍历 picasso 的 requestHandlers,找到合适的 requestHandler,构造 BitmapHunterhunter = forRequest(action.getPicasso(), this, cache, stats, action);hunter.future = service.submit(hunter);    //提交任务hunterMap.put(action.getKey(), hunter);if (dismissFailed) {failedActions.remove(action.getTarget());}
}

接着就是执行 BitmapHunter 的 run() 方法了,前面我们已经介绍过,这里就不赘述了。

总结一下发起图片请求后的整体流程:

  • 类调用次序:Picasso -> RequestCreator -> Dispatcher -> BitmapHunter -> RequestHandler -> PicassoDrawable
  • 一句话概括:Picasso 收到加载及显示图片的任务,创建 RequestCreator 并将它交给 Dispatcher,Dispatcher 创建 BitmapHunter (并为它找到具体的 RequestHandler) 提交到线程池,BitmapHunter 调用具体 RequestHandler,任务通过 MemoryCache 及 Handler(数据获取接口) 获取图片,图片获取成功后通过 PicassoDrawable 显示到 Target 中。

这段概括修改自:http://www.trinea.cn/android/android-image-cache-compare/

一张图片加载时打的 log:

取消、暂停、恢复加载如何实现

除了发出请求,取消、暂停、恢复加载请求的需求也比较常见,比如我们在退出一个页面时,那些还未完成的请求就应该被取消;在快速滑动列表时,可以先暂停请求,等滑动停下时再恢复,这样可以避免发出大量的请求。

我们先来看看 Picasso 是如何实现 取消请求的吧

picasso.load(url) .placeholder(R.drawable.placeholder) .error(R.drawable.error) .tag(context) .into(view);picasso.cancelRequest(view);   picasso.cancelTag(context);    

Picasso 提供了两种取消方法:

  1. picasso.cancelRequest(view); //1.取消特定目标的加载请求
  2. picasso.cancelTag(context); //2.通过 tag 批量取消

先看取消特定目标的加载请求如何实现的:

//Picasso.cancelRequest(view)
public void cancelRequest(@NonNull ImageView view) {cancelExistingRequest(view);
}
//picasso.cancelExistingRequest(view)
void cancelExistingRequest(Object target) {checkMain();Action action = targetToAction.remove(target);    //1.移除要加载数据 map 中的数据if (action != null) {action.cancel();    //2.取消就是通过置一个标志位为 false,置空回调dispatcher.dispatchCancel(action);  //3.移除调度器里保存的未被执行的 action}if (target instanceof ImageView) {ImageView targetImageView = (ImageView) target;DeferredRequestCreator deferredRequestCreator =targetToDeferredRequestCreator.remove(targetImageView);   //获取这个 ImageView 可能有的延迟执行,取消if (deferredRequestCreator != null) {deferredRequestCreator.cancel();}}
}
//Dispatcher.performCancel(action)
void performCancel(Action action) {String key = action.getKey();BitmapHunter hunter = hunterMap.get(key);if (hunter != null) {hunter.detach(action);  //移除 hunter 中的这个 actionif (hunter.cancel()) {  //这个 hunter 没有操作了,移除hunterMap.remove(key);}}if (pausedTags.contains(action.getTag())) {    //如果处于暂停状态,也从暂停列表里移除pausedActions.remove(action.getTarget());}
}
//BitmapHunter.cancel()
public boolean cancel() {return action == null&& (actions == null || actions.isEmpty())&& future != null&& future.cancel(false);
}

从上面的代码可以看到,取消指定目标的请求,主要做的是以下几步:

  1. 取消保存在 Picasso targetToAction map 里的数据
  2. 调用这个目标对应的 Action.cancel() 方法,就是通过置一个标志位为 false,置空回调
  3. 调用 action 对应的 BitmapHunter.detach(action)BitmapHunter.cancel() 方法,停止 runnable 的执行
  4. 如果处于暂停状态,也从暂停列表里移除

可以看到,取消一个请求要修改的状态好多。

接着看下通过 tag 批量取消如何实现:

public void cancelTag(@NonNull Object tag) {List<Action> actions = new ArrayList<>(targetToAction.values());for (int i = 0, n = actions.size(); i < n; i++) {Action action = actions.get(i);if (tag.equals(action.getTag())) {cancelExistingRequest(action.getTarget());}}//...
}

哈哈,其实就是遍历 Picasso 的 targetToAction 列表,如果其中的 action 的 tag 和指定的 tag 一致,就挨个调用上面取消指定目标的方法取消了。

接着看看如何实现的暂停请求。

暂停请求只有一个方法 picasso.pauseTag(context),最后调用到 Dispatcher.performPauseTag(tag) 方法:

//picasso.pauseTag(context)
public void pauseTag(@NonNull Object tag) {if (tag == null) {throw new IllegalArgumentException("tag == null");}dispatcher.dispatchPauseTag(tag);
}
void performPauseTag(Object tag) {if (!pausedTags.add(tag)) {   //首先添加暂停的 set 集合里,如果返回 false,说明这个 tag 已经暂停了return;}//遍历所有的 BitmapHunter,解除、暂停请求for (Iterator<BitmapHunter> it = hunterMap.values().iterator(); it.hasNext();) {BitmapHunter hunter = it.next();Action single = hunter.getAction();List<Action> joined = hunter.getActions();boolean hasMultiple = joined != null && !joined.isEmpty();//这个 Hunter 已经完成请求了,看看下一个是不是你要找的if (single == null && !hasMultiple) {continue;}if (single != null && single.getTag().equals(tag)) {  //找到了要暂停的hunter.detach(single);  //解除pausedActions.put(single.getTarget(), single);  //添加到暂停结合里}if (hasMultiple) {for (int i = joined.size() - 1; i >= 0; i--) {Action action = joined.get(i);if (!action.getTag().equals(tag)) {continue;}hunter.detach(action);pausedActions.put(action.getTarget(), action);}}//如果这个 hunter 没有请求并且停止成功了,就移除if (hunter.cancel()) {it.remove();}}
}

从上面的代码和注释可以看到,暂停指定 tag 的请求比较简单,就这么 2 点:

  1. 把这个 tag 添加到暂停 set 集合里,在其他的提交请求里会根据这个集合判断,如果一个请求在暂停集合里,就不会继续执行
  2. 遍历所有的 BitmapHunter,解除、暂停和这个 tag 关联的请求

最后看 Picasso 如何恢复指定 tag 对应的请求呢?

//picasso.resumeTag(context);
public void resumeTag(@NonNull Object tag) {dispatcher.dispatchResumeTag(tag);
}
//Dispatcher.performResumeTag(tag)
void performResumeTag(Object tag) {//如果这个 tag 并没有暂停,就返回if (!pausedTags.remove(tag)) {return;}//遍历暂停的 action 集合List<Action> batch = null;for (Iterator<Action> i = pausedActions.values().iterator(); i.hasNext();) {Action action = i.next();if (action.getTag().equals(tag)) {if (batch == null) {batch = new ArrayList<>();}batch.add(action);i.remove();}}//把要恢复的 action 找到,发给主线程if (batch != null) {mainThreadHandler.sendMessage(mainThreadHandler.obtainMessage(REQUEST_BATCH_RESUME, batch));}
}

可以看到,在 Dispatcher 中,从暂停的 action 集合里找到要恢复的,然后给主线程的 Handler 发了个消息,我们看主线程 Handler 如何处理的:

case REQUEST_BATCH_RESUME:@SuppressWarnings("unchecked") List<Action> batch = (List<Action>) msg.obj;for (int i = 0, n = batch.size(); i < n; i++) {Action action = batch.get(i);action.picasso.resumeAction(action);}break;
//Picasso.resumeAction(action)
void resumeAction(Action action) {Bitmap bitmap = null;//恢复以后还是先去缓存查if (shouldReadFromMemoryCache(action.memoryPolicy)) {   bitmap = quickMemoryCacheCheck(action.getKey());}if (bitmap != null) {//查到了,直接返回deliverAction(bitmap, MEMORY, action, null);} else {//没查到,再提交到线程池吧enqueueAndSubmit(action);}
}

主线程的处理逻辑也很简单:

  1. 缓存查到就直接返回
  2. 查不到就重新提交的线程池去执行

OK,这一小节我们学习了 Picasso 如何实现取消、暂停、恢复图片加载请求的,收获如下:

  1. 如果一个操作有多种状态,就要定义多种状态的集合
  2. 如果要根据不同的维度去控制状态,还得多定义些维度与状态管理的集合
  3. 在执行操作前要根据这些状态集合决定是否开始或者取消
  4. 方法要分割的够独立,那样就可以在不同状态切换时重复调用,避免复制粘贴代码

动态调整线程池数量的实现

我们知道线程的创建需要开销,在移动设备上尤其如此,如果在网络不佳的情况下发出太多网络请求,最后的结果是大家谁都别想快快完成。

Picasso 的一个优化点就是:可以根据网络状态动态调整线程池数量,代码虽然不难,但我们应该学习学习这种意识。

PicassoExecutorService 就是 Picasso 自定义的线程池:

public class PicassoExecutorService extends ThreadPoolExecutor {private static final int DEFAULT_THREAD_COUNT = 3;    //默认线程数public PicassoExecutorService() {    //使用优先级队列super(DEFAULT_THREAD_COUNT, DEFAULT_THREAD_COUNT, 0, TimeUnit.MILLISECONDS,new PriorityBlockingQueue<Runnable>(), new Utils.PicassoThreadFactory());}//自定义的 FutureTask,重写 compareTo 方法,方便优先级队列进行比较private static final class PicassoFutureTask extends FutureTask<BitmapHunter>implements Comparable<PicassoFutureTask> {private final BitmapHunter hunter;PicassoFutureTask(BitmapHunter hunter) {super(hunter, null);this.hunter = hunter;}@Overridepublic int compareTo(PicassoFutureTask other) {Picasso.Priority p1 = hunter.getPriority();Picasso.Priority p2 = other.hunter.getPriority();return (p1 == p2 ? hunter.sequence - other.hunter.sequence : p2.ordinal() - p1.ordinal());}}
}

可以看到,PicassoExecutorService 的线程池默认配置参数为:

  1. 核心线程数和最大线程数都是 3
  2. 使用优先队列

同时自定义的 FutureTask,重写 compareTo 方法,方便优先级队列进行比较。这在我们需要实现和优先级有关的耗时操作时,可以参考。

接着看它核心的调整线程数的方法 adjustThreadCount()

void adjustThreadCount(NetworkInfo info) {    //在这里调整线程数量if (info == null || !info.isConnectedOrConnecting()) {setThreadCount(DEFAULT_THREAD_COUNT);return;}switch (info.getType()) {case ConnectivityManager.TYPE_WIFI:case ConnectivityManager.TYPE_WIMAX:case ConnectivityManager.TYPE_ETHERNET:setThreadCount(4);break;case ConnectivityManager.TYPE_MOBILE:switch (info.getSubtype()) {case TelephonyManager.NETWORK_TYPE_LTE:  // 4Gcase TelephonyManager.NETWORK_TYPE_HSPAP:case TelephonyManager.NETWORK_TYPE_EHRPD:setThreadCount(3);break;case TelephonyManager.NETWORK_TYPE_UMTS: // 3Gcase TelephonyManager.NETWORK_TYPE_CDMA:case TelephonyManager.NETWORK_TYPE_EVDO_0:case TelephonyManager.NETWORK_TYPE_EVDO_A:case TelephonyManager.NETWORK_TYPE_EVDO_B:setThreadCount(2);break;case TelephonyManager.NETWORK_TYPE_GPRS: // 2Gcase TelephonyManager.NETWORK_TYPE_EDGE:setThreadCount(1);break;default:setThreadCount(DEFAULT_THREAD_COUNT);}break;default:setThreadCount(DEFAULT_THREAD_COUNT);}}private void setThreadCount(int threadCount) {setCorePoolSize(threadCount);setMaximumPoolSize(threadCount);}

从上面的代码我们看到的是:

  1. 在 WIFI 等网络比较好的情况下,Picasso 的核心线程、最大线程数为 4
  2. 在 4G 等情况下,线程数为 3
  3. 在 3G 等情况下,线程数为 2
  4. 在 2G 这种恶劣的情况下,就只有一个线程了

调用线程池的这个方法在 Dispatcher 中:

void performNetworkStateChange(NetworkInfo info) {if (service instanceof PicassoExecutorService) {((PicassoExecutorService) service).adjustThreadCount(info);}//调整线程后,记得将失败的任务重新提交if (info != null && info.isConnected()) {flushFailedActions();}
}private void flushFailedActions() {if (!failedActions.isEmpty()) {Iterator<Action> iterator = failedActions.values().iterator();while (iterator.hasNext()) {Action action = iterator.next();iterator.remove();if (action.getPicasso().loggingEnabled) {Utils.log(OWNER_DISPATCHER, VERB_REPLAYING, action.getRequest().logId());}performSubmit(action, false);}}
}

调用这个方法的是 Dispatcher 的静态内部类,网络广播接收器:


static class NetworkBroadcastReceiver extends BroadcastReceiver {static final String EXTRA_AIRPLANE_STATE = "state";private final Dispatcher dispatcher;NetworkBroadcastReceiver(Dispatcher dispatcher) {this.dispatcher = dispatcher;}void register() {IntentFilter filter = new IntentFilter();filter.addAction(ACTION_AIRPLANE_MODE_CHANGED);if (dispatcher.scansNetworkChanges) {filter.addAction(CONNECTIVITY_ACTION);}dispatcher.context.registerReceiver(this, filter);}void unregister() {dispatcher.context.unregisterReceiver(this);}@SuppressLint("MissingPermission")@Override public void onReceive(Context context, Intent intent) {if (intent == null) {return;}final String action = intent.getAction();if (ACTION_AIRPLANE_MODE_CHANGED.equals(action)) {if (!intent.hasExtra(EXTRA_AIRPLANE_STATE)) {return; // No airplane state, ignore it. Should we query Utils.isAirplaneModeOn?}dispatcher.dispatchAirplaneModeChange(intent.getBooleanExtra(EXTRA_AIRPLANE_STATE, false));} else if (CONNECTIVITY_ACTION.equals(action)) {ConnectivityManager connectivityManager = getService(context, CONNECTIVITY_SERVICE);dispatcher.dispatchNetworkStateChange(connectivityManager.getActiveNetworkInfo());}}
}

至此我们了解了 Picasso 动态调整线程池数量的实现,以后在写复杂业务或者 SDK 时,可以参考这点。

缓存策略

前面的流程中我们看到了 Picasso 中的缓存类 CacheLruCache

public interface Cache {Bitmap get(String key);void set(String key, Bitmap bitmap);int size();int maxSize();void clear();void clearKeyUri(String keyPrefix);
}
public final class LruCache implements Cache {final android.util.LruCache<String, LruCache.BitmapAndSize> cache;//...
}

可以看到 Picasso 使用的其实就是 android.util.LruCache,key 是经过严格计算的,value 是保存 Bitmap 和 size 的包装类。

我们来看看内存缓存的 key 是如何计算的:

//Utils.createKey() 方法:
public static String createKey(Request data, StringBuilder builder) {if (data.stableKey != null) {    //创建请求时我们主动指定的一个 key,默认为空builder.ensureCapacity(data.stableKey.length() + KEY_PADDING);builder.append(data.stableKey);} else if (data.uri != null) {    //uriString path = data.uri.toString();builder.ensureCapacity(path.length() + KEY_PADDING);builder.append(path);} else {builder.ensureCapacity(KEY_PADDING);builder.append(data.resourceId);}builder.append(KEY_SEPARATOR);if (data.rotationDegrees != 0) {    //旋转角度builder.append("rotation:").append(data.rotationDegrees);if (data.hasRotationPivot) {builder.append('@').append(data.rotationPivotX).append('x').append(data.rotationPivotY);}builder.append(KEY_SEPARATOR);}if (data.hasSize()) {    //修改尺寸builder.append("resize:").append(data.targetWidth).append('x').append(data.targetHeight);builder.append(KEY_SEPARATOR);}if (data.centerCrop) {    //裁剪builder.append("centerCrop:").append(data.centerCropGravity).append(KEY_SEPARATOR);} else if (data.centerInside) {builder.append("centerInside").append(KEY_SEPARATOR);}if (data.transformations != null) {    //变换//noinspection ForLoopReplaceableByForEachfor (int i = 0, count = data.transformations.size(); i < count; i++) {builder.append(data.transformations.get(i).key());builder.append(KEY_SEPARATOR);}}return builder.toString();
}

可以看到:对于同一个地址的图片,如果我们在使用 Picasso 请求时使用不同的配置(比如旋转角度不同、裁剪属性不同、修改尺寸不同、变换属性不同),会导致 key 改变、内存缓存无法命中, Picasso 重新进行网络请求。

总结一下 Picasso 的二级缓存策略:

  • Picasso 内存缓存保存的是处理后的 Bitmap,内存缓存 key 是地址和尺寸、裁剪、角度等信息组合而成(见 Utils.createKey()
  • okhttp 的磁盘缓存的是完整图片,磁盘缓存 key 是 url 的 md5 值
  • Picasso 下载一个图片时会下载完整图片到磁盘,但是加载的时候内存缓存是跟尺寸、裁剪效果有关的(见 BitmapHunter
  • 同一张图片不同的尺寸内存缓存无法命中,会再去磁盘加载一次(实际上还要考虑缓存策略),虽然效率比直接去内存读低,但好处是比网络下载快,在使用同一图片时尺寸配置都一样的情况下,相对占用内存也更少

据说 Glide 不会这样,我先立个 flag,后面分析了再回来对比。

public final class LruCache implements Cache {final android.util.LruCache<String, LruCache.BitmapAndSize> cache;public LruCache(@NonNull Context context) {this(Utils.calculateMemoryCacheSize(context));}/** Create a cache with a given maximum size in bytes. */public LruCache(int maxByteCount) {cache = new android.util.LruCache<String, LruCache.BitmapAndSize>(maxByteCount) {@Override protected int sizeOf(String key, BitmapAndSize value) {return value.byteCount;}};}

接着我们在 Picasso.LruCache 的构造函数中看到,它调用了 Utils.calculateMemoryCacheSize(context) 方法来计算要使用的内存:

//Utils.calculateMemoryCacheSize(context)
public static int calculateMemoryCacheSize(Context context) {ActivityManager am = getService(context, ACTIVITY_SERVICE);boolean largeHeap = (context.getApplicationInfo().flags & FLAG_LARGE_HEAP) != 0;int memoryClass = largeHeap ? am.getLargeMemoryClass() : am.getMemoryClass();// Target ~15% of the available heap.return (int) (1024L * 1024L * memoryClass / 7);
}

可以看到,Picasso 使用了可用内存的七分之一(约百分之 15)作为缓存尺寸。

这一段代码复制性很强,我们可以粘贴到自己的工具类里去哈哈。

public final class LruCache implements Cache {//...@Nullable @Override public Bitmap get(@NonNull String key) {BitmapAndSize bitmapAndSize = cache.get(key);return bitmapAndSize != null ? bitmapAndSize.bitmap : null;}@Override public void set(@NonNull String key, @NonNull Bitmap bitmap) {//...int byteCount = Utils.getBitmapBytes(bitmap);//当要放入缓存的图片尺寸大于缓存总容量时,这里会删除掉之前的缓存if (byteCount > maxSize()) {cache.remove(key);return;}cache.put(key, new BitmapAndSize(bitmap, byteCount));}

可以看到,在添加图片内存缓存时,Picasso 会比较图片的尺寸,因此我们在下载图片时,最好注意这么几点:

  1. 让服务端配置多图
  2. 客户端在需要小图时,传入尺寸,不要直接使用原图

这样的话可以避免由于图片太大每次都去下载原图导致的 OOM。

此外我们只看到了内存缓存,没看到磁盘缓存,这是因为:

Picasso 自己没有实现,交给了 Square 的另外一个网络库 okhttp 去实现,这样的好处是可以通过请求 Response Header 中的 Cache-Control 及 Expired 控制图片的过期时间。
http://www.trinea.cn/android/android-image-cache-compare/

如果需要自定义本地缓存就需要重定义 Downloader,然后这样构造 Picasso:


mOkHttpDownloader = new MyOkHttp3Downloader(client);
picasso = new Picasso.Builder(myapp).downloader(mOkHttpDownloader).build();

OK,小结一下 Picasso 缓存策略:

  1. Picasso 的内存缓存的 key 是经过严格计算的,请求时图片属性的修改会导致缓存无法命中,需要重新下载
  2. Picasso 使用了可用内存的七分之一(约百分之 15)作为缓存尺寸
  3. 当要放入缓存的图片尺寸大于缓存总容量时,这里会删除掉之前的缓存

总结一下从 Picasso 中我们能学到什么

借用 Trinea 画的图来整体看一下结构:

在这篇文章中我们先后从自己设想图片加载框架,到认识 Picasso 的核心 API,到对 Picasso 常见功能实现的分析,从底向上地熟悉了这个图片加载框架的结构和原理。

总结一下发起图片请求后的整体流程:

  • 类调用次序:Picasso -> RequestCreator -> Dispatcher -> BitmapHunter -> RequestHandler -> PicassoDrawable

  • 一句话概括:Picasso 收到加载及显示图片的任务,创建 RequestCreator 并将它交给 Dispatcher,Dispatcher 创建 BitmapHunter (并为它找到具体的 RequestHandler) 提交到线程池,BitmapHunter 调用具体 RequestHandler,任务通过 MemoryCache 及 Handler(数据获取接口) 获取图片,图片获取成功后通过 PicassoDrawable 显示到 Target 中。

文章越写越长,我还是把散布在文章中的收获性文字总结到最后,方便大家查看吧。

  1. 如果一个请求参数很多,我们最好用一个类给它封装起来,避免在传递时传递多个参数;如果需要申请很多资源的话,还可以创建一个对象池,节省开销。(从 Request 类学到的)
  2. 通过几个 RequestHandler 的子类我们可以看到 Picasso 的设计多么精巧,每个类即精简又功能独立,我们在开发时最好可以参考这样的代码,先定义接口和基类,然后再考虑不同的实现。
  3. 复杂业务往往需要在子线程中进行,于是需要用到线程池;线程之间切换需要用到 Handler,为了省去创建 Looper 的功夫,就需要使用 HandlerThread;此外还需要持有几个列表、Map,来保存操作数据。(从 Dispatcher 类学到的)
    • 作为调度器,最重要的功能就是给外界提供各种功能的接口,一般我们都使用不同的常量来标识不同的逻辑,在开始写业务之前,最好先定好功能、确定常量。
  4. 我们在编写复杂逻辑或者 SDK 时应该在完成各个子模块以后,在它们的上面增加一层,由这一层来和各个模块交互,给使用者提供统一、简单的调用接口,避免暴露太多内部模块。(从 Picasso 类学到的)
  5. 如果一个操作有多种状态,就要定义多种状态的集合;如果要根据不同的维度去控制状态,还得多定义些维度与状态管理的集合;在执行操作前要根据这些状态集合决定是否开始或者取消。(从取消、暂停、恢复请求学到的)
  6. 对于同一个地址的图片,如果我们在使用 Picasso 请求时使用不同的配置(比如旋转角度不同、裁剪属性不同、修改尺寸不同、变换属性不同),会导致 Picasso 重新加载。(从缓存策略学到的)
    • Picasso 使用了可用内存的七分之一(约百分之 15)作为内存缓存大小
    • Picasso 自己没有实现磁盘缓存,交给了 okhttp 去实现,这样的好处是可以通过请求 Response Header 中的 Cache-Control 及 Expired 控制图片的过期时间。

Thanks

  • https://github.com/square/picasso
  • http://square.github.io/picasso/
  • http://developers.googleblog.cn/2017/01/exifinterface.html
  • http://www.trinea.cn/android/android-image-cache-compare/
  • http://blog.csdn.net/u011337574/article/category/6256044
  • https://wustor.github.io/2018/01/12/Architecture%E4%B8%89Picasso%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90/

Android 框架学习4:一次读懂热门图片框架 Picasso 源码及流程相关推荐

  1. Linux 信号量 源码,一文读懂go中semaphore(信号量)源码

    运行时信号量机制 semaphore 前言 最近在看源码,发现好多地方用到了这个semaphore. 本文是在go version go1.13.15 darwin/amd64上进行的 作用是什么 下 ...

  2. hdfs文档存储服务器,一文读懂HDFS分布式存储框架分析

    一文读懂HDFS分布式存储框架分析 HDFS是一套基于区块链技术的个人的数据存储系统,利用无处不在的私人PC存储空间及便捷的网络为个人提供数据加密存储服务,将闲置的存储空间利用起来,服务于正处于爆发期 ...

  3. hdfs读写流程_一文读懂HDFS分布式存储框架分析

    一文读懂HDFS分布式存储框架分析 HDFS是一套基于区块链技术的个人的数据存储系统,利用无处不在的私人PC存储空间及便捷的网络为个人提供数据加密存储服务,将闲置的存储空间利用起来,服务于正处于爆发期 ...

  4. 【深度学习】一文读懂机器学习常用损失函数(Loss Function)

    [深度学习]一文读懂机器学习常用损失函数(Loss Function) 最近太忙已经好久没有写博客了,今天整理分享一篇关于损失函数的文章吧,以前对损失函数的理解不够深入,没有真正理解每个损失函数的特点 ...

  5. android开发学习之路——连连看之加载图片(三)

    正如前面AbstractBoard类的代码中看到的,当程序需要创建N个Piece对象时,程序会直接调用ImageUtil的getPlayImages()方法去获取图片,该方法将会随机从res\ dra ...

  6. Android之图片加载框架Picasso源码解析

    转载请标明出处: http://blog.csdn.net/hai_qing_xu_kong/article/details/76645535 本文出自:[顾林海的博客] 个人开发的微信小程序,目前功 ...

  7. Android毕业设计——基于Android+Tomcat的网络视频探索系统设计与实现(毕业论文+程序源码)——网络视频探索系统

    基于Android+Tomcat的网络视频探索系统设计与实现(毕业论文+程序源码) 大家好,今天给大家介绍基于Android+Tomcat的网络视频探索系统设计与实现,文章末尾附有本毕业设计的论文和源 ...

  8. android毕业设计——基于Android+Tomcat+JavaEE的旧物交易平台设计与实现(毕业论文+程序源码)——旧物交易平台

    基于Android+Tomcat+JavaEE的旧物交易平台设计与实现(毕业论文+程序源码) 大家好,今天给大家介绍基于Android+Tomcat+JavaEE的旧物交易平台设计与实现,文章末尾附有 ...

  9. 小白前端之路:手写一个简单的vue-router这几年,好像过的好快,怀念我的大学生活。 - 连某人 大三实习生,之前写过简单MVVM框架、简单的vuex、但是看了vue-router的源码(看了

    这几年,好像过的好快,怀念我的大学生活. 连某人 大三实习生,之前写过简单MVVM框架.简单的vuex.但是看了vue-router的源码(看了大概)之后就没有写,趁着周末不用工作(大三趁着不开学出来 ...

最新文章

  1. pandas内置数据集_pandas内置数据集_Pandas中的示例数据集
  2. SegmentFault Hackathon 2018 火热登场,Let's hack!
  3. 谈几个初学者混淆不清的问题
  4. 国际站html代码,国际站必须看得懂的HTML代码
  5. 工作流实战_03_flowable 流程模板部署
  6. 信号槽绑定时出现未有匹配的connect()函数
  7. python mysql工具类_Python工具类(一)—— 操作Mysql数据库
  8. JEECG 3.7 新装亮相,移动APP发布
  9. 《Java程序员面试秘笈》—— 第1章 Java编程基础
  10. Android学习系列(34)--App应用之发布各广告平台版本
  11. 华南农业大学计算机科学与技术专业,华南农业大学住宿
  12. Note_Master-Detail Application(iOS template)_05_ YJYMasterViewController.m
  13. mac 安装问题汇总
  14. 成立不到半年,正面 PK 星巴克,luckin coffee是什么来路?
  15. 2021普华集团数字经济年度盛典在三亚盛大举行
  16. 从少量访问日志还原黑客攻击过程
  17. java乱码base64_JavaScript BASE64算法实现(完美解决中文乱码)
  18. S32K系列之ADC
  19. 线性回归、岭回归和Lasso回归
  20. 【渝粤题库】陕西师范大学151104中级财务会计作业(高起专)

热门文章

  1. ISO27001信息安全体系认证流程
  2. TTL电平,CMOS电平,OC门,OD门基础知识
  3. wcdma基站的重选和切换
  4. 2023年中央民族大学社会工作考研经验分享
  5. scanner 输入
  6. 北京师范大学计算机研究生报录比,师范考研必看!20北京师范大学考研报录比公布...
  7. vue使用XLSX 读取excel日期格式的单元格问题
  8. python五子棋_python 五子棋源码
  9. 游戏消息服务器长链接,游戏服务器匹配 长连接
  10. linux mint中文,Linux Mint