Android开发Android开发经验谈Android开发

Picasso源码完全解析——学习其优秀思想

2019-09-17  本文已影响0人  丶蓝天白云梦

前言

图片加载框架Picasso相信大家都已经用过很多次了,对它们的使用方法也早就熟稔于心了,那么本文就Picasso的源码进行剖析,学习设计者的优秀的代码设计理念和方法。

几个重要的类

在源码解析开始之前,笔者认为有必要对Picasso的几个重要的类进行简单梳理,以便于后面遇到这些组件的时候可以马上知道它的作用是什么。
1、OkHttp3Downloader
Picasso借助该类来下载图片,并把图片缓存在磁盘空间上。实际上,它用的是OkHttp3这个网络通信库来完成下载任务。我们看看它的构造方法:

  public OkHttp3Downloader(final Context context) {
    this(Utils.createDefaultCacheDir(context));
  }

  public OkHttp3Downloader(final File cacheDir) {
    this(cacheDir, Utils.calculateDiskCacheSize(cacheDir));
  }

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

通过Utils.createDefaultCacheDir(context)方法来创建缓存文件夹,通过Utils.calculateDiskCacheSize(cacheDir)来确定磁盘缓存空间的大小。由此我们可以知道,Picasso利用了OkHttp3的下载机制来缓存图片,并且磁盘缓存的大小也是可以配置的,默认实现是可用空间的2%且不少于5MB.

2、LruCache
如果说OkHttp3Downloader实现了磁盘缓存,那么LruCache则是实现了内存缓存。内存缓存的意义在于避免图片过多地堆积在内存中而导致OOM。这里使用的是Lru算法(Least recently used,最近最少使用算法),该算法可以使得经常使用的图片驻留于内存中,避免了反复从磁盘加载图片而导致内存抖动的问题。

3、PicassoExecutorService
这个实际上是一个线程池,它的主要作用就在于把下载任务分配到各个子线程中去执行。

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

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

从构造方法可以看出,该线程池的默认实现是3个核心线程且最大线程数不超过3条。也就是说,默认情况下Picasso在下载图片的时候,最大的同时下载数量是3。但实际上,核心线程和最大线程数是会随着设备的网络状态而改变的,比如WIFI状态下是4条核心线程,而4G状态下是3条核心线程,以此类推。

4、Dispatcher
顾名思义,该类是一个调度器,负责分发、调度和处理Picasso产生的各种事件。在这个调度器内,需要关注的分别是dispatcherThreadhandler这两个成员变量。可以先看一下Dispatcher的构造方法:

Dispatcher(Context context, ExecutorService service, Handler mainThreadHandler,
      Downloader downloader, Cache cache, Stats stats) {
    this.dispatcherThread = new DispatcherThread();
    this.dispatcherThread.start();
    this.handler = new DispatcherHandler(dispatcherThread.getLooper(), this);
    //省略...
  }

其中,DispatcherThread继承自Thread,是一条子线程;而DispatcherHandler则继承自Handler,熟悉Handler的同学,肯定知道这是用于处理线程间通信的常见方法。由此可知,Dispatcher这个调度器的主要工作都是在DispatcherThread这条线程内完成,而线程切换的任务则是DispatcherHandler来完成。

5、Request
Request封装了有关一次图片请求的所有信息,比如图片的url、图片的变换策略等,这些都是不可更改的信息。举个例子来说,Picasso.get().load(url).centerCrop().rotate(15).into(imageview);上面的调用链,Request会封装centerCrop、rotate等信息。与Request相关的是RequestCreator,它可以看作是一个建造器,配置了图片请求的信息。

6、RequestHandler
上面说到Picasso将图片请求封装成了一个Request,而处理Request的组件则是RequestHandler,因为图片的请求是多种多样的,有的是提供了一个URL从网络获取图片;有的则是提供了一个resourceId,从本地加载图片,不同的请求会有不同的加载方式。因此Picasso提供了多个RequestHandler来应对不同的情况,用户也可以自定义RequestHandler来实现自己的需求,只需要重写canHandleRequest方法和load方法,如下所示:

public abstract class RequestHandler {

   public abstract boolean canHandleRequest(Request data);

   @Nullable public abstract Result load(Request request, int networkPolicy) throws IOException;
}

由此看出,Picasso在处理不同的图片请求的时候,将不同请求的实现方式放在了所对应的handler去实现,这样便实现了图片请求和处理请求的解耦合,这样用户自行拓展以适应不同场景下的图片加载需求。

加载图片流程的详细分析

