传送门☞轮子的专栏☞转载请注明☞http://blog.csdn.net/leverage_1229

上周为360全景项目引入了图片缓存模块。因为是在Android4.0平台以上运作,出于惯性,都会在设计之前查阅相关资料,尽量避免拿一些以前2.3平台积累的经验来进行类比处理。开发文档中有一个BitmapFun的示例,仔细拜读了一下,虽说围绕着Bitmap的方方面面讲得都很深入,但感觉很难引入到当前项目中去。

现在的图片服务提供者基本上都来源于网络。对于应用平台而言,访问网络属于耗时操作。尤其是在移动终端设备上,它的显著表现为系统的延迟时间变长、用户交互性变差等。可以想象,一个携带着这些问题的应用在市场上是很难与同类产品竞争的。
说明一下,本文借鉴了 Keegan小钢和安卓巴士的处理模板,主要针对的是4.0以上平台应用。2.3以前平台执行效果未知,请斟酌使用或直接略过:),当然更欢迎您把测试结果告知笔者。

1图片加载流程

首先,我们谈谈加载图片的流程,项目中的该模块处理流程如下:

在UI主线程中,从内存缓存中获取图片,找到后返回。找不到进入下一步;
在工作线程中,从磁盘缓存中获取图片,找到即返回并更新内存缓存。找不到进入下一步;
在工作线程中,从网络中获取图片,找到即返回并同时更新内存缓存和磁盘缓存。找不到显示默认以提示。

2内存缓存类(PanoMemCache)

这里使用Android提供的LruCache类,该类保存一个强引用来限制内容数量,每当Item被访问的时候,此Item就会移动到队列的头部。当cache已满的时候加入新的item时,在队列尾部的item会被回收。

public class PanoMemoryCache {// LinkedHashMap初始容量private static final int INITIAL_CAPACITY = 16;// LinkedHashMap加载因子private static final int LOAD_FACTOR = 0.75f;// LinkedHashMap排序模式private static final boolean ACCESS_ORDER = true;// 软引用缓存private static LinkedHashMap<String, SoftReference<Bitmap>> mSoftCache;// 硬引用缓存private static LruCache<String, Bitmap> mLruCache;public PanoMemoryCache() {// 获取单个进程可用内存的最大值// 方式一:使用ActivityManager服务(计量单位为M)/*int memClass = ((ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE)).getMemoryClass();*/// 方式二:使用Runtime类(计量单位为Byte)final int memClass = (int) Runtime.getRuntime().maxMemory();// 设置为可用内存的1/4(按Byte计算)final int cacheSize = memClass / 4;mLruCache = new LruCache<String, Bitmap>(cacheSize) {@Overrideprotected int sizeOf(String key, Bitmap value) {if(value != null) {// 计算存储bitmap所占用的字节数return value.getRowBytes() * value.getHeight();} else {return 0;}}@Overrideprotected void entryRemoved(boolean evicted, String key, Bitmap oldValue, Bitmap newValue) {if(oldValue != null) {// 当硬引用缓存容量已满时,会使用LRU算法将最近没有被使用的图片转入软引用缓存mSoftCache.put(key, new SoftReference<Bitmap>(oldValue));}}};/** 第一个参数:初始容量(默认16)* 第二个参数:加载因子(默认0.75)* 第三个参数:排序模式(true:按访问次数排序;false:按插入顺序排序)*/mSoftCache = new LinkedHashMap<String, SoftReference<Bitmap>>(INITIAL_CAPACITY, LOAD_FACTOR, ACCESS_ORDER) {private static final long serialVersionUID = 7237325113220820312L;@Overrideprotected boolean removeEldestEntry(Entry<String, SoftReference<Bitmap>> eldest) {if(size() > SOFT_CACHE_SIZE) {return true;}return false;}};}/*** 从缓存中获取Bitmap* @param url* @return bitmap*/public Bitmap getBitmapFromMem(String url) {Bitmap bitmap = null;// 先从硬引用缓存中获取synchronized (mLruCache) {bitmap = mLruCache.get(url);if(bitmap != null) {// 找到该Bitmap之后,将其移到LinkedHashMap的最前面,保证它在LRU算法中将被最后删除。mLruCache.remove(url);mLruCache.put(url, bitmap);return bitmap;}}// 再从软引用缓存中获取synchronized (mSoftCache) {SoftReference<Bitmap> bitmapReference = mSoftCache.get(url);if(bitmapReference != null) {bitmap = bitmapReference.get();if(bitmap != null) {// 找到该Bitmap之后,将它移到硬引用缓存。并从软引用缓存中删除。mLruCache.put(url, bitmap);mSoftCache.remove(url);return bitmap;} else {mSoftCache.remove(url);}}}return null;}/*** 添加Bitmap到内存缓存* @param url* @param bitmap*/public void addBitmapToCache(String url, Bitmap bitmap) {if(bitmap != null) {synchronized (mLruCache) {mLruCache.put(url, bitmap);  }}}/*** 清理软引用缓存*/public void clearCache() {mSoftCache.clear();mSoftCache = null;}
}

