Android Bitmap 的加载和 Cache 缓存 笔记

如何有效的加载一个 Bitmap

原因是 Android 对单个应用所施加的内存限制最大是 16MB,超出后就会出现内存溢出 OOM。

Bitmap 在 Android 中指的是一张图片,那么如何加载一张图片呢?BimapFactory类提供了四类方法:decodeFile,decodeResoure,decodeStream,decodeByteArray,分别对应的是从文件系统、资源、输入流以及字节数组中加载出一个 Bitmap 对象。其中 decodeFiledecodeResource有间接调用了decodeStream方法,这四类方法最终是在Android的底层实现,对应的是 BitmapFactory类的几个native 方法。

如何高效加载 Bitmap 呢?

核心思想是采用 BitmapFactory.Options来加载所需尺寸的图片,因为很多时候 ImageView 并没有图片的原始尺寸那么大,这时候直接把图片的原始尺寸设置给 ImageView 是没必要的,可以通过BitmapFactory.Options按照一定的采样率来加载缩小后的图片,然后在 ImageView 中显示,这样就会降低内存占用从而在一定程度上避免了OOM,提高了 Bitmap 加载时的性能。

以通过BitmapFactory.Options来缩放图片,主要是采用到了它的inSampleSize参数,即采样率

inSampleSize = 1时采样后的图片为原始图片的大小; 当inSampleSize > 1时采样后的图片宽/高为原始图片的大小的 1/inSampleSize,而像素数为原图的 1/(inSampleSize的2次方);

inSampleSize = 2时采样后的图片为原始图片大小的 1/2,像素数为原图的1/4,内存占有大小也为原图的1/4,拿一张 ARGB8888 格式的宽高1024 * 1024 的图片进行压缩,压缩前它的内容占用为1024 * 1024 * 4(该值由图片格式决定)大小 ,当压缩时图片使用采样率为 inSampleSize = 2 去 压缩,那么采样后的图片占用内存为 512 * 512 * 4 大小。

inSampleSize < 1时其作用相当于1,即无压缩效果。最新的官方文档中指出 inSampleSize 的取值应该总为 2 的指数,当 inSampleSize 不为 2 的指数时系统会向下取整并选择一个最接近2的指数来代替,比如 3 系统会选择 2 来代替,但是这个结论并非在所有的 Android 版本上都成立,因此把它当成一个开发建议。

如何获取采样率?

  • BitmapFactory.OptionsinJustDecodeBounds参数设置为 true 并加载图片
  • BitmapFactory.Options中取出图片的原始宽高信息,他们对应于 outWidth 和 outHeight 参数
  • 根据采样率的规则并结合木匾 View 的所需大小计算出采样率 inSampleSize
  • BitmapFactory.OptionsinJustDecodeBounds参数设为 false,然后重新加载图片。

inJustDecodeBounds 参数:当该参数设置为 true 时,BitmapFactory 只会解析图片的原始宽高信息,并不会真正去加载图片,所以这个操作时轻量级的。

代码如下:

public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId, int reqWidth, int reqHeight){
    final BitmapFactory.Options options = new BitmapFactory.Options() ;
    options.inJustDecodeBounds = true ;
    BitmapFactory.decodeResource(res, resId, options);
    // 重新加载
    options.inJustDecodeBounds = false ;
    return BitmpFactory.decodeResource(res, resId, options)
}

/**
* 计算采样率
*/
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 halfHeight = height / 2 ;
        final int halfWidth = width / 2 ;

        while((halfHeight / inSampleSize) >= reqHeight && (halfWidth / inSampleSize >= reqWidth)){
            inSampleSize *= 2 ;
        }
    }
    return inSampleSize ;
}

比如 ImageView 所希望的图片大小为 100 * 100 像素
这时候可以通过如下方式高效的加载并显示图片:

mImageView.setImageBitmap(decodeSampledBitmapFromResource(getResources(), R.id.myimage,100,100)) ;

Android 中常用的缓存策略

缓存策略的一个通用思想,可以在很多场合使用,但是实际开发中经常需要使用的是 Bitmap 做缓存,通过缓存策略,我们就可以不用每次都从服务器或者存储设备中加载图片,这样就可以提高加载效率以及产品的用户体验。

目前比较常用的缓存策略是 LruCache( 最近最少使用算法 ) 和 DiskLruCache(磁盘最近最少使用算法)。其中 LruCache 常被用作内存缓存,DiskLruCache 常被用作存储缓存。

最近最少使用算法:当缓存快满时,会淘汰近期最少使用的缓存目标。

了解一下三级缓存

