Picasso源码分析记录

Picasso,看的版本是v.2.5.2

  • 使用方法,大概这么几种加载资源的形式
Picasso.with(context).load("http://i.imgur.com/DvpvklR.png").into(imageView);

Picasso.with(context).load(R.drawable.landing_screen).into(imageView1);

Picasso.with(context).load("file:///android_asset/DvpvklR.png").into(imageView2);

Picasso.with(context).load(newFile(...)).into(imageView3);

还可以对图片进行一些操作:设置大小、裁剪、加载中&加载错误时显示的图片等,参考Picasso官网

Picasso.with(context)
    .load(url)
    .resize(50, 50)
    .centerCrop()
    .into(imageView)
Picasso.with(context)
    .load(url)
    .placeholder(R.drawable.user_placeholder)
    .error(R.drawable.user_placeholder_error)
    .into(imageView);
  • 类关系图


    Picasso-classes-relation
    Picasso-classes-relation
  • 开始捋源码
    从外部调用看,first,初始化方法:Picasso.java #with(context):单例模式 double-check + 内部静态类Builder,在Picasso.java#Builder().build(){}方法中使用默认或自定义配置初始化Picasso,返回给外部调用。Builder 中初始化了以下配置:
public Picasso build() {
      Context context = this.context;

      if (downloader == null) {  //1
        downloader = Utils.createDefaultDownloader(context);
      }
      if (cache == null) {  //2
        cache = new LruCache(context);
      }
      if (service == null) {  //3
        service = new PicassoExecutorService();
      }
      if (transformer == null) {  //4
        transformer = RequestTransformer.IDENTITY;
      }

      Stats stats = new Stats(cache);  //5

      Dispatcher dispatcher = new Dispatcher(context, service, HANDLER, downloader, cache, stats);  //6

      return new Picasso(context, dispatcher, cache, listener, transformer, requestHandlers, stats,
          defaultBitmapConfig, indicatorsEnabled, loggingEnabled);
    }
  }```
  *  downloader
createDefaultDownloader()里,API 9以上使用 OkHttp (最低支持API 9)、以下使用HttpURLConnection 的 Http 的本地缓存。

  *  cache
  Picasso自定义了一个 LruCache,与 Google 的实现类似,但是是针对 value 为 Bitmap 的精简实现。应该单独写一篇来回顾下[LRU缓存策略]()。
  *  service 
创建线程池,默认是3个执行线程,会根据网络状况再切换线程数。

  * transformer
默认配置是返回原始的 Request,Picasso 文档对于 RequestTransformer 这个接口的解释是
> A transformer that is called immediately before every request is submitted. This can be used to modify any information about a request.
For example, if you use a CDN you can change the hostname for the image based on the current location of the user in order to get faster download speeds.

    如果使用CDN,则可以根据用户的当前位置更改映像的主机名,以获得更快的下载速度。

  * stats 
用于统计下载和缓存的状况,比如总/平均下载数、缓存命中率/未命中率、下载图片的总/平均大小等等

 * dispatcher
用以上的1&2&3&5&new Handler(Looper.getMainLooper()),构造了dispatcher,用于调度任务,handler与主线程进行交互。

  最后用 //6 dispatcher 和一些其他的参数,构造 Picasso返回给外部调用。构造函数如下:
          Picasso(Context context, //1
              Dispatcher dispatcher, //2
              Cache cache, //3
              Listener listener, //4
              RequestTransformer requestTransformer, //5
              List<RequestHandler> extraRequestHandlers, //6
              Stats stats, //7
              Bitmap.Config defaultBitmapConfig, //8
              boolean indicatorsEnabled, //9
              boolean loggingEnabled //10) {
              ...
              ...
              int builtInHandlers = 7; // Adjust this as internal handlers are added or removed.
              int extraCount = (extraRequestHandlers != null ? extraRequestHandlers.size() : 0);
              List<RequestHandler> allRequestHandlers =
              new ArrayList<RequestHandler>(builtInHandlers + extraCount);
              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);
              ...
              ...
           }
  
    第6个参数比较重要,一个默认添加了7个RequestHandler的List,也可以自定义RequestHandler加进去,均继承自RequestHandler,再返回一个只读的List。类名都很见名知意,对应于开篇时的使用方法,不同的RequestHandler去处理不同资源类型的Request。除了以上10个从Builder里传入的参数外,Picasso的构造函数里还有以下几个:
       Picasso(1,2,3,4,5,6,7,8,9,10){
        ...
        this.targetToAction = new WeakHashMap<Object, Action>();  //11
        this.targetToDeferredRequestCreator = new WeakHashMap<ImageView,    DeferredRequestCreator>();  //12
        this.referenceQueue = new ReferenceQueue<Object>();  //13
        this.cleanupThread = new CleanupThread(referenceQueue, HANDLER);  //14
        this.cleanupThread.start();
      }

  Picasso构造好之后,调用** load(File | int | String | Uri) **传入要加载的资源,不管传入的什么类型,都是去构造一个RequestCreator,然后返回。好的,那就接着去看看RequestCreator。RequestCreator的构造函数很简单,其中实例化了这样一个变量:
      private final Request.Builder data;
      RequestCreator(Picasso picasso, Uri uri, int resourceId) {
         ...//省略
        this.data = new Request.Builder(uri, resourceId, picasso.defaultBitmapConfig);
      }
进而调用** load() **方法返回 Request.Builder 后可以调用一些像在前面使用方法里的那些方法,对图像做二次处理,这些方法都是Request.java 里的方法,rotate、resize什么的,其中有一个方法灵活性很高,可以满足自定义的一些需求,就是 ** transform(Transformation transformation) **。比如让图片模糊显示(让我想起了微信某个版本的临时性玩法),等等。详细用法可见 [Picasso — Image Rotation and Transformation](https://futurestud.io/tutorials/picasso-image-rotation-and-transformation)

  接下来,最常用到的方法就是**into(ImageView)**方法了,一系列check以及初次调用没有缓存时,重要的是以下几个方法:

public void into(ImageView target, Callback callback) {
...//省略
Request request = createRequest(System.nanoTime());
String requestKey = createKey(request);
Action action = new ImageViewAction(picasso, target, request, memoryPolicy, networkPolicy, errorResId,errorDrawable, requestKey, tag, callback, noFade);
picasso.enqueueAndSubmit(action);
}

生成一个唯一标识的 requestKey 来构造 ImageViewAction,它继承自Action,封装了一些回调方法:**complete(),error(),cancel()**等。然后作为参数,调用**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);
}

判定不为空&&取消当前target已有请求后,把当前的target和action放入targetToAction中,就是上面构造Picasso时的第11个变量,**cancelExistingRequest(target)**其实就是从这个WeakHashMap里移除。重点的方法来了**submit(action)**:

void submit(Action action) {
dispatcher.dispatchSubmit(action);
}

这个dispatcher就是前面builder里的第6个变量,再来回顾一下它的构造:

Dispatcher dispatcher = new Dispatcher(context, service, HANDLER, downloader, cache, stats);

看了dispatcher手里的牌,接下来要干什么是不是有点不言而喻了?

void dispatchSubmit(Action action) {
handler.sendMessage(handler.obtainMessage(REQUEST_SUBMIT, action));
}

@Override public void handleMessage(final Message msg) {
switch (msg.what) {
case REQUEST_SUBMIT: {
Action action = (Action) msg.obj;
dispatcher.performSubmit(action);
break;
}
...
}

简化一下performSubmit:

void performSubmit(Action action, boolean dismissFailed) {
BitmapHunter hunter = hunterMap.get(action.getKey());
//1
if (hunter != null) {
hunter.attach(action);
return;
}
...
//2
hunter = forRequest(action.getPicasso(), this, cache, stats, action);
hunter.future = service.submit(hunter);
hunterMap.put(action.getKey(), hunter);
...
}

截了两段我觉得比较重要的片段。
* 第1段,没明白attach之后是要干什么,因为直接就return了,按照 **hunter.attach(action)**里的逻辑看是把相同key的action添加到了BitmapHunter内部维护的一个List<Action>中,但是目前没有找到对于List<Action>的主动操作,只是cancel、detach用到了。
* 第2段,先是构造了BitmapHunter

static BitmapHunter forRequest(Picasso picasso, Dispatcher dispatcher, Cache cache, Stats stats,
Action action) {
Request request = action.getRequest();
List<RequestHandler> requestHandlers = picasso.getRequestHandlers();

  // Index-based loop to avoid allocating an iterator.
  //noinspection ForLoopReplaceableByForEach
  for (int i = 0, count = requestHandlers.size(); i < count; i++) {
    RequestHandler requestHandler = requestHandlers.get(i);
    if (requestHandler.canHandleRequest(request)) {
      return new BitmapHunter(picasso, dispatcher, cache, stats, action, requestHandler);
    }
  }

  return new BitmapHunter(picasso, dispatcher, cache, stats, action, ERRORING_HANDLER);
}
通过责任链模式,匹配之前添加的那些RequestHandler谁能处理当前发送过来的Request(匹配request的资源的Uri的scheme等判断逻辑),**requestHandler.canHandleRequest(request)**为true就用这个匹配的requestHandler作为参数构造一个BitmapHunter返回。BitmapHunter是实现了Runnable接口的,再贴一下上面的代码:

hunter.future = service.submit(hunter);

值得注意的是这里hunter内部持有了一个future对象,是Future类型,它代表一个异步任务的计算结果。把**service.submit(Runnable)**的结果赋值给了future,这是为了拿到这个返回结果可以做后续的**cancel()**操作。如果不需要这个结果,一般调用的是**service.execute(Runnable)**。stackoverflow上有一段关于这两个方法的选择问答,说的还蛮清楚的。[Choose between ExecutorService's submit and ExecutorService's execute](http://stackoverflow.com/questions/3929342/choose-between-executorservices-submit-and-executorservices-execute)
然后去看看实现了Runnable接口的BitmapHunter内部的**run()**方法(简化后的):

@Override
public void run() {
...
result = hunt();
if (result == null) {
dispatcher.dispatchFailed(this);
} else {
dispatcher.dispatchComplete(this);
}
...
}

Bitmap hunt() throws IOException {
Bitmap bitmap = null;

if (shouldReadFromMemoryCache(memoryPolicy)) {
  bitmap = cache.get(key);
  if (bitmap != null) {
    stats.dispatchCacheHit();
    loadedFrom = MEMORY;
    }
    return bitmap;
  }
}

data.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 there was no Bitmap then we need to decode it from the stream.
  if (bitmap == null) {
    InputStream is = result.getStream();
      bitmap = decodeStream(is, data);
  }
}

if (bitmap != null) {
  stats.dispatchBitmapDecoded(bitmap);
  if (data.needsTransformation() || exifOrientation != 0) {
    synchronized (DECODE_LOCK) {
      if (data.needsMatrixTransform() || exifOrientation != 0) {
        bitmap = transformResult(data, bitmap, exifOrientation);
      }
      if (data.hasCustomTransformations()) {
        bitmap = applyCustomTransformations(data.transformations, bitmap);  
      }
    }
    if (bitmap != null) {
      stats.dispatchBitmapTransformed(bitmap);
    }
  }
}
return bitmap;

}

先根据缓存策略判断是否允许从缓存中读取,允许的话就从cache中get,get到的话在stats中记录命中了缓存并返回命中的bitmap,不允许或未get到就去从之前匹配到的**RequestHandler.load()**,得到**load()**的结果再判断是否有对结果的二次处理什么的,就不再赘述。主要分析下load()跟进去的方法,比如此次请求的是个url资源,匹配的是**NetworkRequestHandler**,去看下它的**load()**方法(简化后的):

@Override @Nullable public Result load(Request request, int networkPolicy) throws IOException {
Response response = downloader.load(request.uri, request.networkPolicy);
Picasso.LoadedFrom loadedFrom = response.cached ? DISK : NETWORK;
InputStream is = response.getInputStream();
return new Result(is, loadedFrom);
}

先调用downloader的load方法,去获得请求的Response,我开始看的是grep code上的源码2.5.2版,http请求时的downloader还像前文分析的那样分为OkHttpDownloader(API 9以上)和UrlConnectionDownloader(API 9以下),写这篇时再看github上的源码由于OkHttp更新到OkHttp3,只有OkHttp3Downloader这一个用于url请求时的Downloader了。无论哪种其内部逻辑都是请求网络返回Response,然后根据Picasso的缓存策略利用的http的本地缓存。
一大圈的调用,顺利地在BitmapHunter的**run()**方法中拿到result且不为空的话,就调用了**dispatcher.dispatchComplete(this)**,这个方法又调用了:

void dispatchComplete(BitmapHunter hunter) {
handler.sendMessage(handler.obtainMessage(HUNTER_COMPLETE, hunter));
}

case HUNTER_COMPLETE: {
BitmapHunter hunter = (BitmapHunter) msg.obj;
dispatcher.performComplete(hunter);
break;
}

void performComplete(BitmapHunter hunter) {
if (shouldWriteToMemoryCache(hunter.getMemoryPolicy())) {
cache.set(hunter.getKey(), hunter.getResult());
}
hunterMap.remove(hunter.getKey());
batch(hunter);
}
}

在这里,根据缓存策略,将结果里的Bitmap加入内存Cache中,也就是前面实例化的LruCache。然后发送一个HUNTER_DELAY_NEXT_BATCH,
handleMessage(Message)中处理:

case HUNTER_DELAY_NEXT_BATCH: {
dispatcher.performBatchComplete();
break;
}

void performBatchComplete() {
List<BitmapHunter> copy = new ArrayList<>(batch);
batch.clear();
mainThreadHandler.sendMessage(mainThreadHandler.obtainMessage( HUNTER_BATCH_COMPLETE, copy));
logBatch(copy);
}

这个mainThreadHandler也就是最开始Picasso的构造函数里Builder里放入去实例化Dispatcher传入的那个HANDLER,再次回到Picasso.java里去看当时HANDLER的初始化:

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;
//noinspection ForLoopReplaceableByForEach
for (int i = 0, n = batch.size(); i < n; i++) {
BitmapHunter hunter = batch.get(i);
hunter.picasso.complete(hunter);
}
break;
}
...
}
};

一切又都回到了当初入口类里的**complete(BitmapHunter)** ,按照源码逻辑,顺利的话接下来是 **deliverAction(Bitmap result, LoadedFrom from, Action action, Exception e)**,再action.complete(result, from);从上面的分析可知,最终调用的是继承了Action的ImageViewAction的complete方法,let's go(依然是简化版):

@Override
public void complete(Bitmap result, Picasso.LoadedFrom from) {
ImageView target = this.target.get();
Context context = picasso.context;
boolean indicatorsEnabled = picasso.indicatorsEnabled;
PicassoDrawable.setBitmap(target, context, result, from, noFade, indicatorsEnabled);
if (callback != null) {
callback.onSuccess();
}
}

这样就完成对控件的图片设置。

  源码分析下来,发现Picasso其实非常适合作为一个骨架去自定义重要模块的实现,比如LruCache,比如本地缓存的Downloader,优秀的框架是易于扩展的框架。
* 小结:Picasso的源码不多,也就30几个类,通读一遍也就俩小时,但是记录成文字倒是花费了很长时间。这也是我第一次写这么多东西。深知了那些写文章分享技术的人的不易。同时也希望自己可以养成记录的习惯,这对于彻底理解一件事物还是非常有帮助的。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,198评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,663评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,985评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,673评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,994评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,399评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,717评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,407评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,112评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,371评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,891评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,255评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,881评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,010评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,764评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,412评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,299评论 2 260

推荐阅读更多精彩内容