一、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的目录下或程序运行在不同屏幕密度的设备上
- BitmapFactory获取的图片宽/高信息和图片的位置以及程序运行的设备有关
-
例子:
- 解析类
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(); }
-
- 通过Editor完成
-
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有没有发生改变,如果发生改变就不再为他设置图片
- 注意列表因item复用导致图片错位的问题
4、优化列表卡顿
- (1)首先,不要在getView中执行耗时操作
- (2)其次,控制异步任务的执行频率
- eg:考虑在列表滑动时停止加载图片
- 给列表布局设置setOnScrollListener,并在OnScrollListener的onScrollStateChanged方法中判断列表是否处于滑动状态
- eg:考虑在列表滑动时停止加载图片
- (3)硬件加速可以解决莫名的卡顿
- 通过设置 android:hardwareAccelerated=“true” 即可为Activity开启硬件加速