picasso详解及其源码简析

一、前言

图片库作为开发中一个重要的基础组件,其重要性不言而喻。目前来说,比较流行的图片库分别是 Android-Universal-Image-LoaderPicassoGlideFresco。它们的一般特点是:

  • 使用上比较简单,基本上都可以用一句链式代码就直接完成了图片的下载到展示。
  • 多级缓存,至少有 2 级缓存,磁盘缓存以及内存缓存。加上高效的图片处理,极大的减少了OOM 的发生。
  • 多种数据源,网络,磁盘,resource,assets 等。
  • 自动化程度高,图片下载、缓存管理,图片解码、图片处理甚至是 ListView/RecyclerView 的 Adapter 中图片自动取消、展示等,都不用我们操心了。

这篇文章要分析的是 square/picasso。没错,又是 square,谁中你那么优秀呢?不去管它的旧版本如何,这里直接分析其最新版本 2.71828 吧。

二、picasso 简介

1.关于 picasso

picasso,伟大的画家毕加索,这里想必也是在表达致敬之意。square 在 github 项目首页中只用了一句话解释这个库。

一个适用于Android的强大的图像下载和缓存库

image.png

除了 github 项目首页的介绍外,picasso 还另外提供了一个 github page。在这里,它进一步介绍了其具备的几个功能:

  • 处理 ImageView 在适配器中的回收以及取消下载。
  • 使用最少的内存来处理复杂的图像转换。
  • 自动管理磁盘和内存缓存。

同时该页面也给出了一些使用例子,如在 Adapter 中使用,图像变换以及自定义变换、设置 placeholders 等。感兴趣的同学可以自行前往看一看,不过要注意 Picasso 的实例不再从 Picasso.with(context) 开始了,也不再是该页面所展示的 Picasso.get(),而封装了一个 PicassoProvider,然后通过它的静态方法 get() 来获取。

2.集成

gradle 中集成,so easy,每个整 android 的小伙伴都懂的。

implementation 'com.squareup.picasso:picasso:2.71828'

三、源码分析

前面简要介绍了下 picasso,下面便开始进入正题源码分析。不变的原则,从最简单的主路径开始分析。分析之前先来看一看其框架图,是不是似曾相识?

Picasso.jpg

1.demo

picasso 工程里有提供 sample,作为典型的例子,这里选择了从 SampleListDetailAdapter 拿出一段用于 ListView 的 Adapter 中加载列表图片的代码来作为用于主路径分析的 demo。

PicassoProvider.get()
        .load(url)
        .placeholder(R.drawable.placeholder)
        .error(R.drawable.error)
        .resizeDimen(R.dimen.list_detail_image_size, R.dimen.list_detail_image_size)
        .centerInside()
        .tag(context)
        .into(holder.image);

链式调用的一大特点就在于不管多复杂的逻辑,看起来好像都是顺序执行的一样。嗯呵,你可能想到了 RxJava。下面就来逐个逐个分析上面的每一个方法的调用。

2.主路径分析

2.1 构造 Picasso 实例,初始化运行环境

  • PicassoProvider及其 get() 方法
public final class PicassoProvider {
  @SuppressLint("StaticFieldLeak")
  private static volatile Picasso instance;

  @Initializer
  public static Picasso get() {
    // 单例实现
    if (instance == null) {
      synchronized (PicassoProvider.class) {
        if (instance == null) {
          // 获取 context
          Context autoContext = PicassoContentProvider.context;
          if (autoContext == null) {
            throw new NullPointerException("context == null");
          }
          // 通过 Picasso.Builder 来构建 Picasso 实例
          instance = new Picasso.Builder(autoContext).build();
        }
      }
    }
    return instance;
  }

  private PicassoProvider() {
    throw new AssertionError("No instances.");
  }
}

PicassoProvider 通过单例设计模式的方式,在 get() 方法中为我们提供了 Picasso 的实例。也就是说,在应用中,一个进程内只有一个 Picasso 实例。

构建 Picasso 实例是需要 Context 的,这里是从 PicassoContentProvider 处获取。因此,进一步看看 PicassoContentProvider 又是如何提供 Context。

  • PicassoContentProvider 及其 context 的初始化
