Android Bitmap

首先,在Android Studio项目中有两种放置图片的资源文件:

  • drawable资源目录:
    不管是jpg、png、还是9.png,都可以放在这里。除此之外,还有像selector这样的xml文件也是可以放在drawable文件夹下面的。
  • mipmap资源目录:
    一般只是用来放置应用程序的icon的。当然,并非是硬性规定,就是想放图片也可以。使用的方式与drawable一样。

Bitmap的使用

Android中提供了BitmapFactory工具类用以创建不同来源的Bitmap。
比如解析资源文件,本地文件,流等方式。
基本流程都很类似,读取目标文件,转换成输入流,调用native方法解析流,虽然Java层代码没有体现,但是可以猜想到,最后native方法解析完成后,必然会通过JNI调用Bitmap的构造函数(Bitmap的构造函数,是一个给native层调用的方法),完成Java层的Bitmap对象创建。

示例:

1
2
3
4
5
6
Bitmap bitmap = BitmapFactory.decodeResource(getResources(),R.mipmap.ic_launcher);
ivImage.setImageBitmap(bitmap);

// 也是用来设置图片。
// 但封装了bitmap的读入和解析的过程,并且过程是在UI线程完成的,对于性能是有所影响的。
// ivImage.setImageResource(R.mipmap.ic_launcher_round);

Bitmap占用的内存

Bitmap作为位图,需要读入一张图片每一个像素点的数据,其主要占用的内存也正是这些像素数据。

Bitmap内存占用 ≈ 像素数据总大小 = 横向像素数量 × 纵向像素数量 × 每个像素的字节大小

单个像素的字节大小

单个像素的字节大小由Bitmap的一个可配置的参数Config(枚举类Config)来决定
注:在Android系统中,默认Bitmap加载图片,使用24位真彩色模式。

