Picasso源码分析和对比

前面的 Android-Universal-Image-Loader源码分析Glide源码阅读理解一小时 分别讲述了五年前和现在最受欢迎的 Android 图片加载库。今天讲述的picassoSquare公司开源的一个Android图片加载库,可以实现图片下载和缓存功能。它 ImageLoaderGlide 的都有些相同和和不同点以及自己独特的点。

本文参考的 Picasso 源码的版本为 2.71828

官网地址:https://square.github.io/picasso

GitHub地址:https://github.com/square/picasso

Picasso组成部分

Picasso.png

这幅图对应的是 Picasso 的主要组成部分。

  • Picasso :图片加载、转换、缓存的管理类。提供 get 方法获取默认的单例对象,也提供了 Builder 供业务方自定义配置生成自己的单例对象。
  • RequestCreator :提供易用的 API ,内部维护了一个 Request.Builder 对象。用于构建图片下载的Request
  • Request :一个不可变的数据,用于控制图片使用之前的加载和变化。提供 Builder 进行数据的参数设置。
  • Action :图片架加载任务的请求包装,内部有 picassoRequestkeytag 等。
  • Dispatcher :执行任务的分发器,以及任务的暂停、重复、回复等事件的处理。内部注册了 BroadcastReceiver 用来监测网络变化,从而进一步修改线程池的大小。
  • BitmapHunter :核心类负责任务执行具体操作,获取数据,解码数据为 Bitmp 。处理生成的 Bitmap 以及负责当前请求的 transformation 操作。
  • RequestHandler :用于自定义的请求处理类,需要重写 canHandleRequestload 方法。Picasso 内部默认添加了7个 RequestHandler 子类。

Picasso的获取

Picasso 的官网实例中 Picasso.get() 方式可以获取默认的 Picasso 的单例对象进行图片加载。

//com.squareup.picasso.Picasso.java
static volatile Picasso singleton = null;
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;
}

Picasso 的单例使用了双重校验锁( DCL:double-checked locking),相关资料可以参考Java版的7种单例模式

其中的 Context 不需要外部注入使用的是 ContentProviderContext ,这个 ContentProviderPicasso 自己注册在 AndroidManifest.xml 中的。

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.squareup.picasso" >
    <uses-sdk android:minSdkVersion="14" />
    <application>
        <provider
            android:name="com.squareup.picasso.PicassoProvider"
            android:authorities="${applicationId}.com.squareup.picasso"
            android:exported="false" />
    </application>
</manifest>

Picasso的配置

根据 Picasso.Builder 的我们可以知道我们能自定义那些 Picasso 的配置。

//com.squareup.picasso.Picasso$Builder.java
public Picasso build() {
    Context context = this.context;
    if (downloader == null) {//默认的下载为okhttp
        downloader = new OkHttp3Downloader(context);
    }
    if (cache == null) {//默认的内存缓存使用的是LruCache
        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);
    //listener:检测Picasso加载图片过程中的失败以及异常
    //requestHandlers:自定义请求处理模块
    //defaultBitmapConfig:自定义生成Bitmap的配置
    //indicatorsEnabled:是否显示图片来源的指示器
    //loggingEnabled:是否打印Picasso的日志
    return new Picasso(context, dispatcher, cache, listener, transformer, requestHandlers, stats,
        defaultBitmapConfig, indicatorsEnabled, loggingEnabled);
}

显示指示器的图片加载完成之后会变为:

public enum LoadedFrom {
    MEMORY(Color.GREEN),
    DISK(Color.BLUE),
    NETWORK(Color.RED);
    final int debugColor;
    LoadedFrom(int debugColor) {
      this.debugColor = debugColor;
    }
}
indicators.png

Picasso加载数据类型

Picasso 一共提供了4中 load 方法:

public RequestCreator load(@Nullable Uri uri);
public RequestCreator load(@Nullable String path);
public RequestCreator load(@NonNull File file);
public RequestCreator load(@DrawableRes int resourceId);

其中 StringFile 类型都会转化为 Uri 类型,所以 Picasso 支持 UriResourceId 类型。