1、Picasso.get()
该方法的调用是一切流程的起点,通过该方法我们可以获取一个Picasso的实例。在Picasso以前的版本,我们是通过Picasso.with(context)的方式来获取实例的,这限制了我们只能在有上下文context的环境下使用Picasso。我们来看看这个方法的实现以及探究下为什么新版本的Picasso不用context这个参数了。

  //代码清单:Picasso#get()
  public static Picasso get() {
    if (singleton == null) {  //第一次判空
      synchronized (Picasso.class) {
        if (singleton == null) {  //上锁后的第二次判空
          if (PicassoProvider.context == null) {  //确保有context
            throw new IllegalStateException("context == null");
          }
          singleton = new Builder(PicassoProvider.context).build();
        }
      }
    }
    return singleton;
  }

从上面的代码,我们可以看出Picasso使用了DCL(double check lock)形式的单例模式,确保全局只有一个Picasso对象。同时我们注意到context对象是由PicassoProvider.context来提供的,显然PicassoProvider是一个ContentProvider,是Android的四大组件之一,通过它也是可以获取到我们应用的上下文环境的。Picasso通过这样形式的改动,使得Picasso可以适应更多不同的环境,比如在没有context的条件下仅仅利用Picasso进行图片的预下载。

1-1、Picasso实例的构造
Picasso实例的构造是通过构造器模式来进行创建的,Picasso.get()方法获取的是默认配置的Picasso实例,我们也可以通过Picasso.Builder来灵活配置适合我们需求的Picasso实例。我们来看看Picasso.Builder.build()方法,看它是怎样创建一个实例的:

//代码清单1-1:Picasso.Builder#build()
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);

      return new Picasso(context, dispatcher, cache, listener, transformer, requestHandlers, stats,
          defaultBitmapConfig, indicatorsEnabled, loggingEnabled);
    }

显然,这里实例化了上面提到的几个重要组件,如downloader、cache、service、dispatcher等,它们都在Picasso的工作过程中起着重要作用。在Picasso构造方法的内部,还初始化了一系列的RequestHandler,例如ResourceRequestHandlerNetworkRequestHandler等,这些Handler根据不同形式的图片请求来执行相应的逻辑。

2、Picasso#load(String)
通过Picasso.get()获取到Picasso对象后,我们接下来就会通过load的一系列重载方法来确定图片的来源,可以是uri、file或者string等。我们选取其中一个load方法来看看源码:

  //代码清单2:Picasso#load(string)
  public RequestCreator load(@Nullable Uri uri) {
    return new RequestCreator(this, uri, 0);
  }

  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);
  }

这里创建了RequestCreator,前面提到过Picasso将图片的来源url、图片的placeholder、图片的变换操作等一系列信息封装到了Request内,而RequestCreator就相当于是Request的一个构造器。

2-1、RequestCreator的相关方法
以上获取到了RequestCreator实例,通常我们接下来的做法是对这一次的图片请求配置各种功能,举个例子来说:Picasso.get().load(url).placeholder(resId)是我们常见的一种调用,这样给图片设置了占位图。实际上,RequestCreator的一系列方法都是将用户的操作暂存在RequestCreator的成员变量内部,等到用户调用into(imageview)方法时,再把所有参数填充到Request

3、PicassoCreator#into(imageview)
在确定图片的相关操作后,我们最后会调用into方法,也即是:Picasso.get().load(url).placeholder(placeholderResid).into(target);前面的一切工作都是准备工作(获取Picasso实例,设置图片来源以及设置图片的操作),接下来就是Picasso将这些操作完成并显示在target上的过程,我们来详细分析这个过程。先来看into方法的源码:

//代码清单3:PicassoCreator#into()
public void into(ImageView target, Callback callback) {
  long started = System.nanoTime();
  checkMain();  //确保在主线程调用该方法

  if (target == null) {
    throw new IllegalArgumentException("Target must not be null.");
  }

  //如果图片的uri为null,则取消请求,然后设置占位图
  if (!data.hasImage()) {
    picasso.cancelRequest(target);
    if (setPlaceholder) {
      setPlaceholder(target, getPlaceholderDrawable());
    }
    return;
  }

  //如果调用了RequestCreator#fit()方法,那么deferred会被设置为true
  //这是因为fit()方法需要适应ImageView的大小,必须等到ImageView的layout过程完毕才能fit()
  //因此,这里实际上是推迟了图片的加载过程,即Picasso#defer()
  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); //根据RequestCreator的参数来创建一个Request
  String requestKey = createKey(request);   //创建与该Request对应的一个Key

  //如果内存缓存可用,那么直接从内存缓存获取Request对应的Bitmap,并取消请求
  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 action =
      new ImageViewAction(picasso, target, request, memoryPolicy, networkPolicy, errorResId,
          errorDrawable, requestKey, tag, callback, noFade);

  picasso.enqueueAndSubmit(action); //排队,等待调度
}

