Android 图片缓存

前言

本篇主要包含两个方面的内容:

  1. 图片的加载和优化图片的加载
  2. LruCache,DisLruCache的使用与图片的三级缓存

图片的加载和优化

我们在编写Android程序的时候经常要用到很多的图片,在大多数情况下,这些图片都会大于我们程序所需要的大小。我们编写的应用程序都是有一定的内存限制,程序占用了过高的内存就容易出现OOM(Out Of Memory)异常。如下代码可以看出每个应用程序最高可用内存:

    //查询应用程序可用的最大内存
        int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
        Log.e("MainActivity", "Max memory is " + maxMemory + "KB");
2019-06-02 09:09:55.156 4281-4281/com.hdq.study E/MainActivity: Max memory is 393216KB

1. 加载bitmap

Bitmap在Android中指的是一张图片,png格式、jpg等其他常见的图片格式。
BitmapFactory类提供了四个类方法:decodeFile、decodeResource、decodeStream和decodeByteArray,分别用于支持从文件系统、资源、输入流以及字节数组中加载出一个Bitmap对象,其中decodeFile和decodeResource又间接调用了decodeStream方法,这四个类方法最终在Android的底层实现的,对应着BitmapFactory类的几个native方法。

2. 优化加载bitmap

Bitmap优化加载的核心思想就是采用BitmapFactory.Options来加载所需尺寸的图片。

通过BitmapFactory.Options来缩放图片,主要用到了它的inSampleSize参数,即采样率。当inSampleSize为1时,采样后的图片大小为图片的原始大小;当inSampleSize大于1时,比如2,那么采样后的图片宽高均为原图大小的1/2,像素数为原图的1/4,其占有的内存大小也为原图的1/4。
采样率必须是大于1的整数,图片才会有缩小的效果,并且采样率同时作用于宽和高,缩放比例为1/(inSampleSize的2次方),比如inSampleSize为4,那么缩放比例就是1/16。官方文档指出,inSampleSize的取值为2的指数:1、2、4、8、16等等。
使用步骤:

  1. 将BitmapFactory.Options的inJustDecodeBounds参数设为true并加载图片;
  2. 从BitmapFactory.Options中取出图片的原始宽高信息,它们对应于outWidth和outHeight参数;
  3. 根据采样率的规则并结合目标View的所需大小计算出采样率inSampleSize;
  4. 将BitmapFactory.Options的inJustDecodeBounds参数设为false,然后重新加载图片。
 //解码图片的配置选项
        BitmapFactory.Options options = new BitmapFactory.Options();
        //设置options里面的参数,为true,不去真实地解析Bitmap,而是查询Bitmap的宽高信息(禁为bitmap分配内存)
        options.inJustDecodeBounds = true;
        Bitmap bitmap = BitmapFactory.decodeFile("/sdcard/Download/pic.jpg", options);
        Log.e("TAG", "bitmap==" + bitmap);//bitmap=null

        //获取图片的宽高
        int height = options.outHeight;
        int width = options.outWidth;
        Log.e("TAG", "图片的宽度,width==" + width);
        Log.e("TAG", "图片的高度,width==" + height);

        //获取手机屏幕的宽高,拿到窗体管理者
        WindowManager wm = (WindowManager) getSystemService(WINDOW_SERVICE);
        //getDefaultDisplay获取屏幕分辨率
        int screenWidth = wm.getDefaultDisplay().getWidth();
        int screenHeight = wm.getDefaultDisplay().getHeight();
        Log.e("TAG", "屏幕的宽度,width==" + screenWidth);
        Log.e("TAG", "屏幕的高度,width==" + screenHeight);

        //计算图片和屏幕宽高的比例
        int dx = width / screenWidth;
        int dy = height / screenHeight;
       //缩放比例
        int scale = 1;
        //比如图片:960*480  屏幕:480*320  dx=2  dy=1.5,取dx=2,它的宽高都在屏幕里面了
        //dx<1说明图片还没有屏幕高,就不需要缩放了
        if (dx > dy && dy > 1) {
            scale = dx;
        }
        if (dy > dx && dx > 1) {
            scale = dy;
        }

        Log.e("TAG", "scale==" + scale);

        //以缩放的方式把图片加载到手机内存
        options.inSampleSize = scale;
        //真实地解析bitmap
        options.inJustDecodeBounds = false;
        Bitmap bitmap2 = BitmapFactory.decodeFile("/sdcard/Download/pic.jpg", options);
        imageView.setImageBitmap(bitmap2);
