文章目录
前言
在Android应用程序内存问题中,Bitmap内存问题,占了相当的比重,所以弄清楚Bitmap内存管理的机制原理就尤为重要。下面就分析总结一下,Bitmap内存相关的问题。
1.Bitmap简介
Bitmap 是 Android 系统中的图像处理中最重要类之一。Bitmap 可以获取图像文件信息,对图像进行剪切、旋转、缩放,压缩等操作,并可以以指定格式保存图像文件。
有两种方法可以创建 Bitmap 对象,分别是通过 Bitmap.createBitmap() 和 BitmapFactory 的 decode 系列静态方法创建 Bitmap 对象。
下面我们主要介绍 BitmapFactory 的 decode 方式创建 Bitmap 对象。
- decodeFile 从文件系统中加载
- 通过 Intent 打开本地图片或照片
- 根据 uri 获取图片的路径
- 根据路径解析 Bitmap:Bitmap bm = BitmapFactory.decodeFile(path);
- decodeResource 以 R.drawable.xxx 的形式从本地资源中加载
- Bitmap bm = BitmapFactory.decodeResource(getResources(),R.drawable.icon);
- decodeStream 从输入流加载
- Bitmap bm = BitmapFactory.decodeStream(stream);
- decodeByteArray 从字节数组中加载
- Bitmap bm = BitmapFactory.decodeByteArray(myByte,0,myByte.length);
2.BitmapFactory.Options控制图片解码的参数
-
inSampleSize:这是表示采样大小。用于将图片缩小加载出来的,以免占用太大内存,适合缩略图。
-
inJustDecodeBounds:当 inJustDecodeBounds 为 true 时,执行 decodexxx 方法时,BitmapFactory 只会解析图片的原始宽高信息,并不会真正的加载图片
-
inPreferredConfig:用于配置图片解码方式,对应的类型 Bitmap.Config。如果非null,则会使用它来解码图片。默认值为是 Bitmap.Config.ARGB_8888
-
inBitmap:在 Android 3.0 开始引入了 inBitmap 设置,通过设置这个参数,在图片加载的时候可以使用之前已经创建了的 Bitmap,以便节省内存,避免再次创建一个Bitmap。在 Android4.4,新增了允许 inBitmap 设置的图片与需要加载的图片的大小不同的情况,只要 inBitmap 的图片比当前需要加载的图片大就好了。
-
inDendity
扫描二维码关注公众号,回复: 11218307 查看本文章表示这个bitmap的像素密度,根据drawable目录, 图片放在不同的资源目录,会有不同程度缩放
-
inTargetDensity
表示要被画出来时的目标(屏幕)的像素密度,
代码中获取的方式getResources().getDisplayMetrics().densityDpi
通过 BitmapFactory.Options 的这些参数,我们就可以按一定的采样率来加载缩小后的图片,然后在 ImageView 中使用缩小的图片这样就会降低内存占用避免【OOM】,提高了 Bitamp 加载时的性能。
这其实就是我们常说的图片尺寸压缩。尺寸压缩是压缩图片的像素,一张图片所占内存大小的计算方式: 图片类型*宽*高,通过改变三个值减小图片所占的内存,防止OOM,当然这种方式可能会使图片失真 。
3.Bitmap的内存占用
我们在使用图片的时候,选择 jpg、png或者webp,对内存会不会有影响呢?对同一张图片而言,更改jpg为png或其它格式,并不会改变它实际占用的内存大小。Bitmap的大小(在本地磁盘通过属性查看)与Bitmap在内存中占用的大小是两个概念,值也不相同。Bitmap内存占用大小为:width * height * 一个像素点占用的字节数(由Bitmap的像素存储格式决定),只要这些因素没变,占用内存是不会改变的。可通过调用getByteCount()来获取Bitmap占用内存的大小,该方法返回的是可用于存储此位图像素的最小字节数
将这三张图片拷贝至drawable-xxhdpi目录,调用getByteCount方法获取占用内存:
/**
* 解析图片
*/
private void decodeBitmap() {
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.icon_mv);
Log.e("xpf", "decodeBitmap: icon_mv.jpg " + bitmap.getWidth() + "x" + bitmap.getHeight() + "x"
+ bitmap.getConfig() + ",内存总大小" + bitmap.getByteCount());
Bitmap bitmap1 = BitmapFactory.decodeResource(getResources(), R.drawable.icon_mv_p);
Log.e("xpf", "decodeBitmap: icon_mv_p.png " + bitmap1.getWidth() + "x" + bitmap1.getHeight() + "x"
+ bitmap1.getConfig() + ",内存总大小" + bitmap1.getByteCount());
Bitmap bitmap2 = BitmapFactory.decodeResource(getResources(), R.drawable.icon_mv_w);
Log.e("xpf", "decodeBitmap: icon_mv_w.webp " + bitmap2.getWidth() + "x" + bitmap2.getHeight() + "x"
+ bitmap2.getConfig() + ",内存总大小" + bitmap2.getByteCount());
}
打印日志如下:
我们知道由于Android的资源加载机制,图片放在不同的drawable目录,会对图片进行不同程度的拉伸或缩放,如果将图片放到drawable-hdpi目录,图片内存占用内存信息如下:
很明显图片尺寸被放大/拉伸了,内存占用就变大了,与Android资源加载机制相关。有一定的优先级,与设备dpi有关,适配时需要注意。
匹配的优先级:
- 如果在最匹配的目录没有找到对应图片,就会向更高密度的目录查找,直到没有更高密度的目录
- 如果一直往高密度目录均没有查找,Android就会查找drawable-nodpi目录。drawable-nodpi目录中的资源适用于所有密度的设备,不管当前屏幕的密度如何,系统都不会缩放此目录中的资源。因此,对于永远不希望系统缩放的资源,最简单的方法就是放在此目录中;同时,放在该目录中的资源最好不要再放到其他drawable目录下了,避免得到非预期的效果
- 如果在drawable-nodpi目录也没有查找到,系统就会向比最匹配目录密度低的目录依次查找,直到没有更低密度的目录。例如,最匹配目录是xxhdpi,更高密度的目录和nodpi目录查找不到后,就会依次查找drawable-xhdp、drawable-hdpi、drawable-mdpi、drawable-ldpi。
放大还是缩小:
- 如果图片所在目录为匹配目录,则图片会根据设备dpi做适当的缩放调整。
- 如果图片所在目录dpi低于匹配目录,那么该图片被认为是为低密度设备需要的,现在要显示在高密度设备上,图片会被放大。
- 如果图片所在目录dpi高于匹配目录,那么该图片被认为是为高密度设备需要的,现在要显示在低密度设备上,图片会被缩小。
- 如果图片所在目录为drawable-nodpi,则无论设备dpi为多少,保留原图片大小,不进行缩放。
像素存储格式中A代表透明度;R代表红色;G代表绿色;B代表蓝色,单个像素点占用的字节数如下表:
格式 | 意义 | 单个像素占用字节数 |
---|---|---|
ALPHA_8 | 表示8位Alpha位图,即A=8,一个像素点占用1个字节,它没有颜色,只有透明度 | 1 |
ARGB_4444 | 表示16位ARGB位图,即A=4,R=4,G=4,B=4,一个像素点占4+4+4+4=16位 | 2 |
ARGB_8888 | 表示32位ARGB位图,即A=8,R=8,G=8,B=8,一个像素点占8+8+8+8=32位 | 4 |
RGB_565 | 表示16位RGB位图,即R=5,G=6,B=5,它没有透明度,一个像素点占5+6+5=16位 | 2 |
3.Bitmap的高效加载/内存压缩/采样率压缩
想象一下如果一张图片为800 * 800 的RGB_565加载到内存,占用内存大小为800 * 800 * 2=1.22M, 比如显示的Imageview大小为80 * 80 ,一个应用有数不清的图片,如果直接加载到内存,对内存的压力太大了,显然是不合理的。此时,就根据采样率来加载图片。采样率inSampleSize = n, 表示采样后的图片的宽高均为图片原始宽高的1/n, 其中n一般为2的指数,比如1,2,4,8,16等等。如果外界传递给系统的inSampleSize不为2的指数,那么系统会向下取整一个最接近2的指数来代替,比如3,系统会选择2来代替。通过采用率来加载图片,本质是根据一定比例对图片进行尺寸压缩,其流程大致如下:
- 将BitmapFactory.Options的inJustDecodeBounds参数设为true,并加载图片
- 从BitmapFactory.Options中取出图片的原始宽高信息,它们对应于outWidth和outHeight参数
- 根据采样率的规则并结合目标View的所需大小计算出采样率inSampleSize
- 将BitmapFactory.Options的inJustDecodeBounds参数设为false,然后重新加载图片
需要注意的是inJustDecodeBounds参数设为true,BitmapFactory只会解析图片的的原始宽高信息,并不会去真正的加载图片。得到采样率后,inJustDecodeBounds参数设为false,再去真正加载图片,加载的图片就是最终缩放后的图片。
/**
* Bitmap高效加载,采样率压缩,本质是调整bitmap的尺寸
*/
public class ImageResize {
/**
* 返回压缩的图片
* @param context
* @param id
* @param reqWidth
* @param reqHeight
* @param hasAlpha
* @return
*/
public static Bitmap resizeBitmap(Context context, int id, int reqWidth, int reqHeight, boolean hasAlpha) {
Resources resources = context.getApplicationContext().getResources();
BitmapFactory.Options options = new BitmapFactory.Options();
//设置inJustDecodeBounds为true时,并不会真正加载图片,只是解析bitmap的原始宽高信息,轻量级操作
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(resources, id, options);
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
if (!hasAlpha) {
//不需要透明度
options.inPreferredConfig = Bitmap.Config.RGB_565;
}
//设置inJustDecodeBounds为false,真正去加载图片
options.inJustDecodeBounds = false;
return BitmapFactory.decodeResource(resources, id, options);
}
/**
* 获取采样率/缩放系数
* @param options
* @param reqWidth 需要显示的宽度/xml定义的宽度
* @param reqHeight 需要显示的高度/xml定义的高度
* @return
*/
public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
//bitmap的原始宽高(测量宽高)
int width = options.outWidth;
int height = options.outHeight;
int inSampleSize = 1;
if (height > reqHeight || width > reqWidth) {
int halfWidth = width / 2;
int halfHeight = height / 2;
while ((halfHeight / inSampleSize) >= reqHeight && (halfWidth / inSampleSize) >= reqWidth) {
inSampleSize *=2;
}
}
return inSampleSize;
}
}
这就是Bitmap的内存优化角度之一,内存压缩即采样率压缩,本质是通过调整图片的尺寸,前面提到过,图片的占用内存为:宽 * 高 * 单个像素占用字节 ,这三个因素不变,内存占用就不会改变。在BitmapAdapter类的onBindViewHolder方法中,分别采用以下两种方式加载bitmap,对比内存占用情况
@Override
public void onBindViewHolder(@NonNull BitmapViewHolder bitmapViewHolder, int i) {
//原始方法获取bitmap
// Bitmap bitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.icon_mv_w);
// 优化角度一:压缩图片
Bitmap bitmap = ImageResize.resizeBitmap(context, R.drawable.icon_mv, 80, 80, false);
bitmapViewHolder.iv.setImageBitmap(bitmap);
}
2.3之前的像素存储需要的内存是在native上分配的,并且生命周期不太可控,可能需要用户自己回收。 2.3-7.1之间,Bitmap的像素存储在Dalvik的Java堆上,当然,4.4之前的甚至能在匿名共享内存上分配(Fresco采用),而8.0之后的像素内存又重新回到native上去分配,不需要用户主动回收,当java层bitmap被回收后,能及时回收native层的像素数据。8.0之后图像资源的管理更加优秀,极大降低了OOM的发生。
4.Bitmap的缓存策略
主要就是内存缓存和磁盘缓存,内存缓存主要就是利用LruCache通过最近最少使用算法来实现,是基于LinkedHashMap的封装。通过下面简单的例子,便于更形象的理解。
public class LRU {
public static main(String[] args) {
//true 访问排序
LinkedHashMap<String, Integer> map = new LinkedHashMap(0, 0.75F, true);
map.put("一", 1);// 最开始添加(访问)的,它的LRU算法移除概率是最高的(越容易被移除)
map.put("二", 2);
map.put("三", 3);
map.put("四", 4);
map.put("五", 5);// 最后添加(访问)的,它的LRU算法移除概率是最低的(越难被移除)
for(Map.Entry<String, Integer> value : map.entrySet()){
System.out.print(value.getValue);
}
//12345
//使用了某个元素
map.get("三");//最近被访问,就越不可能被回收
for(Map.Entry<String, Integer> value : map.entrySet()){
System.out.print(value.getValue);
}
//12453
}
}
遍历打印集合的value,当达到容量上限时,优先移除最近最少使用,即最先添加或者最先访问的元素会被优先移除。在例子中,元素3最近被访问了,从频率和时间上看,元素1就是最近最少使用,如果达到容量上限,会优先移除元素1,再添加元素。
内存缓存是通过LrcCache来实现,磁盘缓存是通过DiskLruCache实现,下面是内存缓存和磁盘缓存的初始化
public void init(Context context, String dir) {
//初始化复用池
reusePool = Collections.synchronizedSet(new HashSet<WeakReference<Bitmap>>());
ActivityManager activityManager = (ActivityManager) context.getApplicationContext().getSystemService(Context.ACTIVITY_SERVICE);
if (activityManager == null) {
return;
}
//内存大小
int memorySize = activityManager.getMemoryClass();
//返回一张图片占用内存大小,单位:byte
lruCache = new LruCache<String, Bitmap>(memorySize / 8 * 1024 * 1024) {
//返回一张图片占用内存大小
@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) {
//判断是否采用复用内存
if (oldValue.isMutable()) {
//3.0~7.1 bitmap 像素存储在 Java 堆
//8.0及之后,bitmap 像素存储在 Native 堆
reusePool.add(new WeakReference<Bitmap>(oldValue, getReferenceQueue()));
} else {
//如果没有开启bitmap内存复用,那么就回收释放
oldValue.recycle();
}
}
};
try {
//初始化磁盘缓存
diskLruCache = DiskLruCache.open(new File(dir), BuildConfig.VERSION_CODE, 1, 10 * 1024 * 1024);
} catch (IOException e) {
e.printStackTrace();
}
}
添加bitmap缓存的get、put方法
/**
* 内存缓存,将bitmap放入内存
* @param key
* @param bitmap
*/
public void putBitmapToMemory(String key, Bitmap bitmap) {
lruCache.put(key, bitmap);
}
/**
* 从内存缓存中取出Bitmap
* @param key
* @return
*/
public Bitmap getBitmapFromMemory(String key) {
return lruCache.get(key);
}
/**
* 清除内存缓存
*/
public void clearMemory() {
lruCache.evictAll();
}
/**
* 将bitmap放入磁盘缓存
* @param key
* @param bitmap
*/
public void putBitmapToDisk(String key, Bitmap bitmap) {
DiskLruCache.Snapshot snapshot = null;
OutputStream os = null;
try {
snapshot = diskLruCache.get(key);
if (snapshot == null) {
DiskLruCache.Editor editor = diskLruCache.edit(key);
if (editor != null) {
os = editor.newOutputStream(0);
//质量压缩
bitmap.compress(Bitmap.CompressFormat.JPEG, 50, os);
editor.commit();
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (snapshot != null) {
snapshot.close();
}
if (os != null) {
try {
os.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
/**
* 从磁盘缓存中获取bitmap
* @param key
* @return
*/
public Bitmap getBitmapFromDisk(String key, Bitmap resueable) {
DiskLruCache.Snapshot snapshot = null;
Bitmap bitmap = null;
InputStream is = null;
try {
snapshot = diskLruCache.get(key);
if (snapshot == null) {
return null;
}
is = snapshot.getInputStream(0);
BitmapFactory.Options options = new BitmapFactory.Options();
options.inMutable = true;
options.inBitmap = resueable;
bitmap = BitmapFactory.decodeStream(is, null, options);
if (bitmap != null) {
lruCache.put(key, bitmap);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (snapshot != null) {
snapshot.close();
}
}
return bitmap;
}
5.Bitmap的内存复用
如下图所示,在Android 3.0之前,为bitmap申请的内存是不能复用的,每次加载一张bitmap,都会重新为它分配一块内存,这显然是很耗内存的,而且频繁的加载图片,释放内存容易造成内存抖动;在3.0~4.4 版本,内存可以复用,但前提是,两张图片占用的内存大小必须相等;在4.4之后,只要加载的图片,比释放的图片占用内存小,就可以复用内存,大大降低了内存的消耗,也在一定程度上降低了内存抖动的发生的可能性。
Bitmap的内存复用主要有几个细节要注意
-
开启内存复用
//bitmap内存复用,inMutable 为true,表示可变的,才能复用
//下面两行代码就表示了开启了bitmap的内存复用
options.inMutable = true;options.inBitmap = bitmap;
-
判断是否满足内存复用条件
/**
* bitmap内存复用条件判断
* 3.0 之前不能复用
* 3.0~4.4 宽高一样,inSampleSize = 1, 即内存占用一样才可以
* 4.4 之后,只要小于等于可复用的内存就可以
* @return
*/
public Bitmap getReuseable(int width, int height, int inSampleSize) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
return null;
}
Bitmap reuseable = null;
Iterator<WeakReference<Bitmap>> iterator = reusePool.iterator();
while (iterator.hasNext()) {
Bitmap bitmap = iterator.next().get();
if (bitmap != null) {
if (checkBitmapMemory(bitmap, width, height, inSampleSize)) {
reuseable = bitmap;
iterator.remove();
break;
}
} else {
iterator.remove();
}
}
return reuseable;
}
/**
* 校验bitmap是否满足内存复用条件
* @param bitmap
* @param width
* @param height
* @param inSampleSize
* @return
*/
private boolean checkBitmapMemory(Bitmap bitmap, int width, int height, int inSampleSize) {
//3.0~4.4
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
return bitmap.getWidth() == width && bitmap.getHeight() == height && inSampleSize == 1;
}
// 高于4.4
if (inSampleSize > 1) {
width /= inSampleSize;
height /= inSampleSize;
}
int byteCount = width*height*getBytePerPixel(bitmap.getConfig());
//bitmap.getByteCount() 图片内存
//bitmap.getAllocationByteCount() 系统分配内存
return byteCount <= bitmap.getAllocationByteCount();
}
/**
* 通过像素格式计算每一个像素占用字节
* @param config
* @return
*/
private int getBytePerPixel(Bitmap.Config config) {
if (config == Bitmap.Config.ALPHA_8) {
return 1;
} else if (config == Bitmap.Config.ARGB_8888) {
return 4;
} else {
return 2;
}
}
- 如果开启了内存复用,满足复用条件,当从缓存移除元素时,就存放到复用池,否则就回收释放
@Override
protected void entryRemoved(boolean evicted, String key, Bitmap oldValue, Bitmap newValue) {
if (oldValue.isMutable()) {
//3.0~7.1 bitmap 像素存储在 Java 堆
//8.0及之后,bitmap 像素存储在 Native 堆
reusePool.add(new WeakReference<Bitmap>(oldValue, getReferenceQueue()));
} else {
//bitmap回收
oldValue.recycle();
}
}
6.小结
Bitmap的内存管理主要就是这几个部分,不同层级的缓存,内存缓存和磁盘缓存,避免每次都要网络加载。通过采样率加载图片,达到压缩的效果,避免加载bitmap过大造成的内存压力;内存复用,避免了每次加载释放bitmap,都要分配和释放对应的内存空间,有效的缓解了内存抖动。有了对Bitmap内存管理的认识,就比较容易理解图片加载框架原理了。