深入探索Android Bitmap:从原理到实战

一、Bitmap 是什么

在 Android 开发中,Bitmap 是极为重要的基石。简单来说,Bitmap 代表位图,是图片在内存里的具体呈现形式 ,任何诸如 JPEG、PNG、WEBP 等格式的图片,一旦被加载到内存中,就会以 Bitmap 对象的形式存在。从原理上看,Bitmap 本质是像素点的集合,若其宽度为 width,高度为 height,那么该 Bitmap 就由 width * height 个像素构成,其在内存中占用的内存大小为 width * height * 单个像素内存。

为了更直观地理解 Bitmap,我们可以将其类比为日常生活中的照片打印。假设你有一张美丽的风景照片,想要将它打印出来。在打印之前,照片的数据就如同 Bitmap,它包含了图像的所有像素信息,这些像素信息决定了照片上每一个点的颜色和亮度。而打印的过程就像是将 Bitmap 显示在设备屏幕上,打印机需要读取这些像素信息,然后通过墨水或 toner 将图像呈现在纸张上。同样,在 Android 应用中,当我们要在界面上展示一张图片时,就需要将图片文件加载为 Bitmap 对象,然后将其传递给 ImageView 等控件进行显示。

从功能角度来讲,Bitmap 在 Android 开发中就像是一个 "图像容器",承载着图像的像素信息,凭借它,开发者能够在应用中轻松实现加载、显示和处理图像的操作。通过 Bitmap 类,开发者可以创建图像对象,在屏幕上展示或者对其进行更深入的处理,诸如缩放、裁剪、旋转等常见的图像操作,都可以借助 Bitmap 来完成。例如,在一个图片编辑应用中,用户可以通过 Bitmap 对图片进行裁剪,选择自己喜欢的部分进行保留;也可以对图片进行旋转,调整到合适的角度;还能对图片进行缩放,使其适应不同的屏幕尺寸。在一个社交应用中,用户上传的照片可能需要进行压缩和裁剪,以适应服务器的存储和传输要求,这时候就可以使用 Bitmap 来实现这些操作。

二、Bitmap 的内部原理

(一)颜色通道

在 Android 的图像世界里,Bitmap 以 ARGB 的独特顺序存储颜色通道,这里的 ARGB 分别代表 Alpha(透明度)、Red(红色)、Green(绿色)和 Blue(蓝色) 。

Alpha 通道用于掌控像素的透明度,取值范围从 0 到 255 。当 Alpha 为 0 时,像素处于完全透明状态,就如同空气一般不可见;而当 Alpha 达到 255 时,像素则是完全不透明的,稳稳地呈现出自身的颜色。Red、Green 和 Blue 通道则分别表示红色、绿色和蓝色分量的强度,它们的取值范围同样是 0 到 255 。通过这三个通道不同强度的组合,便能调配出丰富多彩的颜色。比如,当 Red 通道取值为 255,Green 和 Blue 通道取值为 0 时,呈现出的就是鲜艳的纯红色;当 Red 和 Green 通道取值均为 255,Blue 通道取值为 0 时,展现的便是充满活力的黄色。

在内存的舞台上,Bitmap 的像素通常是按行有序存储的。以常见的 ARGB_8888 格式为例,每个像素占用 4 个字节的空间,这 4 个字节依次对应 ARGB 四个通道 ,即每个像素占据 32 位。假设我们有一个简单的 2x2 的 Bitmap,其像素数据按行存储,第一个像素的 ARGB 值为 (255, 255, 0, 255),第二个像素的 ARGB 值为 (0, 255, 255, 255),那么在内存中,它们的存储顺序就是第一个像素的 A 通道字节、R 通道字节、G 通道字节、B 通道字节,接着是第二个像素的 A 通道字节、R 通道字节、G 通道字节、B 通道字节。

除了 ARGB_8888 格式,Android 中还有其他常见的颜色格式,如 RGB_565 。在 RGB_565 格式中,没有 Alpha 通道,只专注于颜色的呈现。其中,红色占用 5 位,绿色占用 6 位,蓝色占用 5 位,总共 16 位,即每个像素占用 2 个字节 。这种格式在一些对透明度要求不高、追求节省内存空间的场景中应用广泛。例如,在一些简单的游戏界面中,背景图片可能不需要透明度效果,使用 RGB_565 格式就能在保证一定图像质量的同时,有效减少内存占用,提升游戏的运行性能。 而 ARGB_4444 格式,虽然曾经也被使用,但由于每个通道仅用 4 位存储,导致画质质量较差,如今已不被推荐使用。ALPHA_8 格式则比较特殊,它只保存透明度,不保存颜色,每个像素仅占用 1 个字节,适用于一些只需要透明度信息的特殊场景。

(二)内存占用计算

Bitmap 占用内存大小的计算公式为:大小(字节) = 宽度 × 高度 × 每个像素占用的字节数 。其中,每个像素占用的字节数取决于 Bitmap 的配置 。

以常见的 ARGB_8888 格式为例,每个像素占用 4 个字节。假设有一张宽度为 800 像素,高度为 600 像素的图片,若采用 ARGB_8888 格式,那么它占用的内存大小为 800 × 600 × 4 = 1920000 字节,换算后约为 1.83MB 。而如果采用 RGB_565 格式,每个像素占用 2 个字节,同样尺寸的图片占用的内存大小则为 800 × 600 × 2 = 960000 字节,约为 0.92MB 。通过这两个例子可以明显看出,颜色格式对 Bitmap 内存占用有着显著的影响,在对图像质量要求不是特别高的情况下,选择 RGB_565 格式可以大幅减少内存占用。