public final class PicassoContentProvider extends ContentProvider {
  @SuppressLint("StaticFieldLeak")
  @Nullable static Context context;

  @Override public boolean onCreate() {
    context = getContext();
    return true;
  }
  ......
}

PicassoContentProvider 继承了 ContentProvider,这是一个非常微妙的实现。其利用了 ContentProvider 会在 Application 初始化的时候同时被初始化的原理,从而初始化 context。不得不服!!!这就避免了开发者还需要对其进行全局初始化的步骤,典型是在应用的 Application#onCreate() 进行初始化。

  • Picasso.Builder 及其 build() 方法

先来看 Picasso 的构造方法,其主要目的是获取 ApplicationCotnext,从而避免引起内存泄漏。

public Builder(@NonNull Context context) {
      checkNotNull(context, "context == null");
      this.context = context.getApplicationContext();
    }

再来看 build() 方法。

public Picasso build() {
      Context context = this.context;
      // 初始化磁盘 Cache,也即初始化 OkHttp 的 Cache
      okhttp3.Cache unsharedCache = null;
      if (callFactory == null) {
        File cacheDir = createDefaultCacheDir(context);
        long maxSize = calculateDiskCacheSize(cacheDir);
        unsharedCache = new okhttp3.Cache(cacheDir, maxSize);
        callFactory = new OkHttpClient.Builder()
            .cache(unsharedCache)
            .build();
      }
      // 初始化内存 Cache
      if (cache == null) {
        cache = new PlatformLruCache(Utils.calculateMemoryCacheSize(context));
      }
      // 初始化线程池
      if (service == null) {
        service = new PicassoExecutorService(new PicassoThreadFactory());
      }
      // 初始化内存 Cache 统计器
      Stats stats = new Stats(cache);
      // 初始化分发器
      Dispatcher dispatcher = new Dispatcher(context, service, HANDLER, cache, stats);
      // new 出 Picasso 实例
      return new Picasso(context, dispatcher, callFactory, unsharedCache, cache, listener,
          requestTransformers, requestHandlers, stats, defaultBitmapConfig, indicatorsEnabled,
          loggingEnabled);
    }

磁盘 Cache:对了,磁盘在手机上一般指的就是闪存 Flash 或者外置的 SD Card 了。Picasso 没有单独实现一个磁盘缓存,而是利用了 OkHttp 的缓存管理,使得缓存的有效期与 Http 协议同步。这样做的好处是保证了图片的更新的及时性,而不像其他磁盘缓存通过硬设一个缓存过期时间比如 7 天来判断其是否过期。当然,它可能也会增加多一些网络请求。另外,默认创建的磁盘 Cache 的路径为 App 的内部 data 目录下 cache/picasso-cache,而大小一般为磁盘空间总大小的 2%,但是也不能超过 50 M。

内存 Cache:PlatformLruCache,其内部通过 LruCache 来实现的内存 Cahce。而其大小被限制在应用可获得的最大 heap 内存的 15%。

/** A memory cache which uses a least-recently used eviction policy. */
final class PlatformLruCache {
  final LruCache<String, BitmapAndSize> cache;
  ......
}

线程池: PicassoExecutorService,其继承自ThreadPoolExecutor,在其构造函数中设置了其核心线程数以及最大线程数为 3 个,这就限制了其最多只能有 3 个线程能同时工作。

class PicassoExecutorService extends ThreadPoolExecutor {
  private static final int DEFAULT_THREAD_COUNT = 3;

  PicassoExecutorService(ThreadFactory threadFactory) {
    super(DEFAULT_THREAD_COUNT, DEFAULT_THREAD_COUNT, 0, TimeUnit.MILLISECONDS,
        new PriorityBlockingQueue<Runnable>(), threadFactory);
  }
  ......
}

Stats: 内存 Cache 统计器,统计 Cache 的各个指标,比如命中,未命中等。

分发器Dispatcher:似曾相识的概念,在 OkHttp 中有见到过。这里也是一样,主要管理任务的分发与调度。包括提交,取消,重试以及网络变化的监听策略。

Picasso及其构造函数:Picasso 的构造会使得其持有 Dispatcher,Cache,PlatformLruCache,RequestTransformer 队列,RequestHandler队列等,这些引用都会保存在 Picasso 内部。除此之外,其自身也会初始化一些 RequestHandlers 用于处理已知的各种资源类型。当我们传递不同的资源类型的请求时,其便会选择相应的 RequestHandler 来进行处理。