补充一点,由于4.0平台以后对SoftReference类引用的对象调整了回收策略,所以该类中的软引用缓存实际上没什么效果,可以去掉。2.3以前平台建议保留。

3磁盘缓存类(PanoDiskCache)

public class PanoDiskCache {private static final String TAG = "PanoDiskCache";// 文件缓存目录private static final String CACHE_DIR = "panoCache";private static final String CACHE_FILE_SUFFIX = ".cache";private static final int MB = 1024 * 1024;private static final int CACHE_SIZE = 10; // 10Mprivate static final int SDCARD_CACHE_THRESHOLD = 10;public PanoDiskCache() {// 清理文件缓存removeCache(getDiskCacheDir());}/*** 从磁盘缓存中获取Bitmap* @param url* @return*/public Bitmap getBitmapFromDisk(String url) {String path = getDiskCacheDir() + File.separator + genCacheFileName(url);File file = new File(path);if(file.exists()) {Bitmap bitmap = BitmapFactory.decodeFile(path);if(bitmap == null) {file.delete();} else {updateLastModified(path);return bitmap;}}return null;}/*** 将Bitmap写入文件缓存* @param bitmap* @param url*/public void addBitmapToCache(Bitmap bitmap, String url) {if(bitmap == null) {return;}// 判断当前SDCard上的剩余空间是否足够用于文件缓存if(SDCARD_CACHE_THRESHOLD > calculateFreeSpaceOnSd()) {return;}String fileName = genCacheFileName(url);String dir = getDiskCacheDir();File dirFile = new File(dir);if(!dirFile.exists()) {dirFile.mkdirs();}File file = new File(dir + File.separator + fileName);try {file.createNewFile();FileOutputStream out = new FileOutputStream(file);bitmap.compress(Bitmap.CompressFormat.JPEG, 100, out);out.flush();out.close();} catch (FileNotFoundException e) {Log.e(TAG, "FileNotFoundException");} catch (IOException e) {Log.e(TAG, "IOException");}}/*** 清理文件缓存* 当缓存文件总容量超过CACHE_SIZE或SDCard的剩余空间小于SDCARD_CACHE_THRESHOLD时,将删除40%最近没有被使用的文件* @param dirPath* @return*/private boolean removeCache(String dirPath) {File dir = new File(dirPath);File[] files = dir.listFiles();if(files == null || files.length == 0) {return true;}if(!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {return false;}int dirSize = 0;for (int i = 0; i < files.length; i++) {if(files[i].getName().contains(CACHE_FILE_SUFFIX)) {dirSize += files[i].length();}}if(dirSize > CACHE_SIZE * MB || SDCARD_CACHE_THRESHOLD > calculateFreeSpaceOnSd()) {int removeFactor = (int) (0.4 * files.length + 1);Arrays.sort(files, new FileLastModifiedSort());for (int i = 0; i < removeFactor; i++) {if(files[i].getName().contains(CACHE_FILE_SUFFIX)) {files[i].delete();}}}if(calculateFreeSpaceOnSd() <= SDCARD_CACHE_THRESHOLD) {return false;}return true;}/*** 更新文件的最后修改时间* @param path*/private void updateLastModified(String path) {File file = new File(path);long time = System.currentTimeMillis();file.setLastModified(time);}/*** 计算SDCard上的剩余空间* @return*/private int calculateFreeSpaceOnSd() {StatFs stat = new StatFs(Environment.getExternalStorageDirectory().getPath());double sdFreeMB = ((double) stat.getAvailableBlocks() * (double) stat.getBlockSize()) / MB;return (int) sdFreeMB;}/*** 生成统一的磁盘文件后缀便于维护* 从URL中得到源文件名称,并为它追加缓存后缀名.cache* @param url* @return 文件存储后的名称*/private String genCacheFileName(String url) {String[] strs = url.split(File.separator);return strs[strs.length - 1] + CACHE_FILE_SUFFIX;}/*** 获取磁盘缓存目录* @return*/private String getDiskCacheDir() {return getSDPath() + File.separator + CACHE_DIR;}/*** 获取SDCard目录* @return*/private String getSDPath() {File sdDir = null;// 判断SDCard是否存在boolean sdCardExist = Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED);if(sdCardExist) {// 获取SDCard根目录sdDir = Environment.getExternalStorageDirectory();}if(sdDir != null) {return sdDir.toString();} else {return "";}}/*** 根据文件最后修改时间进行排序*/private class FileLastModifiedSort implements Comparator<File> {@Overridepublic int compare(File lhs, File rhs) {if(lhs.lastModified() > rhs.lastModified()) {return 1;} else if(lhs.lastModified() == rhs.lastModified()) {return 0;} else {return -1;}}}
}