/**
 *
 * @param res
 * @param resId
 * @param reqWidth 期望图片宽(像素)
 * @param reqHeight 期望图片高(像素)
 * @return
 */
public  static Bitmap decodeSampleBitmapFromResource(Resources res,int resId,int reqWidth,int reqHeight){
    final BitmapFactory.Options options= new BitmapFactory.Options();
    options.inJustDecodeBounds=true;
    BitmapFactory.decodeResource(res,resId,options);

    //计算采样率
    options.inSampleSize=calculateInSampleSize(options,reqWidth,reqHeight);

    options.inJustDecodeBounds=false;
    return BitmapFactory.decodeResource(res,resId,options);
}

/**
 * 获取采样率
 * @param options
 * @param reqWidth
 * @param reqHeight
 * @return
 */
private static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
    //获取图片的宽和高
    int width = options.outWidth;
    int height=options.outHeight;
    int inSampleSize=1;

    if (height>reqHeight ||  width>reqWidth){
        final int halfHeight=height/2;
        final int halfWidth=width/2;

        //计算最大的采样率,采样率为2的指数
        while ((halfHeight/inSampleSize)>=reqHeight && (halfHeight/inSampleSize)>=reqWidth){
            inSampleSize *=2;
        }
    }

    return inSampleSize;
}
//ImageView所期望的图片大小为150*150像素,加载显示图片
imageView.setImageBitmap(decodeSampleBitmapFromResource(getResources(),R.drawable.image,150,150));

图片的三级缓存

什么是三级缓存?

  • 内存缓存,优先加载,速度最快

  • 本地缓存,次优先加载,速度快

  • 网络缓存,最后加载,速度慢,浪费流量
    强引用、软引用和弱引用的区别

  • 强引用
    直接的对象引用;内存不足时,JVM也不会被回收。(定义的成员变量都是强引用)

  • 软引用
    SoftReference 当一个对象只有软引用存在时,系统内存不足时此对象会被gc回收;

  • 弱引用
    WeakReference 当一个对象只有弱引用存在时,此对象会随时被gc回收。

  • 虚引用
    PhantomReference 代码被调用的时候,就被清理了。
    软引用示例代码(其他几种用法类似):

private SoftReference mSoftReference;

/**
 * 给imageView加载url对应的图片
 * @param iv
 * @param url
 */
public void display(ImageView iv,String url){
    mSoftReference=new SoftReference(iv);
    mSoftReference.clear();//这里是清除里面的iv图片资源
    //取引用--为null
    ImageView imageView = mSoftReference.get();
}

Android-->早期是davike虚拟机,Android Runtime

  1. 3.0之前,垃圾回收机制和JVM是相同的。(可以用这套软引用存储图片)
  2. 3.0之后,davike虚拟机做了升级,只要GC(回收机制)运行,SoftReference和WeakReference一律回收。(在Android中就没用了)
    Android3.0之前软引用的写法(代码如下):
private static Map> mCaches = new LinkedHashMap>();

/**
 * 给imageView加载url对应的图片
 *
 * @param iv
 * @param url
 */
public void display(ImageView iv, String url) {
    SoftReference reference = mCaches.get(url);
    if (reference==null){
        //内存中没有--》本地去取
    }else {
        Bitmap bitmap = reference.get();
        if (bitmap==null){
            //gc回收了---》本地去取
        }else {
            //内存中有,就显示
        }
    }
}

解决方案:是用了LruCache。

LruCache是线程安全的,定义如下:

public class LruCache{
  private final LinkedHashMap map;
  ...
}

1. LruCache

内存缓存技术对那些大量占用应用程序内存的图片提供了快速访问的方法。其中最核心的类是LruCache(此类在android-support-v4的包中提供)。它的主要算法原理是把最近使用的对象用强引用存储在LinkedHashMap中,并且把最近最少使用的对象在缓存值达到预设定值之前从内存中移除。
在过去,我们经常会使用一种非常流行的内存缓存技术的实现,即软引用或弱引用 (SoftReference or WeakReference)。但是现在已经不再推荐使用这种方式了,因为从 Android 2.3 (API Level 9)开始,垃圾回收器会更倾向于回收持有软引用或弱引用的对象,这让软引用和弱引用变得不再可靠。另外,Android 3.0 (API Level 11)中,图片的数据会存储在本地的内存当中,因而无法用一种可预见的方式将其释放,这就有潜在的风险造成应用程序的内存溢出并崩溃。
LruCache是一个泛型类,它内部采用一个LinkedHashMap以强引用的方式存储外界的缓存对象,它提供了get和put方法来完成缓存的获取和添加操作,当缓存满时,LruCache会移除较早使用的缓存对象,然后再添加新的缓存对象。