另外,这里还注意到,Picasso 实现了 LifecycleObserver 接口。这意味着我们可以将其注册到 LifecycleOwner 的 LifecycleRegistry 中,从而实现生命周期的联动。

public class Picasso implements LifecycleObserver {
  ......
  Picasso(Context context, Dispatcher dispatcher, Call.Factory callFactory,
      @Nullable okhttp3.Cache closeableCache, PlatformLruCache cache, @Nullable Listener listener,
      List<RequestTransformer> requestTransformers, List<RequestHandler> extraRequestHandlers,
      Stats stats, @Nullable Bitmap.Config defaultBitmapConfig, boolean indicatorsEnabled,
      boolean loggingEnabled) {
    this.context = context;
    this.dispatcher = dispatcher;
    this.callFactory = callFactory;
    this.closeableCache = closeableCache;
    this.cache = cache;
    this.listener = listener;
    this.requestTransformers = Collections.unmodifiableList(new ArrayList<>(requestTransformers));
    this.defaultBitmapConfig = defaultBitmapConfig;

    // Adjust this and Builder(Picasso) as internal handlers are added or removed.
    int builtInHandlers = 8;
    int extraCount = extraRequestHandlers.size();
    List<RequestHandler> allRequestHandlers = new ArrayList<>(builtInHandlers + extraCount);

    ......
    allRequestHandlers.add(ResourceDrawableRequestHandler.create(context));
    allRequestHandlers.add(new ResourceRequestHandler(context));
    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(callFactory, stats));
    requestHandlers = Collections.unmodifiableList(allRequestHandlers);

    this.stats = stats;
    this.targetToAction = new LinkedHashMap<>();
    this.targetToDeferredRequestCreator = new LinkedHashMap<>();
    this.indicatorsEnabled = indicatorsEnabled;
    this.loggingEnabled = loggingEnabled;
  }
  ......
}

到这里,Picasso 的运行环境已经基本初始化好了,接下来就是等待用户的请求发送了。

2.2 参数设置,从 RequestCreator 到 Request

前面通过 PicassoProvider.get() 构建了 Picasso 实例,同时也初始化了 Picasso 的运行环境,接下来就是一系列参数的设置。通过 Picasso 的 load() 方法便可得到 Request 的创建器 RequestCreator,我们将需要的参数都送进 RequestCreator 以便能最终构建出我们所需要的那个 Request。

  • Picasso 的 load() 方法,构造 RequestCreator 实例

load() 重载了 4 个不同的方法,分别对应了加载不同的数据源,4 个方法如下。

public RequestCreator load(@Nullable Uri uri) {
    return new RequestCreator(this, uri, 0);
}
public RequestCreator load(@Nullable String path) {
    .......
    return load(Uri.parse(path));
}
public RequestCreator load(@Nullable File file) {
   ......
    return load(Uri.fromFile(file));
}
public RequestCreator load(@DrawableRes int resourceId) {
   ......
    return new RequestCreator(this, null, resourceId);
}

可以看出前 3 个其实都是同一个,形参都将转成 Uri。而从上面的代码也可以看出,最终都是 new 出一个 RequestCreator 实例。并且这里 Uri 与 resourceId 是互斥的,一定只是其中的一个。再来看看 RequestCraetor 的构造方法。

RequestCreator(Picasso picasso, @Nullable Uri uri, int resourceId) {
    ......
    this.picasso = picasso;
    this.data = new Request.Builder(uri, resourceId, picasso.defaultBitmapConfig);
}

这里最主要就是构造了 Request.Builder 实例,并存储在 data 属性中。其主要是用于在设置好参数后,进一步构造出 Request 实例。

  • RequestCreator.placeholder() 以及 error() 方法
public RequestCreator placeholder(@DrawableRes int placeholderResId) {
   ......
    this.placeholderResId = placeholderResId;
    return this;
}
public RequestCreator error(@DrawableRes int errorResId) {
    ......
    this.errorResId = errorResId;
    return this;
}

就是简单的记录下 placeholderResId 以及 errorResId。

  • RequestCreator.resizeDimen() 和 centerInside() 方法