Picasso.get().load(R.drawable.landing_screen).into(imageView1);
Picasso.get().load("file:///android_asset/DvpvklR.png").into(imageView2);
Picasso.get().load(new File(...)).into(imageView3);

Request

Picasso 的简单实用:

Picasso.get().load("http://i.imgur.com/DvpvklR.png").into(imageView);

Picasso 的对图片的一些变化实用:

Picasso.get()
  .load(url)
  .resize(50, 50)
  .centerCrop()
  .into(imageView)

Picasso 的默认图以及错误处理默认图设置:

Picasso.get()
    .load(url)
    .placeholder(R.drawable.user_placeholder)
    .error(R.drawable.user_placeholder_error)
    .into(imageView);

以上的这些设置都是在修改 Request 的成员变量的属性。

RequestCreator的构造

我们知道 Picasso 支持加载 UriResourceId ,所以我们先来看看这两个数据类型的 load 的方法。

//com.squareup.picasso.Picasso.java
public RequestCreator load(@Nullable Uri uri) {
   return new RequestCreator(this, uri, 0);
}
public RequestCreator load(@DrawableRes int resourceId) {
   if (resourceId == 0) {
      throw new IllegalArgumentException("Resource ID must not be zero.");
   }
   return new RequestCreator(this, null, resourceId);
}

RequestCreateor对Request的配置

  • 可以设置 tag 标签,Picasso 可以在暂停、恢复请求的时候操作具有相同的 tag 标签的请求。比如 Activity 或者 Context 我们可以根据这个 tag 标签,做请求的生命周期管理,但是需要注意内存泄漏;
  • 可以设置缓存的额外的 Key ,从而对同一个请求资源做不同的缓存处理;
  • 设置请求的优先级;
  • 设置内存缓存策略,以及网络请求缓存策略;
  • 设置禁用从磁盘缓存或网络加载的图像的进行淡入浅出动画;
  • 设置对图片的转化,转化前的图片必须在转化后手动回收;
  • 设置可以等到图片加载完成确定宽、高之后再进行资源的加载;
  • 设置对图片的宽、高、裁剪方式,旋转角度和解码配置等;

Request的构造和Action的提交

我们通过 Picasso.load 方法构造 RequestCreateorRequest 的构造再下一步调用 RequestCreateor.into 真正出发请求的时候产生。