图片尺寸对内存占用的影响也十分直观。当图片的宽度和高度增加时,像素点的数量会呈指数级增长,从而导致内存占用急剧上升。比如,将上述图片的宽度和高度都翻倍,变为 1600 像素和 1200 像素,采用 ARGB_8888 格式时,内存占用就会变为 1600 × 1200 × 4 = 7680000 字节,约为 7.33MB,是原来的四倍 。这也解释了为什么在加载高清大图时,容易出现内存溢出的问题。

此外,还需要注意的是,在实际的 Android 开发中,Bitmap 除了自身像素数据占用的内存外,还会占用一定的额外内存,用于存储 Bitmap 的配置信息、像素数据等 。并且,当从资源文件中加载 Bitmap 时,其内存占用还可能受到设备密度和资源文件夹密度的影响,这涉及到图片的缩放问题,会进一步影响内存的占用情况。

三、Bitmap 的创建与加载

(一)创建 Bitmap 对象

在 Android 开发中,创建 Bitmap 对象有多种方式,每种方式都适用于不同的场景。

  1. 从资源文件创建:通过BitmapFactory.decodeResource方法可以从资源文件中创建 Bitmap 对象 。例如:
java 复制代码
Resources resources = getResources();
Bitmap bitmap = BitmapFactory.decodeResource(resources, R.drawable.example_image);

在这段代码中,getResources()用于获取当前上下文的资源,R.drawable.example_image是图片资源的 ID,decodeResource方法会根据这个 ID 从资源文件中读取图片数据,并创建对应的 Bitmap 对象 。这种方式适用于应用中预先打包好的图片资源,比如应用的图标、默认背景图片等。

  1. 从本地文件创建:利用BitmapFactory.decodeFile方法可以从本地文件系统中创建 Bitmap 对象 。示例代码如下:
java 复制代码
String filePath = "/sdcard/images/example.jpg";
Bitmap bitmap = BitmapFactory.decodeFile(filePath);

这里的filePath是本地图片文件的路径,decodeFile方法会读取该路径下的图片文件,并将其转换为 Bitmap 对象 。这种方式常用于加载用户本地存储的图片,比如用户拍摄的照片、下载的图片等。

  1. 从输入流创建:通过BitmapFactory.decodeStream方法可以从输入流中创建 Bitmap 对象 。代码示例:
java 复制代码
try {
    InputStream inputStream = new FileInputStream("/sdcard/images/example.jpg");
    Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
    inputStream.close();
} catch (IOException e) {
    e.printStackTrace();
}

在这个例子中,首先创建一个FileInputStream输入流来读取本地图片文件,然后将输入流传递给decodeStream方法,该方法会从输入流中读取图片数据并创建 Bitmap 对象 。最后,别忘了关闭输入流以释放资源。这种方式灵活性较高,不仅可以从文件输入流创建 Bitmap,还可以从网络输入流等其他类型的输入流创建,比如从网络加载图片时就可以使用这种方式。

  1. 从字节数组创建:使用BitmapFactory.decodeByteArray方法可以从字节数组创建 Bitmap 对象 。示例如下:
java 复制代码
byte[] byteArray = getImageBytes();// 假设这个方法用于获取图片的字节数组
Bitmap bitmap = BitmapFactory.decodeByteArray(byteArray, 0, byteArray.length);

这里的byteArray是包含图片数据的字节数组,0表示从字节数组的起始位置开始读取,byteArray.length表示读取的字节数,即整个字节数组 。这种方式适用于图片数据以字节数组形式存在的情况,比如从网络传输过来的图片数据或者从本地文件读取到的字节数组。

  1. 从 Uri 创建:通过ContentResolver获取输入流,再利用BitmapFactory.decodeStream方法从 Uri 创建 Bitmap 对象 。代码如下:
java 复制代码
Uri uri = Uri.parse("content://media/external/images/media/123");// 假设这是图片的Uri
ContentResolver resolver = getContentResolver();
try {
    InputStream inputStream = resolver.openInputStream(uri);
    Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
    inputStream.close();
} catch (IOException e) {
    e.printStackTrace();
}

在这段代码中,首先通过Uri.parse方法将图片的 Uri 解析为Uri对象,然后使用getContentResolver获取ContentResolver对象,再通过openInputStream方法从Uri中获取输入流,最后利用decodeStream方法从输入流中创建 Bitmap 对象 。这种方式常用于获取系统相册中的图片或者其他通过Uri标识的图片资源。

(二)加载图片资源

在加载图片资源时,由于图片的大小和分辨率各不相同,若处理不当,很容易出现内存溢出的问题 。比如,当加载一张分辨率很高的大图片时,其占用的内存可能会超过应用的可用内存,从而导致OutOfMemoryError异常。为了避免这种情况,我们可以通过BitmapFactory.Options来设置参数,优化加载过程。

  1. inJustDecodeBounds 参数:将inJustDecodeBounds设置为true,可以在不实际加载图片到内存的情况下,获取图片的宽高和其他信息 。示例代码如下:
java 复制代码
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(getResources(), R.drawable.large_image, options);
int imageWidth = options.outWidth;
int imageHeight = options.outHeight;

在这段代码中,首先创建一个BitmapFactory.Options对象,并将其inJustDecodeBounds属性设置为true。然后调用decodeResource方法,此时并不会真正加载图片到内存,而是仅仅解析图片的头部信息,将图片的宽度和高度分别存储在options.outWidth和options.outHeight中 。通过这种方式,我们可以在加载图片之前就了解图片的大小,从而根据需要决定是否加载或者如何加载图片。

  1. inSampleSize 参数:inSampleSize用于设置图片的采样率,通过设置合适的采样率,可以缩小图片的尺寸,减少内存占用 。例如,当inSampleSize = 2时,图片的宽高都会被缩小为原来的一半,像素数变为原来的四分之一,内存占用也相应减少 。计算合适的inSampleSize的方法如下:
