前言

最开始接触到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设置一个来自于网络的图片来做分析。

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

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

  • Key值的生成

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

  • 缓存查找

根据Key从缓存中查找的过程是如何呢?接下来我们一起来看一下

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来实现对应用层接口的暴露。


Jensen95
2.9k 声望534 粉丝

连续创业者。