简单来说,into方法所做的工作主要是生成一个Request,并且封装成一个Action,最后通过picasso.enqueueAndSubmit(action)把该动作排队等待执行。最后会调用到Dispatcher#submit方法。

4、Dispatcher#submit(Action)

  void dispatchSubmit(Action action) {
    handler.sendMessage(handler.obtainMessage(REQUEST_SUBMIT, action));
  }

显然,这里通过DispatcherHandler发送了一个submit消息,那么根据前面所述,这个消息将会被投递到DispatcherThread线程。根据Handler的相关知识,该消息会在handler的handleMessage方法得到处理,即:

//Dispatcher.DispatcherHandler#handleMessage
private static class DispatcherHandler extends Handler {
  
  //...

  @Override 
  public void handleMessage(final Message msg) {
    switch (msg.what) {
      case REQUEST_SUBMIT: {
        Action action = (Action) msg.obj;
        dispatcher.performSubmit(action);
        break;
      }

      //...
        
  }
}

4-1、Dispatcher#performSubmit(action)
此时,线程已经被切换到了DispatcherThread,接着调用了performSubmit方法。因此我们可以知道,Dispatcher起到了排队、分发请求、处理请求结果的作用。但实际上请求的处理过程,比如从url上下载图片等都是放到线程池去实现的。我们先来看performSubmit方法如下:

//代码清单4-1:Dispatcher#performSubmit(action)
void performSubmit(Action action, boolean dismissFailed) {
  //省略...

  hunter = forRequest(action.getPicasso(), this, cache, stats, action);
  hunter.future = service.submit(hunter);
  hunterMap.put(action.getKey(), hunter);
  
}

//BitmapHunter#forRequest
static BitmapHunter forRequest(Picasso picasso, Dispatcher dispatcher, Cache cache, Stats stats,
    Action action) {
  Request request = action.getRequest();
  List<RequestHandler> requestHandlers = picasso.getRequestHandlers();

  //找到一个可以处理该Request的RequestHandler
  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);
}

从上面的代码可以看出,先是生成了一个BitmapHunter,这个类的作用顾名思义,就是获取Bitmap,它是一个Runnable,它内部根据Request的不同类型来确定不同的获取方法(实际上是RequestHandler在起作用)。

紧接着,调用了service.submit(hunter)方法,这里的service实际上就是PicassoExecutorService线程池,将BitmapHunter这个runnable投递进了线程池,如果线程池有空闲的线程那么就会执行这个runnable,否则阻塞等待。最终,如果runnable获得执行的机会,它的run()方法会被调用。

5、BitmapHunter#run()
那么代码运行到这里,线程又切换到了PicassoExecutorService内的某一条线程。也就是说,加载图片的工作是在这些子线程内执行的。我们来看看run()

//代码清单5:BitmapHunter#run
@Override public void run() {
  try {
    updateThreadName(data);

    //获取result
    result = hunt();

    if (result == null) {
      //如果加载失败,则分发失败事件
      dispatcher.dispatchFailed(this);
    } else {
      //如果加载成功,则分发成功事件
      dispatcher.dispatchComplete(this);
    }
  } 

  //省略异常状态的处理...
}

代码的逻辑很简单,hunt()是加载细节,如果加载失败就由Dispatcher分发失败事件,反之分发成功事件。

5-1、BitmapHunter#hunt()
接着,我们来探索一下hunt方法的实现方式,首先查看源码如下:

//代码清单5-1:BitmapHunter#hunt()
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解析图片请求
  RequestHandler.Result result = requestHandler.load(data, networkPolicy);  
  if (result != null) {
    loadedFrom = result.getLoadedFrom();
    exifOrientation = result.getExifOrientation();
    bitmap = result.getBitmap();

    // If there was no Bitmap then we need to decode it from the stream.
    // 如果bitmap为空,那么从stream读取字节流解析成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) {
    //省略部分代码...
    
    //对Bitmap进行转换操作,Transformation是一个自定义的转换操作
    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);
          
        }
      }
      
    }
  }

  return bitmap;
}

总的流程可以概括为:先从内存缓存获取,如果没有则交给对应的RequestHandler来进行图片的加载,不同的请求对应了不同的加载方式,这里暂不深究。在获得一个Bitmap对象后,便对这个位图进行了一系列的转换操作,比如图片自身的宽高和目标宽高不一致时要进行缩放,或者用户设置了centerCrop的标志位,那么图片就要保持宽高比列居中显示。这些操作是利用MatrixBitmap.createBitmap来完成的。同时Picasso允许用户自定义转换器Transformation来对图片进行个性化的修改,例如添加水印。具体的加载过程和转换过程,本文暂不进行深究。

