android应用中,一般都会大量用到图片加载,使用不当会引起OOM,在glide出现前,我们可能会经常遇到图片OOM的问题,那么我们假设没有glide,如何来考虑优化呢?这样考虑有助于我们深入理解图片优化,以及性能优化。
我们分三个步骤来详解此问题
- Bitmap基础
- 缩放与缓存优化(glide帮助我们做了这一步的优化,但此处我们自己来简单实现)
- 长图加载优化
Bitmap基础
- 内存大小如何计算
- ARGB_8888: ARGB各占8位,内存为 Width * Height * 4
- RGB_565: R5位,G6位,B5位,内存为Width * Height * 2
- 如何获取 Bitmap 所占内存
- getByteCount(): 返回可用于存储此位图像素的 最小字节数
- BitmapFactory.Options控制解码图片参数
- inDensity: 便是这个bitmap的像素密度,根据drawable目录
- drawable-ldpi:120
- drawable-mdpi:160
- drawable-hdpi:240
- drawable-xhdpi:320
- drawable-xxhdpi:480
- inTargetDensity: 表示要被画出来时的目标(屏幕)的像素密度,代码中获取方式 getResources().getDIsplayMetrics().densityDpi
- inDensity: 便是这个bitmap的像素密度,根据drawable目录
- Bitmap内存压缩:我们在使用图片的时候,选择jpg、png或者webp,对内存都有影响
- BitmapFactory.options
- inJustDecodeBounds: 为true时,decoder将返回null,但是为解析出 outxxx字段
- inPreferredConfig: 设置图片解码后的像素格式,如ARGB_8888/RGB_565
- inSampleSize: 设置图片解码缩放比,如值为2,则加载图片的宽高是原来的1/2,整个图片所占内存的大小是原图的 1/4
- 内存缓存:LRUCache,最近最少使用算法,里面使用的是LinkedHashMap来存储的
缩放优化
- 如果一个ImageView在屏幕上显示的宽度和高度只有100像素,而图片本身宽度和高度有1000像素,那么我们把整个图片完全转成Bitmap加载进来,是不是会造成内存上很大的浪费?答案是肯定的,那么我们解决思路就很明确了,将图片缩放到需要显示的宽度和高度,然后再加载进来,而 BitmapFactory 也提供给了我们方法这么做
- 解决方案
- 计算缩放比例
- 根据缩放比例,获取缩放后图片,然后返回并加载
//1. 计算缩放比例
//w表示图片实际宽度,h表示图片实际高度
//maxW表示图片在屏幕上可显示的最大宽度,maxH表示图片在屏幕上可显示的最大高度
private static int calcuteInSampleSize(int w, int h, int maxW, int maxH) {
int inSampleSize = 1;
if (w > maxW && h > maxH) {
inSampleSize = 2;
while (w / inSampleSize > maxW && h / inSampleSize > maxH) {
inSampleSize *= 2;
}
}
return inSampleSize;
}
//2. 根据缩放比例,获取缩放后图片
public static Bitmap resizeBitmap(Context context, int id, int maxW, int maxH, boolean hasAlpha) {
Resources resources = context.getResources();
BitmapFactory.Options options = new BitmapFactory.Options();
// 设置为true后,再去解析,就只解析 out 参数
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(resources, id, options);
int w = options.outWidth;
int h = options.outHeight;
options.inSampleSize = calcuteInSampleSize(w, h, maxW, maxH);
if (!hasAlpha) {
options.inPreferredConfig = Bitmap.Config.RGB_565;
}
options.inJustDecodeBounds = false;
//根据缩放比例,获取缩放后图片
return BitmapFactory.decodeResource(resources, id, options);
}
缓存优化
在做过缩放优化后,如果在列表的每一项中都有图片的时候,上下滑动列表,为了显示图片,程序会频繁解析图片、快速申请内存、快速释放内存,频繁解析会造成cpu资源的消耗,快速申请和释放会造成内存抖动,从而频繁gc,而gc时会STW(stop the world),从而引起卡顿。
为了解决上述问题,我们采用三级缓存来缓存Bitmap资源,从而优化内存使用。
- LruCache:内存级别,存储后,需要显示时直接取出来,直接显示到ImageView上就行
- 缓存池缓存:内存级别,与Lru不同的是LruCache可以直接复用Bitmap资源,而缓存池只能复用内存空间,简单地说,就是缓存池中Bitmap是一个空对象,使用时需要重新为其赋值,复用内存空间,就不会频繁去申请和释放
- 磁盘缓存:磁盘级别
LruCache
LruCache,使用最近最少使用算法,加载过的图片优先放到LruCache中,需要加载图片时先从LruCache中尝试去取,可以取到时便直接用,不需要重新解析,不需要重新创建Bitmap
- 初始化
ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
//应用可用的内存总大小,单位是Mb,*1024*1024是转化成了字节
int memoryClass = am.getMemoryClass();
LruCache<String, Bitmap> lruCache = new LruCache<String, Bitmap>(memoryClass * 1024 * 1024 / 8) {
// 返回的一张图片大小
@Override
protected int sizeOf(String key, Bitmap value) {
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.KITKAT) {
return value.getAllocationByteCount();
}
return value.getByteCount();
}
@Override
protected void entryRemoved(boolean evicted, String key, Bitmap oldValue, Bitmap newValue) {
oldValue.recycle();
}
};
- 对外提供存入和取出方法
//把bitmap 放入内存
public void putBitmap2Memory(String key, Bitmap bitmap) {
lruCache.put(key, bitmap);
}
//从内存中取出bitmap
public Bitmap getBitmapFromMemory(String key) {
return lruCache.get(key);
}
缓存池缓存
当bitmap从LruCache中移除时,gc可能并未来得及进行清理,这个时候我们通过弱引用把从LruCache中移除的bitmap放到缓存池中,当从LruCache中取不到对应的Bitmap时,就从缓存池中取出一个bitmap,从而达到复用内存空间的目的。
- 初始化
//弱引用,是为了不影响gc回收内存空间,此处创建一个线程安全的Set作为池
Set<WeakReference<Bitmap>> reusablePool = Collections.synchronizedSet(new HashSet<WeakReference<Bitmap>>());
- 当bitmap从LruCache中移除时,添加到缓存池
lruCache = new LruCache<String, Bitmap>(){
@Override
protected void entryRemoved(boolean evicted, String key, Bitmap oldValue, Bitmap newValue) {
if (oldValue.isMutable()) {
// 3.0 bitmap FW层缓存到 native
// <8.0 bitmap FW层缓存到 java
// 8.0 bitmap FW层缓存到 native
reusablePool.add(new WeakReference<Bitmap>(oldValue, getReferenceQueue()));
}
oldValue.recycle();
}
}
- 对外提供复用方法,根据图片宽高
// 3.0前不能复用
// 3.0-4.4,宽高一样,缩放比=1,才能复用
// 4.4之后,只要需要的空间大小够用就可以复用
public Bitmap getReusable(int w, int h, int inSampleSize) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
return null;
}
Bitmap reusable = null;
Iterator<WeakReference<Bitmap>> iterator = reusablePool.iterator();
while (iterator.hasNext()) {
Bitmap bitmap = iterator.next().get();
if (bitmap != null) {
if (checkInBitmap(bitmap, w, h, inSampleSize)) {
reusable = bitmap;
iterator.remove();
break;
}
} else {
iterator.remove();
}
}
return reusable;
}
磁盘缓存
使用DiskLruCache进行磁盘缓存,这样一个图片资源解析成Bitmap后,使用时,就不需要再解析,直接从磁盘中取出放到内存中就可以了
- 初始化
DiskLruCache diskLruCache = DiskLruCache.open(new File(dir), BuildConfig.VERSION_CODE, 1, 10 * 1024 * 1024);
- 对外提供写入和读取方法
//放入磁盘缓存
public void putBitmap2Disk(String key, Bitmap bitmap) {
DiskLruCache.Snapshot snapshot = null;
OutputStream os = null;
snapshot = diskLruCache.get(key);
if (snapshot == null) {
DiskLruCache.Editor edit = diskLruCache.edit(key);
if (edit != null) {
os = edit.newOutputStream(0);
bitmap.compress(Bitmap.CompressFormat.JPEG, 50, os);
edit.commit();
}
}
}
//从磁盘缓存获取bitmap
public Bitmap getBitmapFromDisk(String key, Bitmap reusable) {
DiskLruCache.Snapshot snapshot = null;
Bitmap bitmap = null;
snapshot = diskLruCache.get(key);
if (snapshot == null) {
return null;
}
InputStream is = snapshot.getInputStream(0);
BitmapFactory.Options options = new BitmapFactory.Options();
options.inMutable = true;
options.inBitmap = reusable;
bitmap = BitmapFactory.decodeStream(is, null, options);
if (bitmap != null) {
lruCache.put(key, bitmap);
}
return bitmap;
}
长图加载
如果一个长图有几十兆甚至是上百兆,如果我们直接使用BitmapFactory去解析,将会引起OOM,如何去解决呢?
我们可以使用系统提供的BigmapRegionDecoder来加载指定区域,将宽度缩放到屏幕宽度,先加载屏幕高度的区域,滑动时候再根据滑动区域加载对应的区域,可以大大节省内存占用。
InputStream is = getAssets().open("big.png");
//创建一个BigmapRegionDecoder对象,
BigmapRegionDecoder decoder = BigmapRegionDecoder.newInstance(is,false);
//获取Rect指定区域的图片
Bitmap bitmap = decoder.decodeRegion(Rect,null);
图片压缩
通过哈夫曼编码对图片进行压缩,可以在不影响图片清晰度的情况下,对图片进行大幅压缩,详情请点击Android图片压缩——Luban鲁班压缩
实际应用
实际使用中,我们可以直接使用第三方库来解决,基本原理类似,缓存用glide,大图用BigImageViewer