Picasso(3) - 图片加载流程

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/stupid56862/article/details/81069090

系列文章

Picasso(1) - 使用(踩坑)
Picasso(2) - 自定义配置
Picasso(3) - 图片加载流程
Picasso(4) - Dispatcher
Picasso(5) - Rocket


前言

上一篇文章介绍了 Picasso自定义配置。
通过建造者模式 , 我们可以根据项目需求来自定义 Picasso 。
这篇文章将梳理 Picasso 的整个工作流程 , 从代码的角度来对图片的加载有个宏观的认识。
同时阅读的过程也要带着问题去看源码,多思考为什么框架是这样写的 ? 如果是自己来写 , 会怎么做呢 ?
本篇不会纠结于方法实现的细节。


问题

直接上代码,大家一定会看的一头雾水,那我们先来引入几个问题。

  • 我们使用图片框架的最终目的是什么?
    是为了获取 Bitmap 对象 。
  • 如何描述从不同位置加载图片 ? 例如从网络、磁盘、asset 、相册等。 如何描述图片的加载样式?(裁剪、居中、旋转等)
    因此需要构造一个请求对象 - Request 。
  • 图片加载完后,交给谁来显示 ?
    大多数情况下交给 ImageView , 但有的时候我们需要直接拿到 Bitmap 对象,有的时候需要在通知栏或桌面组件上显示 。
    需要一个对象来描述所需要显示的组件 - Action 。
  • 磁盘 IO 、网络、Bitmap 处理都是耗时操作,需要放到异步线程。 如果仅仅处理这些,我们只需要线程池即可。但是有时需要取消、暂停、错误重试功能,该怎么办呢 ?
    需要一个类来统一管理 ,对事件进行分发 - Dispatcher 。(OkHttp 中也有个 Dispatcher ,作用相同)。
  • 图片从网络或磁盘加载成功后,如何处理图片(Bitmap) 为我们想要的对象呢 ?(比如图片要裁剪、旋转等。)
    需要一个对象来处理图片- BitmapHunter。

Picasso 是如何处理这个过程的呢 ?

类之间的数据传递如下图 :
Picasso -> RequestCreator-> Request-> Action-> Dispatcher->BitmapHunter->Bitmp -> Action
这里写图片描述

接下来分析加载图片的流程 :
首先来看 RequestCreator 。
Picasso 支持链式调用, 当我们调用 Picasso.get().load(uri) 之后,方法的返回值类型变为 RequestCreator , 之后可以调用 placeHolder() 、centerCrop()、into() ,这些方法实际上调用的是 RequestCreator 的方法。

  public RequestCreator load(@Nullable Uri uri) {
    return new RequestCreator(this, uri, 0);
  }

RequestCreator 这个类的作用是什么 ?
从命名上来这个类是来创造 Request 的 。 官方的定义 : 为了创建图片下载请求提供的流式 API 。
目的有两个 : (1) 链式调用。 (2) 创建 Request 。
当调用 into () 方法的时候, Picasso 完成链式调用。

public void into(ImageView target, Callback callback) {
    long started = System.nanoTime();
    //是否在主线程执行
    checkMain();
   // imageView 是否为空
    if (target == null) {
      throw new IllegalArgumentException("Target must not be null.");
    }

// 如果没有为 ImageView 设置 Uri 或 ResourceId ,则显示占位图。
    if (!data.hasImage()) {
// 如果该 ImagView 已经有请求,首先取消掉请求。
      picasso.cancelRequest(target);
      if (setPlaceholder) {
        setPlaceholder(target, getPlaceholderDrawable());
      }
      return;
    }

//deferred (延迟): 是否延迟执行。这个判断什么时候有效呢 ? 当且仅当设置 fit()方法 的时候,延迟加载图片。
//我们知道fit()方法要求图片填满整个 ImageView 。那就意味着必须知道 ImageView 的宽高。
//而 ImageView 的实际宽高要在 View 绘制完成后才能拿到。
//因此 Picasso为 ImageView 添加了 OnPreDrawListener 。 
//当获取到 ImageView 的宽高后, Picasso 重新调用了 resize() 和 into() 方法加载图片 。
    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) {
          setPlaceholder(target, getPlaceholderDrawable());
        }
        picasso.defer(target, new DeferredRequestCreator(this, target, callback));
        return;
      }
      data.resize(width, height);
    }


    Request request = createRequest(started);
   //  requestKey 与 Uri + 图片设置的字符串 。格式为 Uri + 换行符(\n) + 设置 +换行符(\n)  ...
  //   如 : http://i.imgur.com/DvpvklR.png\nrotation:90.0\nresize:3500x3500\ncenterCrop 
    String requestKey = createKey(request);
  // 根据缓存策略,判断是否从内存缓存中获取图片。
    if (shouldReadFromMemoryCache(memoryPolicy)) {
      //从内存缓存中获取 Bitmap 对象
      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;
      }
    }
    // setPlaceholder 默认值为 true。如果不设置 PlaceHolder 话,我们会看到 ImageView  显示白色
    // 如果你在 ListView/ RecyclerView 使用 Picasso 加载图片,请设置PlaceHolder,不然快速滑动,显示一片白色。
    // 如果你把 setPlaceholder 改为 false , 在ListView / RecyclerView 中使用 Picasso 加载图片。
    // 你将首先看到 View 复用前的图片。因此,千万不要改动 setPlaceholder  。
    if (setPlaceholder) {
    // 设置占位图 
      setPlaceholder(target, getPlaceholderDrawable());
    }
  // 构建 Action 对象 
    Action action =
        new ImageViewAction(picasso, target, request, memoryPolicy, networkPolicy, errorResId,
            errorDrawable, requestKey, tag, callback, noFade);

    picasso.enqueueAndSubmit(action);
  }

