Android 知识要点整理(10)----Bitmap缓存策略

Posted znapast

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android 知识要点整理(10)----Bitmap缓存策略相关的知识,希望对你有一定的参考价值。

当我们要加载数量众多的图片到ListView或者GridView的时候,来回滑动ListView或者GridView会导致图片不断的重复加载,如果重复从磁盘或者网络加载,显然会影响加载速度,给用户带来不好的体验。这个时候我们可以考虑使用内存缓存或者磁盘缓存,减少从磁盘或网络加载的次数,提升性能。
android系统为我们提供了内存缓存类LruCache和磁盘缓存类DiskLruCache类。下面我们重点讲一讲这两个类的用法,具体的内部实现原理不做深究。

1.内存缓存

Android 的V4支持库中提供了LruCache类,该类实现了最近最少使用算法用来在缓存空间不够时如何去掉一些缓存图片以释放空间。该类内部用一个LinkedHashMap来存储缓存Bitmap对象的强引用。
为了确定最适合的缓存空间大小,我们要从以下几个方面考虑:

  • 应用可用的内存大小
  • 屏幕上一次性呈现图片的数量
  • 设备的屏幕尺寸和屏幕密度
  • 要加载的Bitmap的尺寸信息和色值通道信息,可能占用的空间大小
  • 图片被访问的次数
  • 数量和质量的平衡;有时候缓存大量的低质量图片会更有用,然后在后台任务中去获取高质量图片

根据以上因素来确定合适的缓存大小。缓存空间太小或太大都不好,太小可能会增加额外的开销,太大有可能导致内存溢出。
下面是一个例子:

public class PhotoBitmapCache extends LruCache<String,Bitmap>
    private static final String TAG = PhotoBitmapCache.class.getSimpleName();
    private static PhotoBitmapCache mInstance;

    /**
     * 单例模式,私有化构造函数
     * @param cacheSize 总共的缓存空间
     */
    private PhotoBitmapCache(int cacheSize)
        super((cacheSize));
    

    /**
     * 获取缓存单例,保证全局只有一个实例
     * @return
     */
    public static synchronized PhotoBitmapCache getCache()
        if(mInstance ==null) 
            //获取虚拟机最大内存,即每个APP可用的内存
            int maxMemory = (int) Runtime.getRuntime().maxMemory() / 1024;
            //缓存空间设置为可用内存的1/4
            int cacheSize = maxMemory / 4;
            mInstance = new PhotoBitmapCache(cacheSize);
            Log.i(TAG,"maxMemory="+maxMemory+"KB;cacheSize="+cacheSize+"KB");
        

        return mInstance;
    

    @Override
    protected int sizeOf(String key, Bitmap value) 
        Log.i(TAG,key+"=>"+value.getByteCount()+"B");
        return value.getByteCount()/1024;
    

    /**
     * 加入缓存
     * @param key 键名 通常是图片路径
     * @param value 键值 Bitmap对象
     */
    public void addBitmapToMemCache(String key,Bitmap value)
        if(getBitmapFromMemCache(key) == null)
            put(key,value);
        
        Log.i(TAG,"free size=>"+(maxSize()-size()));
    

    /**
     * 从缓存中获取Bitmap对象
     * @param key 键名 通常是图片路径
     * @return Bitmap对象
     */
    public Bitmap getBitmapFromMemCache(String key)
        return get(key);
    

然后可以在加载图片的时候先判断缓存中是否有相应的图片:

    private void loadBitmap(ImageView pImageView,String filePath)
        if(cancelPotentialTask(pImageView,filePath))
            //先判断内存中是否有对应的图片
            Bitmap cachedBitmap = PhotoBitmapCache.getCache().get(filePath);
            if(cachedBitmap != null)
                pImageView.setImageBitmap(cachedBitmap);
                return;
            

            //若没有则在后台异步加载
            BitmapWorkerTask task = new BitmapWorkerTask(pImageView);
            //创建占位Drawable
            final AsyncDrawable asyncDrawable =
                    new AsyncDrawable(mPlaceHolderBitmap, task);
            pImageView.setImageDrawable(asyncDrawable);
            task.execute(filePath);

        
    

2.磁盘缓存