Config 占用字节大小(byte) 说明
ALPHA_8 1 单透明通道
RGB_565 2 简易RGB色调
ARGB_4444 4 已废弃
ARGB_8888 4 24位真彩色
RGBA_F16 8 Android 8.0 新增(更丰富的色彩表现HDR
HARDWARE Special Android 8.0 新增 (Bitmap直接存储在graphic memory

示例

1
2
3
4
5
6
7
8
9
Bitmap bitmap = BitmapFactory.decodeResource(getResources(),R.drawable.img);
ivImage.setImageBitmap(bitmap);

// 位图所占用的内存字节数(147456000) = 宽(7680) * 高(4800) * 占用字节大小(ARGB_8888模式为 4 byte)
// 也就是说,2560 * 1600 的图片加载到内存中,需要近 147.456M 。
Log.e("TAG",bitmap.getWidth()+"--"+bitmap.getHeight()); // 7680 -- 4800
Log.e("TAG",bitmap.getByteCount()+""); // 147456000
Log.e("TAG",bitmap.getConfig()+""); // ARGB_8888
Log.e("TAG",bitmap.getDensity()+""); // 480

可以看到,源图片的尺寸数据,和decode到内存中的Bitmap的横纵像素数量实际上缩小了 3 倍。
通过源码,bitmap最终通过canvas绘制出来,而canvas在绘制之前,有一个scale的操作,scale的值由:
scale = (float) targetDensity / density;这一行代码决定。
即缩放的倍率和targetDensity和density相关,而这两个参数都是从传入的options中获取到的。
Options是BitmapFactory中的一个静态内部类,用于配置Bitmap在decode时的一些参数。

  • inDensity:Bitmap位图自身的密度、分辨率
    它与图片存放的资源文件的目录有关,会有不同值:

density | 0.75 | 1 | 1.5 | 2 | 3 | 4
:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:
densityDpi | 0 ~ 120 | 120 ~ 160 | 160 ~ 240 | 240 ~ 320 | 320 ~ 480 | 480 ~ 640
DpiFolder | ldpi | mdpi | hdpi | xhdpi | xxhdpi | xxxhdpi

  • inTargetDensity: Bitmap最终绘制的目标位置的分辨率(默认情况下,和设备分辨率保持一致,很少手动去赋值)
  • inScreenDensity: 设备屏幕分辨率(默认情况下,和设备分辨率保持一致,很少手动去赋值)

接下来查看同一张图片在不同资源目录下的参数变化:
(测试机参数:华为荣耀7,Android6.0系统,getResources().getDisplayMetrics().density = 3(密度),getResources().getDisplayMetrics().xdpi = 422.03(像素密度),getResources().getDisplayMetrics().ydpi = 424.069(像素密度),默认ARGB_8888)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Log.e("TAG",bitmap.getWidth()+"--"+bitmap.getHeight());
Log.e("TAG",bitmap.getByteCount()+"");
// Log.e("TAG",bitmap.getConfig()+""); // ARGB_8888,
// Log.e("TAG",bitmap.getDensity()+""); // 480
// Log.e("TAG",getResources().getDisplayMetrics().density+""); // 3.0
// float xdpi = getResources().getDisplayMetrics().xdpi; // 422.03
// float ydpi = getResources().getDisplayMetrics().ydpi; // 424.069

// 源图片的尺寸为 2560 * 1600
// drawable目录下: 7680 -- 4800, 147456000, 缩放倍率:3
// drawable-ldpi目录下: OOM
// drawable-mdpi目录下: 7680 -- 4800, 147456000, density:1, 缩放倍率:3
// drawable-hdpi目录下: 5120 -- 3200, 65536000, density:1.5, 缩放倍率:2
// drawable-xhdpi目录下: 3840 -- 2400, 36864000, density:2, 缩放倍率:1.5
// drawable-xxhdpi目录下: 2560 -- 1600, 16384000, density:3, 缩放倍率:1
// drawable-xxxhdpi目录下: 920 -- 1200, 9216000, density:4, 缩放倍率:0.75

由此可见:

  • 同一张图片,放在不同资源目录下,其分辨率会有变化。
  • bitmap分辨率越高,其解析后的宽高越小,甚至会小于图片原有的尺寸(即缩放),从而内存占用也相应减少。
  • 图片不特别放置任何资源目录时,其默认使用 mdpi 的分辨率。
    根据公式 scale = (float) targetDensity / density; 这里的 targetDensity 和设备保持一致为 480 ,mdpi 为 160,所以缩放倍率为 3。
  • 资源目录像素密度和设备像素密度一致时,图片尺寸不会缩放。
  • 在这里,也就是当位于drawable-xxhdpi目录下时。
  • 这里会有个匹配的规则,比如手机屏幕的密度是xxhdpi,那么会先从drawable-xxhdpi目录下寻找资源图片,如果没找到,会优先去更高密度的文件夹下依次寻找,如果还没找到,则会到drawable-nodpi文件夹找这张图,还没有,则到更低密度的文件夹下寻找,依次是drawable-xhdpi -> drawable-hdpi -> drawable-mdpi -> drawable-ldpi。
  • 在宽高设为wrap_content并且图片不是特别大时,在低密度文件中找到时,系统会认为这张图是专门为低密度的设备所设计的,如果直接将这张图在当前的高密度设备上使用就有可能会出现像素过低的情况,于是系统自动帮我们做了这样一个放大操作。反之亦然,如果系统是在drawable-xxxhdpi等更高密度的文件夹下面找到这张图的话,它会认为这张图是为更高密度的设备所设计的,如果直接将这张图在当前设备上使用就有可能会出现像素过高的情况,于是会自动帮我们做一个缩小的操作。
  • drawable-nodpi文件夹,这个文件夹是一个密度无关的文件夹,放在这里的图片系统就不会对它进行自动缩放,原图片是多大就会实际展示多大。
  • 一般来讲,在开发中也只需要一套图片资源,并优先放在高密度的drawable文件下,比如drawable-xxhdpi目录下,这样也可以节省内存开支。因为一张原图片被缩小了之后显示其实并没有什么副作用,但是一张原图片被放大了之后显示就意味着要占用更多的内存了。因为图片被放大了,像素点也就变多了,而每个像素点都是要占用内存的(所以在上述查看图片在不同资源目录下的参数变化时,并没有drawable-ldpi的数据,因为图片本身已经挺大了,放在ldpi目录下会直接出现OOM)。
  • 因此,关于Bitmap占用内存大小的公式,可以更细化为:
    Bitmap内存占用 ≈ 像素数据总大小 = 图片宽 × 图片高 × (设备像素密度/资源目录像素密度)^2 × 每个像素的字节大小
    之所以出现平方是因为;横向像素数量 = 图片宽 * (设备像素密度/资源目录像素密度),纵向像素数量 = 图片高× (设备像素密度/资源目录像素密度)。

Bitmap内存优化

Bitmap 的优化主要是通过 BitmapFactory.Options 来根据需要对图片进行采样,采样过程中主要用到了 inSampleSize 参数。

图片占用的内存一般分为”运行时占用的内存”和”存储时本地开销(反映在包大小上)”
关于运行时占用的内存,可以通过以下几种方式来减少内存:

  • 使用低色彩的解析模式,减少单个像素的字节大小。
    适用于对色彩丰富程度不高的场景。默认的ARGB8888占用4个字节,若改用RGB565将只占用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
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
public class MainActivity extends AppCompatActivity {

private ImageView ivImage;
private ImageView ivCommpressImg;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initView();
initData();
}
@Override
public void onRequestPermissionsResult(int requestCode,
String permissions[], int[] grantResults) {
switch (requestCode) {
case 11: {
// If request is cancelled, the result arrays are empty.
if (grantResults.length > 0
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {

// permission was granted, yay! Do the
// contacts-related task you need to do.
compress();
} else {

// permission denied, boo! Disable the
// functionality that depends on this permission.
}
return;
}

// other 'case' lines to check for other
// permissions this app might request
}
}


private void initData() {
// 一般来讲,都是从sd卡中读取图片
int permissionCheck = ContextCompat.checkSelfPermission(this,
Manifest.permission.WRITE_EXTERNAL_STORAGE);
Log.e("TAG",permissionCheck+"---");

if (ContextCompat.checkSelfPermission(this,
Manifest.permission.WRITE_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED) {

// Should we show an explanation?
if (ActivityCompat.shouldShowRequestPermissionRationale(this,
Manifest.permission.WRITE_EXTERNAL_STORAGE)) {

// Show an expanation to the user *asynchronously* -- don't block
// this thread waiting for the user's response! After the user
// sees the explanation, try again to request the permission.
compress();
} else {

// No explanation needed, we can request the permission.

ActivityCompat.requestPermissions(this,
new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
11);

// MY_PERMISSIONS_REQUEST_READ_CONTACTS is an
// app-defined int constant. The callback method gets the
// result of the request.
}
}

Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.img);
ivImage.setImageBitmap(bitmap);
// 也是用来设置图片。
// 但封装了bitmap的读入和解析的过程,并且过程是在UI线程完成的,对于性能是有所影响的。
// ivImage.setImageResource(R.mipmap.ic_launcher_round);

// 位图所占用的内存字节数(147456000) = 宽(7680) * 高(4800) * 占用字节大小(ARGB_8888模式为 4 byte)
// 也就是说,2560 * 1600 的图片加载到内存中,需要近 147.456M 。

// 可以看到,源图片的尺寸数据,和decode到内存中的Bitmap的横纵像素数量实际上缩小了 3 倍。
// 通过源码,bitmap最终通过canvas绘制出来,而canvas在绘制之前,有一个scale的操作,scale的值由:
// scale = (float) targetDensity / density;这一行代码决定。
// 即缩放的倍率和targetDensity和density相关,而这两个参数都是从传入的options中获取到的。

// Log.e("TAG", bitmap.getWidth() + "--" + bitmap.getHeight());
// Log.e("TAG", bitmap.getByteCount() + "");
// Log.e("TAG",bitmap.getConfig()+""); // ARGB_8888,
// Log.e("TAG",bitmap.getDensity()+""); // 480
// Log.e("TAG",getResources().getDisplayMetrics().density+""); // 3.0
// float xdpi = getResources().getDisplayMetrics().xdpi;
// float ydpi = getResources().getDisplayMetrics().ydpi;
// Log.e("TAG",xdpi+"---"+ydpi); // 422.03---424.069


// 华为荣耀7
// 源图片的尺寸为 2560 * 1600
// drawable目录下:// 7680 -- 4800,147456000,缩放倍率:3
// drawable-ldpi目录下:OOM
// drawable-mdpi目录下:// 7680 -- 4800,147456000, 缩放倍率:3 density:1
// drawable-hdpi目录下:// 5120 -- 3200,65536000, 缩放倍率:2 density:1.5
// drawable-xhdpi目录下:// 3840 -- 2400,36864000, 缩放倍率:1.5 density:2
// drawable-xxhdpi目录下:// 2560 -- 1600,16384000, 缩放倍率:1 density:3
// drawable-xxxhdpi目录下:// 1920 -- 1200, 9216000, 缩放倍率:0.75 density:4

// Bitmap内存占用(147456000) = 图片宽(2560) × 图片高(1600)× (设备密度(3)/资源目录密度(1))^2 × 每个像素的字节大小(4)

}

private void compress(){
// 这里为了测试,先在data下放置一张图片
// 这里,将源图片和压缩后的图片都保存在了项目的data下
Resources res = this.getResources();
BitmapDrawable d = (BitmapDrawable) res.getDrawable(R.drawable.img);
Bitmap img = d.getBitmap();

String fn = "image_test.png";

// this.getFilesDir(); 这个是得到当前app目录下的files目录路径
// /data/user/0/com.example.compressapplication/files/image_test.png
String path = this.getFilesDir() + File.separator + fn;
// 放到指定目录下
// String path = Environment.getExternalStorageDirectory()+"/"+fn;
Log.e("TAG",path);

try{
OutputStream os = new FileOutputStream(path);
img.compress(Bitmap.CompressFormat.PNG, 100, os);
os.close();
}catch(Exception e){
Log.e("TAG", "", e);
}

// 开始压缩图片
File file = BitmapCompressUtil.compressBitmap(this,path, 500);
if (file != null) {
Log.e("TAG","压缩完成:"+file.getPath());
Bitmap bitmap1 = BitmapFactory.decodeFile(file.getPath(), null);
ivCommpressImg.setImageBitmap(bitmap1);

// Log.e("TAG",bitmap.getWidth()+"------"+bitmap.getHeight());
// Log.e("TAG",bitmap1.getWidth()+"------"+bitmap1.getHeight());
// Log.e("TAG",bitmap.getByteCount()+"----"+bitmap1.getByteCount());

// 如果是放在指定位置,可以删除
// BitmapCompressUtil.deleteFiles(new File(Environment.getExternalStorageDirectory() + BitmapCompressUtil.PATH));
} else {
Log.e("TAG","file文件为null");
}

}

private void initView() {
ivImage = findViewById(R.id.iv_img);
ivCommpressImg = findViewById(R.id.iv_commpress_img);
}
}
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
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
public class BitmapCompressUtil {

public static final String PATH = "/pic";

/**
* 根据分辨率压缩图片比例
*/
public static Bitmap compressByResolution(String imgPath, int w, int h) {
BitmapFactory.Options opts = new BitmapFactory.Options();
opts.inJustDecodeBounds = true;
BitmapFactory.decodeFile(imgPath, opts);

int width = opts.outWidth;
int height = opts.outHeight;
int widthScale = width / w;
int heightScale = height / h;

int scale;
// 保留压缩比例小的
if (widthScale < heightScale) {
scale = widthScale;
} else {
scale = heightScale;
}

if (scale < 1) {
scale = 1;
}
// Log.e("TAG","图片分辨率压缩比例:" + scale);

opts.inSampleSize = scale;

opts.inJustDecodeBounds = false;

Bitmap bitmap = BitmapFactory.decodeFile(imgPath, opts);

return bitmap;
}
/**
* 根据分辨率压缩
*
* @param srcPath 图片路径
* @param ImageSize 图片大小 单位kb
* @return
*/
public static File compressBitmap(Context mContext,String srcPath, int ImageSize) {
int subtract;
Log.e("TAG","图片处理开始..");
// 分辨率压缩
Bitmap bitmap = compressByResolution(srcPath, 1280, 720);
if (bitmap == null) {
Log.e("TAG","bitmap 为空");
return null;
}
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int options = 100;
// 取得图片旋转角度
int angle = readPictureDegree(srcPath);
// 修复图片被旋转的角度
Bitmap bitmapBefore = rotaingImageView(angle, bitmap);
// 质量压缩方法,这里100表示不压缩,把压缩后的数据存放到baos中
bitmapBefore.compress(Bitmap.CompressFormat.JPEG, options, baos);
Log.e("TAG", "图片分辨率压缩后:" + baos.toByteArray().length / 1024 + "KB");
// 循环判断如果压缩后图片是否大于ImageSize kb,大于继续压缩
while (baos.toByteArray().length > ImageSize * 1280) {
subtract = setSubstractSize(baos.toByteArray().length / 1280);
// 重置baos即清空baos
baos.reset();
// 每次都减少10
options -= subtract;
// 这里压缩options%,把压缩后的数据存放到baos中
bitmapBefore.compress(Bitmap.CompressFormat.JPEG, options, baos);
Log.e("TAG","图片压缩后:" + baos.toByteArray().length / 1024 + "KB");
}
Log.e("TAG","图片处理完成!" + baos.toByteArray().length / 1280 + "KB");

// 这个文件夹,用来存放压缩后得图片
// String temporaryPath= Environment.getExternalStorageDirectory()+PATH;
// File temFile=new File(temporaryPath);
// if (!temFile.exists()){
// temFile.mkdir();
// }
// 放在项目的data目录下
String temFile = mContext.getFilesDir() + File.separator;

File file = new File(temFile,"index.png");
boolean saved = false;
try {
// 使用Bitmap将自身保存为文件
// Log.e("TAG", "Saving File To Cache " + file.getPath());
// FileOutputStream os = new FileOutputStream(file);
// bitmapBefore.compress(Bitmap.CompressFormat.PNG, 100, os);
// os.flush();
// os.close();

//将压缩后的图片保存的本地上指定路径中
FileOutputStream fos = new FileOutputStream(file);
fos.write(baos.toByteArray());
fos.flush();
fos.close();

saved = true;
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}

if (bitmap != null) {
bitmap.recycle();
}
// 压缩成功返回ture
return file;
}

/**
* 读取照片旋转角度
*
* @param path 照片路径
* @return 角度
*/
public static int readPictureDegree(String path) {
int degree = 0;
try {
ExifInterface exifInterface = new ExifInterface(path);
int orientation = exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);
switch (orientation) {
case ExifInterface.ORIENTATION_ROTATE_90:
degree = 90;
break;
case ExifInterface.ORIENTATION_ROTATE_180:
degree = 180;
break;
case ExifInterface.ORIENTATION_ROTATE_270:
degree = 270;
break;
}
} catch (IOException e) {
e.printStackTrace();
}
return degree;
}

/**
* 旋转图片
* @param angle 被旋转角度
* @param bitmap 图片对象
* @return 旋转后的图片
*/
public static Bitmap rotaingImageView(int angle, Bitmap bitmap) {
// Log.e("TAG","开始处理图片被旋转得角度");
Bitmap returnBm = null;
// 根据旋转角度,生成旋转矩阵
Matrix matrix = new Matrix();
matrix.postRotate(angle);
try {
// 将原始图片按照旋转矩阵进行旋转,并得到新的图片
returnBm = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
} catch (OutOfMemoryError e) {
}
if (returnBm == null) {
returnBm = bitmap;
}
if (bitmap != returnBm) {
bitmap.recycle();
}
// Log.e("TAG","图片旋转得角度处理完成...");
return returnBm;
}
/**
* 根据图片的大小设置压缩的比例,提高速度
*
* @param imageMB
* @return
*/
private static int setSubstractSize(int imageMB) {
if (imageMB > 1000) {
return 60;
} else if (imageMB > 750) {
return 40;
} else if (imageMB > 500) {
return 20;
} else {
return 10;
}

}

//flie:要删除的文件夹的所在位置
public static void deleteFiles(File file) {
if (file.isDirectory()) {
File[] files = file.listFiles();
for (int i = 0; i < files.length; i++) {
File f = files[i];
deleteFiles(f);
}
file.delete();//如要保留文件夹,只删除文件,请注释这行
} else if (file.exists()) {
file.delete();
}}

}

备注

参考资料
Android中Bitmap内存优化
Android drawable微技巧

传送门GitHub