LruCache缓存大小设置

对于分配给LruCache的缓存大小,可以直接指定固定的数值,但是更好的做法应该是通过获取最大内存(int)Runtime.getRuntime.maxMemory,然后通过返回的最大内存/int n的大小动态分配给LruCache。

LruCache的存储和读取

LruCache是以为键值对形式存储数据,所以它的读写方法都和HashMap一样,都可以通过key操作。
存储
LruCache.put(Key,Values)
读取
LruCache.get(Key)
构造一个工具类,用来存储图片到缓存和从缓存中读取图片

public class CustomLruCache {
    private LruCache stringBitmapLruCache;
    int maxMemory = (int) Runtime.getRuntime().maxMemory();//获取最大内存
    int cacheSize = maxMemory / 16;//大小为最大内存的1/16
    private static CustomLruCache customLruCache;

    /**
     * 私有化构造方法
     */
    private CustomLruCache() {
        stringBitmapLruCache = new LruCache(cacheSize) {
        @Override
        protected int sizeOf(String key, Bitmap value) {
            return value.getByteCount();
        }
        };
    }

    /**
     * 单例模式获取实例,保证只有一个CustomLruCache对象,同时保证只有一个CustomLruCache.stringBitmapLruCache
     *
     * @return
     */
    public static CustomLruCache getInstance() {
        if (customLruCache == null) {
            customLruCache = new CustomLruCache();
        }
        return customLruCache;
    }

    public void addBitmapToMemoryCache(String key, Bitmap bitmap) {
        if (getBitmapFromMemoryCache(key) != bitmap)//如果缓存中不存在bitmap,就存入缓存
            stringBitmapLruCache.put(key, bitmap);
    }

    public Bitmap getBitmapFromMemoryCache(String key) {
        return stringBitmapLruCache.get(key);
    }
}
public class CustomLruCache {
    private LruCache stringBitmapLruCache;
    int maxMemory = (int) Runtime.getRuntime().maxMemory();//获取最大内存
    int cacheSize = maxMemory / 16;//大小为最大内存的1/16
    private static CustomLruCache customLruCache;

    /**
     * 私有化构造方法
     */
    private CustomLruCache() {
        stringBitmapLruCache = new LruCache(cacheSize) {
        @Override
        protected int sizeOf(String key, Bitmap value) {
            return value.getByteCount();
        }
        };
    }

    /**
     * 单例模式获取实例,保证只有一个CustomLruCache对象,同时保证只有一个CustomLruCache.stringBitmapLruCache
     *
     * @return
     */
    public static CustomLruCache getInstance() {
        if (customLruCache == null) {
            customLruCache = new CustomLruCache();
        }
        return customLruCache;
    }

    public void addBitmapToMemoryCache(String key, Bitmap bitmap) {
        if (getBitmapFromMemoryCache(key) != bitmap)//如果缓存中不存在bitmap,就存入缓存
            stringBitmapLruCache.put(key, bitmap);
    }

    public Bitmap getBitmapFromMemoryCache(String key) {
        return stringBitmapLruCache.get(key);
    }
}

加载网络图片

 AsyncTask bitmapAsyncTask = new AsyncTask() {
        @Override
        protected Bitmap doInBackground(String... params) {
            Bitmap bitmap = null;
            try {
                CustomLruCache customLruCache = CustomLruCache.getInstance();
                bitmap = customLruCache.getBitmapFromMemoryCache(params[0]);
                //先从缓存中读取图片,如果缓存中不存在,再请求网络,从网络读取图片添加至LruCache中
                //启动app后第一次bitmap为null,会先从网络中读取添加至LruCache,如果app没销毁,再执行读取图片操作时
                //就会优先从缓存中读取
                if (bitmap == null) {
                    //从网络中读取图片数据
                    URL url = new URL(params[0]);
                    bitmap = BitmapFactory.decodeStream(url.openStream());
                    //添加图片数据至LruCache
                    customLruCache.addBitmapToMemoryCache(params[0], bitmap);
                }
            } catch (MalformedURLException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }
            return bitmap;
        }
        protected void onPostExecute(Bitmap bitmap) {
            super.onPostExecute(bitmap);
            imageView.setImageBitmap(bitmap);
        }
    };
//加载图片
 bitmapAsyncTask.execute(imageURL);

2. 文件缓存-第三方类DiskLruCache

