Android开发十二《Bitmap的加载和Cache》

2022-03-27  本文已影响0人  独自闯天涯的码农

一、Bitmap的高效加载

1、Bitmap(位图):

指一张图片,常见格式:.png、.jpg等

2、必要性

直接加载大容量的高清Bitmap很容易出现显示不完整、内存溢出OOM的问题(如报错:java.lang.OutofMemoryError)

3、核心思想

为了解决这个问题,就出现了Bitmap的高效加载策略。其核心思想:
假设通过ImageView来显示图片,很多时候ImageView并没有原始图片的尺寸那么大,这个时候把整个图片加载进来后再设置给ImageView,显然是没有必要的,因为ImageView根本没办法显示原始图片。这时候就可以按一定的采样率来将图片缩小后再加载进来,这样图片既能在mageView显示出来,又能降低内存占用从而在一定程度上避免OOM,提高了Bitmap加载时的性能。

4、工具类

1. BitmapFactory类提供的四种加载图片的方法:
方法 描述
decodeFile() 从文件系统加载出一个Bitmap对象
decodeResource() 从资源文件加载出一个Bitmap对象
decodeStream() 从输入流加载出一个Bitmap对象
decodeByteArray() 从字节数组加载出一个Bitmap对象

注意:
decodeFile()和decodeResource()又间接调用decodeStream()。
最终对应着BitmapFactory类的几个native方法;

2. BitmapFactory.Options的参数
  1. inSampleSize参数:即采样率,同时作用于宽/高

注意:根据图片宽高的 实际大小&需要大小,而计算出的缩放比尽可能取最小,避免由于缩小的过多,导致在控件中不能铺满而被拉伸至模糊。

  1. inJustDecodeBounds参数:

注意:
BitmapFactory获取的图片宽高信息和图片的位置以及程序运行的设备有关,会导致获取到不同的结果;
xxhdpi获取图片的大小是mdpi中的三倍;
Bitmap计算规则 = 整个Bitmap占用的就是宽度 * 高度 * 4 byte(ARGB_8888格式的一个像素占用 4byte)一个1024*1024的图片占4MB大小。

3. 加载流程:
  1. 将BitmapFactory.Options.inJustDecodeBounds参数设为true并加载图片。
  2. 从BitmapFactory.Options中取出图片的原始宽高信息,对应outWidth和outHeight参数。
  3. 根据采样率的规则并结合目标View的所需大小计算出采样率inSampleSize。
  4. 将BitmapFactory.Options.inJustDecodeBounds参数设为false,然后重新加载图片。
  /**
     * 对一个Resources的资源文件进行指定长宽来加载进内存, 并把这个bitmap对象返回
     *
     * @param res   资源文件对象
     * @param resId 要操作的图片id
     * @param reqWidth 最终想要得到bitmap的宽度
     * @param reqHeight 最终想要得到bitmap的高度
     * @return 返回采样之后的bitmap对象
     */
public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId, int reqWidth, int reqHeight){
        BitmapFactory.Options options = new BitmapFactory.Options();
        //1.设置inJustDecodeBounds=true获取图片尺寸
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeResource(res,resId,options);
        //3.计算缩放比
        options.inSampleSize = calculateInSampleSize(options,reqHeight,reqWidth);
        //4.再设为false,重新从资源文件中加载图片
        options.inJustDecodeBounds =false;
        return BitmapFactory.decodeResource(res,resId,options);
    }

   /**
     *  一个计算工具类的方法, 传入图片的属性对象和想要实现的目标宽高. 通过计算得到采样值
     * @param options 要操作的原始图片属性
     * @param reqWidth 最终想要得到bitmap的宽度
     * @param reqHeight 最终想要得到bitmap的高度
     * @return 返回采样率
     */
    private static int calculateInSampleSize(BitmapFactory.Options options, int reqHeight, int reqWidth) {
        //2.height、width为图片的原始宽高
        int height = options.outHeight;
        int width = options.outWidth;
        int inSampleSize = 1;
        if(height>reqHeight||width>reqWidth){
            int halfHeight = height/2;
            int halfWidth = width/2;
            //计算缩放比,是2的指数
            while((halfHeight/inSampleSize)>=reqHeight&&(halfWidth/inSampleSize)>=reqWidth){
                inSampleSize*=2;
            }
        }    
        return inSampleSize;
    }

二、Android中的缓存策略

为减少流量消耗,可采用缓存策略。
常用的缓存算法是LRU(Least Recently Used):

1、LruCache(内存缓存)

1. 概念

LruCache类是一个线程安全的泛型类:内部采用一个LinkedHashMap以强引用的方式存储外界的缓存对象,并提供get和put方法来完成缓存的获取和添加操作,当缓存满时会移除较早使用的缓存对象,再添加新的缓存对象。