java 复制代码
public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
    final int height = options.outHeight;
    final int width = options.outWidth;
    int inSampleSize = 1;
    if (height > reqHeight || width > reqWidth) {
        final int heightRatio = Math.round((float) height / (float) reqHeight);
        final int widthRatio = Math.round((float) width / (float) reqWidth);
        inSampleSize = heightRatio < widthRatio? heightRatio : widthRatio;
    }
    return inSampleSize;
}

使用这个方法时,首先需要将BitmapFactory.Options的inJustDecodeBounds属性设置为true,解析一次图片获取其原始宽高。然后将BitmapFactory.Options连同期望的宽度和高度一起传递到calculateInSampleSize方法中,得到合适的inSampleSize值 。最后再将inJustDecodeBounds设置为false,重新加载图片,此时加载的就是缩小后的图片 。示例代码如下:

java 复制代码
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(getResources(), R.drawable.large_image, options);
int inSampleSize = calculateInSampleSize(options, 200, 200);
options.inJustDecodeBounds = false;
options.inSampleSize = inSampleSize;
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.large_image, options);

在这个例子中,首先通过inJustDecodeBounds = true获取图片的原始宽高,然后计算出合适的inSampleSize值,最后将inJustDecodeBounds设置为false,并设置inSampleSize,重新加载图片,这样加载的图片就会根据inSampleSize进行缩放,从而减少内存占用 。通过合理设置inJustDecodeBounds和inSampleSize参数,可以有效地优化图片加载过程,避免内存溢出问题,提高应用的性能和稳定性 。

四、Bitmap 的常见操作

(一)缩放

在 Android 开发中,对 Bitmap 进行缩放是常见的操作之一,主要有根据给定宽高拉伸和按比例缩放两种方式 。

  1. 根据给定宽高拉伸:通过Bitmap.createScaledBitmap方法可以实现根据给定宽高对 Bitmap 进行拉伸 。示例代码如下:
java 复制代码
Bitmap originalBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.example_image);
int newWidth = 200;
int newHeight = 300;
Bitmap scaledBitmap = Bitmap.createScaledBitmap(originalBitmap, newWidth, newHeight, true);

在这段代码中,originalBitmap是原始的 Bitmap 对象,newWidth和newHeight分别是期望的新宽度和新高度 。createScaledBitmap方法会根据指定的宽高对原始 Bitmap 进行拉伸或压缩,返回一个新的 Bitmap 对象 。

  1. 按比例缩放:利用Matrix类可以实现按比例缩放 。示例代码如下:
java 复制代码
Bitmap originalBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.example_image);
float scaleRatio = 0.5f;
Matrix matrix = new Matrix();
matrix.postScale(scaleRatio, scaleRatio);
Bitmap scaledBitmap = Bitmap.createBitmap(originalBitmap, 0, 0, originalBitmap.getWidth(), originalBitmap.getHeight(), matrix, true);

这里的scaleRatio是缩放比例,Matrix类的postScale方法用于设置缩放比例 。通过postScale方法设置好缩放比例后,再使用Bitmap.createBitmap方法创建一个新的按比例缩放后的 Bitmap 对象 。

Matrix类在缩放操作中起着关键作用,它通过一个 3x3 的矩阵来处理位图 。在缩放时,Matrix会根据设置的缩放比例,重新计算位图中各个像素点的位置 。例如,当设置scaleRatio = 0.5f时,位图在 x 和 y 方向上的像素点位置都会变为原来的 0.5 倍,从而实现图像的缩小 。从数学原理上来说,Matrix中的缩放操作是通过矩阵乘法来实现的,它将原始位图的坐标矩阵与缩放矩阵相乘,得到新的坐标矩阵,进而确定缩放后位图中像素点的位置 。 缩放操作在实际应用中用途广泛,比如在适配不同屏幕尺寸时,需要将图片缩放到合适的大小以适应屏幕;在图片浏览应用中,用户可能会对图片进行放大或缩小操作,这都需要使用到 Bitmap 的缩放功能 。

(二)裁剪

裁剪 Bitmap 是指根据指定区域从原始 Bitmap 中提取出一部分图像,创建一个新的 Bitmap 。在 Android 中,可以使用Bitmap.createBitmap方法来实现裁剪 。示例代码如下:

java 复制代码
Bitmap originalBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.example_image);
int x = 50;
int y = 50;
int width = 100;
int height = 100;
Bitmap croppedBitmap = Bitmap.createBitmap(originalBitmap, x, y, width, height);

在这段代码中,x和y是裁剪区域的起始坐标,width和height是裁剪区域的宽度和高度 。createBitmap方法会从原始 Bitmap 的指定位置(x, y)开始,截取指定宽度和高度的图像,创建一个新的 Bitmap 对象 。

裁剪操作在实际应用中有很多用途,比如在头像裁剪功能中,用户上传的头像可能是一张较大的图片,需要裁剪出合适的部分作为头像显示 。假设用户上传了一张全身照,而头像显示区域只需要脸部部分,这时就可以通过裁剪操作,从全身照中提取出脸部区域的图像作为头像 。在图片编辑应用中,用户也可以通过裁剪操作,去除图片中不需要的部分,突出图片的主体内容 。 裁剪操作还可以用于创建图像的缩略图,通过裁剪原始图像的中心部分,并进行适当的缩放,可以得到一个简洁的缩略图 。

(三)旋转

旋转 Bitmap 是将 Bitmap 按照指定的角度进行旋转,在 Android 中可以借助Matrix类来实现 。示例代码如下:

java 复制代码
Bitmap originalBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.example_image);
float rotationAngle = 90;
Matrix matrix = new Matrix();
matrix.setRotate(rotationAngle);
Bitmap rotatedBitmap = Bitmap.createBitmap(originalBitmap, 0, 0, originalBitmap.getWidth(), originalBitmap.getHeight(), matrix, true);

