Bitmap
Android 之 Bitmap
Bitmap 介绍
Bitmap 代表一张位图,位图文件图像效果好(高质量图片格式),但是非压缩格式的,需要占用较大存储空间,不利于网络上传送。而 JPEG 格式就弥补了位图文件这个缺点。
在 Android 中,Bitmap 是图像处理最重要的类之一,用它可以获取图像文件信息,进行图像剪切、旋转、缩放等操作,并可以指定格式保存图像文件。
Bitmap 获取
通过 BitmapFactory 获取
- 通过资源 id decodeResource()
- 通过文件路径
- 通过字节数组
- 通过数据流 decodeStream()
BitmapFactory.Options
- inJustDecodeBounds
设置为 true 后,不会真正分配 Bitmap 所占用的内存空间,仅仅获取一些属性 - inSampleSize
缩放图片采用的比率值 - inPreferredConfig = Bitmap.Config.ARGB_8888
设置图片的色彩模式,默认 ARGB_8888。可选见 Bitmap.Config:ALPHA_8、RGB_565、ARGB_4444(过时)、默认 ARGB_8888。
Bitmap 图片处理
通过 Bitmap 对图片的操作,都是通过 jni 来实现,调用 skia 这个库(具体可以操作 bugly 的 bitmap 占用内存那篇文件)。
- 剪切
Bitmap.createBitmap() - 缩放
Matrix.postScale() - 旋转
Matrix.postRotate() - 平移
Matrix.postTranslate() - 保存
compress()
图片到底储存在哪里?
8.0Bitmap 的像素数据存储在 Native,为什么又改为 Native 存储呢?
因为 8.0 共享了整个系统的内存,测试 8.0 手机如果一直创建 Bitmap,如果手机内存有 1G,那么你的应用加载 1G 也不会 oom。
Bitmap 分块加载(加载巨图之图片)
加载清明上河图,要求我们既不能压缩图片,又不能发生 oom 怎么办?
图片分块加载
图片的分块加载,在地图绘制的情况上最为明显,当想要获取一张尺寸很大的图片的某一小块区域时,就可以用到了图片的分块加载。
如显示:世界地图、清明上河图、微博长图等。
BitmapRegionDecoder
BitmapRegionDecoder 用来解码图片中的一块矩形区域,典型用法是加载一张大图的小部分。
1
2
3
4
//支持传入图片的路径,流和图片修饰符等
BitmapRegionDecoder mDecoder = BitmapRegionDecoder.newInstance(path, false);
//需要显示的区域就有由rect控制,options来控制图片的属性
Bitmap bitmap = mDecoder.decodeRegion(mRect, options);
由于要显示一部分区域,所以要有手势的控制,方便上下的滑动,需要自定义控件,而自定义控件的思路也很简单 1 提供图片的入口 2 重写 onTouchEvent, 根据手势的移动更新显示区域的参数 3 更新区域参数后,刷新控件重新绘制。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
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);
Log.d("mmm", "mDownX=" + mDownX + "mDownY=" + mDownY);
Log.d("mmm", "movex=" + moveX + "movey=" + moveY);
Log.d("mmm", "xDistance=" + xDistance + "yDistance=" + yDistance);
Log.d("mmm", "mImageWidth=" + mImageWidth + "mImageHeight=" + mImageHeight);
Log.d("mmm", "getWidth=" + getWidth() + "getHeight=" + getHeight());
if (mImageWidth > getWidth()) {
mRect.offset(-xDistance, 0);
checkWidth();
//刷新页面
invalidate();
Log.d("mmm", "刷新宽度");
}
if (mImageHeight > getHeight()) {
mRect.offset(0, -yDistance);
checkHeight();
invalidate();
Log.d("mmm", "刷新高度");
}
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();
}
}
}
Reference
- Android 高清加载巨图方案 拒绝压缩图片 (zhanghongyang)
Bitmap 内存
Bitmap 内存如何计算?
占用内存 = (图片宽度/inSampleSize X inTargetDensity/inDensity) X (图片高度/inSampleSize X inTargetDensity/inDensity) X 每个像素所占的内存
通俗点讲就是:内存占用 = 宽 X高 X 每个像素所占的内存
inSampleSize
inSampleSize
表示采样率,为 2 的整数次幂。
设置了 inSampleSize 图片的宽高对应的缩小 inSampleSize 的倍数,如 inSampleSize=2,缩小 4 倍
dpi
Bitmap.Config
Bitmap 到底占多大内存
本地磁盘/网络加载图片
从本地加载或者从网络加载可以用下面的公式计算:
1
图片的长度 * 图片的宽度 * 一个像素点占用的字节数
一张图片在不同 ImageView 宽高内存占用?
一样
本地 drawable 资源文件加载图片
如果从本地资源文件夹加载
1
Bitmap内存占用 ≈ 像素数据总大小 = 图片宽 × 图片高× (当前设备密度dpi/图片所在文件夹对应的密度dpi)^2 × 每个像素的字节大小
同一张图片放进不同的文件夹,图片会被压缩。看下源码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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);
}
压缩比例是由下面的公式得出:
1
scale = (float) targetDensity / density;
可以得出以下结论:
- 同一张图片放在不同的资源目录下,其分辨率会有变化。
- Bitmap 的分辨率越高,其解析后的宽高越小,甚至小于原有的图片(及缩放),从而内存也响应的减少。
- 图片不放置任何资源目录时,其使用默认分辨率 mdpi:160。
- 资源目录分辨率和屏幕分辨率一致时,图片尺寸不会缩放。
Bitmap 内存优化
Bitmap 内存优化从下面四个方面进行优化:
- 编码
- 采样
- 复用
- 匿名共享区
编码 (优化单位像素占用内存)
Android 中提供以下几种编码:
- ALPHA_8 表示 8 位 Alpha 位图,即 A=8,一个像素点占用 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 个字节。
A 代表透明度;R 代表红色;G 代表绿色;B 代表蓝色。
可以通过改变图片格式,来改变每个像素占用字节数,来改变占用的内存,看下面代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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);
//图片格式压缩
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());
输出:
1
2
3
D/mmm: 原图:图片占内存大小=45.776367MB / 宽度=4000高度=3000
D/mmm: 图片宽=4000图片高=3000
D/mmm: 压缩后:图片占内存大小22.887695MB / 宽度=4000高度=3000
宽高没变,我们改变了图片的格式,从 ARGB_8888
变成了 RGB_565
,像素占用字节数减少了一般,根据 log 内存也减少了一半,这种方式可行
采样 inSampleSize
(优化 Bitmap 的加载时的宽高)
通过采样,不加载 bitmap 真实的宽高,通过 inSampleSize
只采样实际控件需要用到的宽高
注意:inSampleSize=2,缩放 1/4,长和宽各缩放 1/2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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());
输出:
1
2
3
4
D/mmm: 原图:图片占内存大小=45.776367MB / 宽度=4000高度=3000
D/mmm: 图片宽=4000图片高=3000
D/mmm: 采样率为=4
D/mmm: 压缩后:图片占内存大小1.4296875MB / 宽度=1000高度=750
这种我们根据 BitmapFactory 的采样率进行压缩 设置采样率,不能小于 1 假如是 2 则宽为之前的 1/2,高为之前的 1/2,一共缩小 1/4 一次类推,我们看到 log ,确实起到了压缩的目的
复用 inBitmap
图片复用指的是 inBitmap
这个属性。
- 不使用 inBitmap
不使用这个属性,你加载三张图片,系统会给你分配三份内存空间,用于分别储存这三张图片;如果用了 inBitmap 这个属性,加载三张图片,这三张图片会指向同一块内存,而不用开辟三块内存空间。
- inBitmap 的限制
- 3.0-4.3 复用的图片大小必须相同;编码必须相同
- 4.4 以上 复用的空间大于等于即可; 编码不必相同
- 不支持 WebP
- 图片复用,这个属性必须设置为 true;options.inMutable = true;
匿名共享区(Ashmem)
Android 系统为了进程间共享数据开辟的一块内存区域,由于这块区域不受应用的 Head 的大小限制,相当于可以绕开 oom,FaceBook 的 Fresco 首次应用到实际中。
限制:5.0 以后就限制了匿名共享内存的使用。
Bitmap 优化手段
LRU 管理 Bitmap
利用 LRU 开管理 Bitmap,给他设置内存最大值,及时回收
Bitmap Pool
Bitmap 对象池
图片的压缩
- 采样压缩,节省内存
- 质量压缩,节省空间,不会改变图片在内存中的大
1
2
bitmap.compress(Bitmap.CompressFormat.JPEG, 20,
new FileOutputStream("sdcard/result.jpg"));