public class LruCache<K, V> {
    private final LinkedHashMap<K, V> map;
...

注:几种引用的含义
强引用:直接的对象引用,不会被gc回收;
软引用:当系统内存不足时,对象会被gc回收;
弱引用:随时会被gc回收;
虚引用:发现就回收。

2. 实现原理:

LinkedHashMap利用一个双重链接链表来维护所有条目item。

常用属性accessOrder:决定LinkedHashMap的链表顺序。
1、值为true:以访问顺序维护链表。
2、值为false:以插入的顺序维护链表。
而LruCache利用是accessOrder=true时的LinkedHashMap实现LRU算法,使得最近访问的数据会在链表尾部,在容量溢出时,将链表头部的数据移除。

3. 使用方法:
  1. 计算当前可用的内存大小;
  2. 分配LruCache缓存容量;
  3. 创建LruCache对象并传入最大缓存大小的参数、重写sizeOf()用于计算每个缓存对象的大小;
  4. 通过put()、get()和remove()实现数据的添加、获取和删除。
  //初始化LruCache对象
public void initLruCache()
{
    //1.获取当前进程的可用内存,转换成KB单位
    int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
    //2.分配缓存的大小
    int maxSize = maxMemory / 8;
    //3.创建LruCache对象并重写sizeOf方法
    lruCache = new LruCache<String, Bitmap>(maxSize)
        {
            @Override
            protected int sizeOf(String key, Bitmap value) {
                // TODO Auto-generated method stub
                return value.getWidth() * value.getHeight() / 1024;
            }
        };
}
//4.LruCache对数据的操作
public void fun()
{
    //添加数据
    lruCache.put("lizhuo", bm1);
    lruCache.put("sushe", bm2);
    lruCache.put("jiqian", bm3);
    //获取数据
    Bitmap b1 = (lruCache.get("lizhuo"));
    Bitmap b2 = (lruCache.get("sushe"));
    Bitmap b3 = (lruCache.get("jiqian"));
    //删除数据
    lruCache.remove("sushe");
}

推荐阅读:详细解读LruCache类LruCache 源码解析

2、DiskLruCache(磁盘缓存)

通过将缓存对象写入文件系统从而实现缓存效果,即磁盘缓存。

与LruCache区别:DiskLruCache非泛型类,不能添加类型,而是采用文件存储,存储和读取通过I/O流处理。

1. 使用方法:
  1. 计算分配DiskLruCache的容量;
  2. 设置缓存目录;
  3. 创建DiskLruCache对象,注意不能通过构造方法来创建, 而是提供open()方法;
  4. 利用Editor、Snapshot和remove()实现数据的添加、获取和删除。
  5. 调用flush()将数据写入磁盘。
2. DiskLruCache的创建:
public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)

参数含义:

  1. directory:磁盘缓存的存储路径。有两种目录:
    SD 上的缓存目录:/sdcard/Android/data/package_name/cache 目录,当应用被卸载后会被删除。
    其他目录:应用卸载后缓存数据还在。
  2. appVersion:当前应用的版本号,一般设为1。
  3. valueCount:单个节点所对应的数据的个数,一般设为1。
  4. maxSize:缓存的总大小,超出这个设定值后DiskLruCache会清除一些缓存。
DiskLruCache mDiskLruCache = null;  
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 File getDiskCacheDir(Context context, String uniqueName) {  
    String cachePath;  
    if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())|| !Environment.isExternalStorageRemovable()) {  
    //当SD卡存在或者SD卡不可被移除,获取路径 /sdcard/Android/data/<application package>/cache
        cachePath = context.getExternalCacheDir().getPath();  
    } else { 
    //反之,获取路径/data/data/<application package>/cache 
        cachePath = context.getCacheDir().getPath();  
    }  
    return new File(cachePath + File.separator + uniqueName);  
}  
//用于获取到当前应用程序的版本号
public int getAppVersion(Context context) {  
    try {  
        PackageInfo info = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);  
        return info.versionCode;  
    } catch (NameNotFoundException e) {  
        e.printStackTrace();  
    }  
    return 1;  
}  
3. 添加缓存操作:通过Editor完成
  1. 获取资源的key值,采用url的md5值作为key;
  2. 通过DiskLruCache.edit() 获取对应key的Editor;
  3. 通过Editor.newOutputStream(0)得到一个输出流;
  4. 通过OutputStream写入数据;
  5. Editor.commit()提交写操作,若发生异常,则调用Editor.abort()进行回退。
//1.返回url的MD5算法结果
String key = hashKeyFormUrl(url);
//2.获取Editor对象
Editor editor = mDiskLruCache.edit(key);
//3.创建输出流,其中常量DISK_CACHE_INDEX = 0
OutputStream outputStream = editor.newOutputStream(DISK_CACHE_INDEX);
//4.写入数据
outputStream.wirte(data);
//5.提交写操作
editor.commit();
4. 查找缓存操作:和缓存添加的过程类似
  1. 获取资源的key值,采用url的md5值作为key;
  2. 通过DiskLruCache.get()获取对应key的Snapshot对象;
  3. 通过Snapshot.getInputStream(0)得到一个输入流(可向下转型为FileInputStream);
  4. 通过InputStream读取数据。