//com.squareup.picasso.RequestCreator.java
public void into(ImageView target) {
    into(target, null);
}
public void into(ImageView target, Callback callback) {
    long started = System.nanoTime();
    checkMain();
    if (target == null) {
      throw new IllegalArgumentException("Target must not be null.");
    }
    if (!data.hasImage()) {//判断uri为空,或者resourceid等于0
      picasso.cancelRequest(target);//取消在target上的请求
      if (setPlaceholder) {//如果设置默认占位图,那么默认图展现在target上
        setPlaceholder(target, getPlaceholderDrawable());
      }
      return;
    }
    if (deferred) {//判断是否需要延迟初始化
        if (data.hasSize()) {//如果已经设置了宽高,那么不能进行延迟请求
            throw new IllegalStateException("Fit cannot be used with resize.");
        }
        int width = target.getWidth();
        int height = target.getHeight();
        if (width == 0 || height == 0) {
            if (setPlaceholder) {//如果设置默认占位图,那么默认图展现在target上
                setPlaceholder(target, getPlaceholderDrawable());
            }//设置延迟加载,具体实现后续有讲解
            picasso.defer(target, new DeferredRequestCreator(this, target, callback));
            return;
        }
        //如果target已经确定边界那么不需要延迟请求
        data.resize(width, height);
    }
    Request request = createRequest(started);//创建Request,如果请求已更改,请复制原始ID和时间戳。
    String requestKey = createKey(request);//创建请求的key,用来标识请求和内存缓存
    if (shouldReadFromMemoryCache(memoryPolicy)) {//是否读取内存缓存(默认内存缓存,可读、可写)
        Bitmap bitmap = picasso.quickMemoryCacheCheck(requestKey);//从urlcache中获取
        if (bitmap != null) {//如果有缓存的bitmap
            picasso.cancelRequest(target);//取消请求,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) {//如果设置默认占位图,那么默认图展现在target上
        setPlaceholder(target, getPlaceholderDrawable());
    }//构建请求任务的Action,进行任务提交
    Action action = new ImageViewAction(picasso, target, request, memoryPolicy, networkPolicy, errorResId,errorDrawable, requestKey, tag, callback, noFade);
    picasso.enqueueAndSubmit(action);
}

上面这段代码主要包含了一下以及逻辑:

  • 是否需要延迟请求,以便于确定加载图片的边界;
  • 创建 Requestrequestkey
    • Request 的创建主要是先调用 RequestCreator 对象中的 Request.Builderbuilder() 方法构造,并且调用 Picasso 的请求转化操作进行 请求 的处理.
    • requestKey 的创建主要是根据当前 RequesturistableKey 以及旋转角度、宽高、裁剪样式和转变操作等构造。
  • 是否需要从内存缓存中读取并加载;
  • 构建请求的任务 Action ,并进行提交;
void enqueueAndSubmit(Action action) {
    Object target = action.getTarget();
    if (target != null && targetToAction.get(target) != action) {
        //检测是否在主线程,去掉target上的之前的action操作
        cancelExistingRequest(target);
        targetToAction.put(target, action);
    }
    //提交任务
    submit(action);
}
void submit(Action action) {
    dispatcher.dispatchSubmit(action);
}

enqueueAndSubmit 方法移除了 ImageView 上之前的操作,以便于加载新的图片。

Action

上面说到了构造 Action ,我们这里来分析一下 Picasso 提供的 Action 类型。

GetAction :仅仅用来加载资源以及进行缓存,无任何回调;

FetchAction :用来加载资源以及进行缓存,只有成功失败回调,没有资源信息回调;

TargetAction :用来加载资源以及进行缓存,可以有有资源信息的成功、失败回调;

ImageViewAction :用来加载资源以及进行缓存,然后将产生的 Bitmap 加载在 ImageView 上。

RemoteViewAction :抽象类,用来加载资源以及进行缓存,然后将产生的 Bitmap 加载在 RemoteView 上。有两个实现类NotificationActionAppWidgetAction ,分别对应通知栏和桌面小部件。

Dispatcher

Dispatcher 作为任务的分发器会将提交任务、恢复任务、暂停任务、取消任务、任务完成、任务失败以及网络状态变化。

我先接着上一步讲述任务的提交:

//com.squareup.picasso.Dispatcher.java
void dispatchSubmit(Action action) {
    //切换任务执行的线程(主线程到异步线程)
   handler.sendMessage(handler.obtainMessage(REQUEST_SUBMIT, action));
}

这里的 handler 是有着一个非主线程的 Looper ,为 DispatcherHandler

//com.squareup.picasso.Dispatcher.java
this.handler = new DispatcherHandler(dispatcherThread.getLooper(), this);
static class DispatcherThread extends HandlerThread {
    DispatcherThread() {
        //THREAD_PRIORITY_BACKGROUND=10
        super(Utils.THREAD_PREFIX + DISPATCHER_THREAD_NAME, THREAD_PRIORITY_BACKGROUND);
    }
}

切换线程执行 Action

//com.squareup.picasso.Dispatcher.java
void performSubmit(Action action) {
    performSubmit(action, true);
}
void performSubmit(Action action, boolean dismissFailed) {
    //如果当前的tag处于paused那么将这个action加入到pausedActions容器
    if (pausedTags.contains(action.getTag())) {
        pausedActions.put(action.getTarget(), action);
        if (action.getPicasso().loggingEnabled) {
            log(OWNER_DISPATCHER, VERB_PAUSED, action.request.logId(),"because tag '" + action.getTag() + "' is paused");
        }
        return;
    }
    //判断是否已经有对应的Bitmap捕捉器
    BitmapHunter hunter = hunterMap.get(action.getKey());
    if (hunter != null) {//如果有那么将当前的action进行依附(情景:多个imageview加载同一个图片资源)
        hunter.attach(action);
        return;
    }
    if (service.isShutdown()) {//任务线程池是否关闭
        if (action.getPicasso().loggingEnabled) {
            log(OWNER_DISPATCHER, VERB_IGNORED, action.request.logId(), "because shut down");
        }
        return;
    }
    //从Picasso中获取对应的Bitmap捕捉器
    hunter = forRequest(action.getPicasso(), this, cache, stats, action);
    //提交Bitmap的捕捉任务
    hunter.future = service.submit(hunter);
    hunterMap.put(action.getKey(), hunter);
    if (dismissFailed) {//失败不做处理,从failedActions移除
        failedActions.remove(action.getTarget());
    }
    if (action.getPicasso().loggingEnabled) {
        log(OWNER_DISPATCHER, VERB_ENQUEUED, action.request.logId());
    }
}

线程池大小

Dispatcher 在构造函数中注册了广播,监听网络状态的变化从而修改线程池执行线程数量的大小。

//com.squareup.picasso.PicassoExecutorService.java
private static final int DEFAULT_THREAD_COUNT = 3;
void adjustThreadCount(NetworkInfo info) {
    if (info == null || !info.isConnectedOrConnecting()) {
      setThreadCount(DEFAULT_THREAD_COUNT);
      return;
    }
    switch (info.getType()) {
      case ConnectivityManager.TYPE_WIFI://WIFI
      case ConnectivityManager.TYPE_WIMAX:
      case ConnectivityManager.TYPE_ETHERNET:
        setThreadCount(4);
        break;
      case ConnectivityManager.TYPE_MOBILE:
        switch (info.getSubtype()) {
            case TelephonyManager.NETWORK_TYPE_LTE:  // 4G
            case TelephonyManager.NETWORK_TYPE_HSPAP:
            case TelephonyManager.NETWORK_TYPE_EHRPD:
                setThreadCount(3);
                break;
            case TelephonyManager.NETWORK_TYPE_UMTS: // 3G
            case TelephonyManager.NETWORK_TYPE_CDMA:
            case TelephonyManager.NETWORK_TYPE_EVDO_0:
            case TelephonyManager.NETWORK_TYPE_EVDO_A:
            case TelephonyManager.NETWORK_TYPE_EVDO_B:
                setThreadCount(2);
                break;
            case TelephonyManager.NETWORK_TYPE_GPRS: // 2G
            case TelephonyManager.NETWORK_TYPE_EDGE:
                setThreadCount(1);
                break;
            default:
                setThreadCount(DEFAULT_THREAD_COUNT);
        }
        break;
      default:
        setThreadCount(DEFAULT_THREAD_COUNT);
    }
}
private void setThreadCount(int threadCount) {
    setCorePoolSize(threadCount);
    setMaximumPoolSize(threadCount);
}

BitmapHunter

BitmapHunter 实现了 Runnable 接口处理资源的加载,它内部有一个 RequestHandlerload 对应的 Uri 或者 ResourceId

BitmapHunter的创建

//com.squareup.picasso.BitmapHunter.java
static BitmapHunter forRequest(Picasso picasso, Dispatcher dispatcher, Cache cache, Stats stats, Action action) {
    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);
}

