前言

记得android开发最开始,图片的加载一直是一个历史性问题。OOM,图片错位,图片渲染速度等种种问题一直困扰着我们这些苦逼的开发人员, 一晃78年过去了,已经涌现的很多非常不错的图片加载框架,分析这些框架的同时,也不经回忆起了当初;一转眼,我们又老了几岁了!

Picasso

square出品的又一利器,当时第一次使用时就被其简洁的调用方式所惊艳。不过这款框架只能算是轻量级,功能上还不是特别繁杂。本文分析的 版本为2.3.2

1 架构

picasso 架构

picasso整体上比较突出的就是可以设置特别多的图片来源,如上图,大概有6种图片来源(BitmapHunter)

1
2
3
4
5
6
7
NetworkBitmapHunter; //常用来源,通过网络请求获取图片资源
AssetBitmapHunter;   //android Assets获取
ContactsPhotoBitmapHunter; //不怎么常用,联系人ContentProvider获取
ContentStreamBitmapHunter; //通过ContentProvier 获取第三方的图片资源
MediaStoreBitmapHunter;//android 媒体数据库中获取图片资源,比如视频封面
ResourceBitmapHunter;//app 获取自带图片资源
FileBitmapHunter;// app 文件系统获取

调度器Dispatcher会调用多线程来从上述这些渠道获取图片资源,并且会根据是否缓存的开关来进行缓存操作。 Picasso值得注意的是,缓存只是采用了LrcCache内存缓存这一级,而磁盘缓存实际跟通用图片加载框架的使用位置还不太一样

2 调度

Dispatcher 状态图

调度器Dispatcher主要有6种状态响应

1
2
3
4
5
6
REQUEST_SUBMIT        //开始获取图片
HUNTER_COMPLETE       //获取成功
HUNTER_DECODE_FAILED  //获取失败
HUNTER_RETRY          //需要重试
NETWORK_STATE_CHANGE  //网络状态变化
AIRPLANE_MODE_CHANGE  //飞行模式状态变化

REQUEST_SUBMIT之后,系统就会开启一个线程用来进行获取图片操作,并且会保留该线程的 Future,用来中断或者取消操作。

HUNTER_COMPLETE之后,Dispatcher会将数据发送到主线程中的Handler来使用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
//Picasso 持有的静态Handler
 static final Handler HANDLER = new Handler(Looper.getMainLooper()) {
    @Override public void handleMessage(Message msg) {
      switch (msg.what) {
        //获取bitmap成功
        case HUNTER_BATCH_COMPLETE: {
          ...
          break;
        }
        case REQUEST_GCED: {
          ...
          break;
        }
      }
    }
  };

一个BitmapHandler会绑定多个相同来源的请求,比如多个渲染目标都加载一张同样来源的图片 这样也避免的内存资源的浪费,在加载完成Bitmap之后,BitmapHandler会遍历自己绑定的 Target,分别渲染Bitmap

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
//Picasso.java
void complete(BitmapHunter hunter) {
    Action single = hunter.getAction();
    List<Action> joined = hunter.getActions();
    ...
    for (int i = 0, n = joined.size(); i < n; i++) {
        Action join = joined.get(i);
        deliverAction(result, from, join);
      }
    }
  }

而多个BitmapHandler也会被绑定在一起,一批一批的发送处理,也是为了避免多次调用主线程

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
  //Picasso.java
  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;
        }
        ...
      }
    }
  };

3 内存缓存

picasso默认情况下只使用了一级内存缓存。注意是默认情况下,内存大小默认使用应用可用内存的15%。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
static int calculateMemoryCacheSize(Context context) {
    ActivityManager am = getService(context, ACTIVITY_SERVICE);
    boolean largeHeap = (context.getApplicationInfo().flags & FLAG_LARGE_HEAP) != 0;
    int memoryClass = am.getMemoryClass();
    if (largeHeap && SDK_INT >= HONEYCOMB) {
      memoryClass = ActivityManagerHoneycomb.getLargeMemoryClass(am);
    }
    // Target ~15% of the available heap.
    return 1024 * 1024 * memoryClass / 7;
  }

