简介
Bitmap代表一张图片,其存储的是像素点,安卓中不同类型的图片如jpeg,png都可以用Bitmap表示。安卓中对图片的裁剪、缩放等一系列的操作都需要把图片文件以Bitmap的形式加载到内存中进行操作。本节就来简单认识下Bitmap,为以后的各种位图处理、图片框架分析打个基础~
一、常见的三个类
- Bitmap
- BitmapFactory
- BitmapFactory.options
1、Bitmap的构造
Bitmap的创建通常是使用BitmapFactory类来创建的,因为通过看Bitmap构造源码可知使用构造函数来创建Bitmap对象的操作都是再jni层来完成的。我们只需使用BitmapFactory即可创建出Bitmap对象。
简单认识下源码~ 可见都不支持我们直接使用构造~
//android.graphics 包下
package android.graphics;
//final类,实现了序列化接口
public final class Bitmap implements Parcelable {
/**
* Private constructor that must receive an already allocated native bitmap
* int (pointer).
* JNI now calls the version below this one.
* This is preserved due to UnsupportedAppUsage.
*/
@UnsupportedAppUsage(maxTargetSdk = 28)
Bitmap(long nativeBitmap,
int width,
int height,
int density,
boolean requestPremultiplied,
byte[] ninePatchChunk,
NinePatch.InsetStruct ninePatchInsets) {
this(nativeBitmap,
width,
height,
density,
requestPremultiplied,
ninePatchChunk,
ninePatchInsets,
true);
}
// called from JNI and Bitmap_Delegate.
Bitmap(long nativeBitmap,
int width,
int height,
int density,
boolean requestPremultiplied,
byte[] ninePatchChunk,
NinePatch.InsetStruct ninePatchInsets,
boolean fromMalloc) {
...
}
}
2、Bitmap的常见普通方法
(1)public void recycle()
- 释放bitmap关联的native对象,并且清除像素数据。
- 注意这个方法不会立即同步释放像素数据,在像素数据没有其他引用时垃圾回收才会收集。
- 方法调用后Bitmap被标记为dead状态,此时若是再调用getPixels() 或者 setPixels() 会抛异常。
- 只有确保bitmap没有使用时才调用这个方法,这个方法通常不使用,因为当不再引用此位图时,正常的GC进程将释放此内存。
(2)宽高:
- public void setWidth(int width)设置宽
- public void setHeight(int height)设置高
- public final int getWidth()获取宽
- public final int getHeight()获取高
(3)配置信息:主要是像素所占字节如ARGB_8888每个像素占四个字节。
- public void setConfig(Config config)设置配置信息
- public final Config getConfig() 获取配置信息
public enum Config {
/**
设置这种配置后每个像素占1字节内存,这种配置只支持透明度通道编码。
每个像素存储单个alpha通道值,不存储颜色值。
通常使用这种配置可以高效存储masks
*/
ALPHA_8 (1),
/**
设置这种配置后每个像素占2字节内存,这种配置只支持RGB三种通道编码。
R:存储5bit,有32种可能
G:存储6bit,有64种可能
B:存储5bit。
这种配置可以产生轻微的视觉效果,如资源不设置抖动时,结果可能显示为绿色。
当不要求高颜色保真度的不透明位图时可以设置这个。
*/
RGB_565 (3),
/**
弃用,推荐使用ARGB_8888
*/
@Deprecated
ARGB_4444 (4),
/**
每个像素存储占四个字节。有RGBA四个通道,每个通道占8位。每个通道有256种可能。
*/
ARGB_8888 (5),
/**
每个像素存储占8个字节。每个通道存储半精度浮点值。
这种配置很适合wide-gamut和 HDR
*/
RGBA_F16 (6),
/**
特殊的配置,bitmap 仅仅使用在内存中,而且bitmap永远是不可变得。
*/
HARDWARE (7);
...
}
(4)public final boolean isRecycled()
- bitmap是否已经被回收
(5)public Bitmap copy(Config config, boolean isMutable)
- 基于原图尺寸创建一个bitmap,可手动配置Config信息,设置像素信息是否可修改。
- copy失败时返回空bitmap
- copy成功返回的位图与原始位图具有相同的密度和颜色空间
- copy时设置Bitmap.Config.ALPHA_8,颜色空间就不可用了。
- copy时设置Bitmap.Config.RGBA_F16,或者原图的配置信息为Bitmap.Config.RGBA_F16时EXTENDED or non-EXTENDED变体可能会根据需要进行调整。
isMutable的意义在于哪里呢,想要操作像素这个必须设置为true否则如使用Canvas操作bitmap时就会抛异常,如下常见栗子:
public Canvas(@NonNull Bitmap bitmap) {
if (!bitmap.isMutable()) {
throw new IllegalStateException(
//不可变的bitmap传递给了Canvas
"Immutable bitmap passed to Canvas constructor"
);
}
throwIfCannotDraw(bitmap);
mNativeCanvasWrapper = nInitRaster(bitmap.getNativeInstance());
mFinalizer = NoImagePreloadHolder.sRegistry.registerNativeAllocation(
this, mNativeCanvasWrapper);
mBitmap = bitmap;
mDensity = bitmap.mDensity;
}
(6) 质量压缩 :压缩bitmap到本地。
public boolean compress(CompressFormat format, int quality, OutputStream stream)
- 方法运行在工作线程
- CompressFormat format:要压缩的图片格式,可为JPEG、PNG、WEBP等。
- int quality:压缩质量,取值范围【0,100】0表示质量最差,100表示压缩到最高视觉质量。
CompressFormat的格式:
1、JPEG:有损压缩,质量与quality有关。
2、PNG:无损压缩,设置这种格式时quality可以忽略。
3、WEBP,已经弃用参考WEBP_LOSSY、WEBP_LOSSLESS。在安卓Q上quality=100时会文件是无损webp格式,quality<100时文件是有损webp格式。
4、WEBP_LOSSY:有损压缩。质量与quality有关。
5、WEBP_LOSSLESS:压缩到WEBP无损格式,这时质量还是可以设置的,但是质量的数值有其他意义了,0代表快速压缩,但是产生的文件较大,100代表压缩时间较长,但是产生文件较小。
(7) public final boolean isMutable()
- 像素是否可修改
(8)public final int getRowBytes()
- 获取bitmap 行之间的像素字节数。
- 注意像素的引用被存储在native层,平时调用getPixels() 或者 setPixels()时,像素被统一处理为32位值,然后根据颜色类别进行了压缩。
- 安卓4.4及不建议使用这个方法计算内存,参考getAllocationByteCount()
public final int getRowBytes() {
if (mRecycled) {
Log.w(TAG, "Called getRowBytes() on a recycle()'d bitmap! This is undefined behavior!");
}
return nativeRowBytes(mNativePtr);
}
(9)public final int getByteCount()
- 返回bitmap像素的最小字节数。安卓4.4及不建议使用这个方法计算内存,参考getAllocationByteCount()。
public final int getByteCount() {
if (mRecycled) {
Log.w(TAG, "Called getByteCount() on a recycle()'d bitmap! "
+ "This is undefined behavior!");
return 0;
}
// int result permits bitmaps up to 46,340 x 46,340
return getRowBytes() * getHeight();
}
(10) public final int getAllocationByteCount()
- 返回bitmap字节所占内存,这个值通常与getByteCount返回相等。
如果位图被重新用于解码其他较小尺寸的位图,或通过手动重新配置。这个值大于getByteCount。参考:
1、reconfigure(int, int, Bitmap.Config),
2、setWidth(int), setHeight(int),
3、setConfig(Bitmap.Config),
4、BitmapFactory.Options.inBitmapreconfigure
(11)public void setHasAlpha(boolean hasAlpha)
- false:设置bitmap所有的像素不透明
- true:bitmap的某些像素可能包含透明值。
- 对于一些配置,这个方法可以忽略,如设置图片RGB_565,RGB_565都不支持像素的透明值。
(12)获取/设置像素值
- public int getPixel(int x, int y):获取x,y坐标的argb颜色int值。
- public Color getColor(int x, int y)获取x,y坐标的Color对象。
- public void setPixel(int x, int y, @ColorInt int color)
- public void setPixels(@ColorInt int[] pixels,
int offset,
int stride,
int x,
int y,
int width,
int height
)
3、Bitmap的重要方法createBitmap
从已有Bitmap中创建新的bitmap最常用的方法就是这个方法了,这个方法提供很多重载方法~
但看下方法源码可大致归纳为如下几类:
/*
方法1
根据源bitmap创建一个子bitmap,通过可选矩阵可进行变换。新的bitmap可能是原图的实例或者是原图的copy。
新的位图使用与原始位图相同的密度(density)和颜色空间初始化。
如果源位图是不可变的(immutable),并且请求的子集与源位图本身相同(x,y,weight、height同),则返回源位图,并且不创建新位图。
除了以下情况外返回的位图始终是“可变”的:
(1)方法返回的就是源位图,并且源位图本身就不可变。
(2)源位图是hardware bitmap,也即Config的值是Bitmap.Config.HARDWARE。
参数:
source:源位图
x:源位图x坐标
y:源位图y坐标
height:要从源位图copy的行数
width:每一行的像素数目
m:要应用到像素上的可选矩阵。
filter:设置为true代表源位图要被过滤。当矩阵包含的操作不仅仅是平移时才适用。
*/
public static Bitmap createBitmap(
@NonNull Bitmap source,
int x,
int y,
int width,
int height,
@Nullable Matrix m,
boolean filter)
/*
方法2
根据给定的宽高创建一个“可变”的Bitmap。
初始化的密度由DisplayMetrics决定。
新的Bitmap使用sRGB颜色空间。
参数:
DisplayMetrics:屏幕相关信息,内部记录了屏幕的大小、密度等信息。
对象通过context.getResources().getDisplayMetrics()来获取。
width: 要创建bitmap宽
height:要创建bitmap高
config:Config信息
*/
public static Bitmap createBitmap(
@Nullable DisplayMetrics display,
int width,
int height,
@NonNull Config config)
/*
方法3:
根据给定的信息返回“可变”的Bitmap
参数:
display:
width:
height:
config:
hasAlpha:当Bitmap的配置为ARGB_8888 或者 RGBA_16F时,这个flag可以用于将bitmap
标记为不透明。这样可以清除黑色而非透明的bitmap。
ColorSpace:bitmap的颜色空间,如果bitmap设置了Bitmap.Config.RGBA_F16和sRGB,
或者Linear sRGB,符合的颜色空间会被应用。
*/
public static Bitmap createBitmap(
@Nullable DisplayMetrics display,
int width,
int height,
@NonNull Config config,
boolean hasAlpha,
@NonNull ColorSpace colorSpace)
/*
方法4:
根据指定的信息,返回一个“不可变”的bitmap。Bitmap的每一个像素值设置为colors数组的值。
初始化的密度(density)为getDensity的值。新创建的位图位于sRGB颜色空间中。
参数:
DisplayMetrics:
colors:sRGB颜色数组,用于初始化Bitmap的像素。
offset:颜色数组内要跳过的颜色。
stride:每行之间数组中的颜色数
width:bitmap宽
height:bitmap高
config:若bitmap设置Config不支持像素的alpha(如RGB_565),此时颜色数组中
的alpha将会被忽略,alpha值当做FF处理。
*/
public static Bitmap createBitmap(
@NonNull DisplayMetrics display,
@NonNull @ColorInt int[] colors,
int offset,
int stride,
int width,
int height,
@NonNull Config config)
/*
方法5:
通过给定的Picture 资源来创建一个“不可变”的bitmap。当给定的宽、高与Picture的宽高不相同
时,Picture会缩放到指定的宽高。
*/
public static @NonNull Bitmap createBitmap(
@NonNull Picture source,
int width,
int height,
@NonNull Config config)
4、BitmapFactory
这个是一个比较常用的类,我们一般通过这个api来获取bitmap对象。这个类提供了一些列的
decodeXXX方法来从文件、流、字节数组中来解码获取bitmap对象。常见的如下:
/*
1、给定一个文件,从文件中解析出bitmap对象。pathName为空或者解析失败时方法返回null
*/
public static Bitmap decodeFile(String pathName)
/*
2、从指定的 图片资源id中解析出Bitmap对象,图片资源解析失败时返回null
*/
public static Bitmap decodeResource(Resources res, int id)
/*
3、从给定的字节数组中解析出一张Bitmap。解析失败返回null
offset:偏移量,一般为0表示解析完整Bitmap。
length:要解析的字节长度,一般为数据总长度。
*/
public static Bitmap decodeByteArray(byte[] data, int offset, int length)
/*
4、从InputStream中解析出Bitmap对象。一般用于网络请求图片资源直接可获取到
InputStream对象
*/
public static Bitmap decodeStream(InputStream is)
/*
5、从文件描述符中解析bitmap,解析失败返回null。解析完成后描述符的位置不会改变,
因此可以多次使用FileDescriptor对象来获取Bitmap对象。
*/
public static Bitmap decodeFileDescriptor(FileDescriptor fd)
看过BitmapFactory源码我们会发现上述这些方法好多都带Options参数的重载,其实Options也是一个重要的类,在解析bitmap时结合这个类可以达到很好的效果。
5、BitmapFactory.Options
这个类定义了很多常见的属性,在解码Bitmap中发挥着重要作用,常见如下:
- inSampleSize
采样率,解码器对原始图像进行二次采样,以返回较小的图像来节省内存。其数值一般为正整数n,代表采样后图片宽高都为原来1/n,占用像素为原来1/n²。
如n为1时,表示采样后的图片和原始图片比例一样,n为2时,表示采样后的图片宽高为原来的1/2,占用像素为原来的1/4.
注意inSampleSize值设置为小于1的数值时都会被当做1处理。我们在进行图片缩放时经常会使用到这个字段。
- inJustDecodeBounds
如果设置为true,解码器不会真正解码位图,所以不用分配内存,但会返回图片的高度宽度信息。
- inDensity
bitmap的像素密度。一般不用,decodeResource源码中有使用。
- inTargetDensity
bitmap最终的像素密度,一般不用,decodeResource源码中有使用。
二、图片的下采样压缩
在大多数情况下,官方建议使用 Glide 库获取、解码和显示应用中的位图。在处理这些任务以及与位图和 Android 上的其他图片相关的其他任务时,Glide 会将大部分的复杂工作抽象出来。内部对“如何高效加载大位图”、“缓存位图”、“管理位图内存”做了很好的封装处理,很方便我们直接使用。
但是我们也可选择直接使用 Android 框架中内置的较低级 API,来亲自实现下这些工作,这里就实现下最简单的“如何高效加载大位图”
首先我们可以拍一张大的照片,放到模拟器的指定目录下:
cacheDir.absolutePath+"/1.jpeg"
然后综合上述所了解的api尝试进行宽高缩小为原图的一半:
private fun scaleAndLoadImage() {
val options = BitmapFactory.Options()
// 1、不真正解码位图 ,不真正解码图片,但是会图片宽高信息获得.
options.inJustDecodeBounds = true
BitmapFactory.decodeFile(cacheDir.absolutePath+"/1.jpeg", options)
// 2、获得图片宽高
val weight = options.outWidth
val height = options.outHeight
// 3、 计算缩放比(手动指定比较快捷,我们这里根据手机尺寸计算,具体实现自己而定)
val scale: Int = calculateScale(weight, height)
// 4、指定缩放比例,宽高都缩放为原图1/2
options.inSampleSize = 2
// 5、真正解码 按照缩放比.
options.inJustDecodeBounds = false
val bitmap = BitmapFactory.decodeFile(cacheDir.absolutePath+"/1.jpeg", options)
image.setImageBitmap(bitmap)
}
这样就成功的缩放了一张图片~
我们还知道,宽高会缩放到原来的1/2,则像素数缩小为原来1/4,不妨一并验证下:
// 直接获取原图bitmap,打印log:
Log.i(TAG, "before scale ByteCount:${bitmap.byteCount/1024/1024}M")
// 宽高缩放为原来1/2打印log:
Log.i(TAG, "after scale ByteCount:${bitmap.byteCount/1024/1024}M")
// log:
I/MainActivity: before scale ByteCount:61M
I/MainActivity: after scale ByteCount:15M
可见像素数变2为了原来1/4,所占内存也是约为原来1/4.
缩放比的值一般根据相应的条件进行计算,如下以手机屏幕宽高为栗子计算:
/**
* 根据屏幕尺寸宽高与图片宽高比例进行计算缩放比。
* @param weight 图片宽
* @param height 图片高
* @return 缩放比例
* @function 计算缩放比例
*/
private fun calculateScale(weight: Int, height: Int): Int {
// 获得手机宽高
val wm = getSystemService(WINDOW_SERVICE) as WindowManager
val display: Display = wm.defaultDisplay
val point = Point()
display.getSize(point)
val phoneWidth: Int = point.x
val phoneHeight: Int = point.y
//默认缩放比为1
var scale = 1
val scaleX = weight / phoneWidth
val scaleY = height / phoneHeight
// 原图宽高大于手机宽高则进行缩放,缩放值取计算结果较大的。
if (scaleX >= scaleY && scaleX > scale) {
scale = scaleX
}
if (scaleY > scaleX && scaleY > scale) {
scale = scaleY
}
return scale
}
三、图片的质量压缩
Bitmap的下采样是为了减小位图所占运行内存,下采样是以减少像素个数来减少所占运行内存,当需要减少图片所占磁盘大小时就需要进行质量压缩了。
前面也了解到了Bitmap#compress方法,这里就实践下质量压缩,还是上述下采样所使用的图片,来我们通过模拟器看下所占磁盘内存:
接下来进行质量压缩:
// 原图
val bitmap = BitmapFactory.decodeFile(cacheDir.absolutePath + "/1.jpeg")
val compressFile = File(cacheDir.absolutePath + "/2.jpeg")
if (!compressFile.exists()) compressFile.createNewFile()
FileOutputStream(compressFile).use { fos ->
//压缩文件,通过输出流缓存到本地
bitmap.compress(Bitmap.CompressFormat.JPEG, 50, fos)
}
可见质量缩设置为50时,文件所占磁盘内存大大缩减~
小结
本节有几个知识点需要留意
1、Bitmap#Config类中常见的几个常量代表含义
2、Bitmap#CompressFormat类中几个常量的含义
3、质量压缩与下采样压缩的区别
- 质量压缩是减小文件所占磁盘的内存,下采样压缩是减小Bitmap所占运行内存大小。
- 质量压缩是通过算法扣掉一些像素附近的相近像素,从而达到减小文件大小的目的。注意质量压缩影响的是文件,对加载到内存中的bitmap是无法影响的。因为Bitmap在内存中的大小是按照像素计算的(宽高总像素以及所占字节)。对于质量压缩并不会改变图片的真实像素。
例如质量压缩:
AB
CD
压缩后
AA
AA
虽然AAAA和ABCD都是占四个像素的位置。但是保存文件时可以通过算法描述A为同样时只保存一个A的信息。这样就减少了文件的磁盘质量。
参考
The end
有了这些基础,后续就可以继续深入了解Bitmap的各种特效处理、图片处理框架的研究了。继续加油 💪🏻