4图片工具类(PanoUtils)

4.1从网络上获取图片:downloadBitmap()

 /*** 从网络上获取Bitmap,并进行适屏和分辨率处理。* @param context* @param url* @return*/public static Bitmap downloadBitmap(Context context, String url) {HttpClient client = new DefaultHttpClient();HttpGet request = new HttpGet(url);try {HttpResponse response = client.execute(request);int statusCode = response.getStatusLine().getStatusCode();if(statusCode != HttpStatus.SC_OK) {Log.e(TAG, "Error " + statusCode + " while retrieving bitmap from " + url);return null;}HttpEntity entity = response.getEntity();if(entity != null) {InputStream in = null;try {in = entity.getContent();return scaleBitmap(context, readInputStream(in));} finally {if(in != null) {in.close();in = null;}entity.consumeContent();}}} catch (IOException e) {request.abort();Log.e(TAG, "I/O error while retrieving bitmap from " + url, e);} catch (IllegalStateException e) {request.abort();Log.e(TAG, "Incorrect URL: " + url);} catch (Exception e) {request.abort();Log.e(TAG, "Error while retrieving bitmap from " + url, e);} finally {client.getConnectionManager().shutdown();}return null;} 

4.2从输入流读取字节数组,看起来是不是很眼熟啊!

public static byte[] readInputStream(InputStream in) throws Exception {ByteArrayOutputStream out = new ByteArrayOutputStream();byte[] buffer = new byte[1024];int len = 0;while((len = in.read(buffer)) != -1) {out.write(buffer, 0, len);}in.close();return out.toByteArray();} 

4.3对下载的源图片进行适屏处理,这也是必须的:)

/*** 按使用设备屏幕和纹理尺寸适配Bitmap* @param context* @param in* @return*/private static Bitmap scaleBitmap(Context context, byte[] data) {WindowManager windowMgr = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);DisplayMetrics outMetrics = new DisplayMetrics();windowMgr.getDefaultDisplay().getMetrics(outMetrics);int scrWidth = outMetrics.widthPixels;int scrHeight = outMetrics.heightPixels;BitmapFactory.Options options = new BitmapFactory.Options();options.inJustDecodeBounds = true;Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length, options);int imgWidth = options.outWidth;int imgHeight = options.outHeight;if(imgWidth > scrWidth || imgHeight > scrHeight) {options.inSampleSize = calculateInSampleSize(options, scrWidth, scrHeight);}options.inJustDecodeBounds = false;bitmap = BitmapFactory.decodeByteArray(data, 0, data.length, options);// 根据业务的需要,在此处还可以进一步做处理...return bitmap;}/*** 计算Bitmap抽样倍数* @param options* @param reqWidth* @param reqHeight* @return*/public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {// 原始图片宽高final int height = options.outHeight;final int width = options.outWidth;int inSampleSize = 1;if (height > reqHeight || width > reqWidth) {// 计算目标宽高与原始宽高的比值final int heightRatio = Math.round((float) height / (float) reqHeight);final int widthRatio = Math.round((float) width / (float) reqWidth);// 选择两个比值中较小的作为inSampleSize的值inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio;if(inSampleSize < 1) {inSampleSize = 1;}}return inSampleSize;}