而具体是怎么实现最近最少使用的剔除算法呢?答案就是对LinkedHashMap的巧妙运用

LinkedHashMap是HashMap的加强版,HashMap所存数据遍历时是无序,而LinkedHashMap可以做到有序, 也就是说谁先存进去,谁就先被取出来。当LinkedHashMap的构造方法第三个参数为true时

1
this.map = new LinkedHashMap<String, Bitmap>(0, 0.75f, true);

在对该数据执行了LinkedHashMap.get(key)操作后,该数据就被放在遍历集合的末尾了。 那么这样的效果就是,哪个Bitmap没有被经常使用,这个Bitmap就一定在遍历时最先出来。

所以当执行清除操作时,直接使用iterator来清理最靠前的Bitmap,直到所有Bitmap占有内存小于当前被设置的 最大内存即可

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
private void trimToSize(int maxSize) {
    while (true) {
      String key;
      Bitmap value;
      synchronized (this) {
        ...
        //当前Bitmap集合所占内存符合最大内存的要求
        if (size <= maxSize || map.isEmpty()) {
          break;
        }
        //清除最靠前的Bitmap数据
        Map.Entry<String, Bitmap> toEvict = map.entrySet().iterator().next();
        key = toEvict.getKey();
        value = toEvict.getValue();
        //清除操作
        map.remove(key);
        //计算当前所有Bitmap所占内存
        size -= Utils.getBitmapBytes(value);
        evictionCount++;
      }
    }
  }

4 磁盘缓存

说picasso有磁盘缓存其实也不太准确,picasso自带的磁盘缓存跟常用图片加载框架的磁盘缓存的使用时机不太一样。

首先picasso的磁盘缓存是网络请求自带的磁盘缓存。如果是OkhttpDownloader的话就是Okhttp自带的DiskLruCache 并且要想使用Okhttp必须导入相应的库才行。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
 static Downloader createDefaultDownloader(Context context) {
    ...
    //异常信息说明,需要的库为
    //com.squareup.okhttp:okhttp:1.6.0 (or newer)
    //com.squareup.okhttp:okhttp-urlconnection:1.6.0 (or newer)
    if (okHttpClient != okUrlFactory) {
      throw new RuntimeException(""
          + "Picasso detected an unsupported OkHttp on the classpath.\n"
          + "To use OkHttp with this version of Picasso, you'll need:\n"
          + "1. com.squareup.okhttp:okhttp:1.6.0 (or newer)\n"
          + "2. com.squareup.okhttp:okhttp-urlconnection:1.6.0 (or newer)\n"
          + "Note that OkHttp 2.0.0+ is supported!");
    }

    return okHttpClient
        ? OkHttpLoaderCreator.create(context)
        : new UrlConnectionDownloader(context);
  }

否则就使用了默认的HttpUrlConnection来实现下载功能,HttpUrlConnection的默认缓存为HttpResponseCache

然后呢,picasso并不是图片首先从缓存中去读取,是当下载该图片时失败,并且失败了两次时就会调用磁盘缓存的图片

1
2
3
4
5
6
7
@Override Bitmap decode(Request data) throws IOException {
    boolean loadFromLocalCacheOnly = retryCount == 0;
    //loadFromLocalCacheOnly取决于retryCount
    //retryCount初始值为2,重试一次自减一次
    Response response = downloader.load(data.uri, loadFromLocalCacheOnly);
    ...
  }

当loadFromLocalCacheOnly为true时就会启动缓存

1
2
3
4
5
6
7
8
9
@Override public Response load(Uri uri, boolean localCacheOnly) throws IOException {
    HttpURLConnection connection = openConnection(uri);
    connection.setUseCaches(true);
    if (localCacheOnly) {
      connection.setRequestProperty("Cache-Control", "only-if-cached,max-age=" + Integer.MAX_VALUE);
    }
    ...
    }
}

在请求时会默认加入only-if-cached,max-age控制,其中max-age追加的值为Integer的MAX_VALUE, 根据http的头部配置说明,only-if-cached表示默认使用缓存,并且只要max-age所跟的最大存活时间没过 就会调用缓存来作为相应结果,经过计算Integer.MAX_VALUE折算的时间其实比一年还要大。