在这段代码中,rotationAngle是旋转角度,Matrix类的setRotate方法用于设置旋转角度 。设置好旋转角度后,通过Bitmap.createBitmap方法创建一个新的旋转后的 Bitmap 对象 。

Matrix类的旋转操作原理是通过矩阵变换来实现的 。在数学上,旋转操作可以用一个旋转矩阵来表示,Matrix类会根据设置的旋转角度生成相应的旋转矩阵,然后将原始 Bitmap 的坐标矩阵与旋转矩阵相乘,得到旋转后 Bitmap 的坐标矩阵,从而确定旋转后 Bitmap 中各个像素点的位置 。

旋转操作在很多场景中都有应用,比如在图片查看器中,当用户发现图片方向不正确时,可以通过旋转操作将图片调整到正确的方向 。在图像识别应用中,有时为了提高识别准确率,需要对图片进行不同角度的旋转,以获取更多的图像特征 。在一些艺术创作应用中,旋转操作可以为图片增添独特的视觉效果,满足用户的创意需求 。

(四)偏移

偏移 Bitmap 是指将 Bitmap 在 x 和 y 方向上进行平移,在 Android 中同样可以使用Matrix类来实现 。示例代码如下:

java 复制代码
Bitmap originalBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.example_image);
float dx = 50;
float dy = 30;
Matrix matrix = new Matrix();
matrix.postTranslate(dx, dy);
Bitmap skewedBitmap = Bitmap.createBitmap(originalBitmap, 0, 0, originalBitmap.getWidth(), originalBitmap.getHeight(), matrix, true);

在这段代码中,dx和dy分别是在 x 方向和 y 方向上的偏移量 。Matrix类的postTranslate方法用于设置偏移量,通过设置偏移量后,再使用Bitmap.createBitmap方法创建一个新的偏移后的 Bitmap 对象 。

Matrix类的偏移操作原理是通过矩阵变换来实现的 。在数学上,偏移操作可以用一个平移矩阵来表示,Matrix类会根据设置的偏移量生成相应的平移矩阵,然后将原始 Bitmap 的坐标矩阵与平移矩阵相乘,得到偏移后 Bitmap 的坐标矩阵,从而确定偏移后 Bitmap 中各个像素点的新位置 。

偏移操作在特殊效果制作中有着广泛的应用,比如在实现图片视差效果时,可以通过对不同图层的 Bitmap 进行不同程度的偏移,营造出深度感 。在一些动画效果中,通过对图片进行动态的偏移,可以实现图片的移动效果,增强动画的趣味性 。在游戏开发中,偏移操作可以用于实现游戏角色的移动、场景的切换等功能 。

五、Bitmap 的性能优化

(一)内存优化策略

1. 采样率压缩

采样率压缩是通过设置inSampleSize来实现的 。inSampleSize表示采样率,它是一个整数值,用于指定加载图片时对图片进行压缩的程度 。当inSampleSize = 2时,图片在宽高方向上都会缩小为原来的一半,像素数变为原来的四分之一,内存占用也相应减少 。这是因为图片占用的内存大小与像素数成正比,而像素数又与宽高的乘积成正比,当宽高都缩小为原来的一半时,像素数就变为原来的四分之一,从而内存占用也变为原来的四分之一 。

下面通过一个实际案例来展示采样率压缩前后内存占用和图片质量的变化 。假设我们有一张分辨率为 2000x1500 像素的图片,原始格式为 ARGB_8888,每个像素占用 4 个字节 。

java 复制代码
// 计算原始图片内存占用
int originalWidth = 2000;
int originalHeight = 1500;
int originalMemory = originalWidth * originalHeight * 4;

在上述代码中,通过图片的宽度originalWidth、高度originalHeight以及每个像素占用的字节数 4,计算出原始图片的内存占用originalMemory 。

接下来进行采样率压缩,设置inSampleSize = 4:

java 复制代码
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = 4;
Bitmap sampledBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.large_image, options);
int sampledWidth = sampledBitmap.getWidth();
int sampledHeight = sampledBitmap.getHeight();
int sampledMemory = sampledWidth * sampledHeight * 4;

在这段代码中,首先创建一个BitmapFactory.Options对象,并设置其inSampleSize为 4 。然后使用BitmapFactory.decodeResource方法根据设置的选项加载图片,得到采样后的Bitmap对象 。接着获取采样后图片的宽度sampledWidth和高度sampledHeight,并计算其内存占用sampledMemory 。

经过计算,原始图片内存占用约为 12MB,而采样率为 4 时,图片宽高变为 500x375 像素,内存占用约为 0.75MB 。从图片质量上看,由于像素数减少,图片会变得模糊一些,尤其是在放大查看时,细节会有所丢失 。但在一些对图片质量要求不是特别高,而更注重内存占用和加载速度的场景中,这种程度的质量损失是可以接受的 。例如,在加载列表中的图片时,用户可能更关注图片的大致内容,而对细节要求不高,此时采用采样率压缩可以有效减少内存占用,提高列表的加载速度和滑动流畅性 。

2. 更改 Bitmap.Config 格式

更改 Bitmap.Config 格式是减少内存占用的另一种有效方法 。常见的 Bitmap.Config 格式有 ARGB_8888、RGB_565、ARGB_4444 和 ALPHA_8 。其中,ARGB_8888 每个像素占用 4 个字节,能表示丰富的颜色和透明度,是默认的格式 ;RGB_565 每个像素占用 2 个字节,不支持透明度,主要用于显示颜色 ;ARGB_4444 每个像素占用 2 个字节,但由于每个通道仅用 4 位存储,画质较差,已不被推荐使用 ;ALPHA_8 每个像素仅占用 1 个字节,只保存透明度,不保存颜色 。

