Android第三方图片加载库Picasso源码解析
qoes4901
8年前
<p>Picasso作为一个非常棒的android第三方图片加载库,在Github上获得了高达7000多的star。</p> <pre> <code class="language-java">Picasso.with(context).load(“image url”).into(imageView);</code></pre> <p>这么简单的一行代码就完成了在Android中加载图片的功能,这其中,Picasso还帮我们自动完成了一些android中处理图片的问题:例如在adapter中ImageView的回收和取消下载,使用最小的内存来完成图片的过渡,自动的内存和磁盘缓存等,的确是非常简单的。</p> <p>但作为开发人员,简单的会用是不够的,毕竟我们在实际项目中可能会遇到各种奇葩问题,这个时候就有必要深入到内部一探究竟了。下面就从源码的角度来讲解一下Picasso的工作原理。我们可以直接在github上 clone下整个Picasso项目或者如果你使用的是studio的gradle依赖管理,那么也可以直接在项目的Extension Library中查看Picasso的源码。</p> <p>我们先看下上面一行代码中用到的三个函数,首先是with函数,它的实现是这个样子的:</p> <pre> <code class="language-java">public static Picasso with(Content context) { if (singleton == null) { synchronized (Picasso.class) { if (singleton == null) { singleton = new Builder().build(); } } } return singleton; }</code></pre> <p>可以看到Picasso使用的是单例模式,并且使用Builder模式创建了一个Picasso的实例,具体如何创建的我们这里先不管,继续往下看。<br> 有了这个的实例之后,直接调用了它的load函数,Picasso重载了几个不同参数的load函数,用以从不同的地点来加载图片:</p> <pre> <code class="language-java">public RequestCreator load(Uri uri) { ... } public RequestCreator load(String path) { ... } public RequestCreator load(int resourceId) { ... }</code></pre> <p>通过load函数,我们最终得到了一个RequestCreator对象,通过这个对象我们就可以定制一些对图片的特殊处理了,这里我们同样不去深究这个RequestCreator内部是如果对图片进行处理的。<br> 最后我们调用了into函数,将加载到的图片赋给一个ImageView控件。我们前面的操作可以说都是为into函数做准备,实际工作的开始,就是在into里面完成的。我们跟进<code>into</code>方法看一下,当然了,我们现在还是主要关注流程,不必过于深究细节,这个方法稍微有点长,但是为了方便起见,我们把它的代码贴在这里:</p> <pre> <code class="language-java">public void into(Target target) { long started = System.nanoTime(); checkMain(); if (target == null) { throw new IllegalArgumentException("Target must not be null."); } if (deferred) { throw new IllegalStateException("Fit cannot be used with a Target."); } if (!data.hasImage()) { picasso.cancelRequest(target); target.onPrepareLoad(setPlaceholder ? getPlaceholderDrawable() : null); return; } Request request = createRequest(started); String requestKey = createKey(request); if (shouldReadFromMemoryCache(memoryPolicy)) { Bitmap bitmap = picasso.quickMemoryCacheCheck(requestKey); if (bitmap != null) { picasso.cancelRequest(target); target.onBitmapLoaded(bitmap, MEMORY); return; } } target.onPrepareLoad(setPlaceholder ? getPlaceholderDrawable() : null); Action action = new TargetAction(picasso, target, request, memoryPolicy, networkPolicy, errorDrawable, requestKey, tag, errorResId); picasso.enqueueAndSubmit(action); }</code></pre> <p>这个方法稍微有些长,但是逻辑还是比较清晰的,我们总结一下:</p> <ol> <li>into会检查当前是否是在主线程上执行。</li> <li>如果我们没有提供一个图片资源并且有设置placeholder,那么就会把我们设置的placeholder显示出来,并中断执行。</li> <li>defered属性我们一般情况下不需要关注,只有当我们调用了RequestCreator的fit方法时defered才为true,但我们几乎不会这样做。</li> <li>接下来就是创建了一个Request对象,我们在前面做得一些设置都会被封装到这个Request对象里面。</li> <li>检查我们要显示的图片是否可以直接在缓存中获取,如果有就直接显示出来好了。</li> <li>缓存没命中,那就只能费点事把源图片down下来了。这个过程是异步的,并且通过一个Action来完成请求前后的衔接工作。</li> </ol> <p>至此,Picasso在主线程中的工作就结束了。通过上面的分析,我们看到Picasso的思想还是很清晰的:首先通过Picasso创建了一个RequestCreator对象,通过这个对象我们可以针对不同的场景来设置一些属性,之后创建出Request对象,最后通过Action来确定异步请求并对请求结果做处理。</p> <p>接下来,我们就深入到内部看下具体的执行。首先看下Picasso构造函数的声明:</p> <pre> <code class="language-java">Picasso(Context context, Dispatcher dispatcher, Cache cache, Listener listener, RequestTransformer requestTransformer, List<RequestHandler> extraRequestHandlers, Stats stats, Bitmap.Config defaultBitmapConfig, boolean indicatorsEnabled, boolean loggingEnabled) { ... }</code></pre> <p>这么多的参数,也基本上涵盖了Picasso内部所有重要的组件。同时我们也看到这个Picasso是不允许外部进行实例化的。在这里,Picasso使用了单例和建造者模式来完成Picasso的实例化,在Builder中实例化了Picasso所需要的这些组件,这里使用Builder模式的另一个好处就是可以让我们根据自己的需求来个性化定制组件。那这些组件到底都是做什么的呢,下面我们就一一探个究竟。</p> <p>既然Picasso是通过Builder来实例化的,那我们就从build函数入手,看一下都做了哪些工作。同样,为了阅读方便,我们把build的代码贴在下面:</p> <pre> <code class="language-java">public Picasso build() { Context context = this.context; if (downloader == null) { downloader = Utils.createDefaultDownloader(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); }</code></pre> <p>build结束后,就为Picasso创建了如下几个对象:<br> Downloader、Cache、ExecutorService、RequestTransformer、Stats和Dispatcher,从名称上我们基本上已经能够猜测出来每个组件都是干嘛的。这些对象传递给Picasso的构造函数后,在构造函数的内部又创建了了不同的RequestHandler,用以对不用的图片资源进行加载。说了这么多是不是有点晕,别急,下面我们会对每个组件做介绍,一点点将Picasso的执行过程串起来。</p> <p>我们先来看下Downloader:它是一个接口,规定了一些通用的方法,这也就意味着,我们可以提供自己的下载器,只要实现这个接口即可,Picasso的扩展能力还是很不错的。这里Picasso默认使用OkHttpClient来作为下载器,同样也是squareup公司开源的一个网络库。</p> <p>Cache:Picasso的缓存,这里实例化的是LruCache,其内部使用的是LinkedHashMap</p> <p>ExecutorService:这里Picasso实现了自己的PicassoExecutorService,它继承了ThreadPoolExecutor,也就是Picasso自己维护了一个线程池,用于异步加载图片。</p> <p>RequestTransformer:主要是对RequestCreator创建的Request进行转换,默认对Request对象不做处理。</p> <p>Stats:这个类只要是维护图片的一些状态</p> <p>Dispatcher:从名字上就可以判断出来,这个类在这里起到了一个调度器的作用,图片要不要开始下载以及下载后Bitmap的返回都是通过这个调度器来执行的,后面我们会对它进行更加详细的讲解。</p> <p>对Picasso几个核心类有了大致了解后,我们再来看它到底是如何执行一个异步请求,又是如何将执行结果返回的。<br> 通过上面的分析我们知道,RequestCreator在into方法的最后会创建一个Action实例,然后调用Picasso的enqueueAndSubmit方法,而最终是调用了Dispatcher的dispatchSubmit方法,也就是我们前面说的,Dispatcher起到了调度器的作用。在Dispatcher内部,Dispatcher定义了DispatcherThread和DispatcherHandler两个内部类,并在Dispatcher的构造函数中对他们经行了实例化,所有的调度也都是通过handler异步的执行的:</p> <pre> <code class="language-java">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); ... }</code></pre> <p>因此,我们看到的dispatchSubmit方法就如下所示:</p> <pre> <code class="language-java">void dispatchSubmit(Action action) { handler.sendMessage(handler.obtainMessage(REQUEST_SUBMIT, action)); }</code></pre> <p>在handler中最终调用了performSubmit方法来触发一个图片的加载,那么我们来看一下这个函数:</p> <pre> <code class="language-java">void performSubmit(Action action, boolean dismissFailed) { 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 hunter = hunterMap.get(action.getKey()); if (hunter != null) { hunter.attach(action); return; } if (service.isShutdown()) { if (action.getPicasso().loggingEnabled) { log(OWNER_DISPATCHER, VERB_IGNORED, action.request.logId(), "because shut down"); } return; } hunter = forRequest(action.getPicasso(), this, cache, stats, action); 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()); } }</code></pre> <p>方法并不发杂,主要是获得BitmapHunter实例,由这个实例来执行实际的下载操作。BitmapHunter本身是Runnable的一个实现,而这个实例最终是交由Picasso线程池进行运行的。</p> <p>那么这个BitmapHunter加载图片完成或失败后是怎么通知UI的呢?我们前面提到Dispatcher在Picasso中起到了一个调度器的作用,当图片加载完毕后自然也是通过这个调度器来更新UI:</p> <pre> <code class="language-java">@Override public void run() { try { updateThreadName(data); if (picasso.loggingEnabled) { log(OWNER_HUNTER, VERB_EXECUTING, getLogIdsForHunter(this)); } result = hunt(); // 通过Dispatcher来处理结果 if (result == null) { dispatcher.dispatchFailed(this); } else { dispatcher.dispatchComplete(this); } } catch() { //各种异常处理 }</code></pre> <p>假如我们的图片成功下载下来了,接下来就看看这个图片是如何被渲染到ImageView中的。</p> <pre> <code class="language-java">void dispatchComplete(BitmapHunter hunter) { handler.sendMessage(handler.obtainMessage(HUNTER_COMPLETE, hunter)); }</code></pre> <p>同样通过handler来发送一个message,我们再看消息处理函数</p> <pre> <code class="language-java">@Override public void handleMessage(final Message msg) { switch (msg.what) { ... case HUNTER_COMPLETE: { BitmapHunter hunter = (BitmapHunter) msg.obj; dispatcher.performComplete(hunter); break; } ... } }</code></pre> <p>我们最终执行了performComplete方法。在这个方法了会自动处理图片的缓存问题,方法实现如下:</p> <pre> <code class="language-java">void performComplete(BitmapHunter hunter) { if (shouldWriteToMemoryCache(hunter.getMemoryPolicy())) { cache.set(hunter.getKey(), hunter.getResult()); } hunterMap.remove(hunter.getKey()); batch(hunter); if (hunter.getPicasso().loggingEnabled) { log(OWNER_DISPATCHER, VERB_BATCHED, getLogIdsForHunter(hunter), "for completion"); } }</code></pre> <p>这个时候,Picasso并不是立即将图片显示出来,而是用到了一个批处理,其实就是把操作先暂存在一个list中,等空闲的时候再拿出来处理,这样做得好处也是尽量减少主线程的执行时间,一方面防止ANR,另一方面快速返回,响应页面的其他渲染操作,防止卡顿用户界面。</p> <pre> <code class="language-java">private void batch(BitmapHunter hunter) { if (hunter.isCancelled()) { return; } batch.add(hunter); if (!handler.hasMessages(HUNTER_DELAY_NEXT_BATCH)) { handler.sendEmptyMessageDelayed(HUNTER_DELAY_NEXT_BATCH, BATCH_DELAY); } }</code></pre> <p>handMessage中对应的处理方法为:</p> <pre> <code class="language-java">@Override public void handleMessage(final Message msg) { switch (msg.what) { ... case HUNTER_DELAY_NEXT_BATCH: { dispatcher.performBatchComplete(); break; } ... } }</code></pre> <p>因此,通过这个batch我们实际看到,最后还是调用了Dispatcher的方法来处理,但由于这个处理并非是在主线程(参考前面Dispatcher构造函数中Handler的实例化),因此我们还需要通过一个主线程的Handler来处理这个请求</p> <pre> <code class="language-java">void performBatchComplete() { List<BitmapHunter> copy = new ArrayList<BitmapHunter>(batch); batch.clear(); mainThreadHandler.sendMessage(mainThreadHandler.obtainMessage(HUNTER_BATCH_COMPLETE, copy)); logBatch(copy); }</code></pre> <p>这个mainThreadHandler是在Dispatcher实例化时由外部传递进来的,我们在前面的分析中看到,Picasso在通过Builder创建时会对Dispatcher进行实例化,在那个地方将主线程的handler传了进来,我们回到Picasso这个类,看到其有一个静态成员变量HANDLER,这样我们也就清楚了。</p> <pre> <code class="language-java">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; } ... } } };</code></pre> <p>到这里,我们的图片马上就要显示出来了。我们前面提到,Picasso中一个Action提供了请求前后的衔接工作,对于我们现在的情况,Picasso使用了ImageViewAction来进行处理,也就是在ImageViewAction中的complete方法完成了最后的图片渲染工作。</p> <pre> <code class="language-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; PicassoDrawable.setBitmap(target, context, result, from, noFade, indicatorsEnabled); if (callback != null) { callback.onSuccess(); } }</code></pre> <p>OK,到此,一个图片从开始下载到成功渲染到ImageView中整个流程就讲完了。其中,Dispatcher那块的调度有点绕,如果不是很理解,可以先熟悉下android中handler的用法,handler本身也是android提供给我们方便进行线程间通信的,就像Picasso的实现一样,善用handler可以帮助我们实现更加流畅的用户UI操作体验。</p> <p><br> <a href="/misc/goto?guid=4959674903714812269">文/_xiaoz(简书)</a></p>