综上,只要请求两次图片资源失败,就使用缓存值,当然前提是缓存值要存在啊。

也说明了picasso的缓存优先级如下

1
MemoryCache > NetWork > DiskCache

5 图片

5.1 错位问题

以前我们防止在列表显示图片时,加载错位,在每个ImageView上都绑定一个tag,当Bitmap返回时,对比tag, 两者的tag一样才会执行渲染。

picasso在处理这样的问题上,比较直接,如果当前target上绑定了一个获取任务,就将该任务中断,执行最新的 绑定任务。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
void enqueueAndSubmit(Action action) {
    Object target = action.getTarget();
    //提交获取请求之前,先判断一下目标Target是否已经绑定了请求任务,有的话取消
    if (target != null) {
      cancelExistingRequest(target);
      targetToAction.put(target, action);
    }
    //然后执行当前新任务
    submit(action);
  }

5.2 转化

picasso支持对Bitmap进行加工,有默认加工和自定义两种方式

默认加工在自定义加工之前,默认加工包括,重调大小,旋转,居中裁剪等。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
//BitmapHandler.java
static Bitmap transformResult(Request data, Bitmap result, int exifRotation) {
    ...
    Matrix matrix = new Matrix();
    if (data.needsMatrixTransform()) {
      //重调大小
      int targetWidth = data.targetWidth;
      int targetHeight = data.targetHeight;
      ...
      //旋转
      if (targetRotation != 0) {
        if (data.hasRotationPivot) {
          matrix.setRotate(targetRotation, data.rotationPivotX, data.rotationPivotY);
        } else {
          matrix.setRotate(targetRotation);
        }
      }
      //居中裁剪
      if (data.centerCrop) {
        float widthRatio = targetWidth / (float) inWidth;
        float heightRatio = targetHeight / (float) inHeight;
        float scale;
        if (widthRatio > heightRatio) {
          scale = widthRatio;
          int newSize = (int) Math.ceil(inHeight * (heightRatio / widthRatio));
          drawY = (inHeight - newSize) / 2;
          drawHeight = newSize;
        } else {
          scale = heightRatio;
          int newSize = (int) Math.ceil(inWidth * (widthRatio / heightRatio));
          drawX = (inWidth - newSize) / 2;
          drawWidth = newSize;
        }
        matrix.preScale(scale, scale);
      } else if (data.centerInside) {
        float widthRatio = targetWidth / (float) inWidth;
        float heightRatio = targetHeight / (float) inHeight;
        float scale = widthRatio < heightRatio ? widthRatio : heightRatio;
        matrix.preScale(scale, scale);
      } else if (targetWidth != 0 && targetHeight != 0 //
          && (targetWidth != inWidth || targetHeight != inHeight)) {
        // If an explicit target size has been specified and they do not match the results bounds,
        // pre-scale the existing matrix appropriately.
        float sx = targetWidth / (float) inWidth;
        float sy = targetHeight / (float) inHeight;
        matrix.preScale(sx, sy);
      }
    }

    if (exifRotation != 0) {
      matrix.preRotate(exifRotation);
    }

    Bitmap newResult =
        Bitmap.createBitmap(result, drawX, drawY, drawWidth, drawHeight, matrix, true);
    if (newResult != result) {
      result.recycle();
      result = newResult;
    }

    return result;
  }
}

自定义加工需要我们实现Transformation接口,在transform方法中去添加具体的转换操作

1
2
3
4
5
6
7
8
//BitmapHandler.java
 static Bitmap applyCustomTransformations(List<Transformation> transformations, Bitmap result) {
    for (int i = 0, count = transformations.size(); i < count; i++) {
      final Transformation transformation = transformations.get(i);
      Bitmap newResult = transformation.transform(result);
      ...
    return result;
  }

总结

以上是关于Picasso的一些比较重要知识点,总体而言,picasso是麻雀虽小五脏俱全,其实笔者这边对于Picasso的磁盘缓存的分析还是意犹未尽,主要是因为 其也不是独立模块。后续会专门抽出一篇来分析磁盘缓存的内容