昨天看了《Android开发艺术探索》的第十二章,Bitmap的加载和Cache,里面实现了一个ImageLoader,还是涉及到了不少知道的,Bitmap的压缩,LruCache,DiskLruCache,同步异步加载,线程池,把这些知识综合起来完成了一个图片加载类ImageLoader。根据书中知识来大概分析一下这个类,免得过段时间又啥都记不得了。
为了高效加载Bitmap,我们有时候需要对Bitmap进行压缩再显示到ImageView上,这里先提供一个图片压缩类ImageResizer。主要利用BitmapFactory.Options及其inSampleSize参数。首先获得图片的原始宽高,然后根据期望的宽高计算出缩放比率inSampleSize值,这里的inSample不会小于1,等于1时就代表不缩放,等于2时就代表长宽各为原来的1/2。且inSampleSize一般为2的倍数,文档中是这样描述的,当然也可以不遵守。这个类比较简单,简单看一下代码,我自己添了一些注释。
/**
*图片缩放
*/
public class ImageResizer {
public ImageResizer() {
}
public Bitmap resizeBitmapFromResources(Resources res, int resId, int reqWidth, int reqHeight) {
BitmapFactory.Options options = new BitmapFactory.Options();
//设置为true之后,不为bitmap分配内存,返回null,但是options会被赋值,以此来获取图片大小
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(res, resId, options);
//计算出合适的缩放比例
options.inSampleSize = caculateInSampleSize(options, reqWidth, reqHeight);
options.inJustDecodeBounds = false;
return BitmapFactory.decodeResource(res, resId, options);
}
public Bitmap resizeBitmapFromFileDescriptor(FileDescriptor fd, int reqWidth, int reqHeight) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFileDescriptor(fd, null, options);
options.inSampleSize = caculateInSampleSize(options, reqWidth, reqHeight);
options.inJustDecodeBounds = false;
return BitmapFactory.decodeFileDescriptor(fd, null, options);
}
public int caculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
if (reqWidth == 0 || reqHeight == 0) {
return 1;
}
int inSampleSize = 1;
//图片的原始宽高
final int originWidth = options.outWidth;
final int originHeight = options.outHeight;
if (originWidth > reqWidth || originHeight > reqHeight) {
final int halfWidth = originWidth / 2;
final int halfHeight = originHeight / 2;
//计算宽高都达到预期标准时的inSampleSize,根据文档描述,一般为2的倍数
while ((halfWidth / inSampleSize) > reqWidth && (halfHeight / inSampleSize) > reqHeight) {
inSampleSize *= 2;
}
}
return inSampleSize;
}
}
接着说说图片加载,作为用户,我的期望肯定不是每次都去网络上加载图片。作为开发者,我们就必须提供一套缓存系统,当用户要拉取一张图片的时候,我们先检查内存缓存中有没有,然后再检查磁盘缓存中有没有,前两者都没有的话,最后再从网络上拉取。那么如何实现内存缓存和磁盘缓存呢?先说一下内存缓存,使用的是LruCache,从Android3.0开始,LruCache已经成为安卓的源代码,所以这也是Google推荐的内存缓存方式,即Least Recently Used,最近最少使用算法,当缓存达到峰值时,会自动清除最近最少使用的数据。磁盘缓存使用的是DiskLruCache,同样这也是Google推荐的缓存方式,但是还没有写入源码中,这个类我会附在文章末尾。原理和LruCache是一样的。在初始化ImageLoader的时候,会同时创建LruCache和DiskLruCache,看一下代码,
/**
* 私有构造函数,不能通过new来产生实例
* 主要完成了mLruCache和mDiskLruCache的初始化
*/
private ImageLoader(Context context) {
mContext = context.getApplicationContext();//传入Application级别的context
//应用程序可用的最大内存
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
//取最大内存的1/8作为内存缓存的大小
int cacheSize = maxMemory / 8;
/**
* 初始化内存缓存
* 返回缓存对象的大小,单位要和cacheSize一样
*/
mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap value) {
return value.getRowBytes() * value.getHeight() / 1024;
}
};
File diskCacheFile = getDiskCacheFile(mContext, DISK_CACAHE_NAME);
if (!diskCacheFile.exists()) {
diskCacheFile.mkdirs();
}
//确保该路径下的可用空间大于磁盘缓存需要的最大空间
if (getUsableSpace(diskCacheFile) > DISK_CACHE_SIZE) {
try {
mDiskLruCache = DiskLruCache.open(diskCacheFile, Utils.getAppVersion(mContext), 1, DISK_CACHE_SIZE);
mIsDiskCacheCreated = true;
} catch (IOException e) {
e.printStackTrace();
}
}
}
这里我们取应用程序最大可用内存的1/8作为内存缓存大小,取50MB作为磁盘缓存大小,当然要判断一下磁盘是否有50MB的剩余空间。初始化两种缓存后,给两种缓存加上添加和读取方法。
内存缓存:
/**
* 向内存缓存中添加Bitmap
*/
private void addBitmapToMemoryCache(String key, Bitmap bitmap) {
if (getBitmapFromMemoryCache(key) == null) {
mMemoryCache.put(key, bitmap);
}
}
/**
* 根据key获取内存缓存中的Bitmap
*/
private Bitmap getBitmapFromMemoryCache(String key) {
return mMemoryCache.get(key);
}
再来看看磁盘缓存的添加和读取。
String key = hashKeyFormUrl(url);
try {
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();
}
} catch (IOException e) {
e.printStackTrace();
}
/**
* 从磁盘缓存中加载图片,加载成功后添加到内存缓存
*/
private Bitmap loadBitmapFromDiskCache(String url, int reqWidth, int reqHeight) {
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);
try {
DiskLruCache.Snapshot snapshot = mDiskLruCache.get(key);
if (snapshot != null) {
FileInputStream inputStream = (FileInputStream) snapshot.getInputStream(DISK_CACHE_INDEX);
FileDescriptor descriptor = inputStream.getFD();
bitmap = mImageResizer.resizeBitmapFromFileDescriptor(descriptor, reqWidth, reqHeight);
if (bitmap != null) {
addBitmapToMemoryCache(key, bitmap);
}
}
} catch (IOException e) {
e.printStackTrace();
}
return bitmap;
}
这里有些地方需要注意,代码中把所有的url经过MD5加密之后当做key,其实并不是为了去加密,因为url中可能包含特殊字符,为了避免不必要的错误,经过MD5加密后使用是一样的。
接着提供同步下载和异步下载。先来看看同步加载的逻辑,首先尝试从内存缓存中读取图片,若没有就尝试从磁盘缓存中读取,最后从网络拉取。看代码:
/**
* 同步加载
*/
public Bitmap loadBitmap(String url, int reqWidth, int reqHeight) {
Bitmap bitmap = loadBitmapFromMemCache(url);
if (bitmap != null) {
Log.d(TAG, "from MemoryCache; url:" + url);
return bitmap;
}
bitmap = loadBitmapFromDiskCache(url, reqWidth, reqHeight);
if (bitmap != null) {
Log.d(TAG, "from DiskCache; url:" + url);
return bitmap;
}
bitmap = loadBitmapFromNet(url, reqWidth, reqHeight);
Log.d(TAG, "from Net; url:" + url);
if (bitmap == null && !mIsDiskCacheCreated) {
Log.e("TAG", "encounter error, DiskLruCache is not created.");
bitmap = downloadBitmapFromUrl(url);
}
return bitmap;
}
这个方法不能再主线程中调用,看看loadBitmapFromNet中的一段代码,
if (Looper.myLooper() == Looper.getMainLooper()) {
throw new RuntimeException("can not visit network from UI Thread.");
}
在主线程中调用时会抛出异常。
接着看看异步加载的逻辑。首先会尝试从内存缓存中读取,成功则直接返回,反之使用线程池的loadBitmap方法,也就是刚才的同步下载的方法,通过Handler+Message更新UI,这样就可以在子线程中调用了。
/**
* 异步下载
* 根据url将图片显示在ImageView上,依次顺序是内存缓存,磁盘缓存,网络下载
*
* @param url 图片url
* @param imageView 绑定的ImageView
* @param reqWidth 期望的宽度
* @param reqHeight 期望的高度
*/
public void bindBitmap(final String url, final ImageView imageView, final int reqWidth, final int reqHeight) {
imageView.setTag(TAG_KEY_URI, url);
Bitmap bitmap = loadBitmapFromMemCache(url);
if (bitmap != null) {
imageView.setImageBitmap(bitmap);
return;
}
Runnable loadBitmapTask = new Runnable() {
@Override
public void run() {
Bitmap bitmap = loadBitmap(url, reqWidth, reqHeight);
if (bitmap != null) {
LoaderResult result = new LoaderResult(imageView, url, bitmap);
mMainHandler.obtainMessage(MESSAGE_POST_RESULT, result).sendToTarget();
}
}
};
THREAD_POOR_EXECUTOR.execute(loadBitmapTask);
}
这里主要线程池和Handler的知识。先来看看线程池THREAD_POOL_EXECUTOR的实现。我们取CPU核心数+1作为核心线程数,取CPU核心数*2+1作为最大线程数,取10s作为线程闲置超时时间,代码如下:
//线程工厂
private static final ThreadFactory sThreadFactory = new ThreadFactory() {
//AtomicInteger,线程安全的加减操作接口
private final AtomicInteger mCount = new AtomicInteger(1);
@Override
public Thread newThread(Runnable r) {
return new Thread(r, "ImageLoader #" + mCount.getAndIncrement());
}
};
//线程池
public static final Executor THREAD_POOR_EXECUTOR = new ThreadPoolExecutor(
CORE_POOL_SIZE, MAX_POOL_SIZE, KEEP_ALIVE_TIME, TimeUnit.SECONDS,
new LinkedBlockingQueue<Runnable>(), sThreadFactory);
在大量并发的情况下使用线程池的优点是很明显的。接着看看里面的Handler机制,这里直接采用主线程的Looper来构造Handler对象,使得ImageLoader可以在非主线程构造,通过发送消息通知更新UI。如下所示:
private Handler mMainHandler = new Handler(Looper.getMainLooper()) {
@Override
public void handleMessage(Message msg) {
LoaderResult result = (LoaderResult) msg.obj;
ImageView imageView = result.imageView;
String uri = (String) imageView.getTag(TAG_KEY_URI);
if (uri.equals(result.url)) {
imageView.setImageBitmap(result.bitmap);
} else {
Log.w(TAG, "set image bitmap,but url has changed, ignored!");
}
}
};
这里采用Tag标记的方式防止列表错位。
大概就是这么多内容了,里面应该有很多主席书中的话,书不能带在身边,就得做做笔记了。下面贴一下完整的ImageLoader类,我自己添加了注释。
/**
* Created by sun on 2016/1/27.
* LruCache+DiskLruCache+ThreadPool综合应用
*/
public class ImageLoader {
private static final String TAG = "ImageLoader";
private static final int TAG_KEY_URI = R.id.imageloader_uri;
private static final int DISK_CACHE_INDEX = 0;
public static final int MESSAGE_POST_RESULT = 1001;
//CPU核心数
private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
//核心线程数
private static final int CORE_POOL_SIZE = CPU_COUNT + 1;
//最大线程数
private static final int MAX_POOL_SIZE = CPU_COUNT * 2 + 1;
//线程存活时间
private static final long KEEP_ALIVE_TIME = 10;
//磁盘缓存目录名称
private static final String DISK_CACAHE_NAME = "bitmap";
//磁盘缓存大小限制,默认为50MB,可修改
private static final long DISK_CACHE_SIZE = 1024 * 1024 * 50;
//IO流大小
private static final int IO_BUFFER_SIZE = 8 * 1024;
//标记内存缓存是否建立
private boolean mIsDiskCacheCreated = false;
//内存缓存
private LruCache<String, Bitmap> mMemoryCache;
//磁盘缓存
private DiskLruCache mDiskLruCache;
//上下文
private Context mContext;
//图片缩放类
private ImageResizer mImageResizer = new ImageResizer();
//线程工厂
private static final ThreadFactory sThreadFactory = new ThreadFactory() {
//AtomicInteger,线程安全的加减操作接口
private final AtomicInteger mCount = new AtomicInteger(1);
@Override
public Thread newThread(Runnable r) {
return new Thread(r, "ImageLoader #" + mCount.getAndIncrement());
}
};
//线程池
public static final Executor THREAD_POOR_EXECUTOR = new ThreadPoolExecutor(
CORE_POOL_SIZE, MAX_POOL_SIZE, KEEP_ALIVE_TIME, TimeUnit.SECONDS,
new LinkedBlockingQueue<Runnable>(), sThreadFactory);
private Handler mMainHandler = new Handler(Looper.getMainLooper()) {
@Override
public void handleMessage(Message msg) {
LoaderResult result = (LoaderResult) msg.obj;
ImageView imageView = result.imageView;
String uri = (String) imageView.getTag(TAG_KEY_URI);
if (uri.equals(result.url)) {
imageView.setImageBitmap(result.bitmap);
} else {
Log.w(TAG, "set image bitmap,but url has changed, ignored!");
}
}
};
/**
* 私有构造函数,不能通过new来产生实例
* 主要完成了mLruCache和mDiskLruCache的初始化
*/
private ImageLoader(Context context) {
mContext = context.getApplicationContext();//传入Application级别的context
//应用程序可用的最大内存
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
//取最大内存的1/8作为内存缓存的大小
int cacheSize = maxMemory / 8;
/**
* 初始化内存缓存
* 返回缓存对象的大小,单位要和cacheSize一样
*/
mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap value) {
return value.getRowBytes() * value.getHeight() / 1024;
}
};
File diskCacheFile = getDiskCacheFile(mContext, DISK_CACAHE_NAME);
if (!diskCacheFile.exists()) {
diskCacheFile.mkdirs();
}
//确保该路径下的可用空间大于磁盘缓存需要的最大空间
if (getUsableSpace(diskCacheFile) > DISK_CACHE_SIZE) {
try {
mDiskLruCache = DiskLruCache.open(diskCacheFile, Utils.getAppVersion(mContext), 1, DISK_CACHE_SIZE);
mIsDiskCacheCreated = true;
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 提供ImageLoader实例
*/
public static ImageLoader build(Context context) {
return new ImageLoader(context);
}
public void bindBitmap(final String url, final ImageView imageView) {
bindBitmap(url, imageView, 0, 0);
}
/**
* 异步下载
* 根据url将图片显示在ImageView上,依次顺序是内存缓存,磁盘缓存,网络下载
*
* @param url 图片url
* @param imageView 绑定的ImageView
* @param reqWidth 期望的宽度
* @param reqHeight 期望的高度
*/
public void bindBitmap(final String url, final ImageView imageView, final int reqWidth, final int reqHeight) {
imageView.setTag(TAG_KEY_URI, url);
Bitmap bitmap = loadBitmapFromMemCache(url);
if (bitmap != null) {
imageView.setImageBitmap(bitmap);
return;
}
Runnable loadBitmapTask = new Runnable() {
@Override
public void run() {
Bitmap bitmap = loadBitmap(url, reqWidth, reqHeight);
if (bitmap != null) {
LoaderResult result = new LoaderResult(imageView, url, bitmap);
mMainHandler.obtainMessage(MESSAGE_POST_RESULT, result).sendToTarget();
}
}
};
THREAD_POOR_EXECUTOR.execute(loadBitmapTask);
}
/**
* 同步加载
* * @param url 图片url
* @param reqWidth 期望的宽度
* @param reqHeight 期望的高度
*/
public Bitmap loadBitmap(String url, int reqWidth, int reqHeight) {
Bitmap bitmap = loadBitmapFromMemCache(url);
if (bitmap != null) {
Log.d(TAG, "from MemoryCache; url:" + url);
return bitmap;
}
bitmap = loadBitmapFromDiskCache(url, reqWidth, reqHeight);
if (bitmap != null) {
Log.d(TAG, "from DiskCache; url:" + url);
return bitmap;
}
bitmap = loadBitmapFromNet(url, reqWidth, reqHeight);
Log.d(TAG, "from Net; url:" + url);
if (bitmap == null && !mIsDiskCacheCreated) {
Log.e("TAG", "encounter error, DiskLruCache is not created.");
bitmap = downloadBitmapFromUrl(url);
}
return bitmap;
}
/**
* 从内存缓存中加载图片
*/
private Bitmap loadBitmapFromMemCache(String url) {
final String key = hashKeyFormUrl(url);
return getBitmapFromMemoryCache(key);
}
/**
* 从磁盘缓存中加载图片,加载成功后添加到内存缓存
*/
private Bitmap loadBitmapFromDiskCache(String url, int reqWidth, int reqHeight) {
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);
try {
DiskLruCache.Snapshot snapshot = mDiskLruCache.get(key);
if (snapshot != null) {
FileInputStream inputStream = (FileInputStream) snapshot.getInputStream(DISK_CACHE_INDEX);
FileDescriptor descriptor = inputStream.getFD();
bitmap = mImageResizer.resizeBitmapFromFileDescriptor(descriptor, reqWidth, reqHeight);
if (bitmap != null) {
addBitmapToMemoryCache(key, bitmap);
}
}
} catch (IOException e) {
e.printStackTrace();
}
return bitmap;
}
/**
* 从网络加载图片流,缓存到磁盘,然后从磁盘缓存中获取图片
*/
private Bitmap loadBitmapFromNet(String url, int reqWidth, int reqHeight) {
if (Looper.myLooper() == Looper.getMainLooper()) {
throw new RuntimeException("can not visit network from UI Thread.");
}
if (mDiskLruCache == null) {
return null;
}
String key = hashKeyFormUrl(url);
try {
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();
}
} catch (IOException e) {
e.printStackTrace();
}
return loadBitmapFromDiskCache(url, reqWidth, reqHeight);
}
/**
* 直接从网络下载图片
*/
private Bitmap downloadBitmapFromUrl(String urlPath) {
Bitmap bitmap = null;
HttpURLConnection connection = null;
BufferedInputStream inputStream = null;
try {
URL url = new URL(urlPath);
connection = (HttpURLConnection) url.openConnection();
inputStream = new BufferedInputStream(connection.getInputStream(), IO_BUFFER_SIZE);
bitmap = BitmapFactory.decodeStream(inputStream);
} catch (Exception e) {
e.printStackTrace();
} finally {
if (connection != null) {
connection.disconnect();
}
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return bitmap;
}
private boolean downloadUrlToStream(String imageUrl, OutputStream outputStream) {
HttpURLConnection connection = null;
BufferedInputStream in = null;
BufferedOutputStream out = null;
try {
URL url = new URL(imageUrl);
connection = (HttpURLConnection) url.openConnection();
in = new BufferedInputStream(connection.getInputStream(), IO_BUFFER_SIZE);
out = new BufferedOutputStream(outputStream, IO_BUFFER_SIZE);
int b;
if ((b = in.read()) != -1) {
out.write(b);
}
return true;
} catch (Exception e) {
e.printStackTrace();
} finally {
if (connection != null) {
connection.disconnect();
}
try {
if (out != null) {
out.close();
}
if (in != null) {
in.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
return false;
}
/**
* 向内存缓存中添加Bitmap
*/
private void addBitmapToMemoryCache(String key, Bitmap bitmap) {
if (getBitmapFromMemoryCache(key) == null) {
mMemoryCache.put(key, bitmap);
}
}
/**
* 根据key获取内存缓存中的Bitmap
*/
private Bitmap getBitmapFromMemoryCache(String key) {
return mMemoryCache.get(key);
}
/**
* 获取磁盘缓存路径
* 放在此路径下当程序卸载时同时会清楚缓存数据
* 当然你也可以自定义其他缓存路径
*/
private File getDiskCacheFile(Context context, String uniqeName) {
String cachePath = null;
if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
cachePath = mContext.getExternalCacheDir().getPath();
} else {
cachePath = mContext.getCacheDir().getPath();
}
return new File(cachePath + File.separator + uniqeName);
}
/**
* 获取可用空间
*/
@TargetApi(Build.VERSION_CODES.GINGERBREAD)
private long getUsableSpace(File path) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) {
return path.getUsableSpace();
}
final StatFs statFs = new StatFs(path.getPath());
return (long) statFs.getBlockSize() * (long) statFs.getAvailableBlocks();
}
/**
* 以url作为tag防止加载时候错乱,为防止url有特殊字符,一律转化为MD5
*
* @param url 图片url
* @return MD5字符串
*/
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();
}
private static class LoaderResult {
public ImageView imageView;
public String url;
public Bitmap bitmap;
public LoaderResult(ImageView imageView, String url, Bitmap bitmap) {
this.imageView = imageView;
this.url = url;
this.bitmap = bitmap;
}
}
}
为了方便大家,提供一个可以直接使用的DiskLruCache类。下载链接:http://download.youkuaiyun.com/detail/sunluyao_/9420403
示例代码托管地址:https://github.com/lulululbj/DailyAndroid
有任何疑问,欢迎加群讨论:261386924