将 Bitmap.Config 格式从 ARGB_8888 改为 RGB_565 时,内存占用会减少一半 。例如,有一张 1000x800 像素的图片,采用 ARGB_8888 格式时,内存占用为 1000 × 800 × 4 = 3200000 字节 。若改为 RGB_565 格式,内存占用则变为 1000 × 800 × 2 = 1600000 字节 。

java 复制代码
BitmapFactory.Options options = new BitmapFactory.Options();
options.inPreferredConfig = Bitmap.Config.RGB_565;
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.example_image, options);

在这段代码中,创建BitmapFactory.Options对象后,将inPreferredConfig属性设置为Bitmap.Config.RGB_565,然后使用该选项从资源文件中解码图片,得到的Bitmap对象就是采用 RGB_565 格式存储的 。

RGB_565 格式适用于对透明度要求不高的场景,比如游戏中的背景图片、一些简单的图标等 。在这些场景中,使用 RGB_565 格式可以在保证一定图像质量的前提下,有效减少内存占用,提升应用的性能 。但需要注意的是,由于 RGB_565 不支持透明度,对于需要透明度效果的图片,如带有透明背景的图标、半透明的遮罩层等,就不能使用这种格式,否则会导致透明度信息丢失,影响图片的显示效果 。

3. Bitmap 复用

Bitmap 复用是指使用Options.inBitmap参数来复用已有的 Bitmap 内存,从而避免重复创建 Bitmap 导致的内存开销 。在 Android 3.0 开始引入了inBitmap设置,通过设置这个参数,在图片加载的时候可以使用之前已经创建了的 Bitmap,以便节省内存,避免再次创建一个 Bitmap 。在 Android 4.4,新增了允许inBitmap设置的图片与需要加载的图片的大小不同的情况,只要inBitmap的图片比当前需要加载的图片大就好了 。

以下是使用Options.inBitmap复用 Bitmap 内存的代码示例:

java 复制代码
// 创建一个可以复用的Bitmap
Bitmap reusableBitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888);
BitmapFactory.Options options = new BitmapFactory.Options();
options.inMutable = true;
options.inBitmap = reusableBitmap;
Bitmap newBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.example_image, options);

在这段代码中,首先创建一个reusableBitmap,然后创建BitmapFactory.Options对象,并将inMutable设置为true,表示该 Bitmap 是可变的,这是复用的必要条件 。接着将inBitmap设置为reusableBitmap,最后使用该选项从资源文件中解码图片,得到的newBitmap就复用了reusableBitmap的内存 。

复用 Bitmap 内存的条件如下:

  • 在 Android 4.4 版本之前,只能重用相同大小的 Bitmap 内存区域 。
  • 在 Android 4.4 及之后,只要inBitmap的图片比将要分配内存的 Bitmap 大就可以复用 。

在使用 Bitmap 复用时,还需要注意以下事项:

  • 复用的 Bitmap 必须是可变的,即inMutable需设置为true 。
  • 复用的 Bitmap 在不再使用时,需要谨慎处理,避免内存泄漏 。
  • 复用的 Bitmap 可能会影响图片的显示效果,比如当复用的 Bitmap 与新图片的格式不一致时,可能会导致颜色偏差等问题 。

为了对比复用前后的内存使用情况,可以使用 Android Profiler 工具进行观察 。在未复用 Bitmap 内存时,每次加载新图片都会分配新的内存空间,内存占用会随着图片的加载不断增加 。而使用 Bitmap 复用后,当新图片的大小符合复用条件时,会复用已有的 Bitmap 内存,内存占用相对稳定,不会因为频繁加载图片而大幅增加 。这在处理大量图片加载的场景中,如图片浏览器、相册应用等,可以显著减少内存的使用,提高应用的性能和稳定性 。

(二)缓存策略

1. LruCache 工具类

LruCache 是一种基于最近最少使用(Least Recently Used,LRU)算法的缓存机制,它适用于有限的缓存空间,如在 Android 开发中的内存缓存 。LruCache 通过维护一个最近使用的缓存项的列表,根据访问顺序淘汰最久未使用的数据 。在 Android 系统中,LruCache 通过以下步骤来实现缓存机制:

  • 创建一个固定大小的缓存容器,使用LinkedHashMap来存储缓存对象 。LinkedHashMap是一种有序的哈希表,它可以根据访问顺序或插入顺序来维护元素的顺序,在 LruCache 中,我们使用它的访问顺序特性,即每次访问一个元素时,该元素会被移动到链表的头部 。
  • 每当有缓存项被访问时,它就会被移动到LinkedHashMap的链表头部 。这是通过LinkedHashMap的afterNodeAccess方法实现的,当调用get方法获取缓存项时,该缓存项会被移动到链表头部,表明它是最近被访问的 。
  • 当缓存容器达到最大容量限制时,最久未被访问的缓存项将位于链表的尾部,LruCache 会将这个尾部元素从缓存中移除 。在put方法中,当缓存大小超过最大容量时,会调用trimToSize方法,该方法会不断移除链表头部(即最久未使用)的元素,直到缓存大小小于等于最大容量 。

下面给出使用 LruCache 管理 Bitmap 缓存的代码示例:

java 复制代码
// 设置缓存大小为10MB
int cacheSize = 10 * 1024 * 1024; 
LruCache<String, Bitmap> mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
    @Override
    protected int sizeOf(String key, Bitmap value) {
        // 重写此方法来定义每个缓存项的大小,这里以图片的字节数组大小为准
        return value.getByteCount();
    }
};

在上述代码中,首先创建一个LruCache对象,设置其缓存大小为 10MB 。然后重写sizeOf方法,用于计算每个缓存项(即 Bitmap)的大小,这里以 Bitmap 的字节数作为大小 。

