Picasso源代码走读
2016-09-23 / modified at 2022-04-04 / 1.7k words / 6 mins
️This article has been over 2 years since the last update.

A brief source code review of Picasso

好久没有碰Android了,花一个小时看下Android下的Picasso吧

1
git clone --depth=1 https://github.com/square/picasso.git

最近看厂里的糟糕代码太多,看优秀的代码速度反而比以前快多了,Picasso由于把大量的网络请求等累活都转到OkHttp3中了,相比于Spring、Tomcat等重量级项目,架构简单得多。

本文按照What How Why原则进行试点写作,按照28定律,看到How的人就是少数的20%了。

本文代码基于Picasso的eac9b2b 版本进行的分析

1. Picasso是什么

Picasso是一个Android下的网络图片加载框架,内部自动实现了线程池控制、网络下载、异步更新界面等功能,举个例子。

1
2
Picasso.with(context).load("http://example.com/123.png")
.into(mImageView);

通过使用Picasso,可以将重心放在业务上,而不用担心图片加载、线程池管理等性能问题。

2. Picasso主要技术点分析

  • 网络请求与下载:直接复用OkHttp,支持HTTP2.0,本文只用知道OkHttp是一个网络框架即可。
  • 消息队列(MQ):直接复用的Android自带的消息队列Handler,比服务器开发各种MQ简单多了
  • 请求分发与回调:在消息队列Handler的基础上进行封装为RequestCreator与Dispathcer,一个用于发送,一个根据Message.What进行路由
  • 图片缓存的实现: 对LinkedHashMap的包装实现LruCache,内部维护着hitCount与missCount,用于对缓存进行统计。
  • 内部线程池:通过网络广播监听器,构造PicassoExecutorService可以在不同网络下设置线程的数量,比如WIfi下是4个线程,4G下是3个线程;并且通过优先队列控制任务的优先级

总体上来说,Picasso是一个代码易懂,不用注释都可以读的项目。

2.1. 图片请求与下载

以下为无缓存进行首次网络请求的场景。当调用Picasso.with(context).load(url)后,将构造一个RequestCreator,它将请求的字符串转为了一个Uri的包装对象

接着调用RequestCreator.into(imageview)时,后面经过多次构造,最后产生了一个Action对象,内部有Url,执行入队操作

1
2
3
4
5
6
7
8
9
10
11
12
//将请求进行入队,即放入Handler中
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);
//放入Map
targetToAction.put(target, action);
}
//通过Handler发送{ REQUEST_SUBMIT , action }
submit(action);
}

REQUEST_SUBMIT进行findUsage搜索,可以发现请求被路由到这里了

1
2
3
4
5
6
7
//method: DispatcherHandler.handlerMessage()
case REQUEST_SUBMIT: {
Action action = (Action) msg.obj;
//分配器将请求转发
dispatcher.performSubmit(action);
break;
}

充分利用intellij的FindUsage的方法,可以减少读码的时间

接着分析performSubmit,可以发现它构造了一个BitmapHunter,这里开始构造匹配Uri的BitmapHunter

1
2
//method: performSubmit()
hunter = forRequest(action.getPicasso(), this, cache, stats, action);

内部代码太长太丑,总的来说就是一个filter操作,根据Uri的Scheme匹配不同的RequestHandler,这里就用js伪代码讲吧

1
2
3
4
5
6
7
8
9
10
//method: forRequest();
//处理各种请求的Handler,比如File,R资源、网络等,在Picasso构造时生成
var List<RequestHandler> requestHandlers;
//通过Url构建的请求
var action;
var requestHandler = requestHandlers.filter(func(handler){
//通过uri.getScheme的正则表达式进行匹配
return handler.canHandleRequest(action.getRequest());
});
var hunter = 通过刚刚过滤的requestHandler进行构造

由于我们的请求是网络请求,它的Scheme的regex是http[s],因此最后过滤出来的是NetworkRequestHandler,它内部维护着一个Downloader,在Picasso最新版中由OkHttp3Downloader实现。

接着hunter将请求提交给PicassoExecutorService线程池

1
hunter.future = service.submit(hunter);

