Android每周一轮子:Picasso

前言

最开始接触到Picasso框架仍是在大三实现的时候,已经很是久远了,Picasso是Android一个远古时代的框架了,同时代的Volley早已被各家弃用,可是该框架实现较为简单适合做为初学者对图片加载库源码学习使用,对于了解图片加载框架的实现原理仍是挺有帮助的。缓存

基础使用

Picasso.get().load("https://ss0.bdstatic.com/70cFvHSh_Q1YnxGkpoWK1HF6hhy/it/u=2205936453,824698011&fm=26&gp=0.jpg").into(binding.iv);

Picasso的使用比较简单,设置一些参数,提供一个url,而后设置加载在哪个图形控件上,剩下的就能够交给框架去完成了,固然咱们也能够给图片设置一些变化操做信息。同时在其内部也帮咱们将图片的缩放相关操做完成了。网络

实现原理

在分析其实现原理以前,咱们不妨先构思一个图片库应该如何去实现,它应该具有哪一些功能?数据结构

首先下载是必须的,其次是缓存,下载完成以后要对其进行缓存,否则每一次都要从新下载,缓存又要涉及到内存缓存和磁盘缓存,同时对于加载的图片也要进行一些缩放等一系列操做,而这过程又都是比较耗时的,所以这些过程都应该在子线程中执行,为了提供线程的利用率,咱们则要利用线程池。当图片下载完成,咱们须要将图片设置到咱们的View上,而这个操做必需要在主线程发生,所以内部提供了一个持有主线程Looper的Handler来负责进行调度。带着这些构思,咱们来跟进看一下源码的具体实现。框架

这里咱们根据上述的测试代码中为ImageView设置一个来自于网络的图片来作分析。oop

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

Load方法返回了一个RequestCreator是对于请求Request的一些包装,再来看一下它的into方法。学习

public void into(ImageView target) {
    into(target, null);
}

内部的into方法的实现是其核心。下面咱们截取一部分代码来进行分析。测试

//建立一个请求,同时根据请求信息生成key值
Request request = createRequest(started);
String requestKey = createKey(request);
//判断是否要走缓存
if (shouldReadFromMemoryCache(memoryPolicy)) {
    //从缓存中查找是否存在
  Bitmap bitmap = picasso.quickMemoryCacheCheck(requestKey);
  if (bitmap != null) {
    picasso.cancelRequest(target);
    //存在则将该Bitmap设置上
    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());
}
//缓存没有匹配到则建立一个ImageViewAction,加入到请求队列
Action action =
    new ImageViewAction(picasso, target, request, memoryPolicy, networkPolicy, errorResId,
        errorDrawable, requestKey, tag, callback, noFade);

picasso.enqueueAndSubmit(action);

对于上述步骤接下来将进行逐一的分析。ui

  • Key值的生成

这里的Key值再也不单纯的只是请求的url,而是其配置的一系列对于图片上的信息,包括宽高等信息。this

  • 缓存查找

根据Key从缓存中查找的过程是如何呢?接下来咱们一块儿来看一下url

Bitmap quickMemoryCacheCheck(String key) {
  Bitmap cached = cache.get(key);
  if (cached != null) {
    stats.dispatchCacheHit();
  } else {
    stats.dispatchCacheMiss();
  }
  return cached;
}

从Picasso的cache中进行查找,而Picasso中的cache的实现是怎么样呢?其内部仍是经过一个LruCache来实现的。内部存储的缓存数据结构

LruCache<String, LruCache.BitmapAndSize>

BitMapAndSize存在每个图片和其大小

static final class BitmapAndSize {
  final Bitmap bitmap;
  final int byteCount;

  BitmapAndSize(Bitmap bitmap, int byteCount) {
    this.bitmap = bitmap;
    this.byteCount = byteCount;
  }
}

其最大的把内存空间是最大内存的大概1/7

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);
}

对于LruCache内部的实现是经过LinkedHashMap来实现的,经过其来实现最近最少未使用的替换规则,LruCache的实现,须要了其SizeOf方法来返回每个对象的大小,LruCache根据这个来进行相应的回收操做,每次加入对象的时候都要进行判断是否超过了最大大小,若是超过了,则要进行trimToSize来不断的减小来删除其中的最近微使用的,同时提供了entryRemoved的回调,每次有被删除的,咱们均可以进行监听而后进行一些操做。

  • LinkedHashMap的实现

