《Android开发艺术探索》学习笔记之Bitmap的高效加载和Cache

一、Bitmap的高效加载

  • Bitmap在Android中指的是一张图片
1、图片的解析
  • BitmapFactory提供了四种方法来解析一张bitmap
    • (1)decodeFile:从文件系统中加载
    • (2)decodeResource:从资源中加载
    • (3)decodeStream:从输入流中加载
    • (4)decodeByteArray:从字节数组中加载
2、图片尺寸的转换
  • 采用BitmapFactory.Options

    • 主要是用到了它的 inSampleSize采样率)参数
    • 采样率同时作用于宽高,导致缩放后的图片大小以采样率的2次方形式递减
    • 缩放比例 = 1 /(inSampleSize)
      • 例如:一张1024×1024像素的图片,采用ARGB8888格式存储,那么内存大小1024×1024×4=4M。如果inSampleSize=2,那么采样后的图片内存大小:512×512×4=1M。
    • 当inSampleSize小于1时,其作用相当于1,即无缩放效果
      • 官方文档中指出,inSampleSize的取值应该总是2的指数。如果外界传递给系统的inSampleSize不为2的指数,那么系统会向下取整并选择一个最接近的2的指数来代替
  • 获取采样率的过程

    • (1)将BitmapFactory.Options的inJustDecodeBounds参数设置为true并加载图片
    • (2)从BitmapFactory.Options中取出图片的原始宽高信息,他们对应于outWidth和outHeight参数
    • (3)根据采样率的规则并结合目标View的所需大小计算出采样率inSampleSize
    • (4)将Bitmapfactory.Options的inJustDecodeBounds参数设为false,然后重新加载图片
  • inJustDecodeBounds:当此参数设为true时,BitmapFactory只会解析图片的原始宽高信息,并不会真正的去加载图片

    • BitmapFactory获取的图片宽/高信息和图片的位置以及程序运行的设备有关
      • 比如同一张图片放在不同的drawable的目录下或程序运行在不同屏幕密度的设备上
  • 例子:

    • 解析类
     public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId, int reqWidth, int reqHeight){ 
        //获取BitmapFactory.Options的实例
        BitmapFactory.Options options = new BitmapFactory.Options();
        
        //解析但不加载图片
        options.inJustDecodeBounds = true; 
        BitmapFactory.decodeResource(res,resId,options); 
        
        //计算缩放比
        options.inSampleSize = calculateInSampleSize(options,reqHeight,reqWidth); 
        
        //重新加载图片 
        options.inJustDecodeBounds =false; 
        return BitmapFactory.decodeResource(res,resId,options); 
         
     } 
     
    private static int calculateInSampleSize(BitmapFactory.Options options, int reqHeight, int reqWidth) { 
        //获取Bitmap的原始宽高
        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; 
    }
    
    
    • 使用
    mImageView.setImageBitmap(
                    decodeSampledBitmapFromResource(
                            getResources(),R.mipmap.ic_launcher,100,100);
    

二、Android中的缓存策略

  • 思想:当应用打算从网络上请求一张照片时,程序会首先从内存中去获取,如果过内存中没有那就从存储设备中获取,如果存储设备中也没有,那就从网络上下载这张图片

  • 缓存策略主要包括:添加获取删除

    • 常用 LRU(Least Recently Used),即近期最少使用算法
    • 采用LruCache充当内存缓存
    • 采用DiskLruCache充当存储设备缓存
1、LruCache
  • 定义:是Android3.1所提供的一个缓存类

    • 通过support-v4兼容包可以兼容到早期的Android版本
  • LruCache是一个泛型类内部采用LinkedHashMap以强引用的方式存储外界的缓存对象

    • 提供了get和put方法来完成缓存的获取和添加操作
    • 当缓存满时,LruCache会移除较早使用的缓存对象,然后再添加新的缓存对象
      • 强引用:直接的对象引用
      • 软引用:当一个对象只有软引用存在时,系统内存不足时此对象会被gc回收
      • 弱引用:当一个对象只有弱引用存在时,此对象会随时被gc回收
  • LruCache是线程安全的

    public class LruCache<K, V> {
        private final LinkedHashMap<K, V> map;
        ···
    }
    
    • 内部方法通过同步实现线程安全
  • LruCache的典型初始化

    int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
    
    //定义LruCache的总容量大小为当前进程的1/8
    int cacheSize = maxMemory / 8;
    
    mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
        @Override
        protected int sizeOf(String key, Bitmap bitmap) {
            return bitmap.getRowBytes() * bitmap.getHeight() / 1024;
        }
    }
    
    • (1)提供缓存的容量大小
    • (2)重写sizeOf方法,计算缓存对象的大小(注意要将单位与缓存容量单位保持一致)
    • (3)有时还需要重写LruCache的entryRemoved方法,此方法会在移除旧缓存时调用,因此可以在entryRemoved中完成一些资源回收任务
  • 从LruCache中获取对象:mMemoryCache.get(key)

  • 向LruCache中添加对象:mMemoryCache.put(key, bitmap)

  • 删除对象:remove