resizeDeimen() 其实主是将参数 dimen 所表达的数值转成实际大小,再调用 resize() 方法,所以这里直接看 resize() 方法即可。

  public RequestCreator resize(int targetWidth, int targetHeight) {
    data.resize(targetWidth, targetHeight);
    return this;
 }
  public RequestCreator centerCrop() {
    data.centerCrop(Gravity.CENTER);
    return this;
 }

这里的 data 是 Request.Builder 实例,所以这里是将参数送进了 RequestBuilder 中去了。和上面的 2 个方法比较一下,为什么参数会被存储在不同的地方?

  • ReqeustCreator.tag()方法
  public RequestCreator tag(@NonNull Object tag) {
    data.tag(tag);
    return this;
  }

tag 用来标记一个 Request ,可以是任意一个对象,如 String,Context,Activity,Fragment等。我们可以通过 tag 来 pause,resume 以及 cancel 具备相同 tag 的 Request。但要注意的是 tag 与 Request 的生命周期是一样长的,一定程度上可能会存在内存泄漏的风险。

关于 RequestCreator 的参数还有很多可以设置的,这里就不一一详细进行讲解了。仅通过类图将其列举出来如下。


RequestCreator

2.3 提交请求 into(ImageView)

  • into() 相关说明,关于 target,关于 fetch()

其实 into() 也是有很多重载的,比如into(BitmapTarget),into(RemoteViews, int viewId, int notificationId,Notification notification) 等。也就是说它还可以将图片显示到 Notification 中以及 BitmapTarget 接口。关于 BitmapTarget 这个接口是什么意思呢?这里看一看其在注释中举的一个例子就可以明白了。

 public class ProfileView extends FrameLayout implements BitmapTarget {
    Override 
    public void onBitmapLoaded(Bitmap bitmap, LoadedFrom from) {
        setBackgroundDrawable(new BitmapDrawable(bitmap));
      }
    Override 
    public void onBitmapFailed(Exception e, Drawable errorDrawable) {
     setBackgroundDrawable(errorDrawable);
      }
    Override 
    public void onPrepareLoad(Drawable placeHolderDrawable) {
       setBackgroundDrawable(placeHolderDrawable);
     }
  }

也就是说我们只实现这个接口,然后在 onBitmapLoaded() 的回调中自己选择应该如何显示该图片。

而与 into() 相同级别的还有另一个方法 fetch(),这个方法主要是只下载图片而不展示到 target 上去。这也是一个极为有用的方法,典型的场景就是预加载。

关于 into() 之外的东西就了解这么多,下面将重要放到 into(ImageView) 上来。into(ImageView) 其只是进一步调用了 into(ImageView,Callback),因此直接来看后面这个重载方法。

  • into(ImageView)提交请求

方法有点长,但其实很简单,不要畏难,耐着性子看完,增加了比较详细的注释了。

public void into(@NonNull ImageView target, @Nullable Callback callback) {
    long started = System.nanoTime();
    // 检查是否为主线程,Picasso 要求请求必须从主线程发起。
    checkMain();
    // target 合法性检查
    if (target == null) {
      throw new IllegalArgumentException("Target must not be null.");
    }
    // 如果没有设置 url 或者  resId,则取消当前 ImageView 的 Request,并直接返回
    if (!data.hasImage()) {
      picasso.cancelRequest(target);
      if (setPlaceholder) {
        setPlaceholder(target, getPlaceholderDrawable());
      }
      return;
    }
    // 如果调用了 fit() ,也就是使得图片大小为适应 ImageView 的大小
    if (deferred) {
      if (data.hasSize()) {
        // fit() 与 resize() 不能同时调用,否则会抛出异常
        throw new IllegalStateException("Fit cannot be used with resize.");
      }
      int width = target.getWidth();
      int height = target.getHeight();
      if (width == 0 || height == 0) {
        // ImageView 没有具体的高度或者还没被渲染出来的情况下
        if (setPlaceholder) {
          setPlaceholder(target, getPlaceholderDrawable());
        }
        picasso.defer(target, new DeferredRequestCreator(this, target, callback));
        return;
      }
      data.resize(width, height);
    }
    // 构建 Request
    Request request = createRequest(started);

    if (shouldReadFromMemoryCache(request.memoryPolicy)) {
      // 允许从内存缓存读取则优先从内存缓存读取
      Bitmap bitmap = picasso.quickMemoryCacheCheck(request.key);
      if (bitmap != null) {
        // 缓存中存在有需要的图片,则取消并返回结果
        picasso.cancelRequest(target);
        RequestHandler.Result result = new RequestHandler.Result(bitmap, MEMORY);
        setResult(target, picasso.context, result, noFade, picasso.indicatorsEnabled);
        if (picasso.loggingEnabled) {
          log(OWNER_MAIN, VERB_COMPLETED, request.plainId(), "from " + MEMORY);
        }
        if (callback != null) {
          callback.onSuccess();
        }
        return;
      }
    }
   // 设置 PlasceHolder
    if (setPlaceholder) {
      setPlaceholder(target, getPlaceholderDrawable());
    }
   // 创建 ImageViewAction 并提交
    Action action = new ImageViewAction(picasso, target, request, errorDrawable, errorResId, noFade,
        callback);
    picasso.enqueueAndSubmit(action);
  }