//1.返回url的MD5算法结果
String key = hashKeyFormUrl(url);
//2.获取Snapshot对象
Snapshot snapshot = mDiskLruCache.get(key);
//3.创建输入流,其中常量DISK_CACHE_INDEX = 0
InputStream inputStream = snapshot.getInputStream(DISK_CACHE_INDEX);
//4.读出数据
int data = inputStream.read();

问题:FileInputStream是一种有序的文件流,调用两次 BitmapFactory.decodeStream()会影响文件流的位置属性,导致第二次解析结果为空。
解决办法:通过文件流得到其对应的文件描述符,再调用 BitmapFactory.decodeFileDescriptor()来加载一张缩放后的图片。

推荐阅读Android DiskLruCache完全解析源码解析

三、Android中的图片加载框架

Picasso

优点

1、自带统计监控功能
支持图片缓存使用的监控,包括缓存命中率、已使用内存大小、节省的流量等。
2、支持优先级处理
每次任务调度前会选择优先级高的任务,比如 App 页面中 Banner 的优先级高于 Icon 时就很适用。
3、支持延迟到图片尺寸计算完成加载
4、支持飞行模式、并发线程数根据网络类型而变
手机切换到飞行模式或网络类型变换时会自动调整线程池最大并发数,比如 wifi 最大并发为 4, 4g 为 3,3g 为 2。
这里 Picasso 根据网络类型来决定最大并发数,而不是 CPU 核数。
5、 “无”本地缓存
无”本地缓存,不是说没有本地缓存,而是 Picasso 自己没有实现,交给了 Square 的另外一个网络库 okhttp 去实现,这样的好处是可以通过请求 Response Header 中的 Cache-Control 及 Expired 控制图片的过期时间。

缺点

不支持GIF, 并且它可能是想让服务器去处理图片的缩放, 它缓存的图片是未缩放的, 并且默认使用ARGB_8888格式缓存图片, 缓存体积大.

Glide

优点

1、 图片缓存->媒体缓存
Glide 不仅是一个图片缓存,它支持 Gif、WebP、缩略图。甚至是 Video,所以更该当做一个媒体缓存。
2、支持优先级处理
3、与 Activity/Fragment 生命周期一致,支持 trimMemory
Glide 对每个 context 都保持一个 RequestManager,通过 FragmentTransaction 保持与 Activity/Fragment 生命周期一致,并且有对应的 trimMemory 接口实现可供调用。
4、支持 okhttp、Volley
Glide 默认通过 UrlConnection 获取数据,可以配合 okhttp 或是 Volley 使用。实际 ImageLoader、Picasso 也都支持 okhttp、Volley。
5、内存友好
① Glide 的内存缓存有个 active 的设计
从内存缓存中取数据时,不像一般的实现用 get,而是用 remove,再将这个缓存数据放到一个 value 为软引用的 activeResources map 中,并计数引用数,在图片加载完成后进行判断,如果引用计数为空则回收掉。
② 内存缓存更小图片
Glide 以 url、viewwidth、viewheight、屏幕的分辨率等做为联合 key,将处理后的图片缓存在内存缓存中,而不是原始图片以节省大小
③ 与 Activity/Fragment 生命周期一致,支持 trimMemory
④ 图片默认使用默认 RGB565 而不是 ARGB888
虽然清晰度差些,但图片更小,也可配置到 ARGB_888。
其他:Glide 可以通过 signature 或不使用本地缓存支持 url 过期

Fresco

优点:

1、图片存储在安卓系统的匿名共享内存, 而不是虚拟机的堆内存中, 图片的中间缓冲数据也存放在本地堆内存, 所以, 应用程序有更多的内存使用,不会因为图片加载而导致oom, 同时也减少垃圾回收器频繁调用回收Bitmap导致的界面卡顿,性能更高.
2、渐进式加载JPEG图片, 支持图片从模糊到清晰加载
3、 图片可以以任意的中心点显示在ImageView, 而不仅仅是图片的中心.
4、JPEG图片改变大小也是在native进行的, 不是在虚拟机的堆内存, 同样减少OOM
5、很好的支持GIF图片的显示

缺点:
  1. 框架较大, 影响Apk体积
  2. 使用较繁琐

总结:

Picasso所能实现的功能,Glide都能做,无非是所需的设置不同。但是Picasso体积比起Glide小太多如果项目中网络请求本身用的就是okhttp或者retrofit(本质还是okhttp),那么建议用Picasso,体积会小很多(Square全家桶的干活)。Glide的好处是大型的图片流,比如gif、Video,如果你们是做美拍、爱拍这种视频类应用,建议使用。
Fresco在5.0以下的内存优化非常好,代价就是体积也非常的大,按体积算Fresco>Glide>Picasso
不过在使用起来也有些不便(小建议:他只能用内置的一个ImageView来实现这些功能,用起来比较麻烦,我们通常是根据Fresco自己改改,直接使用他的Bitmap层)

上一篇下一篇

猜你喜欢

热点阅读