1.开始
Picasso是square公司推出的一个轻量级的开源图片加载框架,用法十分简单,通过下面一行代码即可完成网络图片的加载:
Picasso.get().load("https://www.baidu.com/img/PCtm_d9c8750bed0b3c7d089fa7d55720d6cf.png").into(iv1);
2.加载图片过程分析
Picasso项目整个代码比较少,本文以2.8的java版本进行分析。上面的例子中加载流程如下:
下面进行具体的代码走读
2.1 get()
这个方法通过单例模式创建全局Picasso实例,其中context的获取用了一个小技巧—通过Provider提供,同时可自定义这个对象,结合Builder模式实现自己的处理策略。
// -----------------------------------------------Picasso.java-----------------------------------------------
// 创建单例
public static Picasso get() {
if (singleton == null) {
synchronized (Picasso.class) {
if (singleton == null) {
if (PicassoProvider.context == null) {
throw new IllegalStateException("context == null");
}
singleton = new Builder(PicassoProvider.context).build();
}
}
}
return singleton;
}
// 自定义配置
public static void setSingletonInstance(@NonNull Picasso picasso) {
if (picasso == null) {
throw new IllegalArgumentException("Picasso must not be null.");
}
synchronized (Picasso.class) {
if (singleton != null) {
throw new IllegalStateException("Singleton instance already exists.");
}
singleton = picasso;
}
}
Builder包含一些默认配置,具体如下:
// -----------------------------------------------Picasso.java-----------------------------------------------
public static class Builder {
public Picasso build() {
Context context = this.context;
if (downloader == null) {
downloader = new OkHttp3Downloader(context); // 下载器
}
if (cache == null) {
cache = new LruCache(context); // 内存缓存处理
}
if (service == null) {
service = new PicassoExecutorService(); // 线程池
}
if (transformer == null) {
transformer = RequestTransformer.IDENTITY; // 图片转换策略,默认是原图
}
Stats stats = new Stats(cache);// 图片大小等处理
Dispatcher dispatcher = new Dispatcher(context, service, HANDLER, downloader, cache, stats);// 调度器
// defaultBitmapConfig为null
return new Picasso(context, dispatcher, cache, listener, transformer, requestHandlers, stats,
defaultBitmapConfig, indicatorsEnabled, loggingEnabled);
}
}
2.2 load()
load()方法提供对文件、路径、url和图片资源id等处理方式,方法具体如下图:
这几个load方法都转化为RequestCreator,RequestCreator之后创建了Request。从命名可以猜测,每次图片加载都转换为一个请求Request,具体代码如下:
// -----------------------------------------------Picasso.java-----------------------------------------------
public RequestCreator load(@Nullable Uri uri) {
return new RequestCreator(this, uri, 0);
}
// -----------------------------------------------RequestCreator.java-----------------------------------------------
public class RequestCreator {
RequestCreator(Picasso picasso, Uri uri, int resourceId) {
if (picasso.shutdown) {
throw new IllegalStateException(
"Picasso instance already shut down. Cannot submit new requests.");
}
this.picasso = picasso;
this.data = new Request.Builder(uri, resourceId, picasso.defaultBitmapConfig);
}
}
// -----------------------------------------------Request.java-----------------------------------------------
public final class Request {
public static final class Builder {
Builder(Uri uri, int resourceId, Bitmap.Config bitmapConfig) {
this.uri = uri;
this.resourceId = resourceId;
this.config = bitmapConfig;
}
}
}
此阶段的总体流程如下:
2.3 into()
下面开始重要的图片解析、转换流程。into
方法先做了一些检查工作,之后才开始加载,加载策略分为内存加载、网络加载。内存缓存策略分为NO_CACHE
和NO_STORE
// -----------------------------------------------RequestCreator.java-----------------------------------------------
public void into(ImageView target, Callback callback) {
// 一些检查与处理分支:
// 检查是否主线程,不是的话抛出异常
// 检查url与resourceId,没有图片的话,流程终止
// 检查是否是deferRequest,是的话进行defer处理
// 加载策略:
// 1.优先从内存中读取
// 2.内存中没有的话,再从网络读取
// 如果有具体的转换操作,将构造方法时创建的this.data添加转换处理,默认不进行转换,即是上面的RequestTransformer.IDENTITY
Request request = createRequest(started);
// 内存缓存中key
String requestKey = createKey(request);
// 优先从内存中加载
if (shouldReadFromMemoryCache(memoryPolicy)) {
Bitmap bitmap = picasso.quickMemoryCacheCheck(requestKey);
if (bitmap != null) {
picasso.cancelRequest(target);
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());
}
Action action =
new ImageViewAction(picasso, target, request, memoryPolicy, networkPolicy, errorResId,
errorDrawable, requestKey, tag, callback, noFade);
picasso.enqueueAndSubmit(action);
}
2.3.1 内存加载
内存中保存的key的组成是:路径+转换标识,代码如下:
// -----------------------------------------------RequestCreator.java-----------------------------------------------
static String createKey(Request data, StringBuilder builder) {
if (data.stableKey != null) {
// 默认为null,不走这里
builder.ensureCapacity(data.stableKey.length() + KEY_PADDING);
builder.append(data.stableKey);
} else if (data.uri != null) {
String path = data.uri.toString();
builder.ensureCapacity(path.length() + KEY_PADDING);
builder.append(path);
} else {
builder.ensureCapacity(KEY_PADDING);
builder.append(data.resourceId);
}
builder.append(KEY_SEPARATOR);
// 接下来添加操作的标识
if (data.rotationDegrees != 0) {
builder.append("rotation:").append(data.rotationDegrees);
if (data.hasRotationPivot) {
builder.append('@').append(data.rotationPivotX).append('x').append(data.rotationPivotY);
}
builder.append(KEY_SEPARATOR);
}
if (data.hasSize()) {
builder.append("resize:").append(data.targetWidth).append('x').append(data.targetHeight);
builder.append(KEY_SEPARATOR);
}
if (data.centerCrop) {
builder.append("centerCrop:").append(data.centerCropGravity).append(KEY_SEPARATOR);
} else if (data.centerInside) {
builder.append("centerInside").append(KEY_SEPARATOR);
}
if (data.transformations != null) {
for (int i = 0, count = data.transformations.size(); i < count; i++) {
builder.append(data.transformations.get(i).key());
builder.append(KEY_SEPARATOR);
}
}
return builder.toString();
}
内存中存在就优先从内存中加载,quickMemoryCacheCheck()
就是通过key从内存中查找
// -----------------------------------------------Picasso.java-----------------------------------------------
Bitmap quickMemoryCacheCheck(String key) {
Bitmap cached = cache.get(key);
if (cached != null) {
stats.dispatchCacheHit();
} else {
stats.dispatchCacheMiss();
}
return cached;
}
内存缓存使用了LRU算法,整个类流程如下图:
2.3.2 网络加载
从上面可以看出,创建了一个ImageViewAction
,之后提交到Dispatcher
进行调度。在提到dispatcher之前做了一些检查工作,如非主线程检查、取消已经存在的请求
// -----------------------------------------------RequestCreator.java-----------------------------------------------
public void into(ImageView target, Callback callback) {
...
Action action =
new ImageViewAction(picasso, target, request, memoryPolicy, networkPolicy, errorResId,
errorDrawable, requestKey, tag, callback, noFade);
picasso.enqueueAndSubmit(action);
}
// -----------------------------------------------Picasso.java-----------------------------------------------
void enqueueAndSubmit(Action action) {
Object target = action.getTarget();
if (target != null && targetToAction.get(target) != action) {
// 取消已经存在的Request
cancelExistingRequest(target);
targetToAction.put(target, action);
}
submit(action);
}
void submit(Action action) {
dispatcher.dispatchSubmit(action);
}
// -----------------------------------------------Dispatcher.java-----------------------------------------------
void dispatchSubmit(Action action) {
handler.sendMessage(handler.obtainMessage(REQUEST_SUBMIT, action));
}
Dispatcher
调度器中定义很多执行消息,使用了Handler进行分发。REQUEST_SUBMIT
代表请求的开始,执行了performSubmit
// -----------------------------------------------Dispatcher.java-----------------------------------------------
void performSubmit(Action action, boolean dismissFailed) {
....
// 创建hunter,执行请求
hunter = forRequest(action.getPicasso(), this, cache, stats, action);
hunter.future = service.submit(hunter);
hunterMap.put(action.getKey(), hunter);
if (dismissFailed) {
failedActions.remove(action.getTarget());
}
}
BitmapHunter 实际上是一个Runnable,放在线程池执行后台任务。从上面知道,load方法支持url/file等几种形式,每种形式对应了一个RequestHandler,但是Picasso这个处理的比较简陋,直接通过uri.getScheme()
进行区分,比较容易推测到不支持直接用filePath
// -----------------------------------------------BitmapHunter .java-----------------------------------------------
static BitmapHunter forRequest(Picasso picasso, Dispatcher dispatcher, Cache cache, Stats stats,
Action action) {
// 找到对应的RequestHandler
Request request = action.getRequest();
List<RequestHandler> requestHandlers = picasso.getRequestHandlers();
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);
}
我们跟踪看run(),看看网络加载的处理。通过上面获取到的RequestHandler进行load(),添加对原图转化处理,并将结果通知Dispatcher
// -----------------------------------------------BitmapHunter .java-----------------------------------------------
public void run() {
try {
...
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;
}
}
// 具体的加载
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 (bitmap == null) {
Source source = result.getSource();
try {
bitmap = decodeStream(source, data);
} finally {
try {
source.close();
} catch (IOException ignored) {
}
}
}
}
// 对原图添加转化处理
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;
}
接着我们来看NetworkRequestHandler具体做了什么,可以知道网络请求使用了okhttp,磁盘缓存也使用了okhttp
// -----------------------------------------------BitmapHunter.java-----------------------------------------------
public Result load(Request request, int networkPolicy) throws IOException {
// 用okhttp进行网络请求
okhttp3.Request downloaderRequest = createRequest(request, networkPolicy);
Response response = downloader.load(downloaderRequest);
ResponseBody body = response.body();
if (!response.isSuccessful()) {
body.close();
throw new ResponseException(response.code(), request.networkPolicy);
}
// 用okhttp缓存
Picasso.LoadedFrom loadedFrom = response.cacheResponse() == null ? NETWORK : DISK;
if (loadedFrom == DISK && body.contentLength() == 0) {
body.close();
throw new ContentLengthException("Received response with 0 content-length header.");
}
if (loadedFrom == NETWORK && body.contentLength() > 0) {
stats.dispatchDownloadFinished(body.contentLength());
}
return new Result(body.source(), loadedFrom);
}
private static okhttp3.Request createRequest(Request request, int networkPolicy) {
CacheControl cacheControl = null;
if (networkPolicy != 0) {
if (NetworkPolicy.isOfflineOnly(networkPolicy)) {
cacheControl = CacheControl.FORCE_CACHE;
} else {
CacheControl.Builder builder = new CacheControl.Builder();
if (!NetworkPolicy.shouldReadFromDiskCache(networkPolicy)) {
builder.noCache();
}
if (!NetworkPolicy.shouldWriteToDiskCache(networkPolicy)) {
builder.noStore();
}
cacheControl = builder.build();
}
}
okhttp3.Request.Builder builder = new okhttp3.Request.Builder().url(request.uri.toString());
if (cacheControl != null) {
builder.cacheControl(cacheControl);
}
return builder.build();
}
// -----------------------------------------------OkHttp3Downloader.java-----------------------------------------------
public final class OkHttp3Downloader implements Downloader {
final Call.Factory client;
public Response load(@NonNull Request request) throws IOException {
return client.newCall(request).execute();
}
}
至此,整个网络加载大致梳理完成,对图片的一些解码或者转化,大家可以自己看源码。
into整个流程如下:(刚用plantuml画图_
)
3.总结
- 整个代码不是很多,逻辑看起来比较清晰
- 非主线程抛出异常、支持加载的形式比较简陋
- 缺少activity/fragment中生命周期处理
- 运行自身的sample,对比glide,加载很慢
4.问题
1.Picasso三级缓存流程?
内存缓存(LRU算法)–> 磁盘缓存 -> 网络请求 ,其中磁盘缓存和网络请求使用的是okhttp