认识Bitmap
背景
位图文件(Bitmap),扩展名可以是.bmp或者.dib。它将图像定义为由点(像素)组成,每个点可以由多种色彩表示,包括2、4、8、16、24和32位色彩 。
Bitmap 应该是很多应用中最占据内存空间的一类资源了,Bitmap 也是导致应用 OOM 的常见原因之一。例如,如果使用4048 _ 3036 像素(1200 万像素),使用的Bitmap.Config为 ARGB_8888(Android 2.3 及更高版本的默认设置),将单张照片加载到内存大约需要 48MB 内存(4048 _ 3036 * 4 字节),如此庞大的内存需求可能会立即耗尽应用的所有可用内存,所以在android开发中理解好Bitmap也有助于我们开发出更好质量的App
内存大小的计算
图片所占用的内存和图片的宽度、长度、单位像素占的字节数三个因素有关。
基本知识
首先我们得了解 一些基本参数
- px (pixel) “pixel” 的缩写,像素。是画面显示的基本单位,真实的像素并不是点或者方块(虽然有时这样显示),也没有实际固定长度,只是一个抽象的取样。设计中的像素和实际显示屏上的像素相对应。
- dpi “dots per inch” 的缩写,“每英寸的像素数”,即像素密度

安卓手机种类多样,有各种屏幕像素密度。比如120dpi是低密度(ldpi)类型,160dpi 是中密度(mdpi),240dpi是高密度(hdpi),320dpi是超高密度(xhdpi),480dpi是超超高密度(xxhdpi)
- dp
设备无关像素。关于dp的官方叙述为当屏幕每英寸有160个像素时(也就是160dpi),dp与px等价的,1dp=1px。那么当屏幕为240dpi时,1dp=(240/160)px=1.5px。也就是说dp和px的换算在于dpi这个值,计算的公式为:1dp=(屏幕的dpi/160)px
想要知道在特定一台手机上 1dp 对应多少 px,或者是想要知道屏幕宽高大小,这些信息都可以通过 DisplayMetrics 来获取
1 | val displayMetrics = applicationContext.resources.displayMetrics |
从中就可以提取出几点信息:
- density 等于 3,说明在该模拟器上 1dp 等于 3px
- 屏幕宽高大小为 1920 x 1080 px,即 640 x 360 dp
- 屏幕像素密度为 480dpi
顺带提下 dpi跟drawable关系
如果 drawable 文件夹名不带后缀,那么该文件夹就对应 160dpi,对于 320dpi 的设备来说,应用在选择图片时就会优先从 drawable-xhdpi 文件夹拿,如果该文件夹内没找到图片,就会依照 xxhdpi -> xxxhdpi -> hdpi -> mdpi -> ldpi 的顺序进行查找,优先使用高密度版本,然后从中选择最接近当前屏幕密度的图片资源
| drawable | dpi |
|---|---|
| ldpi | 120 dpi |
| mdpi | 160 dpi |
| hdpi | 240 dpi |
| xhdpi | 320 dpi |
| xxhdpi | 480 dpi |
| xxxhdpi | 640 dpi |
Bitmap.Config
我们都知道颜色经常用ARGB来表示,A表示Alpha,即透明度;R表示red,即红色;G表示green,即绿色;B表示blue,即蓝色。Bitmap的色彩也是用ARGB来表示的,
Bitmap.Config中有Bitmap.Config.ALPHA_8、Bitmap.Config.RGB_565、Bitmap.Config.ARGB_4444、Bitmap.Config.ARGB_8888 四个枚举变量。
- Bitmap.Config.ALPHA_8表示:每个像素占8位,没有色彩,只有透明度A-8,共8位
- Bitmap.Config.ARGB_4444表示:每个像素占16位,A-4,R-4,G-4,B-4,共4+4+4+4=16位。
- Bitmap.Config.RGB_565表示:每个像素占16位,没有透明度,R-5,G-6,B-5,共5+6+5=16位
- Bitmap.Config.ARGB_8888表示:每个像素占32位,A-8,R-8,G-8,B-8,共8+8+8+8=32位。
位数越高,那么可存储的颜色信息越多,图像也就越逼真,占用空间越大
ALPHA_8只有透明度,基本不用,ARGB_4444画质差被弃用,ARGB_8888为常用数据格式,RGB_565不包含透明度,在没有透明度限制的情况下可以替代ARGB_8888,还可以省却一半内存空间。这些格式配置在压缩图片数据方便有很好的用处,现在一些图片下载框架都是支持设置
内存计算
ARGB_8888是最占内存的,因为一个像素占32位,8位=1字节,所以一个像素占4字节的内存。ARGB_4444的一个像素占2个字节。RGB_565的一个像素也是占两个字节。ALPHA_8的一个像素只占一个字节。
一个图片的像素=图片宽度 × 图片长度。
一张图片(BitMap)占用的内存=图片宽度×图片长度×单位像素占用的字节数(图片的像素×单位像素占用的字节数)
假设有一张480x800的图片,四个格式下所占的内存如下
| 类型 | 内存计算 | 占内存大小(B | 占内存大小(KB) |
|---|---|---|---|
| ARGB_8888 | 480×800×4 | 1536000 | 1536000÷1024=1500 |
| ARGB_4444 | 480×800×2 | 768000 | 768000÷1024=750 |
| ARGB_565 | 480×800×2 | 768000 | 768000÷1024=750 |
| ARGB_8 | 480×800×1 | 384000 | 384000÷1024=375 |
一些常用计算
Bitmap使用主要有以下2种
给ImageView设置背景
当做画布来使用
1
2imageView.setImageBitmap(Bitmap bm);
Canvas canvas = new Canvas(Bitmap bm)BItmap创建方法
Bitmap.Options
这个参数的作用非常大,他可以设置Bitmap的采样率,通过改变图片的宽度高度和缩放比例等,以达到减少图片像素数的目的,一言以蔽之,通过设置这个参数我们可以很好的控制显示和使用Bitmap。实际开发过程中,可以灵活设置该值,以降低OOM发生的概率。
inJustDecodeBounds:boolean类型,设为true时,无需要把图片加载入内存就可以获取图片的高度,宽度和图片的MIME类型。
高度通过options.outWidth获取 宽度通过options.outHeight获取,MIME通过options.outMineType获取
inSampleSize:这个字段表示采样率,打个比方说,设置为4,则是从原本图片的四个像素中取一个像素作为结果返回。其余的都被丢弃。可见,采样率越大,图片越小,失真越严重。 如何计算采样率呢?看一下这段代码你就会明白
1
2
3
4
5
6
7
8
9
10
11
12
13
14public int getSampleSize(BitmapFactory.Options options , int dstWidth,int dstHeight){
//dstWidth:表示目前ImageView的宽度
//dstHeight:表示目标ImageView的高度
//option中获取bitmap图片的信息
int rawWidth = options.outWidth;
int rawHeight = options.outHeight;
int sampleSize=1;
if(rawWidth>dstWidth||rawHeight>dstHeight){
float ratioHeight = (float) (rawHeight/dstHeight);
float ratioWidth = (float) (rawWidth/dstWidth);
sampleSize = (int) Math.min(rawHeight, ratioWidth);
}
return sampleSize;
}**inScald:**这个参数表示,在可以缩放时,是否对当前文件进行放缩,如果设置为false就不放缩。设置为true,则会根据文件夹分辨率和屏幕分辨率进行动态缩放。
**inPreferredConfig:**这个参数是用来设置像素的存储格式的
BitmapFactory
BitmapFactory提供了多种创建bitmap的静态方法
1 | //从资源文件中通过id加载bitmap |
BitmapFactory.decodeResource 加载的图片可能会经过缩放,该缩放目前是放在 java 层做的,效率比较低,而且需要消耗 java 层的内存。因此,如果大量使用该接口加载图片,容易导致OOM错误
BitmapFactory.decodeStream 不会对所加载的图片进行缩放,相比之下占用内存少,效率更高。
这两个接口各有用处,如果对性能要求较高,则应该使用 decodeStream;如果对性能要求不高,且需要 Android 自带的图片自适应缩放功能,则可以使用 decodeResource。
常用操作
保存本地图片
1 | public static void writeBitmapToFile(String filePath, Bitmap b, int quality) { |
图片缩放
1 | public static Bitmap bitmapScale(Bitmap bitmap, float scale) { |
对bitmap裁剪
1 | public Bitmap bitmapClip(Context context , int id , int x , int y){ |
Bitmap静态方法
1 | //width和height是长和宽单位px,config是存储格式 |
1.加载图像可以使用BitmapFactory和Bitmap.create系列方法
2.可以通过Options实现缩放图片,获取图片信息,配置缩放比例等功能
3.如果需要裁剪或者缩放图片,只能使用create系列函数
4.注意加载和创建bitmap事通过try catch捕捉OOM异常
Bitmap对图像操作
Bitmap裁剪图像
Bitmap.createBitmap(Bitmap source, int x, int y, int width, int height)
根据源Bitmap对象source,创建出source对象裁剪后的图像的Bitmap。x,y分别代表裁剪时,x轴和y轴的第一个像素,width,height分别表示裁剪后的图像的宽度和高度。 注意:x+width要小于等于source的宽度,y+height要小于等于source的高度。
Bitmap.createBitmap(Bitmap source, int x, int y, int width, int height,Matrix m, boolean filter)
这个方法只比上面的方法多了m和filter这两个参数,m是一个Matrix(矩阵)对象,可以进行缩放,旋转,移动等动作,filter为true时表示source会被过滤,仅仅当m操作不仅包含移动操作,还包含别的操作时才适用。其实上面的方法本质上就是调用这个方法而已。
1 | public static Bitmap createBitmap(Bitmap source, int x, int y, int width, int height) { |
Bitmap缩放,旋转,移动图像
Bitmap缩放,旋转,移动,倾斜图像其实就是通过
Bitmap.createBitmap(Bitmap source, int x, int y, int width, int height,Matrix m, boolean filter)
方法实现的,只是在实现这些功能的同时还可以实现图像的裁剪
1 | // 定义矩阵对象 |
Matrix的postScale和postRotate方法还有多带两个参数的重载方法postScale(float sx, float sy, float px, float py)和postRotate(float degrees, float px, float py),后两个参数px和py都表示以该点为中心进行操作。
注意:虽然Matrix还可以调用postSkew方法进行倾斜操作,但是却不可以在此时创建Bitmap时使用
Bitmap 保存图像与资源释放
1 | bitmap=BitmapFactory.decodeResource(getResources(),R.drawable.feng); |
Matrix拓展类
- setRotate(float degrees, float px, float py) 对图片进行旋转
- setScale(float sx, float sy) 对图片进行缩放
- setTranslate(float dx, float dy) 对图片进行平移
- postTranslate(centerX, centerY) 在上一次修改的基础上进行再次修改set每次操作都是全新的会覆盖上次操作
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//显示原图
ImageView iv_src = (ImageView) findViewById(R.id.iv_src);
//显示副本
ImageView iv_copy = (ImageView) findViewById(R.id.iv_copy);
//[1]先把tomcat.png 图片转换成bitmap 显示到iv_src
Bitmap srcBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.tomcat);
//[1.1]操作图片
// srcBitmap.setPixel(20, 30, Color.RED);
iv_src.setImageBitmap(srcBitmap);
//[2]创建原图的副本
//[2.1]创建一个模板 相当于 创建了一个大小和原图一样的 空白的白纸
Bitmap copybiBitmap = Bitmap.createBitmap(srcBitmap.getWidth(), srcBitmap.getHeight(), srcBitmap.getConfig());
//[2.2]想作画需要一个画笔
Paint paint = new Paint();
//[2.3]创建一个画布 把白纸铺到画布上
Canvas canvas = new Canvas(copybiBitmap);
//[2.4]开始作画
Matrix matrix = new Matrix();
//[2.5]对图片进行旋转
//matrix.setRotate(20, srcBitmap.getWidth()/2, srcBitmap.getHeight()/2);
//[2.5]对图片进行
// matrix.setScale(0.5f, 0.5f);
//[2.6]对图片进行平移
// matrix.setTranslate(30, 0);
//[2.7]镜面效果 如果2个方法一起用
// matrix.setScale(-1.0f, 1);
//post是在上一次修改的基础上进行再次修改 set 每次操作都是最新的 会覆盖上次的操作
// matrix.postTranslate(srcBitmap.getWidth(), 0);
//[2,7]倒影效果
matrix.setScale(1.0f, -1);
//post是在上一次修改的基础上进行再次修改 set 每次操作都是最新的 会覆盖上次的操作
matrix.postTranslate(0, srcBitmap.getHeight());
canvas.drawBitmap(srcBitmap,matrix , paint);
//[3]把copybimap显示到iv_copy上
iv_copy.setImageBitmap(copybiBitmap);常见问题
Bitmap与Canvas,View,Drawable的关系
我们在创建一个Canvas时,可以传入一个Bitmap,Paint在Canvas上的绘制实际上就是绘制在Bitmap对象上的。我们自定义空间所显示的View也是通过Canvas中的Bitmap来显示的。Drawable在内存占用和绘制速度这两个非常关键的点上胜过Bitmap.Bitmap是如何造成内存溢出以及如何避免
Bitmap容易造成内存溢出是由于位图较大,特别是ARGB_8888存储格式的图片如果有几个这种量级的图片在内存中,并且没有及时回收,那会非常容易造成OOM,
如何避免呢?
1.我们可以对位图进行压缩,压缩手段有PNG,JPEG,WEBP
2.对不使用的Bitmap一定要及时回收。
3.在创建Bitmap时使用try catch步骤OOM异常,使程序更健壮,即使发生了OOM也不会闪退,造成不好的使用体验.
Bitmap内存在版本中的变化
- Android1.0~Android2.3 Bitmap的像素数据是分配在Native内存中的,
Bitmap对象在Dalvik堆中占用的数据是很小的,只有width、height、config和指向堆的引用,这样的结果是Bitmaps don’t get monitored well by the GC (it thinks they are the size of a reference),所以GC无法知道当前的内存情况是否乐观,大量创建bitmap可能不会触发到GC,而Native中bitmap的像素数据可能已经占用了过多内存,这时候就会OOM,所以推荐在bitmap使用完之后,调用recycle释放掉Native的内存。
- Android3.0~Android7.1 分配在堆里
Bitmap的数据结构发生了改变,其中多了如下属性,用来存储像素数据 private byte[] mBuffer;
至此像素数据就和bitmap对象一起都分配在堆中了,一起接受GC管理,只要bitmap置为null没有被强引用持有,GC就会把它回收掉,和普通对象一样。
- Android8.0+
这里开始,Bitmap的像素数据又重新回到native分配了,Bitmap数据结构中
private byte[] mBuffer;这个属性不见了,取而代之的是private final long mNativePtr;,其指向的是native的内存地址。为什么呢?
安卓的每个APP都是运行在单独的虚拟机中的,系统同时会有多个APP同时运行,所以分给每个虚拟机内存上限不会太高,一般也就几百M,虚拟机启动时内存上限就是定值,一旦达到内存上限,就会OOM。
但是安卓手机的可用内存普遍已经4、6、8个G,大多数情况下系统还是有剩余内存可用的(其他APP远没有达到自己虚拟机内存上限的情况下),而一个APP中占用内存最多的一般都是Bitmap,所以如果能把系统空余内存空间利用起来,就能大大增加当前APP的可用内存,而把bitmap的像素数据放到native就能解决这个问题,native可以直接使用整个linux系统的内存,不受当前APP所在虚拟机的内存上限控制,这样就可以持续使用内存,直到用完系统的空余内存。这样的坏处是一旦发生OOM,不会被系统的UncaughtExceptionHandler捕获,会直接crash。
常用函数
Drawble和Bitmap互相转换
1 | //drawable 转换成bitmap |
常见函数使用例子
1 |
|
采样率压缩
下面的例子主要用到了 Bitmap 的采样压缩(这个采样率是根据需求来进行生成的),使用到了inBitmap内存复用和 inJustDecodeBounds (这两个字段上面都有介绍)
下面介绍获取采样的流程:
将 BitmapFactory.Options 的 inJustDecodeBounds 参数设置为 true 并加装图片。
从 BitmapFactory.Options 中取出图片的原始宽和高,它们对应于 outWidth 和 outHeight 参数。
根据采样率的规则并结合目标 View 的所需要大小计算出采样率 inSampleSize 。
将 BitmapFactory.Options 的 inJustDecodeBounds 参数设为 false ,然后重新加装图片
1 | /** |
Bitmap 模糊
1 | fun blurBitmap(bitmap: Bitmap, blurRadius: Float) { |






