在UI界面加载一张图片时很简单,然而如果需要加载多张较大的图像,事情就会变得更加复杂。在许多情况下(如ListView、RecyclerView或ViewPager等的组件),屏幕上的图片的总数伴随屏幕的滚动会大大增加,且基本上是无限的。为了使内存使用保持在稳定范围内,防止出现OOM,这些组件会在子iew划出屏幕后,对其进行资源回收,并重新显示新出现的图片,垃圾回收机制会释放掉不再显示的图片的内存空间。但是这样频繁地处理图片的加载和回收不利于操作的流畅性,而内存或者磁盘的Cache就会帮助解决这个问题,实现快速加载已加载的图片。在缓存上,主要有两种级别的Cache:LruCache和DiskLruCache,即内存缓存与磁盘缓存。我们就来借助内存缓存和磁盘缓存来实现一个简易的图片缓存框架。
概要设计
代码实现
BitMapLruCache.java:
1 | import android.content.Context; |
LRUCache本身就是基于LinkHashMap实现的,关于更多LRUCache的内容可以看我的另一篇博客:《LinkHashMap与LRU》
需要注意的是使用此组件需要外部存储的权限与网络访问权限(如果是Android6.0之后的设备还需要动态权限申请):
1 | <uses-permission android:name="android.permission.INTERNET" /> |
关于图片压缩
我们在在展示高分辨率图片的时候,最好先将图片进行压缩。压缩后的图片大小应该和用来展示它的控件大小相近,因为在一个很小的ImageView上显示一张超大的图片不会带来任何视觉上的好处,但却会占用我们相当多宝贵的内存,而且在性能上还可能会带来负面影响。下面我们就来看一看,如何对一张大图片进行适当的压缩,让它能够以最佳大小显示的同时,还能防止OOM的出现。
1 | W/System.err: java.lang.RuntimeException: Canvas: trying to draw too large(552960000bytes) bitmap. |
BitmapFactory这个类提供了多个解析方法(decodeByteArray, decodeFile, decodeResource等)用于创建Bitmap对象,我们应该根据图片的来源选择合适的方法。比如SD卡中的图片可以使用decodeFile方法,网络上的图片可以使用decodeStream方法,资源文件中的图片可以使用decodeResource方法。这些方法会尝试为已经构建的bitmap分配内存,这时就会很容易导致OOM出现。为此每一种解析方法都提供了一个可选的BitmapFactory.Options参数,将这个参数的inJustDecodeBounds属性设置为true就可以让解析方法禁止为bitmap分配内存,返回值也不再是一个Bitmap对象,而是null。虽然Bitmap是null了,但是BitmapFactory.Options的outWidth、outHeight和outMimeType属性都会被赋值。这个技巧让我们可以在加载图片之前就获取到图片的长宽值和MIME类型,从而根据情况对图片进行压缩。如下代码所示:
1 | BitmapFactory.Options options = new BitmapFactory.Options(); |
为了避免OOM异常,最好在解析每张图片的时候都先检查一下图片的大小,除非你非常信任图片的来源,保证这些图片都不会超出你程序的可用内存。
现在图片的大小已经知道了,我们就可以决定是把整张图片加载到内存中还是加载一个压缩版的图片到内存中。以下几个因素是我们需要考虑的:
1、预估一下加载整张图片所需占用的内存
2、为了加载这一张图片你所愿意提供多少内存
3、用于展示这张图片的控件的实际大小
4、当前设备的屏幕尺寸和分辨率
比如,你的ImageView只有128×96像素的大小,只是为了显示一张缩略图,这时候把一张1024×768像素的图片完全加载到内存中显然是不值得的。通过设置BitmapFactory.Options中inSampleSize的值就可以实现。比如我们有一张2048×1536像素的图片,将inSampleSize的值设置为4,就可以把这张图片压缩成512×384像素。原本加载这张图片需要占用13M的内存,压缩后就只需要占用0.75M了(假设图片是ARGB_8888类型,即每个像素点占用4个字节)。下面的方法可以根据传入的宽和高,计算出合适的inSampleSize值:
1 | public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) { |
使用这个方法,首先你要将BitmapFactory.Options的inJustDecodeBounds属性设置为true,解析一次图片。然后将BitmapFactory.Options连同期望的宽度和高度一起传递到到calculateInSampleSize方法中,就可以得到合适的inSampleSize值了。之后再解析一次图片,使用新获取到的inSampleSize值,并把inJustDecodeBounds设置为false,就可以得到压缩后的图片了:
1 | public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId, |
下面的代码非常简单地将任意一张图片压缩成100×100的缩略图,并在ImageView上展示:
1 | mImageView.setImageBitmap(decodeSampledBitmapFromResource(getResources(), R.id.myimage, 100, 100)); |
解决RecyclerView加载图片错乱
因为有ViewHolder的重用机制,每一个Item在移出屏幕后都会被重新使用以节省资源,避免滑动卡顿。所以当使用异步任务加载图片的时候就可能会发生图片加载错乱的问题,既然弄清楚了原因,那么下面就给出解决方式:
1.先将图片预设为本地一个占位图片。(重要!很多错位情况在于复用了其他位置的图片缓存,而当前图片迟迟加载不出来,导致当前图片错位。所以解决之道是先用本地占位图片,快速刷新当前图片。等加载完毕之后,就可以替换掉占位图);
2.通过ImageView.setTag,把url记录在图片内部;
3.把url放进一个请求队列,(这是避免图片很大,请求耗时很长,重复发起url请求);
4.如果请求队列存在url,则将老的url对应图片控件,替换为本次请求图片控件,返回;
5.如果当前url和第一步ImageView.getTag一致,则用新的数据源更新图片,否则返回;
6.如果是重复使用的图片,最好就是访问之后,写入到本地存储上,避免耗时网络请求。
下面给出具体代码实现:(里面的BitMapLruCache正是用的上面的BitMapLruCache)
1 | public class PictureAdapter extends RecyclerView.Adapter<PictureAdapter.MyViewHolder>{ |
- 本文作者: Tim
- 本文链接: https://zouchanglin.cn/2719388914.html
- 版权声明: 本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 许可协议。转载请注明出处!