三级缓存分别指的是内存缓存、磁盘缓存、网络缓存。第一次加载数据内存和磁盘中都没有,所以先从网络加载数据,然后将数据缓存到磁盘和内存中。当下次再次加载已缓存的数据时就不用再去请求网络,直接从磁盘或者内存中读取,然后显示。上述的缓存策略也适用于其他文件类型。

缓存策略主要包含缓存的添加、获取和删除。而缓存的删除主要是因为存储设备的容量大小限制,当缓存容量达到存储设备的最大值,就需要删除旧的缓存。那么如何判断那些属于旧的缓存就需要更好的算法来计算了。目前常用的缓存算法是 LRU(Least Recently Used),最近最少使用算法。

缓存的添加

加载网络图片,然后缓存到本地。

(1)获取到网络图片 url
(2)将图片url转为MD5格式,作为key存储

new Thread(new Runnable() {
    @Override
    public void run() {
        try {
            String imageUrl = "http://ww3.sinaimg.cn/large/0066P23Wjw1f7efqelrh4j30300300si.jpg";
            String key = hashKeyForDisk(imageUrl);
            DiskLruCache.Editor editor = mDiskLruCache.edit(key);
            if (editor != null){
                OutputStream outputStream = editor.newOutputStream(0);
                if (downloadUrlToStream(key,outputStream)){
                    editor.commit();
                }else {
                    editor.abort();
                }
            }
            mDiskLruCache.flush();

        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}).start();

/**
 * 将字符串进行MD5编码
 *  因为 url 和 key 要一一对应,而如果拿 url 作 key 还是有可能重复的,
 *  所以转为 MD5 就不会重复了
 * @param key
 * @return
 */
public String hashKeyForDisk(String key){
    String cacheKey ;
    try {
        MessageDigest mDigest = MessageDigest.getInstance("MD5");
        mDigest.update(key.getBytes());
        cacheKey = bytesToHexString(mDigest.digest()) ;
    } catch (NoSuchAlgorithmException e) {
        cacheKey = String.valueOf(key.hashCode()) ;
    }
    return cacheKey ;
}

/**
 * 将字节转为字符
 * @param bytes
 * @return
 */
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() ;
}

/**
 * 将 url 转为流,成功返回true,失败返回 false
 * @param urlString
 * @param outputStream
 * @return
 */
private boolean downloadUrlToStream(String urlString, OutputStream outputStream){
    HttpURLConnection urlConnection = null ;
    BufferedInputStream in = null ;
    BufferedOutputStream out = null ;
    try {
        URL url = new URL(urlString);
        urlConnection = (HttpURLConnection) url.openConnection();
        in = new BufferedInputStream(urlConnection.getInputStream(), 8 * 1024);
        out = new BufferedOutputStream(outputStream, 8 * 1024);
        int b;
        while ((b = in.read()) != -1){
            out.write(b);
        }
        return true ;

    }  catch (MalformedURLException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    }finally {
        if (urlConnection != null){
            urlConnection.disconnect();
        }
            try {
                if (out != null){
                    out.close();
                }
                if (in != null){
                    in.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
    }
    return false ;
}
缓存的获取

(1)获取到url对应的 key
(2)读取流

try {
    // 读取缓存
    String imageUrl = "http://ww3.sinaimg.cn/large/0066P23Wjw1f7efqelrh4j30300300si.jpg";
    String key = hashKeyForDisk(imageUrl);
    DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key);
    Log.e("222", "onCreate: ----->"+snapShot);
    if (snapShot != null) {
        InputStream is = snapShot.getInputStream(0);
        Bitmap bitmap = BitmapFactory.decodeStream(is);
        imageView.setImageBitmap(bitmap);
    }
} catch (IOException e) {
    e.printStackTrace();
}
缓存的删除
try {
    String imageUrl = "http://ww3.sinaimg.cn/large/0066P23Wjw1f7efqelrh4j30300300si.jpg";
    String key = hashKeyForDisk(imageUrl); 
    mDiskLruCache.remove(key);
} catch (IOException e) {
    e.printStackTrace();
}

优化列表的卡顿现象

ListView 和 GridView 由于要加载大量的子视图,当用户滑动的时候就容易出现卡顿的现象,所以我们要对列表的卡顿现象进行优化。

  • 对列表进行复用
  • 不在getView中做耗时操作
  • 在列表停止时加载数据
  • 开启硬件加速

参考

Android 开发艺术探索

郭霖大神博客

Android DiskLruCache完全解析,硬盘缓存的最佳方案

Android照片墙完整版,完美结合LruCache和DiskLruCache

DiskLruCache 源码来自 JakeWharton 大神

你可能感兴趣的:(Android)