RequestHandler 前面讲述过 Piacasso 自己默认添加了7种,而且我们也可以自定义。

Picasso(Context context, //上线文环境
        Dispatcher dispatcher, //任务分发器
        Cache cache, //内存缓存
        Listener listener,//Picasso的异常回调
        RequestTransformer requestTransformer, //请求拦截处理类
        List<RequestHandler> extraRequestHandlers, //自定义的请求扩展处理
        Stats stats,//内存的状态
        Bitmap.Config defaultBitmapConfig, //Bitmap的解码配置
        boolean indicatorsEnabled, //图片右上角是否显示指示器标识图片来源
        boolean loggingEnabled) {//是否打印日志
    /***部分代码省略***/
    int builtInHandlers = 7; //默认7个,可添加自定义扩展
    //allRequestHandlers的总数
    int extraCount = (extraRequestHandlers != null ? extraRequestHandlers.size() : 0);
    List<RequestHandler> allRequestHandlers = new ArrayList<>(builtInHandlers + extraCount);
    //ResourceRequestHandler必须是列表中的第一个以避免强制其他RequestHandler对request.uri执行null检查涵盖(request.resourceId!= 0)的情况。 
    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));
    /***部分代码省略***/
}

我们从 Picasso 的构造中可以看出 RequestHandler 都有哪些:

  • ResourceRequestHandler :资源类型处理,ResourceId 类型的独有处理类;
  • ContactsPhotoRequestHandlerContactsPhoto 请求处理器,加载com.android.contacts/下的图片。
  • MediaStoreRequestHandlerMediaStore 请求处理器,如果图片是存在MediaStore上的则用这个处理器处理。
  • ContentStreamRequestHandlercontent 开头的类型,加载ContentProvider 提供的图片。
  • AssetRequestHandlerfile://android_asset/ 开头的类型,加载asset目录下的图片。
  • FileRequestHandlerfile 开头的类型,处理文件类型的图片。
  • NetworkRequestHandlerhttp 或者 https 开头的类型,加载网络图片。