2、DiskLruCache
  • DiskLruCache用于实现存储设备缓存

    • 其不属于Android SDK的一部分
  • DiskLruCache的创建

    • 不能通过构造方法,提供了静态的open方法用于创建自身
    public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
    
    • 参数:
      • 第一个参数:磁盘缓存在文件系统中的存储路径
      • 第二个参数:版本号
        • 一般设置为1
        • 当版本号发生改变时,DiskLruCache会清空之前所有的缓存文件
      • 第三个参数:单个节点所对应的数据的个数
        • 一般设置为1
      • 第四个参数:缓存的总大小
        //50MB
        private static final long DISK_CACHE_SIZE = 1024 * 1024 * 50;
        
    • 初始化的例子
       private static final int MAX_SIZE = 10 * 1024 * 1024;//10MB 
       private DiskLruCache diskLruCache;
       private void initDiskLruCache() {
          if (diskLruCache == null || diskLruCache.isClosed()) {
              try {
                  File cacheDir = CacheUtil.getDiskCacheDir(this, "CacheDir");
                  if (!cacheDir.exists()) { 
                      cacheDir.mkdirs(); 
                  } 
                  //初始化DiskLruCache 
                  diskLruCache = DiskLruCache.open(cacheDir, 1, 1, MAX_SIZE); 
              } catch (IOException e) { 
                  e.printStackTrace(); 
              } 
          } 
       }
      
      
  • DiskLruCache的缓存添加(Editor

    • 通过Editor完成
      • Editor表示一个缓存对象的编辑对象

      • 获取Editor

        //图片
        url -> key --edit()--> Editor对象 -> 文件输出流
        
      • 在通过 edit() 获取editor对象时,如果这个缓存正在被编辑,那么edit()会返回null

        • DiskLruCache不允许同时编辑一个缓存对象
      • 将url转变为对应的key的原因:图片的url中可能含有特殊字符,影响直接使用

        • 一般采用url的md5值(加密)作为key
      • 通过 Editor的commit() 来提交写入

        • 如果图片下载发生了异常,还可以通过==Editor的abort()==来回退整个操作
      • 例子:

        • 异步线程写入
        @Override 
        protected Boolean doInBackground(Object... params) { 
            try { 
                //获取Key
                String key = Util.hashKeyForDisk(Util.IMG_URL); 
                
                DiskLruCache diskLruCache = (DiskLruCache) params[0];
                
                //得到DiskLruCache.Editor 
                DiskLruCache.Editor editor = diskLruCache.edit(key); 
                
                if (editor != null) {
                    //获得文件输出流
                    OutputStream outputStream = editor.newOutputStream(0); 
                    
                    if (downloadUrlToStream(Util.IMG_URL, outputStream)) {
                        publishProgress("");
                        //写入缓存 
                        editor.commit(); 
                    } else { 
                        //写入失败 
                        editor.abort(); 
                    } 
                }
                
                diskLruCache.flush(); 
                
            } catch (IOException e) { 
                e.printStackTrace();
                return false; 
            } 
            return true; 
        }
        
        • 将URL转变为Key
        public static 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 static 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(); 
        }
        
        
  • DiskLruCache的缓存查找(Snapshot

    • 过程
      url -> key --get()--> Snapshot -> 文件输入流
      
    • 加载时为避免OOM一般通过BitmapFactory.Options对图片进行处理
      • 问题:但因为FileInputStream是一种有序的文件流,所以两次decodeStream调用会影响文件流的位置属性,导致了第二次decodeStream时得到的是null
      • 解决:通过文件流来得到它对应的文件描述符,然后再通过BitmapFactory.decodeFileDescriptor方法来加载一张缩放后的图片
      • 例子:
        Bitmap bitmap == null;
        String key = hashKeyFormUrl(url);
        
        //通过key去获取Snapshot
        DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key);
        
        if (snapShot != null) {
            FileInputStream fileInputStream = (FileInputStream)snapShot.getInputStream(DISK_CACHE_INDEX);
            FileDescriptor fileDescriptor = fileInputStream.getFD();
            bitmap = mImageResizer.decodeSampledBitmapFromFileDescriptor(fileDescriptor, reqWidth, reqHeight);
            if (bitmap != null) {
                addBitmapToMemoryCache(key, bitmap);
            }
        }
        

三、ImageLoader的实现

  • 需求:
    • 图片的同步加载
    • 图片的异步加载
    • 图片压缩
    • 内存缓存
    • 磁盘缓存
    • 网络拉取
1、图片的压缩
  • 单独抽象一个ImageResizer类用于图片压缩
2、内存缓存和磁盘缓存的实现
3、同步加载和异步加载接口的设计
  • 同步:
    • 在loadBitmap方法中依次调用内存加载、存储设备加载、和网络加载
  • 异步:
    • 注意列表因item复用导致图片错位的问题
      • 解决:在给ImageView设置图片之前都为他检查url有没有发生改变,如果发生改变就不再为他设置图片
4、优化列表卡顿
  • (1)首先,不要在getView中执行耗时操作
  • (2)其次,控制异步任务的执行频率
    • eg:考虑在列表滑动时停止加载图片
      • 给列表布局设置setOnScrollListener,并在OnScrollListener的onScrollStateChanged方法中判断列表是否处于滑动状态
  • (3)硬件加速可以解决莫名的卡顿
    • 通过设置 android:hardwareAccelerated=“true” 即可为Activity开启硬件加速
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值