Android 源码解析: 图片加载库Picasso 1
Picasso是一个轻量级的图片缓存库。 Picasso不仅实现了图片异步加载的功能,还解决了android中加载图片时需要解决的一些常见问题:
1.在adapter中需要取消已经不在视野范围的ImageView图片资源的加载,否则会导致图片错位,Picasso已经解决了这个问题。
2.使用复杂的图片压缩转换来尽可能的减少内存消耗
3.自带内存和硬盘二级缓存功能
Picasso有如下特性:
- 处理Adapter中的
ImageView
回收和取消已经回收ImageView的下载进程 - 使用最少的内存完成复杂的图片转换,比如把下载的图片转换为圆角等
- 自动添加磁盘和内存缓存
和其他一些下载图片项目的主要区别之一是:使用4.0+系统上的HTTP缓存来代替磁盘缓存。
同类比较
picasso 和Volley都是用http响应的内容做缓存, 这样可以检测该内容是否过期,如果过期则重新请求新数据,和浏览器缓存一样的策略。
但是在实际应用中我发现大部分的图片缓存都不会过期,也就是一个地址对应一个图片(大部分情况下都是这样 不存在图片过期的问题),并且有些情况应用还有保存图片到本地的功能。
所以综合对比情况如下:
1. picasso 比 AUIL(Android-Universal-Image-Loader) 的代码简单写
2. picasso 具有检测缓存是否失效并重新获取功能
3. AUIL具有保存图片或者二次利用图片的性能优势,直接拿过来就可以用了 不用再次解析http响应数据。
使用:
Picasso使用的方法汇总:
Picasso.with(context).load("http://i.imgur.com/DvpvklR.png").into(imageView);
Picasso.with(context).load(url).into(view);
Picasso.with(context).load(url) .resize(50, 50).centerCrop().into(imageView);
//这里的placeholder将resource传入通过getResource.getDrawable取资源,所以可以是张图片也可以是color id
Picasso.with(context).load(url).placeholder(R.drawable.user_placeholder).error(R.drawable.user_placeholder_error).into(imageView);
Picasso.with(context).load(R.drawable.landing_screen).into(imageView1);
Picasso.with(context).load("file:///android_asset/DvpvklR.png").into(imageView2);
Picasso.with(context).load(new File(...)).into(imageView3);
//这里显示notification的图片
Picasso.with(activity).load(Data.URLS[new Random().nextInt(Data.URLS.length)]).resizeDimen(R.dimen.notification_icon_width_height, R.dimen.notification_icon_width_height).into(remoteViews, R.id.photo, NOTIFICATION_ID, notification);
//这里是通过设置tag标签,就是当前传过来的context,这样就可以根据这个context tag来pause和resume显示了
Picasso.with(context).load(url).placeholder(R.drawable.placeholder).error(R.drawable.error).fit().tag(context).into(view);
//监听onScrollStateChanged的时候调用执行
picasso.resumeTag(context);
picasso.pauseTag(context);
Picasso.with(context).load(contactUri).placeholder(R.drawable.contact_picture_placeholder).tag(context).into(holder.icon);
//这个onpause方法里的这段代码还是很有意思的
@Override protected void onPause() {
super.onPause();
if (isFinishing()) {
// Always cancel the request here, this is safe to call even if the image has been loaded.
// This ensures that the anonymous callback we have does not prevent the activity from
// being garbage collected. It also prevents our callback from getting invoked even after the
// activity has finished.
Picasso.with(this).cancelRequest(imageView);
}
}
// Trigger the download of the URL asynchronously into the image view.
Picasso.with(context)
.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);
//Picasso.with使用的是单例模式
Picasso.with(this).cancelTag(this);
如上,Picasso 的使用是非常简单的:
1
|
Picasso.with(context).load( "http://i.imgur.com/DvpvklR.png" ).into(imageView);
|
/**
* The global default {@link Picasso} instance.
* <p>
* This instance is automatically initialized with defaults that are suitable
* to most implementations.
* <ul>
* <li>LRU memory cache of 15% the available application RAM</li>
* <li>Disk cache of 2% storage space up to 50MB but no less than 5MB. (Note:
* this is only available on API 14+ <em>or</em> if you are using a standalone
* library that provides a disk cache on all API levels like OkHttp)</li>
* <li>Three download threads for disk and network access.</li>
* </ul>
* <p>
* If these settings do not meet the requirements of your application you can
* construct your own with full control over the configuration by using
* {@link Picasso.Builder} to create a {@link Picasso} instance. You can
* either use this directly or by setting it as the global instance with
* {@link #setSingletonInstance}.
*/
public static Picasso with(Context context) {
if (singleton == null) {
synchronized (Picasso.class) {
if (singleton == null) {
singleton = new Builder(context).build();
}
}
}
return singleton;
}
- 一个典型的单例模式
- 使用的是延时初始化来保证Picasso只有在使用到的时候,才会初始化对象
- 使用同步代码块的办法来保证多个线程中同时请求的时候,能够保证只创建唯一的单例对象。
- 同步对象为类对象,保证同步范围整个JVM中。
- 使用构造者模式,通过Builder类构建了一个默认配置的Picasso的对象。
/** Create the {@link Picasso} instance. */
public Picasso build() {
Context context = this.context;
if (downloader == null) {
<span style="color:#ff0000;">downloader = Utils.createDefaultDownloader(context);</span>
}
if (cache == null) {
<span style="color:#ff0000;">cache = new LruCache(context);</span>
}
if (service == null) {
<span style="color:#ff0000;">service = new PicassoExecutorService();</span>
}
if (transformer == null) {
<span style="color:#ff0000;">transformer = RequestTransformer.IDENTITY;</span>
}
<span style="color:#ff0000;"> Stats stats = new Stats(cache);
Dispatcher dispatcher = new Dispatcher(context, service, HANDLER,
downloader, cache, stats);</span>
<span style="color:#ff6666;"> return new Picasso(context, dispatcher, cache, listener, transformer,
requestHandlers, stats, defaultBitmapConfig, indicatorsEnabled,
loggingEnabled);</span>
}
}
- Downloader
- DownLoader就是下载用的工具类,在Picasso当中,如果OKHttp可以使用的话,就会默认使用OKHttp,如果无法使用的话,就会使用UrlConnectionDownloader(默认使用HttpURLConnection实现)。
- Cache
- 默认实现为LruCache,就是使用LinkedHashMap实现的一个Cache类,注意的一个地方就是,在其他的地方,我们一般默认的是限制的capacity,但是这个地方我们是限制的总共使用的内存空间。因此LruCache在实现的时候,其实简单理解就是将LinkedHashMap封装,然后基于LinkedHashMap的方法实现Cache的方法,在Cache的set()方法的时候,会不断计算当前还可以使用的空间大小,要是超出范围,则删除之前保存的数据。
- ExecutorService
- 默认的实现为PicassoExecutorService,该类也比较简单,其实就是ThreadPoolExecutor,在其功能的基础上继续封装,在其中有一个比较细心的功能就是,Picasso通过PicassoExecutorService设置线程数量,来调整在2G/3G/4G/WiFi不同网络情况下的不同表现。
- RequestTransformer
- ReqeustTransformer是一个接口,用来预处理Reqeust,可以用来将请求进行预先处理,比如改个域名啥的。
- Stats
- 主要是一些统计信息,比如cache hit/miss,总共下载的文件大小,下载过的图片数量,转换的图片数量等等。
- Dispatcher
- Picasso当中,分发任务的线程,这是我们以后要重点研究的一个类,先标记一下,这个Dispatcher主要做了以下的事情:
- 启动了一个DispatcherThread线程
- 初始化了一个用来处理消息的DispatcherHandler,注意,根据Dispatcher中默认配置,该Handler所有数据的处理是在DispatcherThread之上。
- 初始化并注册了一个网络状态广播接收器。
- Picasso当中,分发任务的线程,这是我们以后要重点研究的一个类,先标记一下,这个Dispatcher主要做了以下的事情:
/**
* Start an image request using the specified URI.
* <p>
* Passing {@code null} as a {@code uri} will not trigger any request but will
* set a placeholder, if one is specified.
*
* @see #load(File)
* @see #load(String)
* @see #load(int)
*/
public RequestCreator load(Uri uri) {
return new <span style="color:#ff0000;">RequestCreator</span>(this, uri, 0);
}
/**
* Start an image request using the specified path. This is a convenience
* method for calling {@link #load(Uri)}.
* <p>
* This path may be a remote URL, file resource (prefixed with {@code file:}),
* content resource (prefixed with {@code content:}), or android resource
* (prefixed with {@code android.resource:}.
* <p>
* Passing {@code null} as a {@code path} will not trigger any request but
* will set a placeholder, if one is specified.
*
* @see #load(Uri)
* @see #load(File)
* @see #load(int)
* @throws IllegalArgumentException
* if {@code path} is empty or blank string.
*/
public RequestCreator load(String path) {
if (path == null) {
return new RequestCreator(this, null, 0);
}
if (path.trim().length() == 0) {
throw new IllegalArgumentException("Path must not be empty.");
}
return load(Uri.parse(path));
}
/**
* Start an image request using the specified image file. This is a
* convenience method for calling {@link #load(Uri)}.
* <p>
* Passing {@code null} as a {@code file} will not trigger any request but
* will set a placeholder, if one is specified.
* <p>
* Equivalent to calling {@link #load(Uri) load(Uri.fromFile(file))}.
*
* @see #load(Uri)
* @see #load(String)
* @see #load(int)
*/
public RequestCreator load(File file) {
if (file == null) {
return new RequestCreator(this, null, 0);
}
return load(Uri.fromFile(file));
}
/**
* Start an image request using the specified drawable resource ID.
*
* @see #load(Uri)
* @see #load(String)
* @see #load(File)
*/
public RequestCreator load(int resourceId) {
if (resourceId == 0) {
throw new IllegalArgumentException("Resource ID must not be zero.");
}
return new RequestCreator(this, null, resourceId);
}
Picasso通过调用load(Uri uri),返回了一个传入Picasso对象和要访问的网址或路径的Uri的ReqeustCreator。
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;
data = new Request.Builder(uri, resourceId, picasso.defaultBitmapConfig);
}
/**
* Asynchronously fulfills the request into the specified {@link ImageView}.
* <p>
* <em>Note:</em> This method keeps a weak reference to the {@link ImageView}
* instance and will automatically support object recycling.
*/
public void into(ImageView target) {
into(target, null);
}
into(ImageView imageview)实际上调用的是into(ImageView imageview, Callback callback)方法,我们查看其源代码:
/**
* Asynchronously fulfills the request into the specified {@link ImageView}
* and invokes the target {@link Callback} if it's not {@code null}.
* <p>
* <em>Note:</em> The {@link Callback} param is a strong reference and will
* prevent your {@link android.app.Activity} or {@link android.app.Fragment}
* from being garbage collected. If you use this method, it is <b>strongly</b>
* recommended you invoke an adjacent
* {@link Picasso#cancelRequest(android.widget.ImageView)} call to prevent
* temporary leaking.
*/
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()) {
picasso.cancelRequest(target);
if (setPlaceholder) {
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) {
setPlaceholder(target, getPlaceholderDrawable());
}
picasso.defer(target,
new DeferredRequestCreator(this, target, callback));
return;
}
data.resize(width, height);
}
Request request = createRequest(started);
String requestKey = createKey(request);
if (shouldReadFromMemoryCache(memoryPolicy)) {
Bitmap bitmap = picasso.quickMemoryCacheCheck(requestKey);
if (bitmap != null) {
picasso.cancelRequest(target);
setBitmap(target, picasso.context, bitmap, MEMORY, noFade,
picasso.indicatorsEnabled, request.isShowGif);
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 = new ImageViewAction(picasso, target, request, memoryPolicy,
networkPolicy, errorResId, errorDrawable, requestKey, tag, callback,
noFade);
picasso.enqueueAndSubmit(action);
}
- 首先检查当前是否工作在主线程,如果不是在主线程上的话,直接抛出异常退出。
- 然后判断data.hasImage(),这里面这个函数命名比较迷惑人了,一开始我还以为是判断是否已经有解析好的图片,但再往下看一看源代码,原来这个代码是判断Uri和resourceId是否有设置了一个,如果没有,则说明该Reqeust没有数据源,是错误的。
- 如果没有Uri或者resource Id,那么则取消该请求,并设置默认的显示图片并退出。
- 该图片是否需要延时执行(这个属性还不太清楚在哪里配置的,不清楚是手动指定还是自动设置)。如果需要延时执行,则按照以下步骤执行。
- 判断是否设置过targetSize,如果已经设置过,则抛出异常退出 (不太明白为什么要检查是否已经要设置过targetSize)
- 然后获取target,即ImageView对应的长和宽,如果长和宽都是0,那么就设置默认的图片,并构建一个DeferredRequestCreator,放入Picasso对应的队列当中。
- 重新设置data即ReqeustCreator对应的targetWidth和targetHeight
- 创建Reqeust,生成requestKey
- 判断是否需要跳过MemoryCache,如果不跳过,那么就尝试获取图片,并取消对应的请求,进行回调。
- 如果需要设置默认的图片,则在这里进行设置
- 生成对应的ImageViewAction
- 将生成的ImageViewAction添加到队列当中。
into()方法总结:检查配置的合法性,然后根据配置决定是放入延时队列还是立刻执行,如果立刻执行,则创建对应的请求并尝试从MemoryCache中获取,如果不成功,则生成对应的Action并提交到Picasso的队列当中。然后就是Picasso的任务调度了。