可能一个数据类型有多个处理的 RequestHandler ,但从生成 BitmapHunter 的方法我们可以看出来最终由 RequestHandler 的添加顺序确定。

BitmapHunter.run

执行请求任务,以及进行相应的异常捕获处理。

//com.squareup.picasso.BitmapHunter.java
@Override public void run() {
    try {
        updateThreadName(data);//更新线程名称以方便与打印日志
        if (picasso.loggingEnabled) {
            log(OWNER_HUNTER, VERB_EXECUTING, getLogIdsForHunter(this));
        }
        result = hunt();//获取Bitmap
        if (result == null) {//处理失败回调
            dispatcher.dispatchFailed(this);
        } else {//处理成功回调
            dispatcher.dispatchComplete(this);
        }
    } catch (NetworkRequestHandler.ResponseException e) {//处理网络响应异常
        if (!NetworkPolicy.isOfflineOnly(e.networkPolicy) || e.code != 504) {
            exception = e;
        }
        dispatcher.dispatchFailed(this);
    } catch (IOException e) {//处理io异常
        exception = e;
        dispatcher.dispatchRetry(this);
    } catch (OutOfMemoryError e) {//处理内存异常
        StringWriter writer = new StringWriter();
        //创建当前的内存数据快照,将其数据进行打印
        stats.createSnapshot().dump(new PrintWriter(writer));
        exception = new RuntimeException(writer.toString(), e);
        dispatcher.dispatchFailed(this);
    } catch (Exception e) {//处理其他异常
        exception = e;
        dispatcher.dispatchFailed(this);
    } finally {//线程结束
        Thread.currentThread().setName(Utils.THREAD_IDLE_NAME);
    }
}

Bitmap的获取