接着往下看 picasso.enqueueAndSubmit() 方法 :

   void enqueueAndSubmit(Action action) {
        // target 一般情况下是  ImageView 、 RemoteView 或 Target 对象。
        // targetToAction 是  WeakHashMap() 的实例 ,  key 为 ImageView /target , value 为 Action 。
        Object target = action.getTarget();
      //下方代码做了一个判断, Target 已经存在,取消掉对应的请求。
     // 这个判断很重要, 解决了 ListView/RecyclerView 快速滑动, ImageView 复用导致图片错位的问题。 
        if (target != null && targetToAction.get(target) != action) {
            cancelExistingRequest(target);
            targetToAction.put(target, action);
        }
        submit(action);
    }

然后调用了 submit 方法 , 也就是调用了 dispatcher 的 dispatchSubmit() 方法。

  void submit(Action action) {
        dispatcher.dispatchSubmit(action);
    }
...
  // 调用  DispatcherHandler  的 sendMessage() 方法
  void dispatchSubmit(Action action) {
    handler.sendMessage(handler.obtainMessage(REQUEST_SUBMIT, action));
  }
...
      switch (msg.what) {
        case REQUEST_SUBMIT: {
          Action action = (Action) msg.obj;
          // 在  handleMessage 中调用 performSubmit 方法。
          dispatcher.performSubmit(action);
          break;
        }
...

我们发现最终是调用 dispatcher.performSubmit(action) 。看到这里,大家应该有疑问 ! 为什么这里要用 Handler , 直接调用 dispatcher.performSubmit() 方法不行吗 ?
这个 Handler.handleMessage() 其实是在 HandlerThread 线程执行的 , 也就是说这里进行了线程切换 , 切换到 HandlerThread 线程 。HandlerThread 继承自 Thread ,是 android 平台所特有的。同时从命名中也可以看出
在 Dispatcher 中以 perform 命名开头的方法都是在 HanderThread 中执行。

 void performSubmit(Action action, boolean dismissFailed) {
    //  pausedTags 是 LinkedHashSet  实例
    //  首选判断下载请求是否在暂停集合中。如果在暂停集合中,直接返回,不再下载。
    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;
    }
    // BitmapHunter 是 LinkedHashMap 实例,存储的 key 为 url + 图片配置  value  为 BitmapHunter 对象 。
    // 判断当前请求是否正在下载。如果正在下载, 将 action 添加到  BitmapHunter 的请求集合中。
    // 目的为了避免重复下载 。
    BitmapHunter hunter = hunterMap.get(action.getKey());
    if (hunter != null) {
      hunter.attach(action);
      return;
    }
    // 线程池是否关闭, 这个判断大多数情况下用不到。
    // 当你创建了多个 Picasso 实例, 关闭 Picasso 相应资源的时候才用到。
    if (service.isShutdown()) {
      if (action.getPicasso().loggingEnabled) {
        log(OWNER_DISPATCHER, VERB_IGNORED, action.request.logId(), "because shut down");
      }
      return;
    }
    // 根据 Action 对象创建 BitmapHunter 对象, BitmapHunter 实现了 Runnable 接口。
    hunter = forRequest(action.getPicasso(), this, cache, stats, action);
    // 将任务提交到线程池。这里有个赋值操作,拿到 Feture 对象。目的是为了提供取消任务的功能。
    hunter.future = service.submit(hunter);
    hunterMap.put(action.getKey(), hunter);
    if (dismissFailed) {
      failedActions.remove(action.getTarget());
    }
    // 打印日志 
    if (action.getPicasso().loggingEnabled) {
      log(OWNER_DISPATCHER, VERB_ENQUEUED, action.request.logId());
    }
  }