最后终于交给干活的人(OkHttp3Downloader)了,走到NetworkRequestHandler的load方法

1
2
//交给OkHttp3Downloader进行处理网络
Response response = downloader.load(request.uri, request.networkPolicy);

load方法就是OkHttp的同步阻塞调用的网络请求了,请求完成后将Body解码为Bitmap。

注意这里是同步阻塞调用,也就是说OkHttp内部没有维护请求队列与线程池

最后完成请求后通过Handler发送消息

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

经过多次Handler的消息传递,最终调用Action的complete方法,在UI线程更新ImageView

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//method: ImageViewAction.complete()
@Override public void complete(Bitmap result, Picasso.LoadedFrom from) {
if (result == null) {
throw new AssertionError(
String.format("Attempted to complete action with no result!
%s", this));
}

ImageView target = this.target.get();
if (target == null) {
return;
}

Context context = picasso.context;
boolean indicatorsEnabled = picasso.indicatorsEnabled;
//更新ImageView
PicassoDrawable.setBitmap(target, context, result, from, noFade, indicatorsEnabled);

if (callback != null) {
callback.onSuccess();
}
}

综上,总的来说,就是首先构造一个Uri,然后构造Action,根据Uri选择匹配的RequestHandler,最终交给定制过的Picasso线程池执行RequestHandler的load方法,获取并解码图片后,通过Handler在UI线程更新imageVIew中的PicassoDrawable

2.2. Picasso的缓存实现

Picasso维护着一个基于内存的LruCache,通过maxSize控制缓存容量

1
2
3
4
5
//file: LruCache.java
//双端链表与HashMap实现的LinkedHashMap,旧元素在前,反复使用的新元素在后
final LinkedHashMap<String, Bitmap> map;
private final int maxSize;
private int size;

关于LRU,要注意的是OkHttp是基于文件的,而Picasso是基于内存的缓存,它们两个是没有关系的。

有关图片的缓存优化,这里就可以看出来了。主要就是通过减小Bitmap的体积或者LRU的maxSize来实现了,具体就是用更小的图片Config、分辨率、用更小的LRUcache。

2.3. Picasso中的线程池

Picasso专门定制了一个线程池,可以控制优先级与线程数,构造函数如下

1
2
3
4
PicassoExecutorService() {
super(DEFAULT_THREAD_COUNT, DEFAULT_THREAD_COUNT, 0, TimeUnit.MILLISECONDS,
new PriorityBlockingQueue<Runnable>(), new Utils.PicassoThreadFactory());
}

注意PriorityBlockingQueue是基于堆排序实现的,每个Runnable任务需要实现Comparable接口,以实现优先级的区分,通俗的说,就是优先级高的任务可以插队到前面执行。

至于线程池中线程数量的控制,通过NetworkBroadcastReceiver广播接收器控制,当网络状态发生改变后,通过Handler发送网络情况,线程池收到消息后,对线程数量进行调整。

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
//method: PicassoExecutorService.adjustThreadCount()
switch (info.getType()) {
case ConnectivityManager.TYPE_WIFI:
...
setThreadCount(4);
break;
case ConnectivityManager.TYPE_MOBILE:
switch (info.getSubtype()) {
case TelephonyManager.NETWORK_TYPE_LTE: // 4G
...
setThreadCount(3);
break;
case TelephonyManager.NETWORK_TYPE_UMTS: // 3G
...
setThreadCount(2);
break;
case TelephonyManager.NETWORK_TYPE_GPRS: // 2G
case TelephonyManager.NETWORK_TYPE_EDGE:
setThreadCount(1);
break;
default:
setThreadCount(DEFAULT_THREAD_COUNT);//3
}
break;
default:
setThreadCount(DEFAULT_THREAD_COUNT);
}

3. 总结

Picasso通过高效利用Handler消息队列,实现异步请求、下载与分发;通过RequestHandler列表,支持多种Uri协议下的图片下载;通过LruCache实现基于内存的缓存;通过定制的线程池,实现优先级与线程数的控制;

最后总的推荐就是,Picasso代码量不高,推荐各位有时间看一下,以后的大趋势是Android会少而精,看不懂源码的说不定会失业的。