//com.squareup.picasso.BitmapHunter.java
Bitmap hunt() throws IOException {
    Bitmap bitmap = null;
    //是否允许从内存缓存中读取Bitmap
    if (shouldReadFromMemoryCache(memoryPolicy)) {
        bitmap = cache.get(key);
        if (bitmap != null) {//如果内存缓存有那么直接返回
            stats.dispatchCacheHit();
            loadedFrom = MEMORY;
            if (picasso.loggingEnabled) {
                log(OWNER_HUNTER, VERB_DECODED, data.logId(), "from cache");
            }
            return bitmap;
        }
    }
    //获取对应的网络数据处理状态
    networkPolicy = retryCount == 0 ? NetworkPolicy.OFFLINE.index : networkPolicy;
    //对应的RequestHandler加载数据得到Result
    RequestHandler.Result result = requestHandler.load(data, networkPolicy);
    if (result != null) {
        loadedFrom = result.getLoadedFrom();//获取数据来源(内存、磁盘、文件)
        exifOrientation = result.getExifOrientation();//获取旋转角度
        bitmap = result.getBitmap();//获取Bitmap
        //如果没有位图,则需要从流中对其进行解码。
        if (bitmap == null) {
            Source source = result.getSource();
            try {
                bitmap = decodeStream(source, data);
            } finally {
                try {//关闭数据流
                    source.close();
                } catch (IOException ignored) {
                }
            }
        }
    }
    if (bitmap != null) {//Bitmap不为空
        if (picasso.loggingEnabled) {
            log(OWNER_HUNTER, VERB_DECODED, data.logId());
        }//更新内存数据状态
        stats.dispatchBitmapDecoded(bitmap);
        //如果Bitmap需要变换或者需要进行方向调整
        if (data.needsTransformation() || exifOrientation != 0) {
            synchronized (DECODE_LOCK) {
                //是否可以进行方向的调整
                if (data.needsMatrixTransform() || exifOrientation != 0) {
                    //Bitmap使用矩阵进行方向的变换
                    bitmap = transformResult(data, bitmap, exifOrientation);
                    if (picasso.loggingEnabled) {
                        log(OWNER_HUNTER, VERB_TRANSFORMED, data.logId());
                    }
                }
                if (data.hasCustomTransformations()) {//是否自定义了Bitmap的转换
                    //处理Bitmap的变换,如果Bitmap有的新的对象,那么之前的Bitmap必须主动回收
                    bitmap = applyCustomTransformations(data.transformations, bitmap);
                    if (picasso.loggingEnabled) {
                        log(OWNER_HUNTER, VERB_TRANSFORMED, data.logId(), "from custom transformations");
                    }
                }
            }
            if (bitmap != null) {//有了新的Bitmap产生,更新内存数据状态
                stats.dispatchBitmapTransformed(bitmap);
            }
        }
    }
    return bitmap;
}

这里用一张网络图片举例说明怎么进行的数据获取:

我们根据 Picasso 中默认添加的 RequestHandler 了解到处理网络任务的 NetworkRequestHandler

//com.squareup.picasso.NetworkRequestHandler.java
@Override public Result load(Request request, int networkPolicy) throws IOException {
    okhttp3.Request downloaderRequest = createRequest(request, networkPolicy);
    Response response = downloader.load(downloaderRequest);//OKHTTP进行下载返回Response
    ResponseBody body = response.body();//获取Request的Body
    if (!response.isSuccessful()) {//如果接口失败那么跑出异常并关闭响应
        body.close();
        throw new ResponseException(response.code(), request.networkPolicy);
    }
    //判断响应的数据是否为http的缓存
    Picasso.LoadedFrom loadedFrom = response.cacheResponse() == null ? NETWORK : DISK;
    //判断http的缓存是否无效
    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);
}

磁盘缓存

从文章一开始的结构图和代码都没有体现磁盘缓存 ,那么 Picasso磁盘缓存怎么拥有的?为了回答这个问题我们先将上面的 downloader.load 来再次看一下,在一开始我们看到 Picasso 默认的 downloaderOkHttp3Downloader

Downloader :一种从外部资源(例如磁盘缓存和网络)加载图像的机制。

public interface Downloader {
  //从互联网下载指定的图像。如果无法成功加载请求的URL,则抛出IOException。 
  @NonNull Response load(@NonNull okhttp3.Request request) throws IOException;
  //允许对此进行清理,包括关闭磁盘缓存和其他资源
  void shutdown();
}

这里我们就发现一个很有意思的现象,Downloader 的接口包含了 Okhtt3Request 。所以这就限定了 Picasso 的请求只能使用 Okhttp3 (毕竟都是 Square 公司的当然使用自己产品)。