其内部LinkedHashMapEntry是继承自HashMap的Node来实现的具有一个before和after指针。在调用其get和put方法以后都会调用afterNodeAccess方法,经过对于其先后节点的操做来实现一个转化。

  • 内存查找不到时如何请求

ImageViewAction继承自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);
}

至此,其将会经过Dispatcher来进行执行。Dispatcher内部的消息执行全是经过Handler的方式来实现的,经过Handler传递消息,而后接收到相应的消息以后调用相应的方法。

hunter = forRequest(action.getPicasso(), this, cache, stats, action);
hunter.future = service.submit(hunter);
hunterMap.put(action.getKey(), hunter);

将其添加到线程池之中就行执行。这里对于线程池的实现对网络进行了监听,经过对于网络不通类型的监听来实现对于线程池中核心线程数目的调度,不通的网络状况下开启的线程数目是不同的。

corePoolSize:核心线程数。

maximumPoolSize:线程池所能容纳的最大线程数,当活动线程达到这个数值以后,任务就会阻塞。

keepAliveTime:非核心线程闲置超时时长,超过这个时长就会被回收,当allowCoreThreadTimeOut设置为true的时候,其对于核心线程也是一样生效的。

unit:KeepLiveTime参数的时间单位

workQueue:线程池中的任务队列

threadFactory:线程工厂用来建立新的线程。

线程池是经过BlockingQueue来管理相应的执行任务的。

  • BitmapHunter

BitmapHunter顾名思义,就是用来找Bitmap的,调用其Hunt方法,hunt方法中会先进行缓存中的查询,而后再进行网络请求,若是获取不到Bitmap则从返回的流中进行decode出一个Bitmap来。具体请求的执行是经过RequestHandler来进行的,对于不一样类型资源的加载拥有不一样的Handler。内部线程的调度也是借助于主线程的Handler来实现,BitmapHunter其实是一个Runnable,对于图片的下载,缓存和处理相关逻辑都封装在其中。

  • DownloadManager

对于磁盘缓存的实现,Picasso直接借助了OKhttp自身的实现,对于NetworkRequestHandler的实现其中的load方法会调用OkHttp的下载实现来进行下载,最后经过new Result(body.source(), loadedFrom)来返回,而后在BitmapHunter中对其进行解码,创造Bitmap出来。

Bitmap bitmap = BitmapFactory.decodeStream(stream, null, options);

最后根据加载回来的图片,应用上相应的配置以后,建立一个Bitmap出来,建立完成以后,开始应用上一些变化的操做。最后返回一个处理好的Bitmap,返回的方式是经过Dispatcher的线程调度策略,经过调度以后,还进行了一个Bitmap的预加载操做。

在进行图片的缩放的时候,在计算的时候,咱们不须要先将图片进行加载,只须要测量其宽高,计算好加载比例以后,咱们才须要将其装载到内存之中。经过设置其inJustDecodeBounds为true,咱们能够先获取其宽高,而后计算比如例以后,再进行解码加载。

if (hunter.result != null) {
  hunter.result.prepareToDraw();
}

在进行加载以前,调用prepareToDraw方法将Bitmap先提早加载到GPU上,提高后面的绘制效率,而后在调用相应的ImageView设置ImageDrawable的方法。

设计亮点

  • 网络类型切换的自适应调整
case NETWORK_STATE_CHANGE: {
  NetworkInfo info = (NetworkInfo) msg.obj;
  dispatcher.performNetworkStateChange(info);
  break;
}

当网络类型发生变化的时候,会自动进行线程池核心线程数的变化,网络类型越差,线程池中核心线程数量越少。

总结

Picasso的实现上,经过BitmapHunter来进行数据的获取和处理,经过Cache内的LruCache来实现内存中的存储,经过Dispatcher来实现线程间的调度,最上层的Picasso来实现对应用层接口的暴露。

相关文章
相关标签/搜索