在使用 LruCache 时,设置缓存大小是一个关键步骤 。缓存大小设置得过小,可能导致缓存命中率低,频繁地从磁盘或网络加载图片,影响性能;而设置得过大,则可能占用过多内存,导致应用出现内存溢出的风险 。一般来说,可以根据应用的需求和设备的内存状况来定制缓存大小 。例如,可以使用Runtime.getRuntime().maxMemory()方法来获取应用能够使用的最大内存,并据此计算 LruCache 的大小 。比如,将缓存大小设置为可用内存的 1/8:

java 复制代码
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
int cacheSize = maxMemory / 8; 
LruCache<String, Bitmap> mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
    @Override
    protected int sizeOf(String key, Bitmap value) {
        return value.getByteCount() / 1024; 
    }
};

在处理缓存命中与未命中的情况时,代码如下:

java 复制代码
public Bitmap loadBitmap(String url) {
    Bitmap bitmap = mMemoryCache.get(url);
    if (bitmap == null) {
        // 缓存未命中,从网络或磁盘加载图片
        bitmap = downloadBitmap(url); 
        if (bitmap != null) {
            mMemoryCache.put(url, bitmap);
        }
    }
    return bitmap;
}

在loadBitmap方法中,首先尝试从 LruCache 中获取指定 URL 对应的 Bitmap 。如果获取到了(即缓存命中),则直接返回该 Bitmap 。如果未获取到(即缓存未命中),则从网络或磁盘加载图片(这里假设downloadBitmap方法用于从网络或磁盘加载图片) 。加载成功后,将图片存入 LruCache,并返回该 Bitmap 。通过这样的方式,有效地利用了 LruCache 的缓存机制,减少了重复加载图片的开销,提高了应用的性能和响应速度 。

2. 内存缓存与磁盘缓存

内存缓存和磁盘缓存各有优缺点 。内存缓存的优点是速度快,因为数据存储在内存中,读取速度非常快,可以快速地获取图片并显示在界面上,极大地提升了用户体验 。而且内存缓存与应用的生命周期紧密相关,当应用关闭时,内存缓存中的数据会自动被清除,不需要额外的清理操作 。但是,内存缓存的缺点也很明显,它的容量有限,因为手机的内存资源是有限的,不能无限制地存储图片 。而且内存缓存的数据容易丢失,当系统内存不足时,可能会回收内存缓存中的数据,导致缓存失效 。

磁盘缓存的优点是容量大,手机的磁盘空间相对较大,可以存储更多的图片 。并且磁盘缓存的数据相对持久,即使应用关闭后重新打开,磁盘缓存中的数据仍然存在,不需要重新加载 。然而,磁盘缓存的缺点是读取速度相对较慢,因为磁盘的读写速度比内存慢很多,从磁盘读取图片会增加加载时间,影响用户体验 。而且磁盘 I/O 操作会消耗一定的系统资源,频繁的磁盘读写可能会影响系统性能 。

在实际应用中,通常会结合使用内存缓存和磁盘缓存来提高图片加载效率 。结合使用的原理是,首先从内存缓存中查找图片,如果找到,则直接使用,这样可以快速加载图片,提高用户体验 。如果内存缓存中未找到图片,则从磁盘缓存中查找 。如果磁盘缓存中存在图片,则将其加载到内存中,并同时存入内存缓存,以便下次可以直接从内存缓存中获取 。如果磁盘缓存中也没有图片,则从网络加载图片,加载成功后,将图片存入内存缓存和磁盘缓存,以备后续使用 。

下面给出磁盘缓存的实现思路和代码示例 。实现磁盘缓存可以使用DiskLruCache类,它是一个专门用于磁盘缓存的工具类 。