方法里的代码都加了详细的注释,并且总结流程图如下。

Into.jpg

流程图上看也确实步骤比较多,但其实关键步骤就 2 个。其一,先判断是否内存缓存是否已经存储图片了,如果有就用内存缓存的图片。其二,如果没有就构造一个 ImageViewAction 来提交到 Picasso 的请求队列。说到缓存,这里有一个重要的知识点,关于缓存的 key,它是在 Request 被 build 出来后一起被创建的,其在 Request 的 createKey() 方法中。

private String createKey(StringBuilder builder) {
    Request data = this;
    // key 的主要部分
    if (data.stableKey != 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);
    }
   // key 的分隔线
    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);
    }

    //noinspection ForLoopReplaceableByForEach
    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();
  }

代码看起来也有点长,但简单理解下,cache key 主要由主体部分 + 参数部分构成。主体部分一般是指 url 或者 resId,而参数部分主要就是其是否有旋转,resize 等。那这样一来就得到一个结论,就是同一个图片在内存中可能因为要显示的参数不一样会有不同的缓存。这样做的好处自然是提高了显示速度,而坏处自然就是占用的内存会较多。

2.4 入队请求,分发请求Dispatcher,执行请求 BitmapHunter

  • 入队到 Picasso

在 into(ImageView) 中,如果没有命中 Cache,最后一步就是封装 ImageViewAction,并提交给 picasso,由其进一步处理。

Action action = new ImageViewAction(picasso, target, request, errorDrawable, errorResId, noFade,
        callback);
picasso.enqueueAndSubmit(action);

先来看一看 enqueueAndSubmit

void enqueueAndSubmit(Action action) {
    Object target = action.getTarget();
    if (targetToAction.get(target) != action) {
      // This will also check we are on the main thread.
      cancelExistingRequest(target);
      targetToAction.put(target, action);
    }
    submit(action);
  }

Picasso 中用 targetToAction 来保存了 target 与 action 之间的关系。这里首先从里面找 target 是否已经关联了其他的 action,如果已经关联了则先要将其取消。这里的场景就是特别适合 Adapter 了。在 Adapter 中由于 ImageView 是会被重用的,那么就一定会存在一个 target 对应不同的 action 的情况。

  • 取消之前可能存在的 Request

这里先来看一下任务的取消 cancelExistingRequest() 的实现,然后再去看 submit()。

void cancelExistingRequest(Object target) {
    checkMain();
    Action action = targetToAction.remove(target);
    if (action != null) {
      action.cancel();
      // 从 dispatcher 中取消
      dispatcher.dispatchCancel(action);
    }
    if (target instanceof ImageView) {
      ImageView targetImageView = (ImageView) target;
     // 如果有延迟的 request,也要将其取消,这里假设没有吧。
      DeferredRequestCreator deferredRequestCreator =
          targetToDeferredRequestCreator.remove(targetImageView);
      if (deferredRequestCreator != null) {
        deferredRequestCreator.cancel();
      }
    }
  }