内存缓存可以提高加载图片的速度,但是我们不能完全依赖内存缓存,因为内存缓存空间有限,而且在进程被结束后缓存也不复存在,对于网络图片应用,下次启动还是需要先从网络上获取图片。这时我们可以考虑加上磁盘缓存,磁盘缓存在进程被结束了任然存在。

DiskLruCache

Android系统没有提供直接可用的DiskLruCache类,但是官网提供了DiskLruCache的一个实现。该类主要实现LRU算法逻辑,管理磁盘空间的分配和释放,但是没有提供类似DiskLruCache的get/put操作。为了使用更加方便,我们需要对其再进行封装。代码如下:

public class PhotoBitmapDiskCache 
    private static final int APP_VERSION = 1;
    private static final int VALUE_COUNT = 1;
    private static final String DEFAULT_CACHE_DIR = "PhotoBitmapDiskCache";
    private static final long DEFAULT_CACHE_SIZE = 100 * 1024 * 1024; // 100MB
    private static DiskLruCache mDiskCache;
    private static PhotoBitmapDiskCache mInstance;
    private static String mCacheDir;

    /**
     * 构造函数,为保证全局唯一性,私有化
     *
     * @param cacheDir  缓存目录
     * @param cacheSize 缓存空间的可用大小
     */
    private PhotoBitmapDiskCache(File cacheDir, long cacheSize) 
        try 
            mDiskCache = DiskLruCache.open(cacheDir, APP_VERSION, VALUE_COUNT, cacheSize);
         catch (IOException e) 
            e.printStackTrace();
        
    

    /**
     * 获取单例,可能耗时,所以建议在工作线程中调用
     *
     * @param cacheDir  缓存文件
     * @param cacheSize 缓存空间大小,已字节为单位
     * @return PhotoBitmapDiskCache
     */
    public static synchronized PhotoBitmapDiskCache getDiskCache(File cacheDir, long cacheSize) 
        if (mDiskCache == null || mInstance == null || !TextUtils.equals(cacheDir.getName(), mCacheDir)) 
            mInstance = new PhotoBitmapDiskCache(cacheDir, cacheSize);
            mCacheDir = cacheDir.getName();
        
        return mInstance;

    

   /**
     * 添加到磁盘缓存
     * @param key  键名,通常是图片路径
     * @param value Bitmap对象
     */
    public void put(String key, Bitmap value) 
        if (mDiskCache == null) return;
        OutputStream outputStream = null;
        DiskLruCache.Editor editor = null;
        try 
            //DiskLruCache的实现中直接用键名作为文件名
            //为了避免键名中出现“/”的路径符号,使用hash值作为真实的键名
            editor = mDiskCache.edit(key.hashCode() + "");
            outputStream = editor.newOutputStream(0);
            value.compress(Bitmap.CompressFormat.WEBP, 100, outputStream);
            editor.commit();
         catch (IOException e) 
            e.printStackTrace();
            try 
                editor.abort();
             catch (IOException e1) 
                e1.printStackTrace();
            
         finally 
            if (outputStream != null) 
                try 
                    outputStream.close();
                 catch (IOException e) 
                    e.printStackTrace();
                
            
        
    

   /**
     * 从磁盘缓存获取Bitmap对象
     * @param key  键名,通常是图片路径
     * @return Bitmap对象
     */
    public Bitmap get(String key) 
        if (mDiskCache == null) return null;
        InputStream inputStream = null;
        Bitmap bitmap = null;
        DiskLruCache.Editor editor = null;
        try 
            editor = mDiskCache.edit(key.hashCode() + "");

            inputStream = editor.newInputStream(0);
            bitmap = BitmapFactory.decodeStream(inputStream);
            editor.abort();
         catch (Exception e) 
            e.printStackTrace();
         finally 
            if (inputStream != null) 
                try 
                    inputStream.close();
                 catch (IOException e) 
                    e.printStackTrace();
                
            
        
        return bitmap;
    


修改BitmapWorkerTask类

从磁盘缓存加载图片的过程有可能是比较耗时的,所以这个工作应该放在后台线程中执行。修改后的BitmapWorkerTask代码如下:

/**
 * 异步加载图片类
 * Object[0] 图片resId,Object[1] Resouces 对象,
 * 或者
 * Object[0] file path
 * Bitmap 得到的Bitmap对象
 */