public final class OkHttp3Downloader implements Downloader {
    @VisibleForTesting final Call.Factory client;
    private final Cache cache;
    private boolean sharedClient = true;
    //创建使用OkHttp的新下载器。这会将图像缓存安装到您的应用程序中缓存目录。
    public OkHttp3Downloader(final Context context) {
        this(Utils.createDefaultCacheDir(context));
    }
    public OkHttp3Downloader(final File cacheDir) {
        this(cacheDir, Utils.calculateDiskCacheSize(cacheDir));
    }
    public OkHttp3Downloader(final Context context, final long maxSize) {
        this(Utils.createDefaultCacheDir(context), maxSize);
    }
    public OkHttp3Downloader(final File cacheDir, final long maxSize) {
        this(new OkHttpClient.Builder().cache(new Cache(cacheDir, maxSize)).build());
        sharedClient = false;
    }
    public OkHttp3Downloader(OkHttpClient client) {
        this.client = client;
        this.cache = client.cache();
    }
    public OkHttp3Downloader(Call.Factory client) {
        this.client = client;
        this.cache = null;
    }
    //同步执行
    @NonNull @Override public Response load(@NonNull Request request) throws IOException {
        return client.newCall(request).execute();
    }
    //关闭缓存
    @Override public void shutdown() {
        if (!sharedClient && cache != null) {
            try {
                cache.close();
            } catch (IOException ignored) {
            }
        }
    }
}

Downloader 的实现我们可以看出来我们 Picasso 的磁盘缓存是利用的 Http 协议做的磁盘缓存。

图片数据的呈现

我们在将 Bitmap 获取的之后,下一步就应该展现在 ImageView 上。除此之前还应该处理内存缓存、成功失败等回调。

内存缓存写入

//com.squareup.picasso.Dispatcher.java
void performComplete(BitmapHunter hunter) {
    //如果允许写入内存缓存,那么将bitmap写入cache
    if (shouldWriteToMemoryCache(hunter.getMemoryPolicy())) {
        cache.set(hunter.getKey(), hunter.getResult());
    }
    //移除key对应的BitmpHunter
    hunterMap.remove(hunter.getKey());
    batch(hunter);//处理BitmapHunter
    if (hunter.getPicasso().loggingEnabled) {
        log(OWNER_DISPATCHER, VERB_BATCHED, getLogIdsForHunter(hunter), "for completion");
    }
}
private void batch(BitmapHunter hunter) {
    if (hunter.isCancelled()) {//如果Bitmap捕捉任务已经被取消
        return;
    }
    if (hunter.result != null) {//重建所有与待画位图相关的缓存。
        hunter.result.prepareToDraw();
    }
    batch.add(hunter);//将BitmapHunter添加都batch中进行批量的回调处理,500ms一批
    if (!handler.hasMessages(HUNTER_DELAY_NEXT_BATCH)) {
         handler.sendEmptyMessageDelayed(HUNTER_DELAY_NEXT_BATCH, BATCH_DELAY);
    }
}

prepareToDraw :绘制准备:重建该bitmap相关联的缓存来绘制。在可清除的bitmap中,此方法会尝试确保像素已经被解码。

交付数据

//com.squareup.picasso.Dispatcher.java
void complete(BitmapHunter hunter) {
    Action single = hunter.getAction();//获取BitmapHunter的Action
    List<Action> joined = hunter.getActions();//获取获取BitmapHunter的Actions
    boolean hasMultiple = joined != null && !joined.isEmpty();//需要处理多个Action
    boolean shouldDeliver = single != null || hasMultiple;//是否有交付对象
    if (!shouldDeliver) {
        return;
    }
    Uri uri = hunter.getData().uri;//数据源的Uri
    Exception exception = hunter.getException();//数据加载过程中的异常
    Bitmap result = hunter.getResult();//数据对应Bitmap
    LoadedFrom from = hunter.getLoadedFrom();//数据来源
    if (single != null) {//Action的数据交付
      deliverAction(result, from, single, exception);
    }
    if (hasMultiple) {
        for (int i = 0, n = joined.size(); i < n; i++) {
            Action join = joined.get(i);//Action的数据交付
            deliverAction(result, from, join, exception);
        }
    }
    //如有有异常监听,并且有异常,那么进行回调
    if (listener != null && exception != null) {
        listener.onImageLoadFailed(this, uri, exception);
    }
}
//com.squareup.picasso.Picasso.java
private void deliverAction(Bitmap result, LoadedFrom from, Action action, Exception e) {
    if (action.isCancelled()) {//请求已经被取消
        return;
    }
    if (!action.willReplay()) {//是否需要等待监听网络状态重新加载
        targetToAction.remove(action.getTarget());
    }
    if (result != null) {
        if (from == null) {
            throw new AssertionError("LoadedFrom cannot be null.");
        }
        action.complete(result, from);//将Bitmap赋给View
    } else {
        action.error(e);
    }
}
//com.squareup.picasso.ImageViewAction.java
@Override 
public void complete(Bitmap result, Picasso.LoadedFrom from) {
    if (result == null) {
        throw new AssertionError(String.format("Attempted to complete action with no result!\n%s", this));
    }
    ImageView target = this.target.get();
    if (target == null) {
        return;
    }
    Context context = picasso.context;
    boolean indicatorsEnabled = picasso.indicatorsEnabled;//在target上显示Bitmap
    PicassoDrawable.setBitmap(target, context, result, from, noFade, indicatorsEnabled);
    if (callback != null) {
        callback.onSuccess();
    }
}