那么,当hunt()方法执行完毕之后,会返回bitmap对象,我们顺着代码清单5往下走,下面就调用了dispatcher.dispatchComplete(this)方法,与上面出现过的dispatcher.dispatchSubmit一样,把这个事件交给了Dispatcher去分发和处理。我们自然而然就会想到,这里会进行线程的切换,从PicassoExecutorService线程池的某一条线程切换到了DispatcherThread。最终,在DispatcherHandler#handleMessage处理这个完成事件。

6、Dispatcher#performComplete(BitmapHunter)
代码会运行到这个方法,我们直接来看源码:

  void performComplete(BitmapHunter hunter) {
    //如果条件允许,那么把该bitmap缓存到内存
    if (shouldWriteToMemoryCache(hunter.getMemoryPolicy())) {
      cache.set(hunter.getKey(), hunter.getResult());
    }
    hunterMap.remove(hunter.getKey());  //从map移除这个已完成的hunter
    batch(hunter);  //进行批处理
    
  }

  private void batch(BitmapHunter hunter) {
    if (hunter.isCancelled()) {
      return;
    }
    if (hunter.result != null) {
      hunter.result.prepareToDraw();
    }
    batch.add(hunter);  //添加到batch列表内
    if (!handler.hasMessages(HUNTER_DELAY_NEXT_BATCH)) {
      handler.sendEmptyMessageDelayed(HUNTER_DELAY_NEXT_BATCH, BATCH_DELAY);
    }
  }

从上面源码可以看出,该hunter会被添加到一个batch的列表内,同时延迟发送一个HUNTER_DELAY_NEXT_BATCH消息,这意味着,第一个hunter完成后,会被添加到batch列表,然后延迟200ms发送batch消息。此时如果有别的hunter到达,也会被一一添加到batch列表,直到一开始的batch消息得到处理。这里利用了批处理的思想,在200ms的等待时间内,会暂存多个hunter请求,时间到了之后便切换到主线程进行UI的显示,这样就不用频繁地进行线程切换,可以提升UI显示的流畅性。

7、Dispatcher#performBatchComplete
最后,在DispatcherThread会处理HUNTER_DELAY_NEXT_BATCH消息,我们来看该代码:

  void performBatchComplete() {
    List<BitmapHunter> copy = new ArrayList<>(batch);
    batch.clear();
    mainThreadHandler.sendMessage(mainThreadHandler.obtainMessage(HUNTER_BATCH_COMPLETE, copy));
    logBatch(copy);
  }

这里的mainThreadHandler是持有主线程Looper的handler,它发送的消息都会在主线程得到处理。实际上,它是在Dispatcher实例化的时候由Picasso传递进来的,那么它的源码可以在Picasso类中找到:

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
          for (int i = 0, n = batch.size(); i < n; i++) {
            BitmapHunter hunter = batch.get(i);
            hunter.picasso.complete(hunter);
          }
          break;
        }
      }
  };

此时,线程环境已经由DispatcherThread切换到了UI Thread.在主线程内,逐个遍历batch列表,对里面的每一个hunter进行最后的收尾工作,把bitmap填充到imageview上。

8-1、Picasso#complete
下面就是收尾工作,我们直接看源码:

//代码清单8-1:Picasso#complete
void complete(BitmapHunter hunter) {
    //获取hunter所含有的Action
    Action single = hunter.getAction();           
    //hunter可能对应多个Action,对同一图片的同一操作的多个请求会保存在一个hunter内
    //避免不必要的重复加载步骤。
    List<Action> joined = hunter.getActions();    

    boolean hasMultiple = joined != null && !joined.isEmpty();
    boolean shouldDeliver = single != null || hasMultiple;

    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);
    }
  }

这里对hunter内的所有Action进行遍历操作,每一个Action都有自己要设置的imageview对象。对每一个Action,进一步调用了deliverAction方法。

8-2、Picasso#deliverAction

  //代码清单8-2:Picasso#deliverAction
  private void deliverAction(Bitmap result, LoadedFrom from, Action action, Exception e) {
    //省略...

    if (result != null) {
      action.complete(result, from);
      
    } else {
      action.error(e);
      
  }

这里的Action是ImageViewAction实例,因此最后会调用ImageViewAction#complete()方法。实际上,用户完全可以继承Action来实现不同的需求。默认实现的ImageViewAction是为了把图片填充到ImageView.

8-3、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!\n%s", this));
    }

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

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

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