任务提交到线程池以后,最终调用了 Runnable 的 run() 方法。那么我们来看下 BimmapHunter 实现的 run ()方法。

  @Override public void run() {
    try {
      //更新线程名称
      updateThreadName(data);
      //打印日志
      if (picasso.loggingEnabled) {
        log(OWNER_HUNTER, VERB_EXECUTING, getLogIdsForHunter(this));
      }
     // 重点 :  获取 Bitmap 对象。
      result = hunt();
      // bitmap 为空
      if (result == null) {
        dispatcher.dispatchFailed(this);
      } else {
        // bitmap 获取成功
        dispatcher.dispatchComplete(this);
      }
    } catch (NetworkRequestHandler.ResponseException e) {
      if (!NetworkPolicy.isOfflineOnly(e.networkPolicy) || e.code != 504) {
        exception = e;
      }
       //服务器返回状态码,不在 200~300 之间。
      dispatcher.dispatchFailed(this);
    } catch (IOException e) {
      exception = e;
      //  下载重试
      dispatcher.dispatchRetry(this);
    } catch (OutOfMemoryError e) {
      StringWriter writer = new StringWriter();
      stats.createSnapshot().dump(new PrintWriter(writer));
      exception = new RuntimeException(writer.toString(), e);
      // 处理 Bitmap 过程中OOM
      dispatcher.dispatchFailed(this);
    } catch (Exception e) {
      exception = e;
      dispatcher.dispatchFailed(this);
    } finally {
       // 修改线程名称
      Thread.currentThread().setName(Utils.THREAD_IDLE_NAME);
    }
  }

我们可以看到上述的核心逻辑有两个。
1. hunt() 方法获取 Bitmap 对象
2. 使用 Dispatcher 进行事件的分发。

首选来分析下 hunt() 方法 。代码比较多,但是逻辑是非常简单的。

  Bitmap hunt() throws IOException {
    Bitmap bitmap = null;
    // 是否从内存缓存中获取 Bitmap 
    if (shouldReadFromMemoryCache(memoryPolicy)) {
      bitmap = cache.get(key);
      // 从内存缓存中获取到了图片,直接返回 bitmap 。
      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;
    //这里是重点 :  执行网络请求或者从磁盘加载 Bitmap 的地方。
    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 {
            //noinspection ConstantConditions If bitmap is null then source is guranteed non-null.
            source.close();
          } catch (IOException ignored) {
          }
        }
      }
    }

    if (bitmap != null) {
      if (picasso.loggingEnabled) {
        log(OWNER_HUNTER, VERB_DECODED, data.logId());
      }
      stats.dispatchBitmapDecoded(bitmap);
    // Bitmap 是否需要进行额外处理。
      if (data.needsTransformation() || exifOrientation != 0) {
    // 这里比较有意思, hunt() 方法只在 run() 方法中执行。
    // 为什么这里还是使用了 synchronized  呢? 翻译下 DECODE_LOCK 的注释,
    // 全局锁为了保证同一时间只有一个后台线程在 decode 图片, 避免内存抖动和潜在的 OOM 。
    // 有趣的是作者声称 ,这个写法是可耻的窃取自 Volley ! 哈哈 !
  private static final Object DECODE_LOCK = new Object();
        synchronized (DECODE_LOCK) {
          // Bitmap 是否需要重新调整大小、旋转。
          //如果你设置了resize()、rotate() 会执行下面方法。
          if (data.needsMatrixTransform() || exifOrientation != 0) {
            bitmap = transformResult(data, bitmap, exifOrientation);
            if (picasso.loggingEnabled) {
              log(OWNER_HUNTER, VERB_TRANSFORMED, data.logId());
            }
          }
        //  Bitmap 是否需要进行额外的转换 ,这个什么时候用呢 ? 
        //  我们知道 Picasso 是不支持圆角或者圆图的,我们日常开发又经常有这样的需求。
        //  因此我们可以自定义 Transformation  。
        //  而自定义的 Transformation 就是在 这个时候调用的。
          if (data.hasCustomTransformations()) {
            bitmap = applyCustomTransformations(data.transformations, bitmap);
            if (picasso.loggingEnabled) {
              log(OWNER_HUNTER, VERB_TRANSFORMED, data.logId(), "from custom transformations");
            }
          }
        }
        if (bitmap != null) {
          stats.dispatchBitmapTransformed(bitmap);
        }
      }
    }

    return bitmap;
  }

