目录
1、Bitmap到底占多少内存
1.1、Android API计算方式
- 在API12开始提供了getByteCount()方法,用来计算Bitmap所占的内存。
- 在API19开始getAllocationByteCount()方法代替了getByteCount()。
1.1.1、getByteCount()和getAllocationByteCount()区别
- 一般情况下两者是相等的。
- 在Bitmap复用的情况下,getByteCount()表示新解码图片所占内存大小(并非实际大小,实际大小是复用的那个Bitmap的大小),而getAllocationByteCount()则表示被复用Bitmap所占的内存大小。
1.2、Bitmap占用内存的计算公式
从磁盘加载或者从网络加载的计算公式如下:
图片的长度 * 图片的宽度 * 一个像素点占用的字节数
如果从资源文件夹中加载,会怎么样呢?
首先把同一张图片放进不同的资源文件夹会发生什么?
- 同一张图片放进不同的文件夹,图片会被压缩
加载资源文件中图片调用BitmapFactory.decodeResource()
方法,该方法内部最终调用如下底层代码进行图片的压缩。
if (env->GetBooleanField(options, gOptions_scaledFieldID)) {
const int density = env->GetIntField(options, gOptions_densityFieldID);
const int targetDensity = env->GetIntField(options, gOptions_targetDensityFieldID);
const int screenDensity = env->GetIntField(options, gOptions_screenDensityFieldID);
if (density != 0 && targetDensity != 0 && density != screenDensity) {
scale = (float) targetDensity / density;
}
}
...
int scaledWidth = decoded->width();
int scaledHeight = decoded->height();
if (willScale && mode != SkImageDecoder::kDecodeBounds_Mode) {
scaledWidth = int(scaledWidth * scale + 0.5f);
scaledHeight = int(scaledHeight * scale + 0.5f);
}
...
if (willScale) {
const float sx = scaledWidth / float(decoded->width());
const float sy = scaledHeight / float(decoded->height());
bitmap->setConfig(decoded->getConfig(), scaledWidth, scaledHeight);
bitmap->allocPixels(&javaAllocator, NULL);
bitmap->eraseColor(0);
SkPaint paint;
paint.setFilterBitmap(true);
SkCanvas canvas(*bitmap);
canvas.scale(sx, sy);
canvas.drawBitmap(*decoded, 0.0f, 0.0f, &paint);
}
我们可以看到压缩比例由如下公式得出
scale = (float) targetDensity / density;
图片的缩放比例和targetDensity和density有关,targetDensity是设备的屏幕像素密度,density是图片对应的资源文件夹对应的屏幕像素密度。
其中density和Bitmap存放的资源目录有关,不同的资源目录有不同的值
density | 0.75 | 1 | 1.5 | 2 | 3 | 4 |
---|---|---|---|---|---|---|
densityDpi | 120 | 160 | 240 | 320 | 480 | 560 |
DpiFolder | ldpi | mdpi | hdpi | xhdpi | xxhdpi | xxxhdpi |
可以得出如下结论:
- 1、同一张图片放在不同的目录下,分辨率会发生变化。
- 2、图片不在资源目录中(如drawable中),其使用的默认dpi为160。
- 3、当设备的像素密度和资源文件夹的像素密度相同时,加载图片时不发生缩放。
放在资源文件夹下的图片占用内存计算公式如下:
Bitmap内存占用 ≈ 像素数据总大小 = 图片宽 × 图片高× (当前设备密度dpi/图片所在文件夹对应的密度dpi)^2 × 每个像素的字节大小
2、Bitmap内存优化从下面五个方面进行优化
- 编码
- 采样
- 复用
- 缓存
- 匿名共享区
2.1、编码
Android 中提供以下几种编码
- 1、ALPHA_8:表示8位Alpha位图,即A=8,它只有透明度没有颜色,1个像素占1个字节。
- 2、ARGB_4444:表示16位ARGB位图,即A=4,R=4,G=4,B=4,一个像素点占2个字节。
- 3、RGB_565:表示16位RGB位图,即R=5,G=6,B=5。它没有透明度,一个像素点占2个字节。
- 4、ARGB_8888:表示32位ARGB位图,即A=8,R=8,G=8,B=8。一个像素点占4个字节。
可以通过修改图片的编码格式来修改一个像素点所占的内存大小,来达到修改Bitmap所占内存的目的。
Bitmap originBitmap=BitmapFactory.decodeResource(getResources(),R.mipmap.reuse);
iv_origin.setImageBitmap(originBitmap);
Log.e(TAG,"原图大小:"+originBitmap.getAllocationByteCount());
BitmapFactory.Options options=new BitmapFactory.Options();
//一般通过修改Bitmap的编码格式为RGB_565来达到压缩目的时,不建议修改为ARGB_4444,图片失真严重
options.inPreferredConfig=Bitmap.Config.RGB_565;
Bitmap compressBitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.reuse, options);
Log.e(TAG,"压缩后大小:"+compressBitmap.getAllocationByteCount());
iv_reused.setImageBitmap(compressBitmap);
输出log如下:
E/MainActivity: 原图大小:4762800
E/MainActivity: 压缩后大小:2381400
可见压缩后的图片所占内存大小为原图的一半,通过修改图片的编码格式可以实现图片的压缩。
2.2、采样
修改BitmapFactory.Options.inSampleSize
可以修改图片的宽高,来达到修改图片的占用内存。
BitmapFactory.Options options = new BitmapFactory.Options();
//不获取图片,不加载到内存中,只返回图片属性
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(photoPath, options);
//图片的宽高
int outHeight = options.outHeight;
int outWidth = options.outWidth;
Log.d("mmm", "图片宽=" + outWidth + "图片高=" + outHeight);
//计算采样率
int i = utils.computeSampleSize(options, -1, 1000 * 1000);
//设置采样率,不能小于1 假如是2 则宽为之前的1/2,高为之前的1/2,一共缩小1/4 一次类推
options.inSampleSize = i;
Log.d("mmm", "采样率为=" + i);
//图片格式压缩
//options.inPreferredConfig = Bitmap.Config.RGB_565;
options.inJustDecodeBounds = false;
Bitmap bitmap = BitmapFactory.decodeFile(photoPath, options);
float bitmapsize = getBitmapsize(bitmap);
Log.d("mmm","压缩后:图片占内存大小" + bitmapsize + "MB / 宽度=" + bitmap.getWidth() + "高度=" + bitmap.getHeight());
看下打印信息
07-09 11:02:11.714 8010-8010/com.example.jh.rxhapp D/mmm: 原图:图片占内存大小=45.776367MB / 宽度=4000高度=3000
07-09 11:02:11.715 8010-8010/com.example.jh.rxhapp D/mmm: 图片宽=4000图片高=3000
07-09 11:02:11.715 8010-8010/com.example.jh.rxhapp D/mmm: 采样率为=4
07-09 11:02:11.944 8010-8010/com.example.jh.rxhapp D/mmm: 压缩后:图片占内存大小1.4296875MB / 宽度=1000高度=750
这种我们根据BitmapFactory 的采样率进行压缩 设置采样率,不能小于1 假如是2 则宽为之前的1/2,高为之前的1/2,一共缩小1/4 一次类推,我们看到log ,确实起到了压缩的目的。
2.3、复用
Bitmap的复用就需要用到BitmapFactory.Options.inBitmap
属性
这个属性又什么作用?
不使用这个属性,你加载三张图片,系统会给你分配三份内存空间,用于分别储存这三张图片。
如果用了inBitmap这个属性,加载三张图片,这三张图片会指向同一块内存,而不用开辟三块内存空间。
inBitmap的限制
- 3.0-4.3
-- 复用的图片大小必须相同
-- 编码必须相同- 4.4以上
-- 复用的空间大于等于即可
-- 编码不必相同- 不支持WebP
- 图片复用,这个属性必须设置为true; options.inMutable = true;
复用的实例
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
public void reuseBitmap(View view) {
BitmapFactory.Options options=new BitmapFactory.Options();
options.inMutable=true;
options.inDensity=320;
options.inTargetDensity=320;
Bitmap origin= BitmapFactory.decodeResource(getResources(),R.mipmap.origin,options);
iv_origin.setImageBitmap(origin);
Log.e(TAG,origin.toString());
Log.e(TAG,"origin:getByteCount:"+origin.getByteCount()+",origin:getAllocationByteCount:"+origin.getAllocationByteCount());
options.inDensity=320;
options.inTargetDensity=160;
options.inMutable=true;
options.inBitmap=origin;
Bitmap reuseBitmap=BitmapFactory.decodeResource(getResources(),R.mipmap.reuse,options);
iv_reused.setImageBitmap(reuseBitmap);
Log.e(TAG,reuseBitmap.toString());
Log.e(TAG,"origin:getByteCount:"+origin.getByteCount()+",origin:getAllocationByteCount:"+origin.getAllocationByteCount());
Log.e(TAG,"reuseBitmap:getByteCount:"+reuseBitmap.getByteCount()+",reuseBitmap:getAllocationByteCount:"+reuseBitmap.getAllocationByteCount());
}
2.4、图片的缓存
使用LruCache进行内存缓存。LruCache底层使用LinkedHashMap存储数据,并在达到设置的最大内存前将最近最少使用的数据删除。使用LruCache可以避免内存的频繁创建和销毁带来的内存开销。
在实现项目中还会结合DiskLruCache磁盘缓存一起使用。
2.5、匿名共享内存(Ashmem)
匿名共享内存是为了进程间共享数据分配的一块内存,在Android5.0之前,Bitmap可以存储在匿名共享内存上,实现像素数据伪存储在Native内存上,一个典型的例子就是Fresco,Fresco为了提高5.0之前图片处理的性能,就很有效的利用了这个特性,但是在Android5.0后就限制了匿名共享内存的使用。
3、图片到底储存在哪里?
2.3之前 | 3.0~7.1 | 8.0之后 | |
---|---|---|---|
bitmap对象 | Java Heap | Java Heap | Java Heap |
像素数据 | Native Heap | Java Heap | Native Heap |
迁移原因 | ---- | 解决Native Bitmap内存泄漏 | 共享整个系统的内存减少OOM |
- 在2.3之前像素数据是存储在Native内存上的,但是生命周期不可控,需要手动调用Bitmap.recycle()进行回收。
- 在3.0~7.1之间,Bitmap像素数据存储在Dalvik的Java Heap上,甚至在4.4之前,可以在匿名共享内存上分配,实现像素数据伪存储在Native内存上。
- 在8.0之后,Bitmap像素数据又重新回到了Native内存上,并且不需要手动回收内存,它共享整个系统的内存,极大的减少了OOM。
比如在8.0手机上如果一直创建Bitmap,如果手机内存有1G,那么你的应用加载1G也不会OOM。
4、图片的压缩
图片的压缩一般有两种:
- 通过修改采样率进行压缩,上面已经讲过。
- 质量压缩。
bitmap.compress(Bitmap.CompressFormat.JPEG, 20,
new FileOutputStream("sdcard/result.jpg"));
- 质量压缩不会改变图片所占内存的大小,它改变的是图片存储的大小。
质量压缩不会减少图片的像素,它是在保持像素的前提下改变图片的位深及透明度,来达到压缩图片的目的,图片的长,宽,像素都不会改变,那么bitmap所占内存大小是不会变的。
- 质量压缩对png格式这种图片没有作用,因为png是无损压缩。
- 质量压缩会导致图片失真。
- 如果想压缩图片大小,还想做到不失真可以使用jpeg方式对图片压缩。具体可查看Android性能优化篇之图片压缩优化
5、如何加载高清大图
如果有需求,要求我们既不能压缩图片,又不能发生oom怎么办,这种情况我们需要加载图片的一部分区域来显示,下面我们来了解一下BitmapRegionDecoder这个类,加载图片的一部分区域,他的用法很简单。
//支持传入图片的路径,流和图片修饰符等
BitmapRegionDecoder mDecoder = BitmapRegionDecoder.newInstance(path, false);
//需要显示的区域就有由rect控制,options来控制图片的属性
Bitmap bitmap = mDecoder.decodeRegion(mRect, options);
由于要显示一部分区域,所以要有手势的控制,方便上下的滑动,需要自定义控件,而自定义控件的思路也很简单。
- 1 提供图片的入口
- 2 重写onTouchEvent, 根据手势的移动更新显示区域的参数
- 3 更新区域参数后,刷新控件重新绘制
public class BigImageView extends View {
private BitmapRegionDecoder mDecoder;
private int mImageWidth;
private int mImageHeight;
//图片绘制的区域
private Rect mRect = new Rect();
private static final BitmapFactory.Options options = new BitmapFactory.Options();
static {
options.inPreferredConfig = Bitmap.Config.RGB_565;
}
public BigImageView(Context context) {
super(context);
init();
}
public BigImageView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public BigImageView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
}
/**
* 自定义view的入口,设置图片流
*
* @param path 图片路径
*/
public void setFilePath(String path) {
try {
//初始化BitmapRegionDecoder
mDecoder = BitmapRegionDecoder.newInstance(path, false);
BitmapFactory.Options options = new BitmapFactory.Options();
//便是只加载图片属性,不加载bitmap进入内存
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(path, options);
//图片的宽高
mImageWidth = options.outWidth;
mImageHeight = options.outHeight;
Log.d("mmm", "图片宽=" + mImageWidth + "图片高=" + mImageHeight);
requestLayout();
invalidate();
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//获取本view的宽高
int measuredHeight = getMeasuredHeight();
int measuredWidth = getMeasuredWidth();
//默认显示图片左上方
mRect.left = 0;
mRect.top = 0;
mRect.right = mRect.left + measuredWidth;
mRect.bottom = mRect.top + measuredHeight;
}
//第一次按下的位置
private float mDownX;
private float mDownY;
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mDownX = event.getX();
mDownY = event.getY();
break;
case MotionEvent.ACTION_MOVE:
float moveX = event.getX();
float moveY = event.getY();
//移动的距离
int xDistance = (int) (moveX - mDownX);
int yDistance = (int) (moveY - mDownY);
if (mImageWidth > getWidth()) {
mRect.offset(-xDistance, 0);
checkWidth();
//刷新页面
invalidate();
}
if (mImageHeight > getHeight()) {
mRect.offset(0, -yDistance);
checkHeight();
invalidate();
}
mDownX = event.getX();
mDownY = event.getY();
break;
case MotionEvent.ACTION_UP:
break;
default:
}
return true;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Bitmap bitmap = mDecoder.decodeRegion(mRect, options);
canvas.drawBitmap(bitmap, 0, 0, null);
}
/**
* 确保图不划出屏幕
*/
private void checkWidth() {
Rect rect = mRect;
int imageWidth = mImageWidth;
int imageHeight = mImageHeight;
if (rect.right > imageWidth) {
rect.right = imageWidth;
rect.left = imageWidth - getWidth();
}
if (rect.left < 0) {
rect.left = 0;
rect.right = getWidth();
}
}
/**
* 确保图不划出屏幕
*/
private void checkHeight() {
Rect rect = mRect;
int imageWidth = mImageWidth;
int imageHeight = mImageHeight;
if (rect.bottom > imageHeight) {
rect.bottom = imageHeight;
rect.top = imageHeight - getHeight();
}
if (rect.top < 0) {
rect.top = 0;
rect.bottom = getHeight();
}
}
}