代码出现了新的一个类PicassoDrawable,它继承自BitmapDrawable,那么到现在就很清楚了,Picasso是以Drawable的形式把图片设置进ImageView的,通过这样的形式,Picasso可以最后在图片上添加一些信息。比如,开启了Debug模式后,所加载的图片的右下角会有不同颜色的角标来表示图片的来源(网络、内存或磁盘),这个功能的实现就是借助于BitmapDrawable.draw方法在画布上添加额外的信息。

8-4、PicassoDrawable#setBitmap

  static void setBitmap(ImageView target, Context context, Bitmap bitmap,
      Picasso.LoadedFrom loadedFrom, boolean noFade, boolean debugging) {
    Drawable placeholder = target.getDrawable();
    if (placeholder instanceof Animatable) {
      ((Animatable) placeholder).stop();
    }
    PicassoDrawable drawable =
        new PicassoDrawable(context, bitmap, placeholder, loadedFrom, noFade, debugging);
    target.setImageDrawable(drawable);  //最后的最后,把drawable设置进imageview上
  }

上面生成了PicassoDrawable,这里不但传递了bitmap,也转递了loadedFrom信息,用于debug模式下判断图片来源。最后,调用了ImageView的方法设置了图片。

至此,Picasso加载图片的一次完整流程便完成了。

小结

纵观整个Picasso的加载图片的流程,其中涉及了多次的线程切换以及多个组件的协同工作,为了方便读者的理解,笔者绘制了整体的流程图并标注出了线程切换的时机,读者可以结合流程图来梳理一下上面的源码解析。

思路借鉴

经过上面的源码学习,我们可以发现Picasso有很多优秀的设计思想值得我们去学习。
1、单例模式
对于全局只需要一个实例的库来说,应该设计为单例模式。这种库往往承担了繁重的工作,并且开销不小,如果系统内存在多个实例,那么就会造成额外的开销。比如Picasso的线程池默认有多条子线程,如果Picasso不是单例模式的,那么就会有频繁创建线程池、回收线程池的操作,这完全是没有必要的。只要设计为单例模式,全局提供统一的入口方法,这样不但节省了内存的消耗,同时也有利于马上定位到问题所在。

2、模块化设计
当一个系统实现的功能比较复杂的时候,我们可以利用模块化的思想抽离出一个个子模块,这些子模块可以独立成一个单独的子系统,也可以相互配合以实现整个系统的功能。Picasso也是这样设计的,它的子模块包括downloader、cache和stats等子模块,每一个子模块负责不同的功能,相互之间没有联系,极大程度上消除了类之间的耦合关系,修改一个模块也不会影响到另一模块的正常工作。

因此,我们在设计一个库或者系统的时候,可以考虑将库的功能分成不同的子模块,各个模块各司其职,尽量降低代码的耦合度。

3、多态和可拓展性
多态是面向对象编程的三大特性之一,多态简单地说就是父类类型的变量可以引用子类的实例,这样的好处在于子类可以灵活多变适应不同的场景的同时也遵循了一定规则的约束。Picasso的Downloader就是一个例子,它是一个接口,默认的实现是OkHttp3Downloader,它允许用户自己实现Downloader接口,并在Picasso的构造器中添加这样一个自定义的下载器。这是模块化和多态的结合,下载器是一个可以替换的模块,用户只需要遵循某些约定即可。除此之外,还有RequestHandler,它作为一个抽象类,仅定义了需要实现的几个方法,具体不同图像的加载方式由不同的RequestHandler去实现。

在代码中运用了多态的思想后,这意味着我们的代码是可拓展的,可以适应未来可能出现的不同需求。我们可以将库的某一模块定义为抽象类或者接口,具体的实现可以根据具体需求而定。

4、批处理思想
批处理思想是指等请求聚集到一定数量或者经过一段时间后再一起处理的思想。如果请求涉及到了跨线程处理甚至跨进程处理,并且请求的数量在短时间内是密集的,那么如果对于每一个单一请求都进行一次线程间/进程间通信,显然这会频繁地切换线程造成很大的开销。如果利用了批处理思想,那么每隔一段时间处理一批请求,只需要切换一次线程。Picasso在处理完BitmapHunter后就是利用了批处理思想,等待200ms后再切换到UI线程进行UI的显示。

在实时性要求不高的场景下(延迟200ms后展示图片,完全可以接受),我们可以善于利用批处理思想,降低切换线程/进程带来的性能开销。

上一篇下一篇

猜你喜欢

热点阅读