5使用decodeByteArray()还是decodeStream()?

讲到这里,有童鞋可能会问我为什么使用BitmapFactory.decodeByteArray(data, 0, data.length, opts)来创建Bitmap,而非使用BitmapFactory.decodeStream(is, null, opts)。你这样做不是要多写一个静态方法readInputStream()吗?
没错,decodeStream()确实是该使用情景下的首选方法,但是在有些情形下,它会导致图片资源不能即时获取,或者说图片被它偷偷地缓存起来,交还给我们的时间有点长。但是延迟性是致命的,我们等不起。所以在这里选用decodeByteArray()获取,它直接从字节数组中获取,贴近于底层IO、脱离平台限制、使用起来风险更小。

6引入缓存机制后获取图片的方法

/*** 加载Bitmap* @param url* @return*/private Bitmap loadBitmap(String url) {// 从内存缓存中获取,推荐在主UI线程中进行Bitmap bitmap = memCache.getBitmapFromMem(url);if(bitmap == null) {// 从文件缓存中获取,推荐在工作线程中进行bitmap = diskCache.getBitmapFromDisk(url);if(bitmap == null) {// 从网络上获取,不用推荐了吧,地球人都知道~_~bitmap = PanoUtils.downloadBitmap(this, url);if(bitmap != null) {diskCache.addBitmapToCache(bitmap, url);memCache.addBitmapToCache(url, bitmap);}} else {memCache.addBitmapToCache(url, bitmap);}}return bitmap;}

7工作线程池化管理

有关多线程的切换问题以及在UI线程中执行loadBitmap()方法无效的问题,请参见另一篇博文: 使用严苛模式打破Android4.0以上平台应用中UI主线程的“独断专行”。
有关工作线程的处理方式,这里推荐使用定制线程池的方式,核心代码如下:

// 线程池初始容量
private static final int POOL_SIZE = 4;
private ExecutorService executorService;
@Override
public void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);// 获取当前使用设备的CPU个数int cpuNums = Runtime.getRuntime().availableProcessors();// 预开启线程池数目executorService = Executors.newFixedThreadPool(cpuNums * POOL_SIZE);...executorService.submit(new Runnable() {// 此处执行一些耗时工作,不要涉及UI工作。如果遇到,直接转交UI主线程pano.setImage(loadBitmap(url));});...}

我们知道,线程构造也是比较耗资源的。一定要对其进行有效的管理和维护。千万不要随意而行,一张图片的工作线程不搭理也许没什么,当使用场景变为ListView和GridView时,线程池化工作就显得尤为重要了。Android不是提供了AsyncTask吗?为什么不用它?其实AsyncTask底层也是靠线程池支持的,它默认分配的线程数是128,是远大于我们定制的executorService。

