最近一直在和LruCache、DisLruCache、Bitmap打交道,了解了很多知识点,所以总结了写了个图片下载库,可以更直观深刻的体会其中。想给取个优雅的名字想不起来所以就叫NeacyImageLoader体验地址:https://github.com/Neacy/NeacyImageLoader
第一步,构建下载图片所需要的变量参数。
我们所需要的的两个参数分别是
private LruCache<String, Bitmap> mLruCache;
private DiskLruCache mDiskLruCache;
然后我们需要给这两个变量定义一些配置常量,比如说缓存空间地址,缓存空间大小,以及会用到的线程池配置等等。
/**
* 缓存空间大小
*/
private static final int CACHE_DIS_SIZE = 250 * 1024 * 1024;// 250M
private int CACHE_MEM_SIZE = (int) (Runtime.getRuntime().maxMemory() / 1024 / 8);// kb为单位
private static final String DIR_DISK_NAME = "image_cache";
private static final int IMAGE_TAG_KEY = R.id.neacy_imageloader_id;
/**
* 为线程池配置常量参数
*/
private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
private static final int CORE_THREAD_SIZE = CPU_COUNT + 1;
private static final int CORE_THREAD_MAX_SIZE = CPU_COUNT * 2 + 1;
private static final int THREAD_TIME = 10;
完成以上常量定义之后我们开始初始化LruCache和DiskLruCache两个缓存变量,并且我们整个Demo中缓存的是Bitmap对象。
初始化LruCache:
private void initMemoryCache() {
mLruCache = new LruCache<String, Bitmap>(CACHE_MEM_SIZE) {
@Override
protected int sizeOf(String key, Bitmap value) {
return (value.getRowBytes() * value.getHeight()) / 1024;// kb为单位
}
@Override
protected void entryRemoved(boolean evicted, String key, Bitmap oldValue, Bitmap newValue) {
super.entryRemoved(evicted, key, oldValue, newValue);
mEntryBitmaps.add(new SoftReference<Bitmap>(oldValue));
}
};
}
初始化DiskLruCache:
private void initDiskCache(Context context) {
File dir = new File(context.getExternalCacheDir().getPath() + File.separator + DIR_DISK_NAME);
if (!dir.exists()) {
dir.mkdirs();
}
try {
if (null == mDiskLruCache || mDiskLruCache.isClosed()) {
mDiskLruCache = DiskLruCache.open(dir, 1, 1, CACHE_DIS_SIZE);
}
} catch (IOException e) {
e.printStackTrace();
}
}
缓存对象肯定存在读取操作 我们我们也把读写操作写出来,这样在后面的Bitmap处理肯定会用到的。
/**
* 写入缓存
*/
public void addBitmaptoCache(String url, Bitmap bitmap) {
if (TextUtils.isEmpty(url) || bitmap == null) {
return;
}
if (null != mLruCache) {
mLruCache.put(url, bitmap);
}
}
/**
* 从LruCache中读取缓存
*/
public Bitmap getBitmapFromMemCache(String url) {
if (TextUtils.isEmpty(url) || mLruCache == null) return null;
return mLruCache.get(url);
}
/**
* 从DiskLruCache中读取缓存
*/
public Bitmap getBitmapFromDiskCache(String url) {
if (TextUtils.isEmpty(url) || mDiskLruCache == null) return null;
FileInputStream is = null;
Bitmap bitmap = null;
try {
DiskLruCache.Snapshot snapshot = mDiskLruCache.get(url);
if (null != snapshot) {
is = (FileInputStream) snapshot.getInputStream(0);
// 压缩图片
bitmap = ImageResizer.decodeBitmapFromFileDescriptor(is.getFD(), reqWidth, reqHeight, this);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
LoadUtil.closeInputSream(is);
}
return bitmap;
}
Ok,完成上面的缓存配置和读取功能后我们这个时候就要考虑高效的显示Bitmap问题了。
第二,高效显示Bitmap问题
我们先定义了这么一个set
private Set<SoftReference<Bitmap>> mEntryBitmaps = new HashSet<SoftReference<Bitmap>>();
做什么呢?用于存放当LruCache中的Bitmap由于缓存空间满了被移除的Bitmap然后在Options.inBitmap中重复利用起来,提高了整体的性能。其中的比对方式如是:
/**
* 读取inBitmap
*/
public Bitmap getInBitmap(BitmapFactory.Options option) {
Bitmap mResultBitmap = null;
if (!mEntryBitmaps.isEmpty()) {
Iterator<SoftReference<Bitmap>> it = mEntryBitmaps.iterator();
Bitmap item;
while(it.hasNext()) {
item = it.next().get();
if (item != null && item.isMutable()) {
if (isCanReuseinBitmap(item, option)) {
mResultBitmap = item;
it.remove();
break;
}
} else {
it.remove();
}
}
}
return mResultBitmap;
}
private boolean isCanReuseinBitmap(Bitmap bitmap, BitmapFactory.Options option) {
int width = option.outWidth / option.inSampleSize;
int heitht = option.outHeight / option.inSampleSize;
return bitmap.getWidth() == width && bitmap.getHeight() == heitht;
}
所以我们在压缩裁剪的时候调用这个方法就好了。
public static void getCanUseInBitmap(ImageLoader imageLoader, BitmapFactory.Options options) {
options.inMutable = true;
Bitmap inBitmap = imageLoader.getInBitmap(options);
if (null != inBitmap) {
options.inBitmap = inBitmap;
}
return;
}
所以一个完整的图片压缩裁剪的方法就完成了:
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFileDescriptor(fd, null, options);
options.inSampleSize = caculateInsampleSize(options, reqWidth, reqHeight);
options.inJustDecodeBounds = false;
getCanUseInBitmap(imageLoader, options);
return BitmapFactory.decodeFileDescriptor(fd, null, options);
其中caculateInsampleSize方法用以获取inSampleSize方法太多这里就不贴出来了。
第三、开始处理获取Bitmap
我们都是先从LruCache中先读取有的话就直接显示出来,没有的话读取DiskLruCache,最后再没有读取网络下载图片。
public void loadImage(final String url, final ImageView imageView) {
imageView.setTag(IMAGE_TAG_KEY, url);
final String key = LoadUtil.hashKeyForDisk(url);
final Bitmap bitmap = getBitmapFromMemCache(key);
if (null != bitmap) {
Log.w("Jayuchou", "--- load image from memory ---");
imageView.setImageBitmap(bitmap);
} else {
imageView.setImageResource(R.mipmap.ic_launcher);
// 开启线程 先读取磁盘中的图片 再没有数据读取网络的数据
Runnable mLoadRunnable = new Runnable() {
@Override
public void run() {
// 快速滑动的时候暂停操作
synchronized (mPauseLock) {
Log.w("Jayuchou", "---isPauseWork out = " + isPauseWork);
while (isPauseWork) {
Log.w("Jayuchou", "---isPauseWork = " + isPauseWork);
try {
mPauseLock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
Bitmap bmp = getBitmapFromDiskCache(key);
if (null == bmp) {
Log.w("Jayuchou", "--- load image from net ---");
try {
DiskLruCache.Editor editor = mDiskLruCache.edit(key);
if (null != editor) {
OutputStream os = editor.newOutputStream(0);
if (getBitmapFromHttp(url, os)) {
editor.commit();
bmp = getBitmapFromDiskCache(key);
addBitmaptoCache(key, bmp);// 加入缓存中
} else {
editor.abort();
}
}
} catch (IOException e) {
e.printStackTrace();
}
} else {
Log.w("Jayuchou", "--- load image from disk ---");
addBitmaptoCache(key, bmp);// 加入缓存中
}
LoadResult result = new LoadResult(url, bmp, imageView);
mMainHandler.obtainMessage(0, result).sendToTarget();
}
};
mImageLoaderExecutor.execute(mLoadRunnable);
}
}
其中getBitmapFromHttp这个方法就是纯粹的从网络下载数据并写入DiskLruCache中去。
我们将网络得到的Bitmap存入磁盘之后 再根据DisLruCache方法读取即可。然后我们把Bitmap封装到LoadResult类,使用Handler回调到主线程供ImageView显示,Handler中实现的代码片段:
public void handleMessage(Message msg) {
super.handleMessage(msg);
LoadResult result = (LoadResult) msg.obj;
ImageView imageView = result.getImageView();
String key = (String) imageView.getTag(IMAGE_TAG_KEY);
if (key.equals(result.getUrl())) {
imageView.setImageBitmap(result.getBitmap());
}
}
当然其中我们还使用到了线程池
private static final Executor mImageLoaderExecutor = new ThreadPoolExecutor(CORE_THREAD_SIZE, CORE_THREAD_MAX_SIZE,
THREAD_TIME, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(), mThreadFactory);
最后,还处理了下快速滑动的时候暂停线程加载,这样整体的性能表现的更优。用到的方法就是同步所的wait和notifyAll配对使用。
public void setPauseWork(boolean isPauseWork) {
synchronized (mPauseLock) {
this.isPauseWork = isPauseWork;
if (!isPauseWork) {// 当快速滑动停止的时候通知线程释放暂停操作
mPauseLock.notifyAll();
}
}
}
快速滑动的时候暂停操作:
// 快速滑动的时候暂停操作
synchronized (mPauseLock) {
while (isPauseWork) {
try {
mPauseLock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
然后对GridView配置监听快速滑动事件:
mGridView.setOnScrollListener(new AbsListView.OnScrollListener() {
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
switch (scrollState) {
case SCROLL_STATE_IDLE:// 快速滑动或者手指移开屏幕的时候继续加载
imageLoader.setPauseWork(false);
break;
case SCROLL_STATE_TOUCH_SCROLL:
break;
case SCROLL_STATE_FLING:// 手指快速滑动的时候暂停图片加载提高性能
imageLoader.setPauseWork(true);
break;
}
}
上面就是这个简单的图片下载库代码逻辑。
然后在Activity中使用只要按如下方式就好了。
imageLoader = new ImageLoader(getApplicationContext());
imageLoader.setReqWidthAndHeight(200, 200);
imageLoader.loadImage(Images.imageUrls[position], holder.image);
通过自己动手写了个简易的下载库,对世面常见的高级下载库就能更熟悉他们的套路了。