对于加载Bitmap,系统提供了 BitmapFactory
类并提供四类方法:decodeFile()
、decodeResource()
、decodeStream()
、decodeByteArray()
分别用于支持文件系统、资源、输入流以及字节数组中加载一个Bitmap对象,其中 decodeFile()
、decodeResource()
内部间接调用了 decodeStream()
方法。
通过 BitmapFactory.Options
来缩放图片,主要用 inSampleSize
即采样率。
一般inSampleSize设置的大小以2的次方形式对图片宽高、缩放比例进行影响,对图片宽高为 1/inSampleSize
,对缩放比例(像素数、占有内存大小)为 1/(inSampleSize的2次方)
当inSampleSize=1,采样后的图片大小为图片原始宽高,像素数为原图大小,占有内存大小为原图大小
当inSampleSize=2,采样后的图片大小为图片原始宽高的1/2,像素数为原图大小的1/4,占有内存大小为原图大小的1/4
如:
一张图片 1024*1024
,假定采用 ARGB8888
格式存储,占有内存为 1024*1024*4
,图片大小为 4MB
设置inSampleSize=2,采样后的图片大小为 512*512*4
,图片大小为 1MB
设置 BitmapFactory.Options
的 inJustDecodeBounds
为true(此时不会加载图片,会对图片进行解析)
从 BitmapFactory.Options
获取图片的原始宽高信息,对应 outWidth
和 outHeight
结合所需要的图片大小计算采样率 inSampleSize
设置 BitmapFactory.Options
的i nJustDecodeBounds
为false,重新加载图片
public static Bitmap decodeSampleBitmapFromResource(Resource res, int resId, int reqWidth, int reqHeight) {
final BitmapFactory.Options = new BitmapFactory.Options();
//开始解析图片
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(res, resId, options);
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
//结束解析图片
options.inJustDecodeBound = false;
return BitmapFactory.decodeResource(res, resId, options);
}
public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
final int width = options.outWidth;
final int height = options.outHeidht;
int inSampleSize = 1; //设置默认采样率,原始图片宽高
//计算采样率inSampleSize
[1]要保证 原始图片宽高>需要的宽高(如果原始图片宽高小于我们需要的宽高,图片会被拉伸导致模糊),否则直接默认采样率inSampleSize
[2]如果第一个条件成立,通过循环计算inSampleSize,直到 图片宽高>=需要的宽高
if (width > reqWidth || heidht > reqHeight) {
final int halfWidth = width / 2;
final int halfHeigth = height / 2;
while ((halfWidth / inSampleSize) >= reqWidth && (halfHeight / inSampleSize) >= reqHeight) {
inSampleSize *= 2;
}
}
return inSampleSize;
}
注意:在实际项目开发中,要缩放的图片原始大小和图片被放置的分辨率目录有关,比如图片放在 mipmap-hdpi
和 mipmap-xhdpi
图片大小就不同
Lru(Least Recently Used)
即最近最少使用算法,当缓存满时,会优先淘汰那些近期最少使用的缓存对象。
采用Lru算法的缓存有两种:LruCache
和 DiskLruCache
,LruCache用于实现内存缓存,DiskLruCache用于实现存储设备缓存
项目中的缓存使用
在一些项目中通常会结合使用内存缓存和存储设备缓存,比如第一次从网络中获取到图片,将图片缓存到存储设备缓存和内存缓存,当再一次加载图片时,会首先从内存缓存获取查找需要的图片是否存在(从内存缓存获取图片的速度最快),如果没有,会到存储设备缓存获取查找需要的图片是否存在,如果没有,最后才再一次从网络获取图片。
内部采用 LinkedHashMap
以强引用的方式存储外界的缓存对象,提供 get
和 put
方法完成缓存的获取和添加。
强引用:直接的对象引用。
软引用:当一个对象只有软引用存在时,系统内存不足时此对象才会被gc回收。
弱引用:当一个对象只有弱引用存在时,此对象会随时被gc回收。
private LruCache<String, Bitmap> mMemoryCache;
//设置缓存大小并计算bitmap大小
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
int cacheSize = maxMemory / 8;
mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap bitmap) {
return bitmap.getRowBytes() * bitmap.getHeight() / 1024;
}
};
//获取缓存对象
mMemoryCache.get(key);
//添加缓存对象
mMemoryCache.put(key, bitmap);
sizeOf()
方法的作用是计算缓存对象的大小,返回的大小单位要和总容量 cacheSize
单位一致。
以上代码设置了缓存总容量为进程可用内存的1/8,单位为KB,sizeOf()返回bitmap大小。
在一些特殊情况下,还需要重写LruCache的 entryRemoved()
方法,LruCache在移除旧缓存时会调用该方法,可以在entryRemoved
中完成一些回收工作。
https://github.com/JakeWharton/DiskLruCache
DiskLruCache
的创建通过 open(File directory, int appVersion, int valueCount, long maxSize)
方法获取对象
directory:缓存目录路径。如果希望应用卸载后删除缓存文件,就选择sd卡上的缓存目录;如果要保留则选择sd卡上的其他特定目录。
appVersion:设置成1即可。当版本号改变时,之前的缓存文件会被清空,但大多数时候版本号更改缓存文件仍然有效。
valueCount:表示单个节点所对应的数据的个数,一般设为1即可。
maxSize:缓存的总容量。
private DiskLruCache mDiskLruCache;
private static final long DISK_CACHE_SIZE = 1024 * 1024 * 50;//50MB
File diskCacheDir = getDiskCacheDir(mContext, "bitmap");
if (!diskCacheDir.exists) {
diskCacheDir.mkdirs();
}
mDiskLruCache = DiskLruCache.open(diskCacheDir, 1, 1, DISK_CACHE_SIZE);
详细操作步骤:
通过图片的url将其转换成key(该key一般为md5,转为md5是防止url有特殊符号),通过key获取到Editor对象
通过Editor对象获取文件输出流,将图片写入缓存文件中
缓存完成后调用editor.commit()方法提交才正式写入为缓存,如果写入期间出现异常,调用editor.abort()回滚操作。
private static final int DISK_CACHE_INDEX = 0; //因为在DiskLruCache创建时open()方法中的valueCount设置为1,这里需要设置为0
String key = hashKeyFromUrl(url);
DiskLruCache.Editor editor = mDiskLruCache.edit(key);
if (editor != null) {
OutputStream outputStream = editor.newOutputStream(DISK_CACHE_INDEX);
if (downloadUrlToStream(url, outputStream)) {
editor.commit();
} else {
editor.abort();
}
mDiskLruCache.flush();
}
//将网络下载的图片写入到缓存文件中
private boolean downloadUrlToStream(String urlString, OutputStream outputStream) {}
private String hashKeyFromUrl(String url) {
String cacheKey;
try {
final MessageDigest mDigist = MessageDigest.getInstance("MD5");
mDigest.update(url.getBytes());
cacheKey = bytesToHexString(mDigest.digest());
} catch(Exception e) {
cacheKey = String.valueOf(url.hashCode());
}
return cacheKey;
}
private String bytesToHexString(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < bytes.length; i++) {
String hex = Integer.toHexString(OxFF & bytes[i]);
if (hex.length == 1) {
sb.append('0');
}
sb.append(hex);
}
return sb.toString();
}
详细操作步骤:
获取到图片url将其转换成key,通过key获取到 Snapshot
对象
通过 Snapshot
对象获取到文件输入流就能读取到Bitmap对象(一般情况下不会去获取原始图片大小,会对获取的图片进行缩放,但使用 BitmapFactory.Options
对 FileInputStream
的缩放会存在问题,原因是 FileInputStream
是有序的文件流,而缩放图片要两次调用 decodeStream()
方法,这会导致第二次 decodeStream()
时返回null。
解决方法:通过文件流获取对应的文件描述符,再通过 BitmapFactory.decodeFileDescriptor()
方法加载获取缩放后的图片)
private ImageResizer mImageResizer = new ImageResizer();//自己封装的一个图片缩放封装类
Bitmap bitmap = null;
String key = hashKeyFromUrl(url);
DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key);
if (snapShot != null) {
FileInputStream fis = (FileInputStream) snapShot.getInputStream(DISK_CACHE_INDEX);
FileDescriptor fd = fis.getFD();
bitmap = mImageResizer.decodeSampleBitmapFromFileDescriptor(fd, reqWidth, reqHeight);
if (bitmap != null) {
addBitmapToMemoryCache(key, bitmap);
}
}
public class ImageResizer {
public ImageResizer() {}
private Bitmap decodeSampleBitmapFromFileDescriptor(FileDescriptor fd, int reqWidth, int reqHeight) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFileDescriptor(fd, null, options);
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
options.inJustDecodeBounds = false;
return BitmapFactory.decodeFileDescriptor(fd, null, options);
}
}