写点什么

Android 框架解析:Picasso 核心功能实现原理

作者:拭心
  • 2021 年 12 月 05 日
  • 本文字数:13372 字

    阅读完需:约 44 分钟

Android 框架解析:Picasso 核心功能实现原理

经过前面对核心 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,构造 BitmapHunter 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 的 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 中的这个 action    if (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; }
@Override public 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: // 4G case TelephonyManager.NETWORK_TYPE_HSPAP: case TelephonyManager.NETWORK_TYPE_EHRPD: setThreadCount(3); break; case TelephonyManager.NETWORK_TYPE_UMTS: // 3G case 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: // 2G case 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) {    //uri    String 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 ForLoopReplaceableByForEach for (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 类学到的)


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


  1. 我们在编写复杂逻辑或者 SDK 时应该在完成各个子模块以后,在它们的上面增加一层,由这一层来和各个模块交互,给使用者提供统一、简单的调用接口,避免暴露太多内部模块。(从 Picasso 类学到的)

  2. 如果一个操作有多种状态,就要定义多种状态的集合;如果要根据不同的维度去控制状态,还得多定义些维度与状态管理的集合;在执行操作前要根据这些状态集合决定是否开始或者取消。(从取消、暂停、恢复请求学到的)

  3. 对于同一个地址的图片,如果我们在使用 Picasso 请求时使用不同的配置(比如旋转角度不同、裁剪属性不同、修改尺寸不同、变换属性不同),会导致 Picasso 重新加载。(从缓存策略学到的)


  • Picasso 使用了可用内存的七分之一(约百分之 15)作为内存缓存大小

  • Picasso 自己没有实现磁盘缓存,交给了 okhttp 去实现,这样的好处是可以通过请求 Response Header 中的 Cache-Control 及 Expired 控制图片的过期时间。

发布于: 2 小时前阅读数: 6
用户头像

拭心

关注

Never Settle! 2017.11.30 加入

字节跳动高级 Android 工程师,主要从事性能优化相关工作

评论

发布
暂无评论
Android 框架解析:Picasso 核心功能实现原理