第十二章 Bitmap的加载和Cache
本章主要介绍了三个方面的知识:
(1)图片加载:如何有效的加载一个Bitmap
(2)缓存策略:LruCache和DiskLruCache
(3)列表的滑动流畅性:如何优化列表的卡顿现象
##12.1、BItmap的高效加载
首先有4种Bitmap的加载方法,都是有BitmapFactory提供的
加载方法 | 加载来源 |
---|---|
decodeFile | 文件 |
decodeResource | 资源 |
decodeStream | 输入流 |
decodeByteArray | 字节数组 |
*其中decodeFile和decodeResource是间接调用了decodeStream方法
(1)压缩方法
加载所需尺寸的图片。即原本图片很大,但是实际上需要的尺寸很小,比如头像只需要缩略图,此时可以计算出采样率,然后根据采样率来加载图片。
通过设置BitmapFactory.Options对象的inSampleSize来进行图片的采样计算和缩放,这里需要了解的是整个图片缩放比例是1/(inSampleSize的2次方),而宽高为原来的1/inSampleSize,即inSampleSize为1时不缩放,为2时缩放为原来的1/4(宽高都为原来的1/2),为4时缩放为原来的1/16(宽高都为原来的1/4)
采样原则:缩放比例一定要大于等于需求宽高。比如ImageView的大小为100*100像素,原始图片为200*300,那么inSampleSize应该为2,此时压缩后的图片为100*150 >= 100*100
(2)采样缩放流程
总体流程应该分为:获取图片的宽高->获取所需的宽高->计算采样率->压缩图片
具体流程如下:
*1、将BitmapFactory.Options的inJustDecodeBounds参数设为true,并通过BitmapFactory和Options加载图片。
*2、从BitmapFactory.Options中获取到原始图片的宽高,对应outWidth和outHeight。
*3、通过实际所需的宽高reqWidth和reqHeight以及outWidth和outHeight计算出采样率,并设置到Options的inSampleSize中。
*4、将BitmapFactory.Options的inJustDecodeBounds参数设为false,然后重新通过BitmapFactory和Options加载图片。
上面流程的代码实现如下
public class ImageResizer {
public ImageResizer(){}
/**
* 从资源文件中获取相应的图片
* @param res
* @param resId
* @param reqWidth
* @param reqHeight
* @return
*/
public Bitmap decodeSampleBitmapFromResource(Resources res, int resId, int reqWidth, int reqHeight)
{
Options options = new Options();
//设置只加载宽高标志位
options.inJustDecodeBounds = true;
//加载原始图片宽高到Options中
BitmapFactory.decodeResource(res, resId, options);
//计算采样率,通过所需宽高和原始图片宽高
options.inSampleSize = calculateSampleSize(reqWidth, reqHeight, options);
//还原并再次加载图片
options.inJustDecodeBounds = false;
return BitmapFactory.decodeResource(res, resId, options);
}
/**
* 从文件描述符中获取相应的图片
* @param reqWidth
* @param reqHeight
* @param options
* @return
*/
public Bitmap decodeSampleBitmapFromFileDescriptor(FileDescriptor fd, int reqWidth, int reqHeight)
{
Options options = new Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFileDescriptor(fd, null, options);
options.inSampleSize = calculateSampleSize(reqWidth, reqHeight, options);
options.inJustDecodeBounds = false;
return BitmapFactory.decodeFileDescriptor(fd, null, options);
}
//计算采样率
public static int calculateSampleSize(int reqWidth, int reqHeight, Options options)
{
//如果传入0参数,则将采样率设成1,即不压缩
if (reqWidth == 0 || reqHeight == 0) {
return 1;
}
int inSampleSize = 1;
int width = options.outWidth;
int height = options.outHeight;
//当所需宽高比实际宽高小时才进行压缩
if(reqWidth < width && reqHeight < height)
{
int halfWidth = width >>= 1;
int halfHeight = height >>= 1;
//保证压缩后的宽高不能小于所需宽高
while(reqWidth <= halfWidth && reqHeight <= halfHeight)
{
inSampleSize <<= 1;
halfWidth /= inSampleSize;
halfHeight /= inSampleSize;
}
}
return inSampleSize;
}
}
实际使用的时候根据需要压缩图片,比如ImageView所期望的图片大小为100*100,则可以这样实现
iv.setImageBitmap(mImageResizer.decodeSampleBitmapFromResource(getResources(), R.drawable.lizhuo, 100, 100));
##12.2、Android中的缓存策略
Android中三级缓存策略:内存-磁盘-网络。即在获取资源时比如图片,先从内存缓存中读取,如果没有则从磁盘缓存中读取,最后还没有再从网络中拉取图片。
Android中通过LruCache实现内存缓存,通过DiskLruCache实现磁盘缓存,它们采用的都是LRU(Least Recently Used)最近最少使用算法来移除缓存
##12.2.1 LruCache
LruCache是一个泛型类, 它内部采用了一个LinkedHashMap以强引用的方式存储外界的缓存对象, 其提供了get和put方法来完成缓存的获取和添加的操作. 当缓存满了时, LruCache会移除较早使用的缓存对象, 然后在添加新的缓存对象.
LruCache是线程安全的。
LruCache 典型初始化过程:
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
int cacheSize = maxMemory / 8;
mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap value) {
return value.getRowBytes() * value.getHeight() / 1024;
}
};
这里只需要提供缓存的总容量大小(一般为进程可用内存的1/8)并重写 sizeOf 方法即可.sizeOf方法作用是计算缓存对象的大小。这里大小的单位需要和总容量的单位(这里是kb)一致,因此除以1024。一些特殊情况下,需要重写LruCache的entryRemoved方法,LruCache移除旧缓存时会调用entryRemoved方法,因此可以在entryRemoved中完成一些资源回收工作.
获取和添加方法
mMemoryCache.get(key)
mMemoryCache.put(key,bitmap)
##12.2.2、DiskLruCache
DiskLruCache的创建
public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
(1)File directory: 表示存储路径. 可以选择SD卡上的缓存目录, 指/sdcard/Andriod/data/package_name/cache目录
(2)int appVersion: 表示应用的版本号, 一般设为1即可. 当版本号发生改变的时候DiskLruCache会清空之前所有的缓存文件
(3)int valueCount: 表示单个节点所对应的数据的个数, 一般设为1.
(4)long maxSize: 表示缓存的总大小, 当缓存大小超出设定值, DiskLruCache会清除一些缓存而保证总大小不大于这个设定值.
DiskLruCache的缓存添加
DiskLruCache的缓存添加的操作是通过Editor完成的, Editor表示一个缓存对象的编辑对象.
如果还是缓存图片为例子, 每一张图片都通过图片的url为key, 这里由于url可能会有特殊字符所以采用url的md5值作为key. 根据这个key就可以通过edit()来获取Editor对象, 如果这个缓存对象正在被编辑, 那么edit()就会返回null. 即DiskLruCache不允许同时编辑一个缓存对象.
当用.edit(key)获得了Editor对象之后. 通过editor.newOutputStream(0)就可以得到一个文件输出流. 由于之前open()方法设置了一个节点只能有一个数据. 所以在获得输出流的时候传入常量0即可.
有了文件输出流, 可以当网络下载图片时, 图片就可以通过这个文件输出流写入到文件系统上.最后,要通过Editor中commit()来提交写操作, 如果下载中发生异常, 那么使用Editor中abort()来回退整个操作.
DiskLruCache的缓存读取
private Bitmap loadBitmapFromDiskCache(String url, int reqWidth, int reqHeight) throws IOException
{
if(Looper.myLooper() == Looper.getMainLooper())
Log.w(TAG, "it's not recommented load bitmap from UI Thread");
if(mDiskLruCache == null)
return null;
Bitmap bitmap = null;
String key = hashKeyForDisk(url);
Snapshot snapshot = mDiskLruCache.get(key);
if(snapshot != null)
{
FileInputStream fileInputStream = (FileInputStream) snapshot.getInputStream(DISK_CACHE_INDEX);
FileDescriptor fd = fileInputStream.getFD();
bitmap = mImageResizer.decodeSampleBitmapFromFileDescriptor(fd, reqWidth, reqHeight);
if(bitmap != null)
addBitmapToMemoryCache(key, bitmap);
}
return bitmap;
}
##12.2.3、ListView的列表错位问题
(1)问题描述
在Adapter中,通常会复用View来优化ListView或者GridView的加载,而复用View带来的问题就是,图片在ListView列表中显示的位置错乱,即本应该显示B图片的位置显示了A图片
(2)问题解决
问题的根源在于图片A在被加载ImageView之前,ListView发生滑动导致ImageView被itemB复用,此时该ImageView就不能显示图片A了。那么就从根源入手,在图片A被加载到ImageView之前做一个判断,判断该ImageView是否还是对应的是itemA,如果是则将图片加载到ImageView当中,如果不是则放弃加载,所以问题就变成了,如何判断ImageView对应的item已经改变了
方法步骤:
*****1、每次getView的复用布局控件时,对会被复用的控件设置一个标签(在这里就是对ImageView设置标签),这里使用图片的url作为标签内容,然后再异步加载图片
*****2、在图片下载完成后要加载到ImageView之前做判断,判断该ImageView的标签内容是否和图片的url一样
如果一样说明ImageView没有被复用,可以将图片加载到ImageView当中;
如果不一样,说明ListView发生了滑动,导致其他item调用了getView从而将该ImageView的标签改变,此时放弃图片的加载
具体实现代码
(1)在getView中给ImageView设置标签内容
imageView.setTag(uri);//对应imageView.getTag()
//or
imageView.setTag(TAG_KEY_URI, uri);//对应imageView.getTag(TAG_KEY_URI) TAG_KEY_URL是常量
(2)在给ImageView设置图片前判断图片的uri是否和ImageView的标签内容
if(uri.equals(imageView.getTag(TAG_KEY_URI)))
{
//如果相等才设置图片
imageView.setImageBitmap(bitmap);
}else
{
Log.w(TAG,"set image bitmap, but url has changed, ignored!");
}
##12.3、ListView或GridView的优化
(1)不要在Adapter的getView中执行耗时操作
列表的滑动触发最多的方法就是Adapter的getView方法,如果getView方法里面执行的操作太耗时,就会造成卡顿现象。如果有耗时的操作比如像上面的从网络加载图片,那么就应该开启新线程异步加载图片。
(2)控制异步任务的执行频率
如果用户刻意频繁上下滑动,getView方法会不停调用,从而产生大量的异步任务。可以考虑在列表滑动停止加载图片;给ListView或者GridView设置 setOnScrollListener 并在 OnScrollListener 的 onScrollStateChanged 方法中判断列表是否处于滑动状态,如果是的话就停止加载图片。
public void onScrollStateChanged(AbsListView view, int scrollState) {
// TODO Auto-generated method stub
if(scrollState == OnScrollListener.SCROLL_STATE_IDLE)
{
mImageAdapter.setIsGridViewIdle(true);
mImageAdapter.notifyDataSetChanged();
}else
{
mImageAdapter.setIsGridViewIdle(false);
}
}
然后在getView方法中,仅当列表静止时才能加载图片
if(mIsGridViewIdle)
{
imageView.setTag(url);
mImageLoader.bindBitmap(url, imageView, mImageWidth,mImageWidth);
}
(3)在Activity中开启硬件加速
android:hardwareAccelerated="true";
图片优化延申:
Android图片压缩与优化的几种方式:
Android目前常用的图片格式有png,jpeg和webp,
png:无损压缩图片格式,支持Alpha通道,Android切图素材多采用此格式
jpeg:有损压缩图片格式,不支持背景透明,适用于照片等色彩丰富的大图压缩,不适合logo
webp:是一种同时提供了有损压缩和无损压缩的图片格式,派生自视频编码格式VP8,从谷歌官网来看,无损webp平均比png小26%,有损的webp平均比jpeg小25%~34%,无损webp支持Alpha通道,有损webp在一定的条件下同样支持,有损webp在Android4.0(API 14)之后支持,无损和透明在Android4.3(API18)之后支持
1、质量压缩
质量压缩并不会改变图片在内存中的大小,仅仅会减小图片所占用的磁盘空间的大小,因为质量压缩不会改变图片的分辨率,而图片在内存中的大小是根据width*height*一个像素的所占用的字节数计算的,宽高没变,在内存中占用的大小自然不会变,质量压缩的原理是通过改变图片的位深和透明度来减小图片占用的磁盘空间大小,所以不适合作为缩略图,可以用于想保持图片质量的同时减小图片所占用的磁盘空间大小。另外,由于png是无损压缩,所以设置quality无效,以下是实现方式:
/**
* 质量压缩
*
* @param format 图片格式 jpeg,png,webp
* @param quality 图片的质量,0-100,数值越小质量越差
*/
public static void compress(Bitmap.CompressFormat format, int quality) {
File sdFile = Environment.getExternalStorageDirectory();
File originFile = new File(sdFile, "originImg.jpg");
Bitmap originBitmap = BitmapFactory.decodeFile(originFile.getAbsolutePath());
ByteArrayOutputStream bos = new ByteArrayOutputStream();
originBitmap.compress(format, quality, bos);
try {
FileOutputStream fos = new FileOutputStream(new File(sdFile, "resultImg.jpg"));
fos.write(bos.toByteArray());
fos.flush();
fos.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
2、采样率压缩
采样率压缩是通过设置BitmapFactory.Options.inSampleSize,减小图片的分辨率,进而减小图片所占用的磁盘空间和内存大小。
/**
*
* @param inSampleSize 可以根据需求计算出合理的inSampleSize
*/
public static void compress(int inSampleSize) {
File sdFile = Environment.getExternalStorageDirectory();
File originFile = new File(sdFile, "originImg.jpg");
BitmapFactory.Options options = new BitmapFactory.Options();
//设置此参数是仅仅读取图片的宽高到options中,不会将整张图片读到内存中,防止oom
options.inJustDecodeBounds = true;
Bitmap emptyBitmap = BitmapFactory.decodeFile(originFile.getAbsolutePath(), options);
options.inJustDecodeBounds = false;
options.inSampleSize = inSampleSize;
Bitmap resultBitmap = BitmapFactory.decodeFile(originFile.getAbsolutePath(), options);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
resultBitmap.compress(Bitmap.CompressFormat.JPEG, 100, bos);
try {
FileOutputStream fos = new FileOutputStream(new File(sdFile, "resultImg.jpg"));
fos.write(bos.toByteArray());
fos.flush();
fos.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
3、缩放压缩
通过减少图片的像素来降低图片的磁盘空间大小和内存大小,可以用于缓存缩略图
public void compress(View v) {
File sdFile = Environment.getExternalStorageDirectory();
File originFile = new File(sdFile, "originImg.jpg");
Bitmap bitmap = BitmapFactory.decodeFile(originFile.getAbsolutePath());
//设置缩放比
int radio = 8;
Bitmap result = Bitmap.createBitmap(bitmap.getWidth() / radio, bitmap.getHeight() / radio, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(result);
RectF rectF = new RectF(0, 0, bitmap.getWidth() / radio, bitmap.getHeight() / radio);
//将原图画在缩放之后的矩形上
canvas.drawBitmap(bitmap, null, rectF, null);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
result.compress(Bitmap.CompressFormat.JPEG, 100, bos);
try {
FileOutputStream fos = new FileOutputStream(new File(sdFile, "sizeCompress.jpg"));
fos.write(bos.toByteArray());
fos.flush();
fos.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
总结
1、使用webp格式的图片可以在保持清晰度的情况下减小图片的磁盘大小,是一种比较优秀的,google推荐的图片格式
2、质量压缩可以减小图片占用的磁盘空间,不会减小在内存中的大小
3、采样率压缩可以通过改变分辨率来减小图片所占用的磁盘空间和内存空间大小,但是采样率只能设置2的n次方,可能图片的最优比例在中间
4、尺寸压缩同样也是通过改变分辨率来减小图片所占用的磁盘空间和内存空间大小,缩放的尺寸没有什么限制