public class BitmapWorkerTask extends AsyncTask<Object, Void, Bitmap> 
    private final WeakReference<ImageView> imageViewReference;
    private final int requireWidth;
    private final int requireHeight;
    private final PhotoBitmapCache mMemCache;
    private final File mDiskCacheDir;
    private PhotoBitmapDiskCache mDiskCache;
    private int dataResId = 0;
    private String dataFilePath = null;
    private long mDiskCacheSize = 100*1024*1024; //100M

    public BitmapWorkerTask(final ImageView imageView) 
        // 弱引用,保证ImageView可以被正常回收
        imageViewReference = new WeakReference<ImageView>(imageView);
        mMemCache = PhotoBitmapCache.getCache();
        //此处获取磁盘缓存的目录,缓存类在doInBackground方法中实例化
        mDiskCacheDir = BitmapUtils.getDiskCacheDir(imageView.getContext(),"photodiskcache");

        requireWidth = imageView.getWidth();
        requireHeight = imageView.getHeight();
    

    // 在后台加载图片
    @Override
    protected Bitmap doInBackground(Object... params) 
        Bitmap bitmap = null;
        Object param0 = params[0];
        //先从磁盘缓存获取
        bitmap = getBitmapFromDiskCache(param0.toString());
        if(bitmap != null)
            mMemCache.addBitmapToMemCache(param0.toString(), bitmap);
            return bitmap;
        

        Log.e("DEBUG","get from others");
        //若缓存中没有,再从其他地方获取
        if(param0 instanceof Integer)
            Resources res = (Resources)params[1];
            dataResId = (int)param0;
            bitmap =  BitmapUtils.decodeSampledBitmapFromResource(res, dataResId, requireWidth, requireHeight);
        else if(param0 instanceof String)
            dataFilePath = (String)param0;
            Log.e("FUCK","PATH=>"+dataFilePath);
            bitmap = BitmapUtils.decodeSampledBitmapFromFile(dataFilePath,requireWidth,requireHeight);
        
        mMemCache.addBitmapToMemCache(param0.toString(), bitmap);

        //同时添加到磁盘缓存
        putBitmapToDiskCache(param0.toString(), bitmap);

        return bitmap;
    

    private void putBitmapToDiskCache(String key, Bitmap pBitmap) 
        if(mDiskCache == null)//实例化磁盘缓存类对象
            mDiskCache = PhotoBitmapDiskCache.getDiskCache(mDiskCacheDir,mDiskCacheSize);
        
        synchronized (BitmapWorkerTask.class) //线程间互斥
            Log.e("DEBUG","put into disk");
            mDiskCache.put(key,pBitmap);
        
    

    private Bitmap getBitmapFromDiskCache(String key) 
        if(mDiskCache == null)
            mDiskCache = PhotoBitmapDiskCache.getDiskCache(mDiskCacheDir,mDiskCacheSize);
        
        synchronized (BitmapWorkerTask.class) //线程间互斥
            Log.e("DEBUG","get from disk");
            return mDiskCache.get(key);
        
    

    // 完成后返回Bitmap给UI线程
    @Override
    protected void onPostExecute(Bitmap bitmap) 
        if(isCancelled())
            bitmap = null;
        

        if (imageViewReference != null && bitmap != null) 
            final ImageView imageView = imageViewReference.get();
            if (getBitmapWorkerTask(imageView)== this && imageView != null) //若imageview还没被回收则设置图片,否则啥都不做
                imageView.setImageBitmap(bitmap);
            
        
    

    public int getDataResId() 
        return dataResId;
    

    public String getDataFilePath() 
        return dataFilePath;
    

    //获取和ImageView关联的任务
    private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) 
        if (imageView != null) 
            final Drawable drawable = imageView.getDrawable();
            if (drawable instanceof AsyncDrawable) 
                final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
                return asyncDrawable.getBitmapWorkerTask();
            
        
        return null;
    

这样实现后就可以同时使用磁盘缓存和内存缓存了。

以上是关于Android 知识要点整理(10)----Bitmap缓存策略的主要内容,如果未能解决你的问题,请参考以下文章

Android 知识要点整理(12)----Animation(动画)

Android知识要点整理----控制相机

Android 知识要点整理(13)----网络连接

Android 知识要点整理(12)----Animation(动画)

Android知识要点整理----文件分享

Android知识要点整理(19)----Gradle 之构建变体