接下来我们看 Dispatcher 如何分发 Bitamp 。

  void performComplete(BitmapHunter hunter) {
    // 是否存储 Bitmap 到内存当中。
    if (shouldWriteToMemoryCache(hunter.getMemoryPolicy())) {
      cache.set(hunter.getKey(), hunter.getResult());
    }
    //从 hunterMap (你可以理解为下载队列,当然这个不是队列哦 !) 中移除。
    hunterMap.remove(hunter.getKey());
    // 批处理 hunter ,你可能对这个方法有疑惑,接着往下看。
    batch(hunter);
    if (hunter.getPicasso().loggingEnabled) {
      log(OWNER_DISPATCHER, VERB_BATCHED, getLogIdsForHunter(hunter), "for completion");
    }
  }

我们注意到 performComplete() 最后又调用了 batch() 方法。 batch()方法是来做什么呢 ?

  private void batch(BitmapHunter hunter) {
    // 请求是否已经取消
    if (hunter.isCancelled()) {
      return;
    }
    if (hunter.result != null) {
      hunter.result.prepareToDraw();
    }
    batch.add(hunter);
    if (!handler.hasMessages(HUNTER_DELAY_NEXT_BATCH)) {
       //延迟批处理
      handler.sendEmptyMessageDelayed(HUNTER_DELAY_NEXT_BATCH, BATCH_DELAY);
    }
  }

Picasso 并不是将每个请求的回调,立即切换到主线程。而是每 200 ms 处理一次。
为什么 Picasso 这么写, 我没有找到答案 !
但是从性能方面分析,应该是为了避免同一时间大量回调,阻塞主线程 ,因此做了延迟批处理。
这样写的坏处是什么呢 ?
如果一张图片磁盘缓存中加载,那么意味着要经过 200 ms 才能显示 , 这显然不太合理,因为磁盘缓存加载也是非常快的 , Github 也有人遇到了同样问题。
怎么解决呢 ?
Github 有人解决的方法是不使用 Gradle 依赖 Picasso , 把 Picasso 下载到本地,然后手动改为 50 ms 。
如果不是老板一定要处理这个问题 , 个人建议还是不要这么改 。 我建议你提前调用 fetch() 方法 ,预加载图片到内存当中。

延迟批处理 200 ms 后, 最终调用了 mainThreadHandler 的 sendMessage() 方法 。经过这么长时间的分析,终于要在主线程显示图片了。

  void performBatchComplete() {
    // 200 ms 内需要分发的 BitmapHunter
    List<BitmapHunter> copy = new ArrayList<>(batch);
    batch.clear();
    //切换到主线程显示图片
    mainThreadHandler.sendMessage(mainThreadHandler.obtainMessage(HUNTER_BATCH_COMPLETE, copy));
    logBatch(copy);
  }

Picasso 主线程的 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
         // 遍历 200ms 内需要处理的 BitmapHunter
          for (int i = 0, n = batch.size(); i < n; i++) {
            BitmapHunter hunter = batch.get(i);
            hunter.picasso.complete(hunter);
          }
          break;

最终调用 Picasso 的 complete () 方法 。

 void complete(BitmapHunter hunter) {
     // 初始请求
      Action single = hunter.getAction();
    // 重复请求
      List<Action> joined = hunter.getActions();
     //有重复请求
      boolean hasMultiple = joined != null && !joined.isEmpty();
    //是否应该分发
      boolean shouldDeliver = single != null || hasMultiple;
    // 如果请求都被取消了,直接 return 。
      if (!shouldDeliver) {
      return;
    }

    Uri uri = hunter.getData().uri;
    Exception exception = hunter.getException();
    Bitmap result = hunter.getResult();
    LoadedFrom from = hunter.getLoadedFrom();
    //分发单个请求
    if (single != null) {
      deliverAction(result, from, single, exception);
    }
    // 遍历分发重复请求
    if (hasMultiple) {
      //noinspection ForLoopReplaceableByForEach
      for (int i = 0, n = joined.size(); i < n; i++) {
        Action join = joined.get(i);
        deliverAction(result, from, join, exception);
      }
    }
    // 全局回调监听,加载失败。
    if (listener != null && exception != null) {
      listener.onImageLoadFailed(this, uri, exception);
    }
  }

接下来就不贴代码了 , 调用 Acton 的 complete 方法 ,将 Bitmap封装到 PicassoDrawable 交给Picasso 显示图片。

总结

Picasso 加载图片的流程到此结束。在看源码的过程当中, 我们看到对象不断的转换,不免有疑惑。
面向对象编程,还是要搞明白该对象是为了解决什么而存在的 !

猜你喜欢

转载自blog.csdn.net/stupid56862/article/details/81069090