如何高效使用和管理Bitmap--图片缓存管理模块的设计与实现相关推荐

  1. Android之图片缓存管理

    如果每次加载同一张图片都要从网络获取,那代价实在太大了.所以同一张图片只要从网络获取一次就够了,然后在本地缓存起来,之后加载同一张图片时就从缓存中加载就可以了.从内存缓存读取图片是最快的,但是因为内存 ...

  2. Android图片缓存管理

    在一个app中,图片资源是处处存在的,加载图片的流程一般是: 1 先从缓存中读取 2 若缓存中不存在,从SD卡中读取 3 若SD卡中也不存在,则从服务器拉取 后两个步骤纯碎属于业务逻辑,暂且不表,这里 ...

  3. 图片缓存框架解析与设计

    今天讲解下图片缓存框架的解析与设计,在学习面向对象的任何框架之前,你要做的一件事情就是熟悉设计模式,废话不多说,直接进入主题,. 1.首先看一幅图,这幅图很清晰的告诉你,缓存框架要做哪些事情: 2 . ...

  4. [译]Flutter缓存管理库flutter_cache_manager

    本文翻译自pub: flutter_cache_manager | Flutter Package (flutter-io.cn) 译时版本: flutter_cache_manager 3.3.0 ...

  5. (附源码)基于SSM框架的图片分享及评价网站设计与实现 毕业设计201524

    ssm图片分享及评价网站 摘 要 大数据时代下,数据呈爆炸式地增长.为了迎合信息化时代的潮流和信息化安全的要求,利用互联网服务于其他行业,促进生产,已经是成为一种势不可挡的趋势.在图片分享及评价的要求 ...

  6. (附源码)ssm+mysql+基于SSM框架的图片分享及评价网站设计与实现 毕业设计201524

    ssm图片分享及评价网站 摘 要 大数据时代下,数据呈爆炸式地增长.为了迎合信息化时代的潮流和信息化安全的要求,利用互联网服务于其他行业,促进生产,已经是成为一种势不可挡的趋势.在图片分享及评价的要求 ...

  7. 基于SSM框架的图片分享及评价网站设计与实现 毕业设计-附源码201524

    ssm图片分享及评价网站 摘 要 大数据时代下,数据呈爆炸式地增长.为了迎合信息化时代的潮流和信息化安全的要求,利用互联网服务于其他行业,促进生产,已经是成为一种势不可挡的趋势.在图片分享及评价的要求 ...

  8. 基于SSM框架的图片分享及评价网站设计与实现毕业设计源码201524

    ssm图片分享及评价网站 摘 要 大数据时代下,数据呈爆炸式地增长.为了迎合信息化时代的潮流和信息化安全的要求,利用互联网服务于其他行业,促进生产,已经是成为一种势不可挡的趋势.在图片分享及评价的要求 ...

  9. Android 高效显示Bitmap图片

    Android 高效显示Bitmap图片 本文会介绍一些处理与加载Bitmap对象的常用方法,这些技术能够使得程序的UI不会被阻塞,并且可以避免程序超出内存限制.如果我们不注意这些,Bitmap会迅速 ...

最新文章

  1. swiftswift3.0自己封装的快速构建页面的方法
  2. 【蓝桥杯】 交换瓶子
  3. Android异步编程
  4. 【小白学习C++ 教程】十二、C++面向对象编程中的构造函数和析构函数
  5. 如何优雅的实现界面跳转 之 统跳协议 - DarwinNativeRouter
  6. 6.第一个程序Hello World
  7. 【计算机组成原理】双端口RAM和多模块存储器
  8. c++ 项目_罗纳尔多相信C罗从事技巧类项目,其成就不会亚于他在足坛的成绩
  9. idea package放在什么位置_NBA现役球员大排名,你会把20岁的东契奇放在什么位置?...
  10. 你的核心竞争力真的是技术么?
  11. 【简易设计模式07】适配器模式
  12. ExactScan Pro 18.12.24 Mac 破解版 万能扫描仪整合工具
  13. spring boot 1.x和 2.x通过代码修改默认address和端口
  14. Vue返回上一页保留数据
  15. 基于STM32F429IGT6的NAND FLASH读写测试(CUBEMX)
  16. hdu3397 线段树
  17. c中double型比较大小
  18. Eclipse中Android SDK Manager无法打开
  19. 使用PHP的curl爬取百度搜索页相关搜索词
  20. Mybatis的Dao层实现

热门文章

  1. Oracle数据库的四种启动方式
  2. win7桌面取消显示计算机,教您win7系统关闭右下角显示桌面功能的解决办法
  3. android mm 修改路径,Android 编译系统模块
  4. android 旋转生命周期,生命周期-如何区分方向更改和离开应用程序android
  5. linux 集成调试工具,linux下can调试工具canutils安装过程记录
  6. 如何填满word页面_Word应用分隔符的使用
  7. java完成一个学生信息调查程序_利用Java设计一个简单的学生信息管理程序
  8. 测试工程师,必须掌握的shell变量知识
  9. s2sh框架搭建mysql_S2SH项目框架搭建(完全注解)
  10. 采用Locust对grpc协议进行压测