zoukankan      html  css  js  c++  java
  • [置顶] 如何高效使用和管理Bitmap--图片缓存管理模块的设计与实现

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

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

            现在的图片服务提供者基本上都来源于网络。对于应用平台而言,访问网络属于耗时操作。尤其是在移动终端设备上,它的显著表现为系统的延迟时间变长、用户交互性变差等。可以想象,一个携带着这些问题的应用在市场上是很难与同类产品竞争的。
            说明一下,本文借鉴了 Keegan小钢和安卓巴士的处理模板,主要针对的是4.0以上平台应用。2.3以前平台执行效果未知,请斟酌使用或直接略过:),当然更欢迎您把测试结果告知笔者。
    一、图片加载流程
            首先,我们谈谈加载图片的流程,项目中的该模块处理流程如下:
    1.在UI主线程中,从内存缓存中获取图片,找到后返回。找不到进入下一步;
    2.在工作线程中,从磁盘缓存中获取图片,找到即返回并更新内存缓存。找不到进入下一步;
    3.在工作线程中,从网络中获取图片,找到即返回并同时更新内存缓存和磁盘缓存。找不到显示默认以提示。

    二、内存缓存类(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) {
                @Override
                protected int sizeOf(String key, Bitmap value) {
                    if(value != null) {
                        // 计算存储bitmap所占用的字节数
                        return value.getRowBytes() * value.getHeight();
                    } else {
                        return 0;
                    }
                }
                
                @Override
                protected 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;
                @Override
                protected 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以前平台建议保留。
    三、磁盘缓存类(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; // 10M
        private 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> {
            @Override
            public int compare(File lhs, File rhs) {
                if(lhs.lastModified() > rhs.lastModified()) {
                    return 1;
                } else if(lhs.lastModified() == rhs.lastModified()) {
                    return 0;
                } else {
                    return -1;
                }
            }
        }
    }

    四、图片工具类(PanoUtils)
    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;
        }   

    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();
        }    

    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;
        }

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

    /**
         * 加载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;
        }

    七、工作线程池化
            有关多线程的切换问题以及在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。

  • 相关阅读:
    LeetCode 811. Subdomain Visit Count (子域名访问计数)
    LeetCode 884. Uncommon Words from Two Sentences (两句话中的不常见单词)
    LeetCode 939. Minimum Area Rectangle (最小面积矩形)
    LeetCode 781. Rabbits in Forest (森林中的兔子)
    LeetCode 739. Daily Temperatures (每日温度)
    三种方式实现按钮的点击事件
    239. Sliding Window Maximum
    14.TCP的坚持定时器和保活定时器
    13.TCP的超时与重传
    12.TCP的成块数据流
  • 原文地址:https://www.cnblogs.com/jiangu66/p/3198999.html
Copyright © 2011-2022 走看看