Bitmap的加载和cache
Bitmap的高效加载
1、如何加载Bitmap?
Bitmap在Android中是指一张图片。Android中BitmapFactory提供了四类方法:
1、decodeFile:从文件中加载
2、decodeResource:从资源中加载
3、decodeStream:输入流中加载
4、decodeByteArray:字节数组
2、如何高效加载Bitmap?
核心思想是采用BitmapFactory.Options来加载所需尺寸的图片。
.
BitmapFactory.Options缩放图片,需要一个采样率参数,主要用到了InSampleSize参数。
当InSampleSize为1时,采样后的图片为原始大小;当InSampleSize大于1时,例如2,采样后的图片的宽、高均为原来的1/2,像素则为原来的1/4,占有的内存大小也为原来的1/4;
注意:只有InSampleSize大于1时,图片才会有缩放效果。
3、获取图片的采样率?
1、将BitmapFactory.Options的inJustDecodeBounds设置为true,并加载图片。
2、从BitmapFactory.Options中读取图片的原始宽高信息,他们对应outWidth和outHeight参数。
3、根据采样率的规则并结合目标View的所需大小计算出采样率InSampleSize
44、将BitmapFactory.Options的inJustDecodeBounds设置为false,重新加载图片
编写工具类代码如下:
public class BitmapUtils {
private static final String TAG="BitmapUtils";
/**
* 用于返回压缩尺寸后的图片
*
* @param path 图片文件路径
* @param reqWidth 用于显示图片的目标ImageView宽度
* @param reqHeight 用于显示图片的目标ImageView高度
* @return
*/
public static Bitmap decodeSampledBitmap(String path,
int reqWidth, int reqHeight){
final BitmapFactory.Options options=new BitmapFactory.Options();
//设置inJustDecodeBounds为true,表示只加载图片的边框信息
options.inJustDecodeBounds=true;
//加载图片
BitmapFactory.decodeFile(path,options);
Log.e(TAG,"start "+options.inSampleSize);
//计算图片的采样率
options.inSampleSize = calculateInSampleSize(options, reqWidth,
reqHeight);
Log.e(TAG,"end "+options.inSampleSize);
//设置inJustDecodeBounds为false,重新加载图片
return BitmapFactory.decodeFile(path,options);
}
/**
* 计算图片的采样率
*
* @param options
* @param reqWidth
* @param reqHeight
* @return
*/
private static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
final int height=options.outHeight;
final int width=options.outWidth;
//原始图片采样率,设置为1
int inSampleSize=1;
if (height!=0&&width!=0){
//如果options输出的高度和宽度大于显示图片的ImageView的高度和宽度
if (height>reqHeight||width>reqHeight){
final int heightRatio=Math.round((float) height/(float) reqHeight);
final int widthRatio=Math.round((float)width/(float)reqWidth);
//计算出options输出的宽高和显示图片的imageView宽高比
inSampleSize=heightRatio<widthRatio?heightRatio:widthRatio;
}
}
return inSampleSize;
}
}
这个时候,我们如果要在尺寸为100*100像素的ImageView显示一张图片,如下使用:
//path为图片的路径
iv.setImageBitmap(BitmapUtils.decodeSampledBitmap(path,100,100));
Android中的缓存策略
缓存策略:
内存存储、设备存储、网络获取。
当请求一张图片时,首先从内存中获取;如果没有则从设备中获取;如果设备中也没有在从网络中下载。
一般来说,缓存策略主要包含缓存的添加、获取和删除这三类操作。
目前常用的一种缓存算法是LRU,LRU是近期最少使用算法。核心思想史,当缓存满时,会优先淘汰那些最少使用的缓存对象。采用LRU算法的缓存有两种:LruCache和DiskLruCache,LruCache用于实现内存缓存,而DiskLruCache则充当了存储设备缓存。
LruCache
LruCache是一个泛型类,它内部采用了一个LinkedHashMap以强引用的方式存储外接的缓存对象,并且提供了get和put方法来完成缓存的获取和添加操作。
先说一下引用类型:
强引用:直接的对象引用;
软引用:当一个对象只有软引用存在时,系统内存不足时此对象会被gc回收。
弱引用:当一个对象只有弱引用存在时,此对象会随时被gc回收。
LruCache是线程安全的:
public class LruCache<K, V> {
private final LinkedHashMap<K, V> map;
//初始化
public LruCache(int maxSize) {
if (maxSize <= 0) {
throw new IllegalArgumentException("maxSize <= 0");
}
this.maxSize = maxSize;
this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
}
....
}
上面代码中LruCache初始化时,我们需要给一个最大容量。一般设置为当前进程可用内存的1/8,单位是kb。
/**获取当前进程可用最大内存*/
int maxMemory= (int) (Runtime.getRuntime().maxMemory()/1024);
LruCache<String,Bitmap> lruCache=new LruCache<String, Bitmap>(maxMemory/8){
/**重写sizeOf方法计算缓存的大小*/
@Override
protected int sizeOf(String key, Bitmap value) {
return value.getRowBytes()*value.getHeight()/1024;
}
/**移除缓存对象需要调用次方法*/
@Override
protected void entryRemoved(boolean evicted, String key, Bitmap oldValue, Bitmap newValue) {
super.entryRemoved(evicted, key, oldValue, newValue);
}
};
/**获取缓存对象*/
lruCache.get(key);
/**存入缓存对象*/
lruCache.put(key,bitmap);
/**移除缓存对象*/
lruCache.remove(key);
DiskLruCache
DiskLruCache用于实现存储设备缓存。DiskLruCache并不是AndroidSDK的一部分。
下载地址
1、DiskLruCache的创建
//directory:表示磁盘缓存在文件系统中的存储路径。
//appVersion:应用的版本号
//valueCount:表示单个节点对应数据的个数,一般设置为1
//maxSize:表示缓存的总大小,比如50MB
public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
throws IOException {
if (maxSize <= 0) {
throw new IllegalArgumentException("maxSize <= 0");
}
if (valueCount <= 0) {
throw new IllegalArgumentException("valueCount <= 0");
}
// prefer to pick up where we left off
DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
if (cache.journalFile.exists()) {
try {
cache.readJournal();
cache.processJournal();
cache.journalWriter = new BufferedWriter(new FileWriter(cache.journalFile, true),
IO_BUFFER_SIZE);
return cache;
} catch (IOException journalIsCorrupt) {
// System.logW("DiskLruCache " + directory + " is corrupt: "
// + journalIsCorrupt.getMessage() + ", removing");
cache.delete();
}
}
// create a new empty cache
directory.mkdirs();
cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
cache.rebuildJournal();
return cache;
}
2、DiskLruCache的缓存添加
DiskLruCache添加的操作是通过Editor完成的,Editor表示一个缓存对象的编辑对象。首先获取到图片url所对应的key。
private String hashKeyFormUrl(String url) {
String cacheKey;
try {
/**使用url的MD5值作为key*/
final MessageDigest mDigest = MessageDigest.getInstance("MD5");
mDigest.update(url.getBytes());
cacheKey = bytesToHexString(mDigest.digest());
} catch (NoSuchAlgorithmException 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(0xFF & bytes[i]);
if (hex.length() == 1) {
sb.append('0');
}
sb.append(hex);
}
return sb.toString();
}
然后将图片的url转为key之后,就可以获取到Editor对象了。然后通过此Editor获取到一个文件输出流,需要注意的是:前面的DiskLruCache的open方法中设置了一个节点只能有一个数据,因此下面的DISK_CACHE_INDEX常量设置为0即可。
String key = hashKeyFormUrl(url);
DiskLruCache.Editor editor = mDiskLruCache.edit(key);
if (editor != null) {
OutputStream outputStream = editor.newOutputStream(DISK_CACHE_INDEX);
}
有了输出流之后,当网络下载图片时,就可以通过这个文件输出流写入到文件系统上。
public boolean downloadUrlToStream(String urlString,
OutputStream outputStream) {
HttpURLConnection urlConnection = null;
BufferedOutputStream out = null;
BufferedInputStream in = null;
try {
final URL url = new URL(urlString);
urlConnection = (HttpURLConnection) url.openConnection();
in = new BufferedInputStream(urlConnection.getInputStream(),
IO_BUFFER_SIZE);
out = new BufferedOutputStream(outputStream, IO_BUFFER_SIZE);
int b;
while ((b = in.read()) != -1) {
out.write(b);
}
return true;
} catch (IOException e) {
Log.e(TAG, "downloadBitmap failed." + e);
} finally {
if (urlConnection != null) {
urlConnection.disconnect();
}
MyUtils.close(out);
MyUtils.close(in);
}
return false;
}
然后呢,通过Editor.commit()来提交写入操作,如果图片下载发生异常,还可以通过Editor的abort()方法来回退整个操作。
OutputStream outputStream = editor.newOutputStream(DISK_CACHE_INDEX);
if (downloadUrlToStream(url, outputStream)) {
editor.commit();
} else {
editor.abort();
}
mDiskLruCache.flush();
3、DiskLruCache的缓存查找
和缓存的添加过程类似,缓存查找过程也需要将url转换为key,然后通过DiskLruCache的get方法分得到一个Snapshot对象,接着再通过Snapshot对象可以的到缓存的文件输出流,通过文件输出流就可以获取到bitmap对象。
Bitmap bitmap = null;
/**获取到键值*/
String key = hashKeyFormUrl(url);
DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key);
if (snapShot != null) {
FileInputStream fileInputStream = (FileInputStream)snapShot.getInputStream(DISK_CACHE_INDEX);
FileDescriptor fileDescriptor = fileInputStream.getFD();
bitmap = mImageResizer.decodeSampledBitmapFromFileDescriptor(fileDescriptor,
reqWidth, reqHeight);
if (bitmap != null) {
addBitmapToMemoryCache(key, bitmap);
}
}
自定义图片加载框架的实现
图片加载框架应该具备以下功能:
图片异步加载
图片的同步加载
图片压缩
内存缓存
磁盘缓存
网络获取
1、内存缓存和磁盘缓存的实现
private LruCache<String, Bitmap> mMemoryCache;
private DiskLruCache mDiskLruCache;
private ImageLoader(Context context) {
mContext = context.getApplicationContext();
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
int cacheSize = maxMemory / 8;
/**设置内存缓存大小为当前进程可用最大内存的1/8*/
mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap bitmap) {
return bitmap.getRowBytes() * bitmap.getHeight() / 1024;
}
};
File diskCacheDir = getDiskCacheDir(mContext, "bitmap");
if (!diskCacheDir.exists()) {
diskCacheDir.mkdirs();
}
//判断磁盘可用空间是否大于所需储存空间
if (getUsableSpace(diskCacheDir) > DISK_CACHE_SIZE) {
try {
/**设置硬盘缓存为50MB*/
mDiskLruCache = DiskLruCache.open(diskCacheDir, 1, 1,
DISK_CACHE_SIZE);
mIsDiskLruCacheCreated = true;
} catch (IOException e) {
e.printStackTrace();
}
}
}
然后完成缓存的添加和获取:
/**
* 添加bitmap到缓存中
* @param key
* @param bitmap
*/
private void addBitmapToMemoryCache(String key, Bitmap bitmap) {
if (getBitmapFromMemCache(key) == null) {
mMemoryCache.put(key, bitmap);
}
}
/**
* 根据key值获取缓存对象
* @param key
* @return
*/
private Bitmap getBitmapFromMemCache(String key) {
return mMemoryCache.get(key);
}
磁盘缓存的读取西药通过Snapshot来完成,通过Snap可以得到磁盘缓存对象对应的FileInputStream,但是FileInputStream无法便捷的进行压缩,所以通过FileDescriptor来加载压缩后的图片,最后将加载后的Bitmap添加到内存缓存中。
/**
* 从网络获取图片
* @param url
* @param reqWidth
* @param reqHeight
* @return
* @throws IOException
*/
private Bitmap loadBitmapFromHttp(String url, int reqWidth, int reqHeight)
throws IOException {
if (Looper.myLooper() == Looper.getMainLooper()) {
throw new RuntimeException("can not visit network from UI Thread.");
}
if (mDiskLruCache == null) {
return null;
}
String key = hashKeyFormUrl(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();
}
return loadBitmapFromDiskCache(url, reqWidth, reqHeight);
}
/**
* 从磁盘中读取缓存内容
* @param url
* @param reqWidth
* @param reqHeight
* @return
* @throws IOException
*/
private Bitmap loadBitmapFromDiskCache(String url, int reqWidth,
int reqHeight) throws IOException {
if (Looper.myLooper() == Looper.getMainLooper()) {
Log.w(TAG, "load bitmap from UI Thread, it's not recommended!");
}
if (mDiskLruCache == null) {
return null;
}
Bitmap bitmap = null;
/**获取到key值*/
String key = hashKeyFormUrl(url);
DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key);
if (snapShot != null) {
FileInputStream fileInputStream = (FileInputStream)snapShot.getInputStream(DISK_CACHE_INDEX);
FileDescriptor fileDescriptor = fileInputStream.getFD();
bitmap = mImageResizer.decodeSampledBitmapFromFileDescriptor(fileDescriptor,
reqWidth, reqHeight);
if (bitmap != null) {
//添加缓存对象到内存中
addBitmapToMemoryCache(key, bitmap);
}
}
return bitmap;
}
2、同步加载和异步加载接口的设计
同步加载接口需要外部在线程中调用,同步加载比较耗费时间。
同步加载实现:
/**
* load bitmap from memory cache or disk cache or network.
* @param uri http url
* @param reqWidth the width ImageView desired
* @param reqHeight the height ImageView desired
* @return bitmap, maybe null.
*/
public Bitmap loadBitmap(String uri, int reqWidth, int reqHeight) {
/**在内存中查找*/
Bitmap bitmap = loadBitmapFromMemCache(uri);
if (bitmap != null) {
Log.d(TAG, "loadBitmapFromMemCache,url:" + uri);
return bitmap;
}
/**在硬盘中查找*/
try {
bitmap = loadBitmapFromDiskCache(uri, reqWidth, reqHeight);
if (bitmap != null) {
Log.d(TAG, "loadBitmapFromDisk,url:" + uri);
return bitmap;
}
bitmap = loadBitmapFromHttp(uri, reqWidth, reqHeight);
Log.d(TAG, "loadBitmapFromHttp,url:" + uri);
} catch (IOException e) {
e.printStackTrace();
}
/**网络加载*/
if (bitmap == null && !mIsDiskLruCacheCreated) {
Log.w(TAG, "encounter error, DiskLruCache is not created.");
bitmap = downloadBitmapFromUrl(uri);
}
return bitmap;
}
需要注意的是:上面这个方法不能再主线程中使用,否则会抛出异常。
if (Looper.myLooper() == Looper.getMainLooper()) {
throw new RuntimeException("can not visit network from UI Thread.");
}
在这类进行了判断,判断当前的Looper是否为主线程的Looper,如果是主线程,就抛出异常。
异步加载实现:
public void bindBitmap(final String uri, final ImageView imageView) {
bindBitmap(uri, imageView, 0, 0);
}
public void bindBitmap(final String uri, final ImageView imageView,
final int reqWidth, final int reqHeight) {
imageView.setTag(TAG_KEY_URI, uri);
/**内存读取*/
Bitmap bitmap = loadBitmapFromMemCache(uri);
if (bitmap != null) {
imageView.setImageBitmap(bitmap);
return;
}
Runnable loadBitmapTask = new Runnable() {
@Override
public void run() {
/**使用线程池加载图片*/
Bitmap bitmap = loadBitmap(uri, reqWidth, reqHeight);
if (bitmap != null) {
LoaderResult result = new LoaderResult(imageView, uri, bitmap);
//发送消息,通过Handler中转
mMainHandler.obtainMessage(MESSAGE_POST_RESULT, result).sendToTarget();
}
}
};
THREAD_POOL_EXECUTOR.execute(loadBitmapTask);
}
bindBitmap中使用到了线程池和Handler。
/**线程池内,线程数量*/
private static final int CORE_POOL_SIZE = CPU_COUNT + 1;
//CPU核心数的2倍+1
private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;
//线程闲置超时时长为10秒
private static final long KEEP_ALIVE = 10L;
private static final ThreadFactory sThreadFactory = new ThreadFactory() {
private final AtomicInteger mCount = new AtomicInteger(1);
public Thread newThread(Runnable r) {
return new Thread(r, "ImageLoader#" + mCount.getAndIncrement());
}
};
/**THREAD_POOL_EXECUTOR 实现*/
public static final Executor THREAD_POOL_EXECUTOR = new ThreadPoolExecutor(
CORE_POOL_SIZE, MAXIMUM_POOL_SIZE,
KEEP_ALIVE, TimeUnit.SECONDS,
new LinkedBlockingQueue<Runnable>(), sThreadFactory);
为什么要采用线程池呢?
1、如果我们加载一张单个的图片,直接使用普通的线程加载就可以。但是如果列表中有大量的图片需要加载,随着列表滑动会产生大量的线程,整体效率会低下。
2、没有采用AsyncTask,AsyncTask在3.0以上无法实现并发效果,可以通过改造AsyncTask或者使用AsyncTask的executeOnexecutor方法的形式来执行异步任务。
这里选择线程池和Handler来提供ImageLoader的并发能力和访问UI的能力。
Handler的实现?
ImageLoader直接采用了主线程的Looper来构造Handler对象,这就使得ImageLoader可以在非主线程中构造了。
private Handler mMainHandler = new Handler(Looper.getMainLooper()) {
@Override
public void handleMessage(Message msg) {
LoaderResult result = (LoaderResult) msg.obj;
ImageView imageView = result.imageView;
imageView.setImageBitmap(result.bitmap);
String uri = (String) imageView.getTag(TAG_KEY_URI);
if (uri.equals(result.uri)) {
imageView.setImageBitmap(result.bitmap);
} else {
Log.w(TAG, "set image bitmap,but url has changed, ignored!");
}
};
};
在以上的代码中,为了解决View复用造成的列表错位问题,给ImageView设置图片之前,都会检查它的url有没有发生改变,如果发生改变就不在给它设置图片。
ImageLoader的使用
加载图片时使用方式:
/**首先给控件设置相应的tag标签*/
imageView.setTag(uri);
/**从左到右参数依次为图片的url、显示目标控件ImageView、显示的宽度、显示的高度*/
mImageLoader.bindBitmap(uri, imageView, mImageWidth, mImageWidth);