Android设备作为客服端,最明显的特点就是:将服务端数据按照某种格式展示给Android用户。图片又是其中最重要的数据加载格式,所以对图片加载和缓存的学习和掌握是一个Android开发人员必备的基础技能。本篇主要学习研究Bitmap的加载和Cache,Bitmap在Android中指的是一张图片,常见的有JPG和PNG格式。由于Bitmap的特殊性和Android对单个应用所限制的内存空间(16M)。很容易导致内存溢出,所以日常开发中,我们尤其需要注意Bitmap的加载处理。BitmapFactory给我们提供了四类加载Bitmap的方法:decodeFile(文件系统)、decodeResource(资源)、decodeSream(输入流)和decodeByteArray(字节数组)。如何高效加载Bitmap,关键就在于处理图片展示的所需尺寸,使图片适配imageview。
接下来从图片压缩、LruCache创建和使用、DiskLruCache创建和使用、同步异步加载接口的设计。
图片压缩
public Bitmap decodeSampledBitmapFromResource(Resources res,
int resId, int reqWidth, int reqHeight) {
// 设置options.inJustDecodeBounds = true;只是获取图片的宽高信息,并不是真正的加载
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(res, resId, options);
// 计算 inSampleSize 压缩比率
options.inSampleSize = calculateInSampleSize(options, reqWidth,
reqHeight);
// 按照压缩比率真正加载图片
options.inJustDecodeBounds = false;
return BitmapFactory.decodeResource(res, resId, options);
}
public Bitmap decodeSampledBitmapFromFileDescriptor(FileDescriptor fd, int reqWidth, int reqHeight) {
// 设置options.inJustDecodeBounds = true;只是获取图片的宽高信息,并不是真正的加载
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFileDescriptor(fd, null, options);
// 计算 inSampleSize 压缩比率
options.inSampleSize = calculateInSampleSize(options, reqWidth,
reqHeight);
// 按照压缩比率真正加载图片
options.inJustDecodeBounds = false;
return BitmapFactory.decodeFileDescriptor(fd, null, options);
}
public int calculateInSampleSize(BitmapFactory.Options options,
int reqWidth, int reqHeight) {
if (reqWidth == 0 || reqHeight == 0) {
return 1;
}
// 获取图片原始宽高
final int height = options.outHeight;
final int width = options.outWidth;
Log.d(TAG, "origin, w= " + width + " h=" + height);
int inSampleSize = 1;
if (height > reqHeight || width > reqWidth) {
final int halfHeight = height / 2;
final int halfWidth = width / 2;
// 初始inSampleSize=1,为图片原大小,如果原大小大于要求的宽高
// 计算inSampleSize是否为比率的2的指数
// height and width larger than the requested height and width.
while ((halfHeight / inSampleSize) >= reqHeight
&& (halfWidth / inSampleSize) >= reqWidth) {
inSampleSize *= 2;
}
}
Log.d(TAG, "sampleSize:" + inSampleSize);
return inSampleSize;
}
图片的压缩处理主要通过BitmapFactory.Options参数来对图片进行采样缩放。通过以上代码实现可以达到缩放的目的以适配imageview。
LruCache创建和使用
LruCache是Android3.1所提供的一个缓存类,建议使用support-v4兼容的版本。LRUCache是一个泛型类,它内部采用了一个LinkedHashMap以强引用的方式缓存对象,提供get和put来完成缓存获取和添加。
LruCache创建:
private LruCache<String, Bitmap> mMemoryCache;
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;
}
};
实现sizeof方法,用来计算缓存对象的大小,总容量大小为当前进程的1/8.
LruCache的存取:
private void addBitmapToMemoryCache(String key, Bitmap bitmap) {
if (getBitmapFromMemCache(key) == null) {
mMemoryCache.put(key, bitmap);
}
}
private Bitmap getBitmapFromMemCache(String key) {
return mMemoryCache.get(key);
}
DiskLruCache创建和使用
DiskLruCache不属于Android sdk的一部分,但得到官方的推荐。在Android studio上使用,可直接在项目下的gradle下添加compile ‘com.jakewharton:disklrucache:2.0.2’ 即可直接使用。若在eclipse中使用,可以再GitHub下载jar包或者源码,稍微修改编译错误即可使用。
地址为:
DiskLruCache创建:
private DiskLruCache mDiskLruCache;
File diskCacheDir = getDiskCacheDir(mContext, "bitmap");
if (!diskCacheDir.exists()) {
diskCacheDir.mkdirs();
}
if (getUsableSpace(diskCacheDir) > DISK_CACHE_SIZE) {
try {
mDiskLruCache = DiskLruCache.open(diskCacheDir, 1, 1,
DISK_CACHE_SIZE);
mIsDiskLruCacheCreated = true;
} catch (IOException e) {
e.printStackTrace();
}
open方法共提供四个参数:第一个表示磁盘缓存在文件系统中的路径,默认是/sdcard/Android/data/package_name/cache目录,应用卸载会一并删除,可修改路径(如不需要删除)。第二个表示应用版本号,为1即可。第三个是单个节点数 ,为1即可。第四个表示缓存的总大小,这里设置为50M.
public File getDiskCacheDir(Context context, String uniqueName) {
boolean externalStorageAvailable = Environment
.getExternalStorageState().equals(Environment.MEDIA_MOUNTED);
final String cachePath;
if (externalStorageAvailable) {
cachePath = context.getExternalCacheDir().getPath();
} else {
cachePath = context.getCacheDir().getPath();
}
return new File(cachePath + File.separator + uniqueName);
}
private long getUsableSpace(File path) {
if (Build.VERSION.SDK_INT >= VERSION_CODES.GINGERBREAD) {
return path.getUsableSpace();
}
final StatFs stats = new StatFs(path.getPath());
return stats.getBlockSizeLong()* stats.getAvailableBlocksLong();
}
DiskLruCache的存取:
存取较LruCache更为复杂和麻烦,缓存添加操作通过editor完成,流程为:获取图片URL对应的key——根据key获取editor对象——editor获得输出流——editor.commit();
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();
}
private String hashKeyFormUrl(String url) {
String cacheKey;
try {
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();
}
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;
}
DiskLruCache的存取过程涉及到创建输出流然后再从网络下载图片加载到系统文件最后editor提交处理。
缓存获取
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;
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;
}
同步加载接口
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;
}
异步加载接口
public void asyncLoadBitmap(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);
mMainHandler.obtainMessage(MESSAGE_POST_RESULT, result).sendToTarget();
}
}
};
THREAD_POOL_EXECUTOR.execute(loadBitmapTask);
}
同步加载接口的整个流程很清晰,首先会从内部缓存添加,获取不到从DiskLruCache外部缓存添加,这个过程中会下载网络图片添加到内部缓存,网络下载也会直接添加到DiskLruCache缓存中。异步加载接口是为了使加载更为完善。
本文并非是本人原创代码,是从书中学习到的原生imageLoad的简单源码。原创作者是大神任玉刚。但是针对自身的项目需求,做过一些分析调整。若需要demo,可从大神的GitHub上下载。地址为