所以主要是将 Dispatcher 中的 action 取消。Dispatcher 中主要用了一个 HandlerThread 来管理,并且所有关于请求的提交,取消等都是通过消息来执行的。如下所示。

  static final int REQUEST_SUBMIT = 1;
  static final int REQUEST_CANCEL = 2;
  static final int HUNTER_COMPLETE = 4;
  static final int HUNTER_RETRY = 5;
  static final int HUNTER_DECODE_FAILED = 6;
  static final int NETWORK_STATE_CHANGE = 9;
  static final int AIRPLANE_MODE_CHANGE = 10;
  static final int TAG_PAUSE = 11;
  static final int TAG_RESUME = 12;
  static final int REQUEST_BATCH_RESUME = 13;

消息如何执行,这里就不详细说明了。Dispatcher 中的 action 取消明显就是通过 REQUEST_CANCEL 来执行,而 REQUEST_CANCEL 对应的就是 performCance()。

void performCancel(Action action) {
   // 取消对应的 hunter
    String key = action.request.key;
    BitmapHunter hunter = hunterMap.get(key);
    if (hunter != null) {
      hunter.detach(action);
      if (hunter.cancel()) {
        hunterMap.remove(key);
        if (action.picasso.loggingEnabled) {
          log(OWNER_DISPATCHER, VERB_CANCELED, action.request.logId());
        }
      }
    }
    // 如果在 pause 队列中,则移除
    if (pausedTags.contains(action.getTag())) {
      pausedActions.remove(action.getTarget());
      if (action.picasso.loggingEnabled) {
        log(OWNER_DISPATCHER, VERB_CANCELED, action.request.logId(),
            "because paused request got canceled");
      }
    }
    // 失败队列中包含了,也移除
    Action remove = failedActions.remove(action.getTarget());
    if (remove != null && remove.picasso.loggingEnabled) {
      log(OWNER_DISPATCHER, VERB_CANCELED, remove.request.logId(), "from replaying");
    }
  }

hunter 是什么呢?hunter 的中文意思是猎人或者搜寻者的意思。这里就是负责执行实际请求的类 BitmapHunter,它实现了 Runnable 接口,是 Dispatcher 中 ExecuteService 线程池的实际调度单位。这里先看一下它的 cancel(),后面还会讲它的 submit()。

  boolean cancel() {
    return action == null
        && (actions == null || actions.isEmpty())
        && future != null
        && future.cancel(false);
  }

future 是 Future<?> 类型,它是 ExecuteService.submit() 的返回值,这里我们可以通过其 cancel() 方法来取消 BitmapHunter 的执行。

  • 提交 Request

回到 Picasso 的 enqueueAndSubmit() 中来,下一步就是 submit(action)。submit() 将 action 通过 Dispatcher.dispatchSubmit()提交到 Dispatcher ,Dispatcher 将其转化成消息 REQUEST_SUBMIT 发送到消息队列中,而 REQUEST_SUBMIT 所对应的处理方法是 performSubmit(),因此从 performSubmit() 的代码开始分析 Request 的提交。

void performSubmit(Action action, boolean dismissFailed) {
    ......
    // 已经提交了
    BitmapHunter hunter = hunterMap.get(action.request.key);
    if (hunter != null) {
      hunter.attach(action);
      return;
    }
    // 线程池已经关闭
    if (service.isShutdown()) {
      ......
      return;
    }
   // 创建一个新的 BitmapHunter
    hunter = forRequest(action.picasso, this, cache, stats, action);
   // 将 hunter 提交给线程池
    hunter.future = service.submit(hunter);
   // 已经提交的 hunter 保存在 hunterMap 中
    hunterMap.put(action.request.key, hunter);
    ......
  }

代码中关键的逻辑是创建 hunter 并提交给线程 ExecuteService。这里需要关注一下 forRequest 的实现,其里面有一个关键步骤就是为 Request 选择一下合适的 Handler。

static BitmapHunter forRequest(Picasso picasso, Dispatcher dispatcher,
      PlatformLruCache cache, Stats stats, Action action) {
    Request request = action.request;
    List<RequestHandler> requestHandlers = picasso.getRequestHandlers();
    // 选择合适的 Handler 并创建 BitmapHunter
    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);
  }

假设这里给的是 url 地址,且是以 http / https 开头的,那被选中的就是 NetworkRequestHandler。

  • 执行 Request