Picasso延迟加载

为什么需要延迟加载呢?因为我们在View 上进行图片加载的时候不确定 View 是否已经被绘制完确定了宽、高。只有确定宽高我们才能从数据中解码出响应大小的 Bitmap 。所以延迟加载只是为了等待 View 被绘制完。

class DeferredRequestCreator implements OnPreDrawListener, OnAttachStateChangeListener {
    private final RequestCreator creator;
    @VisibleForTesting final WeakReference<ImageView> target;
    @VisibleForTesting Callback callback;
    /***部分代码被省略***/
    @Override public void onViewAttachedToWindow(View view) {
        view.getViewTreeObserver().addOnPreDrawListener(this);
    }
    @Override public void onViewDetachedFromWindow(View view) {
        view.getViewTreeObserver().removeOnPreDrawListener(this);
    }
    @Override public boolean onPreDraw() {
        ImageView target = this.target.get();
        if (target == null) {
            return true;
        }
        ViewTreeObserver vto = target.getViewTreeObserver();
        if (!vto.isAlive()) {
            return true;
        }
        int width = target.getWidth();
        int height = target.getHeight();
        if (width <= 0 || height <= 0) {//宽、高不合法继续监听
            return true;
        }//移除监听,取消请求延迟,设置size大小,重写请求
        target.removeOnAttachStateChangeListener(this);
        vto.removeOnPreDrawListener(this);
        this.target.clear();
        this.creator.unfit().resize(width, height).into(target, callback);
        return true;
    }
    void cancel() {
        creator.clearTag();//清除tag
        callback = null;
        ImageView target = this.target.get();
        if (target == null) {
            return;
        }
        this.target.clear();//清除引用
        target.removeOnAttachStateChangeListener(this);//移除监听
        ViewTreeObserver vto = target.getViewTreeObserver();
        if (vto.isAlive()) {//移除监听
            vto.removeOnPreDrawListener(this);
        }
    }
    /***部分代码被省略***/
}

我们通过监听 ViewWindow 上的状态变化,以及监听 View 绘制来进行宽高的获取。

统计监控

class Stats {
    private static final int CACHE_HIT = 0;//缓存命中
    private static final int CACHE_MISS = 1;//缓存没命中
    private static final int BITMAP_DECODE_FINISHED = 2;//图片解码
    private static final int BITMAP_TRANSFORMED_FINISHED = 3;//图片转码
    private static final int DOWNLOAD_FINISHED = 4;//数据下载完成
    /***部分代码省略***/
}

Picasso 通过 Stats 监控了内存和使用情况包括内存命中率,内存占用大小,流量消耗等。在产生 OutOfMemoryError 的时候会对当前内存的使用做一份快照并进行日志输出。

总结

前面的 Android-Universal-Image-Loader源码分析Glide源码阅读理解一小时 有过 GlideImageLoader 的对比,这次我们将 Picasso 与这两个图片加载库再次进行对比。

WEBP :在 Android 4.0 (API level 14)中支持有损的WebP图像,在Android 4.3(API level 18)和更高版本中支持无损和透明的 WebP 图像。

对比.png

文章到这里就全部讲述完啦,若有其他需要交流的可以留言哦

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

推荐阅读更多精彩内容