利用DiskLruCache从网络上获取到之后都会存入到本地缓存中,因此即使手机在没有网络的情况下依然能够加载显示图片数据。DiskLruCache存储的位置没有限制,但是一般选择存储在context.ExternolStorageCacheDir(),即这个手机的外部存储这个app的私有区域,即/sdcard/Android/data/应用包名/cache,因为是存储在外部存储私有区域,当app被卸载时,这部分的内容会被一起清除。

DiskLruCache地址:
https://android.googlesource.com/platform/libcore/+/android-4.1.1_r1/luni/src/main/java/libcore/io/DiskLruCache.java
或者可以在Jake大神的Github上找到:
https://github.com/JakeWharton/DiskLruCache

使用DiskLruCache

实例化DiskLruCache是通过 DiskLruCache.open(File directory, int appVersion, int valueCount, long maxSize),四个参数分别:为directory缓存的路径;appVersion 应用版本;alueCount 指定同一个key可以对应多少个缓存文件,一般指定为1;maxSize 指定可以缓存多少字节的数据。
工具类

  public class DiskLruCacheHelper {
        DiskLruCache mDiskLruCache = null;
        static DiskLruCacheHelper diskLruCacheHelper;

        private DiskLruCacheHelper(Context context) {
            try {
                File cacheDir = getDiskCacheDir(context, "bitmap");
                //如果文件不存在,则创建
                if (!cacheDir.exists()) {
                    cacheDir.mkdirs();
                }
                mDiskLruCache = DiskLruCache.open(cacheDir, getAppVersion(context), 1, 10 * 1024 * 1024);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        public static DiskLruCacheHelper getInstance(Context context) {
            if (diskLruCacheHelper == null)
                diskLruCacheHelper = new DiskLruCacheHelper(context);
            return diskLruCacheHelper;
        }

        public File getDiskCacheDir(Context context, String uniqueName) {
            String cachePath;
            if (isExternalStorageWritable()) {
                cachePath = context.getExternalCacheDir().getPath();//如果挂载了sdcard,获取外部存储私有区域路径
            } else {
                cachePath = context.getCacheDir().getPath();//如果没有挂载sdcard,则获取内部存储缓存区域
            }
            return new File(cachePath + File.separator + uniqueName);
        }

        /**
         * 检查外部存储是否可用
         *
         * @return
         */
        private boolean isExternalStorageWritable() {
            String state = Environment.getExternalStorageState();
            if (state.equals(Environment.MEDIA_MOUNTED)) {
                return true;//挂载了sdcard,返回真
            } else {
                return false;//否则返回假
            }
        }

        /**
         * 获取应用版本号
         * 当版本号改变,缓存路径下存储的所有数据都会被清除掉,因为DiskLruCache认为
         * 当应用程序有版本更新的时候,所有的数据都应该从网上重新获取。
         *
         * @param context
         * @return
         */
        public int getAppVersion(Context context) {
            try {
                PackageInfo info = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
                return info.versionCode;
            } catch (PackageManager.NameNotFoundException e) {
                e.printStackTrace();
            }
            return 1;
        }

        /**
         * 写入图片数据到文件缓存
         *
         * @param imageUrl
         * @param bitmap
         */
        public void writeToCache(String imageUrl, Bitmap bitmap) {
            try {
                String key = hashKeyForDisk(imageUrl);
                DiskLruCache.Editor editor = mDiskLruCache.edit(key);
                if (editor != null) {
                    OutputStream outputStream = editor.newOutputStream(0);
                    if (bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream)) {
                        editor.commit();
                    } else {
                        editor.abort();
                    }

                }
                mDiskLruCache.flush();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }


        /**
         * 从缓存读取数据
         *
         * @param imageUrl
         * @return
         */

        public Bitmap readFromCache(String imageUrl) {
            Bitmap bitmap = null;
            try {
                String key = hashKeyForDisk(imageUrl);
                DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key);
                if (snapShot != null) {//如果文件存在,读取数据转换为Bitmap对象
                    InputStream is = snapShot.getInputStream(0);
                    bitmap = BitmapFactory.decodeStream(is);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
            return bitmap;
        }

        /**
         * 将文件名转换成"MD5"编码
         *
         * @param key
         * @return
         */
        public String hashKeyForDisk(String key) {
            String cacheKey;
            try {
                final MessageDigest mDigest = MessageDigest.getInstance("MD5");
                mDigest.update(key.getBytes());
                cacheKey = bytesToHexString(mDigest.digest());
            } catch (NoSuchAlgorithmException e) {
                cacheKey = String.valueOf(key.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();
        }

    }

你可能感兴趣的:(Android 图片缓存)