当一个 Runnable 被提交到线程池 ExecuteService 中去之后,接下来就是等待其被度到,也就是执行其方法 run()。

@Override public void run() {
    try {
      ......
      result = hunt();
      ......
    } catch (NetworkRequestHandler.ResponseException e) {
     ......
    } finally {
      Thread.currentThread().setName(Utils.THREAD_IDLE_NAME);
    }
  }

精简一下代码,关键就是调用 hunt()。

Result hunt() throws IOException {
    // 1.先判断缓存中是否存在,如果存在则直接返回
    if (shouldReadFromMemoryCache(data.memoryPolicy)) {
      Bitmap bitmap = cache.get(key);
      if (bitmap != null) {
        ......
        return new Result(bitmap, MEMORY);
      }
    }
    ......
    final AtomicReference<Result> resultReference = new AtomicReference<>();
    final AtomicReference<Throwable> exceptionReference = new AtomicReference<>();
    final CountDownLatch latch = new CountDownLatch(1);
    try {
      // 2. 通过 NetworkRequestHandler 加载图片,并等待其返回。
      requestHandler.load(picasso, data, new RequestHandler.Callback() {
        @Override public void onSuccess(@Nullable Result result) {
          resultReference.set(result);
          latch.countDown();
        }

        @Override public void onError(@NonNull Throwable t) {
          exceptionReference.set(t);
          latch.countDown();
        }
      });

      latch.await();
    } catch (InterruptedException ie) {
      ......
    }
    ......
    Bitmap bitmap = result.getBitmap();
    if (bitmap != null) {
      ......
      // 3.执行所有的 Transformation,获取最终的 result
      List<Transformation> transformations = new ArrayList<>(data.transformations.size() + 1);
      if (data.needsMatrixTransform() || result.getExifRotation() != 0) {
        transformations.add(new MatrixTransformation(data));
      }
      transformations.addAll(data.transformations);
      result = applyTransformations(picasso, data, transformations, result);
     .....
    }
    return result;
  }

关键也就是注释中 3 个步骤:
(1) 先判断缓存中是否存在,如果存在则直接返回。这个在 into() 中也有判断。
(2) 通过 NetworkRequestHandler 加载图片,并等待其返回。关于 NetworkRequestHandler 的 load() 方法的实现,其实就是 OkHttp 的请求过程,这里就没有必要展开了。同时这里使用了 CountDownLatch 类来使得当前线程可以等待异步线程执行完成。这里简要了解一下其原理,即当 CountDownLatch.await() 使得当线程进入 awaiting 状态后,只有通过 countDown() 使得其计数变成 0 后才会唤醒当前的线程。具体原理图如下。

CountDownLatch工作原理-图片来自网络

另外还使用了 AtomicReference,类似的还有 AtomicInteger 等,主要作用便是在并发编程中保证数据操作的原子性。这些都是并发编程的内容,这里只作了解即可。

(3) 执行所有的 Transformation,获取最终的 result。这里的 Transformation 就是通过 RequestCreator 的 transform() 方法所添加的。Transformation 是一个接口,一般我们可在其方法 transform() 中进行自定义处理,处理完后再将新的图片返回。

到这里,请求的提交到执行就分完成了。至此,整个从 Picasso 的初始化到构建 Request 设置参数,再到提交 request 以及 执行 request 都分析完毕了。

四、总结

Picasso 总体来说,其源码难度较小,分析起来也比较轻松。其中最重要的两个细节点在于:

  • 每一个 Target 同一时刻只对应一个 Action,当同一个 Target 有新的请求时,当前 Action 会被取消掉。这就解决了 Adapter 中 ImageView 的重用问题,使得其不会因重用而导致图片显示错乱。
  • 图片在缓存中的 key,key 不仅包括了 url 地址/资源 id,同时还包括了其形态参数,比如大小,旋转等。这就使得同一图片可能会存在多个缓存文件。

下面再以 Picasso 的框架图来完成这篇文章的全部总结。

Picasso.jpg

最后,感谢你能读到并读完此文章。受限于作者水平有限,如果存在错误或者疑问都欢迎留言讨论。如果我的分享能够帮助到你,也请记得帮忙点个赞吧,鼓励我继续写下去,谢谢。

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