铺垫
我们为什么需要解决图片的异步加载问题?
我们在使用列表控件(如ListView RecyclerView)异步加载图片的时候,在快速滑动或者网络不好的情况下,会出现图片错位、重复、闪烁等问题,其实这些问题,根源上是由图片异步加载以及View对象被复用造成的。
比如说ListView有100个item,一个屏幕只显示10个item,一个item对应一个view对象,当列表中的item数量很多的时候,我们不可能给每个item都创建一个view对象,因此我们需要复用,我们在复用View对象的时候,View对象之中的ImageView对象也被复用了。比如说,第11个item的view复用了第1个item view对象,那么imageview同时就被复用了,所以当图片没下载出来的时候,第11个item显示的就是复用的数据,也就是第1个item的图片数据。
目前的解决方法:
在给imageView设置图片之前,先将url设置为Tag,使用Async Task将图片下载的操作放在子线程中,返回结果之后,首先判断当前imageView要加载的图片的url是否等于刚才下载出来的图片的url,如果是的话,则为imageView设置图片,否则说明imageView已经被重用到其他item,不做处理。
核心代码如下:
holder.userImage.setTag(url);
AsyncTask<String,Void,Bitmap> asyncTask = new AsyncTask<String,Void,Bitmap>(){
....
@Override
protected Bitmap doInBackground(String ... params) {
....
/*download image*/
return bitmap;
}
@Override
protected void onPostExecute(Bitmap bitmap) {
super.onPostExecute(bitmap);
if(url.equals(holder.userImage.getTag())) {
holder.userImage.setImageBitmap(bitmap);
}
}
}.execute(url);
}
上述方法存在的问题:
如果加载高分辨率的大图的话,这种方案很有可能会崩溃。原因在于,imageView本身有个尺寸,图片也有个尺寸,最好是图片尺寸和imageView匹配。如果图片太大,就需要缩放以节省内存,代码中的方式缺乏这一步,所以加载大图的话,大概率会挂。同时由于bitmap很大,所以还要做好回收。
铺垫:
一张图片占用的内存取决于什么
公式计算:分辨率*像素点大小,像素点大小取决于像素点的数据格式,glide的bitmap默认的格式是RGB_565,但是picasso用的是ARGB_8888,RGB_565每个像素点占据2B,ARGB_8888每个像素点占4B,因此采用不同的颜色通道,像素点的大小也是不同的。为了保证图片质量,官方默认使用ARGB_8888格式,导致图片的每个像素会占用4个Byte大小。
实践:
内置到res目录下一张高清图片,然后用imageView渲染出来
如下:一张4k大图
存放到drawable-nodpi文件夹下面,并修改代码,将res文件夹下的这张图片的位置代替之前的url。
并运行代码。
Log:
以上的log证明发生了相对频繁的GC,由于导致GC的原因是Alloc,说明大量的bitmap给内存造成了负担。
由于android 8.0之后的内存分配是在native heap上,所以8.0之后的Bitmap消耗内存可以无限增长,直到耗尽系统内存,也不会提示Java OOM。
但是上述实践仍然表明,代码存在很多缺点,包括在加载到ImageView之前没有修改图片尺寸,没有定期回收bitmap等等。因此,我们引入图片加载框架。
图片加载框架
1. Android Universal ImageLoader
https://github.com/nostra13/Android-Universal-Image-Loader
使用过程:
- 添加dependency
implementation 'com.nostra13.universalimageloader:universal-image-loader:1.9.5'
-
代码中:
ImageLoader.getInstance().displayImage(url, holder.userImage);
运行报错
- 查看官方文档
在第一次使用ImageLoader之前需要对它进行配置,因此在代码中添加配置信息。
运行,列表界面成功加载。
-
继续看官方文档的configuration
File cacheDir = StorageUtils.getCacheDirectory(context); ImageLoaderConfiguration config = new ImageLoaderConfiguration.Builder(context) .memoryCacheExtraOptions(480, 800) // default = device screen dimensions .diskCacheExtraOptions(480, 800, null) .taskExecutor(...) .taskExecutorForCachedImages(...) .threadPoolSize(3) // default .threadPriority(Thread.NORM_PRIORITY - 2) // default .tasksProcessingOrder(QueueProcessingType.FIFO) // default .denyCacheImageMultipleSizesInMemory() .memoryCache(new LruMemoryCache(2 * 1024 * 1024)) .memoryCacheSize(2 * 1024 * 1024) .memoryCacheSizePercentage(13) // default .diskCache(new UnlimitedDiskCache(cacheDir)) // default .diskCacheSize(50 * 1024 * 1024) .diskCacheFileCount(100) .diskCacheFileNameGenerator(new HashCodeFileNameGenerator()) // default .imageDownloader(new BaseImageDownloader(context)) // default .imageDecoder(new BaseImageDecoder()) // default .defaultDisplayImageOptions(DisplayImageOptions.createSimple()) // default .writeDebugLogs() .build();
例如其中threadPoolSize可以用来设置线程池的大小。
- 还可以使用一些options来设置一些图片加载时的条件,例如:cacheInMemory设置让加载出来的图片存储在内存中,具体的option内容请查看官方文档。
工作流程
ImageLoader收到加载及显示图片的任务,并将它交给ImageLoaderEngine,ImageLoaderEngine分发任务带具体线程池去执行,任务通过cache(内存和磁盘)以及ImageDownloader获取图片,中间可能会经过BitmapProcessor和ImageDecoder处理,最终转换成Bitmap交给BitmapDisplayer在Imageware中显示出来
优点
- 可以在view滚动中暂停图片加载(通过PauseOnScrollListener接口)。
- 可以对多个模块进行单独配置,并且在配置上也很方便灵活。例如线程池,内存缓存策略,硬盘缓存策略,图片显示选项等
- 支持多级缓存以及多种缓存算法,可以根据使用场景选择合适的缓存策略,这几个框架都可以配置缓存算法,不过 ImageLoader 默认实现了较多缓存算法,如 Size 最大先删除、使用最少先删除、最近最少使用、先进先删除、时间最长先删除等。
- 可以根据ImageView的大小对Bitmap进行裁剪,减少Bitmap占用过多的内存。
其中它的缓存设计也是我们最常见的一种三级缓存设计,后面的三种图片加载框架都采用了类似的设计,即内存缓存,磁盘缓存和网络缓存。
三级缓存设计不仅在性能上可以降低Cpu的处理开销,在内存占用上也可以达到较好的控制避免OOM的发生,从而保证良好的图片加载体验。
缺点
在某些场景,比如在图片较多的应用,尤其是长列表视图中,当用户快速滚动视图时,由于图片较大较多,内存缓存大小的限制会导致频繁的Bitmap回收,进而导致内存的缓存命中率严重下降最终影响到图片的加载效率。
2. Picasso
Picasso是Square公司出品的一个强大的图片下载和缓存图片库。
优点:
- 图片的质量高
- picasso没有自定义本地缓存的接口,而是默认使用http的本地缓存,api9以上的使用Okhttp,可以灵活的控制本地缓存(通过Response Header的cache-control和expired控制图片的过期时间)---判断完内存缓存有没有命中之后,会先判断http缓存有没有命中,如果有的话,再缓存图片到内存,如果http缓存也没有命中,才会从图片加载网络,然后http缓存图片到本地,再缓存到内存,显示图片。
- 自带统计监控,比如说缓存命中率,内存使用大小,节省的流量等。
- 支持优先级处理
- 支持飞行模式,并发线程数根据网络类型变化
3. Glide
优点:
支持Gif
activity生命周期绑定:你可以通过传递Activity或者Fragment的context给Glide.with(),然后glide就会非常智能的同Activitity的生命周期集成,比如OnResume或者onPause()。
缓存上相比picasso进行了很多优化,比如,依据Activity的生命周期按使用场景对内存缓存进行了进一步的划分,做到了及时释放不必要的内存的同时也避免了当前或者即将要使用的缓存不会被回收而导致重新加载的情况。同时,利用了android4.4以上系统支持Bitmap复用的特性,进一步降低了Bitmap的创建开销。
4. Glide和Picasso的对比
-
图片的缓存机制 :
Picasso是下载图片然后缓存完整的大小到本地,比如说图片的大小是1080p的,之后如果我需要同一张图片,就会返回这张full size的,如果我需要resize,也是对这种full size的做resize。
-
glide会先下载图片,然后改变图片的大小,以适应imageView的要求,然后缓存到本地 (缓存的是和ImageView尺寸相同的图片)。所以如果你是下载同一张图片,但是设定两个不一样大小的imageView, 那么glide实际上是会缓存两份。
这种缓存机制的优点就是glide的加载显示非常快,但是picasso会有一些延迟。
-
加载图片的时间:
- 第一次下载图片时(缓存中没有)
和之前一样,picasso是直接把图缓存,但是glide需要改变图片大小再缓存,这会耗费一定的时间,因此在实际试验中,picasso会比glide快。
-
缓存中已经有下载好的图片时
glide比picasso快,原因还是因为缓存机制的区别。因为Picasso从缓存中拿到的图片,还要先去resize之后,再设定给imageView,但是glide不需要这样。
其他功能对比:
glide支持gif ; picasso不支持gif。
灵活性:相对来讲,glide比较灵活。可以根据需求来客制化,从而缩减glide库的大小。
图片的质量:glide的bitmap默认的格式是RGB_565,但是picasso用的是ARGB_8888,所以glide图片质量上不如picassso,但是glide的内存消耗只占picasso一半。
5. Fresco
优点:
- 支持gif,图片加载的效率更高
- 支持图片从模糊到清晰的渐进式加载
- 图片可以以任意的中心点显示在ImageView上
- 图片的解码环节比其他三个框架好,因此图片的加载效率更高
- 采用了类似GC的引用计数机制,使不再使用的图片对象可以更早的被回收,降低内存的开销。
缺点:
- 包很大,用法复杂
- API不够简洁
6. SDK大小
从各个框架的SDK大小来说,他们的体积分别对应如下:
- UIL:133k
- Picasso: 118k
- Glide:430k
- Fresco:>=500k
可以看出,UIL 和 Picasso 包体积较小,对安装包影响不大;Glide 包体积略大,但是其功能十分强大,内部实现及其复杂,其方法数较多。Fresco 包体积很大,但是用户可以根据自己需求有选择的添加动画依赖库和 WebP 支持包,这种模块化机制使得基础 Fresco 库很轻,增加一个库大约包体积会增加 100 KB。
总结
Universal Image Loader是早期比较有代表性的图片加载库,虽然目前已经停止维护,不再推荐使用,但是其架构设计和实现依然值得借鉴。Picasso的设计充分体现了Square公司在架构设计上一贯的简洁易用风格(链式调用)。Glide充分吸收了Picasso的优点,并在此基础上做了大量的优化和改进。Fresco 可以说是综合了之前图片加载库的优点并将性能优化到极致,但它的包很大,用法比较复杂,API不够简洁。