java 复制代码
public class DiskLruCacheHelper {
    private static final String CACHE_DIR = "image_cache"; 
    private static final int APP_VERSION = 1; 
    private static final int VALUE_COUNT = 1; 
    private static final int MAX_SIZE = 10 * 1024 * 1024; 
    private DiskLruCache mDiskLruCache;
    public DiskLruCacheHelper(Context context) {
        try {
            File cacheDir = new File(context.getCacheDir(), CACHE_DIR);
            mDiskLruCache = DiskLruCache.open(cacheDir, APP_VERSION, VALUE_COUNT, MAX_SIZE);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    public void put(String key, Bitmap bitmap) {
        try {
            DiskLruCache.Editor editor = mDiskLruCache.edit(key);
            if (editor != null) {
                OutputStream outputStream = editor.newOutputStream(0);
                bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream);
                editor.commit();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    public Bitmap get(String key) {
        try {
            DiskLruCache.Snapshot snapshot = mDiskLruCache.get(key);
            if (snapshot != null) {
                InputStream inputStream = snapshot.getInputStream(0);
                return BitmapFactory.decodeStream(inputStream);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }
    public void close() {
        try {
            mDiskLruCache.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

在上述代码中,DiskLruCacheHelper类封装了磁盘缓存的操作 。在构造函数中,通过DiskLruCache.open方法打开磁盘缓存,指定缓存目录CACHE_DIR、应用版本APP_VERSION、每个键对应的值的数量VALUE_COUNT以及缓存的最大大小MAX_SIZE 。put方法用于将 Bitmap 存入磁盘缓存,首先获取DiskLruCache.Editor,然后通过editor.newOutputStream获取输出流,将 Bitmap 压缩后写入输出流,最后提交编辑 。get方法用于从磁盘缓存中获取 Bitmap,首先获取DiskLruCache.Snapshot,如果获取到,则通过Snapshot.getInputStream获取输入流,再使用BitmapFactory.decodeStream方法将输入流解码为 Bitmap 。close方法用于关闭磁盘缓存 。通过这样的实现,有效地利用了磁盘缓存,结合内存缓存,可以大大提高图片加载的效率和稳定性 。

六、Bitmap 在实际项目中的应用场景

(一)图片展示

在 Android 开发中,ImageView、RecyclerView 和 ListView 是展示图片的常用控件 。在 ImageView 中展示 Bitmap 较为简单,通过setImageBitmap方法即可实现 。例如:

java 复制代码
ImageView imageView = findViewById(R.id.imageView);
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.example_image);
imageView.setImageBitmap(bitmap);

在 RecyclerView 和 ListView 中展示 Bitmap 时,通常需要结合适配器来实现 。以 RecyclerView 为例,首先需要创建一个适配器,在适配器的onBindViewHolder方法中设置 ImageView 的 Bitmap 。示例代码如下:

java 复制代码
public class MyAdapter extends RecyclerView.Adapter<MyAdapter.ViewHolder> {
    private List<Bitmap> bitmapList;
    public MyAdapter(List<Bitmap> bitmapList) {
        this.bitmapList = bitmapList;
    }
    @Override
    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_layout, parent, false);
        return new ViewHolder(view);
    }
    @Override
    public void onBindViewHolder(ViewHolder holder, int position) {
        holder.imageView.setImageBitmap(bitmapList.get(position));
    }
    @Override
    public int getItemCount() {
        return bitmapList.size();
    }
    public static class ViewHolder extends RecyclerView.ViewHolder {
        public ImageView imageView;
        public ViewHolder(View itemView) {
            super(itemView);
            imageView = itemView.findViewById(R.id.imageView);
        }
    }
}

在使用这些控件展示 Bitmap 时,防止图片闪烁和错位是非常重要的 。图片闪烁和错位的主要原因是异步加载和 View 复用 。当 ListView 或 RecyclerView 滑动时,由于 View 的复用机制,可能会出现图片加载完成后显示到错误的位置,或者图片在加载过程中频繁闪烁的情况 。

为了防止图片闪烁,我们可以在图片加载完成前设置一个默认的占位图 ,这样可以避免图片在加载过程中出现空白闪烁的情况 。同时,在图片加载完成后,通过判断 ImageView 的 Tag 来确保图片显示在正确的位置 。例如:

java 复制代码
class ImageLoader {
    private LruCache<String, Bitmap> mMemoryCache;
    private Map<ImageView, String> imageViewMap;
    public ImageLoader() {
        int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
        int cacheSize = maxMemory / 8;
        mMemoryCache = new LruCache<String, Bitmap>(cacheSize);
        imageViewMap = new HashMap<>();
    }
    public void addBitmapToMemoryCache(String key, Bitmap bitmap) {
        if (getBitmapFromMemoryCache(key) == null) {
            mMemoryCache.put(key, bitmap);
        }
    }
    public Bitmap getBitmapFromMemoryCache(String key) {
        return mMemoryCache.get(key);
    }
    public void loadImage(final String url, final ImageView imageView) {
        imageViewMap.put(imageView, url);
        imageView.setImageResource(R.drawable.default_image); 
        Bitmap bitmap = getBitmapFromMemoryCache(url);
        if (bitmap != null) {
            imageView.setImageBitmap(bitmap);
        } else {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    Bitmap downloadedBitmap = downloadBitmap(url); 
                    if (downloadedBitmap != null) {
                        addBitmapToMemoryCache(url, downloadedBitmap);
                        final Bitmap finalBitmap = downloadedBitmap;
                        imageView.post(new Runnable() {
                            @Override
                            public void run() {
                                if (imageViewMap.get(imageView) != null && imageViewMap.get(imageView).equals(url)) {
                                    imageView.setImageBitmap(finalBitmap);
                                }
                            }
                        });
                    }
                }
            }).start();
        }
    }
    private Bitmap downloadBitmap(String url) {
        // 这里实现从网络下载图片的逻辑
        // 例如使用HttpURLConnection或OkHttp
        return null;
    }
}

在上述代码中,ImageLoader类实现了图片的加载和缓存功能 。在loadImage方法中,首先将 ImageView 和对应的图片 URL 存入imageViewMap中,然后设置默认的占位图 。接着从内存缓存中获取图片,如果存在则直接显示;如果不存在,则开启一个线程从网络下载图片 。下载完成后,将图片存入内存缓存,并通过post方法在主线程中更新 ImageView 的显示,同时通过判断imageViewMap中 ImageView 对应的 URL 来确保图片显示在正确的位置 。通过这种方式,可以有效地防止图片闪烁和错位,提升用户体验 。

(二)图像处理

在滤镜、图像合成、图像识别等图像处理场景中,Bitmap 都有着广泛的应用 。

在滤镜处理中,我们可以通过操作 Bitmap 的像素来实现各种滤镜效果 。以灰度化为例,灰度化是将彩色图像转换为黑白图像的过程,其原理是根据一定的权重将 RGB 三个通道的值转换为一个灰度值 。常见的灰度化算法有加权平均法,计算公式为:Gray = 0.299 * R + 0.587 * G + 0.114 * B 。下面是使用 Java 实现灰度化的代码示例:

java 复制代码
public static Bitmap grayScale(Bitmap bitmap) {
    int width = bitmap.getWidth();
    int height = bitmap.getHeight();
    Bitmap grayBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
    for (int y = 0; y < height; y++) {
        for (int x = 0; x < width; x++) {
            int pixel = bitmap.getPixel(x, y);
            int alpha = Color.alpha(pixel);
            int red = Color.red(pixel);
            int green = Color.green(pixel);
            int blue = Color.blue(pixel);
            int gray = (int) (0.299 * red + 0.587 * green + 0.114 * blue);
            grayBitmap.setPixel(x, y, Color.argb(alpha, gray, gray, gray));
        }
    }
    return grayBitmap;
}

在这段代码中,首先创建一个与原始 Bitmap 大小相同的新 Bitmap,用于存储灰度化后的图像 。然后通过双重循环遍历原始 Bitmap 的每一个像素,获取其 ARGB 值,根据灰度化公式计算出灰度值,最后将灰度值设置到新的 Bitmap 中 。通过这种方式,实现了将彩色 Bitmap 转换为灰度 Bitmap 的功能 。

图像合成是将多个 Bitmap 合并成一个 Bitmap 的过程 。例如,将一个水印图片合成到另一个图片上 。实现思路是创建一个新的 Bitmap,其大小为两个图片中较大的尺寸 ,然后分别将两个图片绘制到新 Bitmap 的指定位置 。示例代码如下:

java 复制代码
public static Bitmap mergeBitmaps(Bitmap background, Bitmap watermark) {
    int width = Math.max(background.getWidth(), watermark.getWidth());
    int height = Math.max(background.getHeight(), watermark.getHeight());
    Bitmap mergedBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
    Canvas canvas = new Canvas(mergedBitmap);
    canvas.drawBitmap(background, 0, 0, null);
    canvas.drawBitmap(watermark, width - watermark.getWidth(), height - watermark.getHeight(), null);
    return mergedBitmap;
}

在这段代码中,首先计算出合并后 Bitmap 的宽度和高度,然后创建一个新的 Bitmap 。接着创建一个Canvas对象,将背景图片绘制到Canvas的 (0, 0) 位置,再将水印图片绘制到Canvas的右下角位置 。最后返回合成后的 Bitmap 。

在图像识别领域,Bitmap 也发挥着重要作用 。例如,在简单的字符识别中,首先将图像转换为 Bitmap,然后对 Bitmap 进行预处理,如灰度化、二值化等操作,以突出图像的特征 。二值化是将图像转换为只有黑白两种颜色的图像,其原理是根据一个阈值将像素值分为两类 。示例代码如下:

java 复制代码
public static Bitmap binaryzation(Bitmap bitmap, int threshold) {
    int width = bitmap.getWidth();
    int height = bitmap.getHeight();
    Bitmap binaryBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
    for (int y = 0; y < height; y++) {
        for (int x = 0; x < width; x++) {
            int pixel = bitmap.getPixel(x, y);
            int alpha = Color.alpha(pixel);
            int red = Color.red(pixel);
            int green = Color.green(pixel);
            int blue = Color.blue(pixel);
            int gray = (int) (0.299 * red + 0.587 * green + 0.114 * blue);
            int newPixel = gray > threshold? Color.WHITE : Color.BLACK;
            binaryBitmap.setPixel(x, y, Color.argb(alpha, newPixel, newPixel, newPixel));
        }
    }
    return binaryBitmap;
}

在这段代码中,首先创建一个与原始 Bitmap 大小相同的新 Bitmap,用于存储二值化后的图像 。然后通过双重循环遍历原始 Bitmap 的每一个像素,获取其 ARGB 值,计算出灰度值 。根据设定的阈值,将灰度值与阈值进行比较,如果灰度值大于阈值,则将该像素设置为白色;否则设置为黑色 。最后将处理后的像素值设置到新的 Bitmap 中,完成二值化操作 。经过预处理后的 Bitmap 可以进一步用于特征提取和模式匹配,以实现图像识别的功能 。

七、总结

Bitmap 在 Android 开发中占据着举足轻重的地位,作为图片在内存中的呈现形式,它为开发者提供了强大的图像操作能力。从原理上看,Bitmap 通过像素点集合存储图像信息,其颜色通道和内存占用的计算方式决定了图像的显示效果和内存开销 。在实际应用中,我们需要根据不同的需求选择合适的创建和加载方式,并且要注意优化性能,避免内存溢出等问题 。

通过本文的介绍,我们深入了解了 Bitmap 的内部原理,包括颜色通道的存储方式和内存占用的计算方法 。掌握了 Bitmap 的创建与加载方式,学会了如何通过BitmapFactory.Options参数来优化加载过程,避免内存溢出 。还熟悉了 Bitmap 的常见操作,如缩放、裁剪、旋转和偏移,以及这些操作在实际应用中的实现方法 。在性能优化方面,我们探讨了内存优化策略和缓存策略,包括采样率压缩、更改 Bitmap.Config 格式、Bitmap 复用以及 LruCache 和磁盘缓存的使用 。最后,我们分析了 Bitmap 在实际项目中的应用场景,如图片展示和图像处理,并且给出了相应的代码示例和解决方案 。

相关推荐
投笔丶从戎3 小时前
Kotlin Multiplatform--03:项目实战
android·开发语言·kotlin
居然是阿宋3 小时前
Android Canvas API 详细说明与示例
android
wuli玉shell5 小时前
spark-Schema 定义字段强类型和弱类型
android·java·spark
东风西巷6 小时前
BLURRR剪辑软件免费版:创意剪辑,轻松上手,打造个性视频
android·智能手机·音视频·生活·软件需求
JhonKI6 小时前
【MySQL】行结构详解:InnoDb支持格式、如何存储、头信息区域、Null列表、变长字段以及与其他格式的对比
android·数据库·mysql
ab_dg_dp7 小时前
Android 位掩码操作(&和~和|的二进制运算)
android
潜龙952719 小时前
第3.2.3节 Android动态调用链路的获取
android·调用链路
追随远方20 小时前
Android平台FFmpeg音视频开发深度指南
android·ffmpeg·音视频
撰卢21 小时前
MySQL 1366 - Incorrect string value:错误
android·数据库·mysql
恋猫de小郭21 小时前
Flutter 合并 ‘dot-shorthands‘ 语法糖,Dart 开始支持交叉编译
android·flutter·ios