Android Bitmap 详解:关于 Bitamp 你所要知道的一切

android.jpg

本文已授权微信公众号 : code小生
(codexiaosheng) 在微信公众平台原创首发

前言

在平时的 Android 开发中,与 Bitmap 打交道可以说是再常见不过的事了。我在写这篇文章之前,对于 Bitmap 相关的一些东西总是模模糊糊,比如 Bitmap 的文件大小还有占用内存大小的区别,还有对 Bitmap 压缩的几种方法各自的区别和用途是什么,等等

在这篇文章中,我将会把在 Bitmap 中相关的知识点都一一介绍,如果你也是对 Bitmap 总是感觉模模糊糊的话, 相信你看完这篇文章后一定会有所收获

目录

一、Bitmap 的创建
二、Bitmap 的颜色配置信息与压缩方式信息
三、Bitmap 的转换与保存
四、Bitmap 的文件大小
五、Bitmap 占用内存的大小
六、影响 Bitmap 占用内存大小的因素
七、Bitmap 的加载优化与压缩
八、Bitmap 的其他操作

一、Bitmap 的创建

我们如何创建一个 Bitamap 对象呢?Google 给我们提供了两种方式:

    1. Bitmap 的静态方法 createBitmap(XX)


      image.png
    1. BitmapFactory 的 decodeXX 系列静态方法


      image.png

二、Bitmap 的颜色配置信息与压缩方式信息

Bitmap 中有两个内部枚举类:Config 和 CompressFormat,Config 是用来设置颜色配置信息的,CompressFormat 是用来设置压缩方式的

image.png

Config

Config 类描述了一个 Bitmap 是如何存储像素信息的,它影响了图片的质量(颜色深度)以及显示透明/不透明颜色的能力

颜色格式 描述 每个像素占用内存大小
Bitmap.Config.ALPHA_8 颜色信息只由透明度组成 8 位,即 1 字节
Bitmap.Config.ARGB_4444 颜色信息由透明度与R(Red),G(Green),B(Blue)四部分组成,每个部分都占4位,总共占16位 16 位,即 2 字节
Bitmap.Config.ARGB_8888 颜色信息由透明度与R(Red),G(Green),B(Blue)四部分组成,每个部分都占8位,总共占32位。是Bitmap默认的颜色配置信息,也是最占空间的一种配置 32 位,即 4 字节
Bitmap.Config.RGB_565 颜色信息由R(Red),G(Green),B(Blue)三部分组成,R占5位,G占6位,B占5位,总共占16位 16 位,即 2 字节

关于图片的颜色格式,有几点需要注意:

  1. Bitmap 默认的图片格式是 ARGB_8888
  2. 图片占用内存的大小与图片的颜色格式相关, 占用内存的大小 = 图片的宽度 × 图片的高度 × 每个像素占用的内存大小
  3. 当我们需要做性能优化或者防止 OOM 的时候,可以将 Bitamp 的颜色配置该为 RGB_565 ,它的占用内存大小是 ARGB_8888的一半
    例如:
  val options = BitmapFactory.Options()
  options.inPreferredConfig = Bitmap.Config.RGB_565  // 设置bitmap的颜色格式
  val bitmap = BitmapFactory.decodeResource(resources, R.drawable.pic, options)

注意: RGB_565 是不支持透明度的,如果你需要显示带有透明度的图片,不要用此格式

CompressFormat

CompressFormat 描述了将 Bitmap 以什么方式压缩,它有3个值:

压缩方式 描述
Bitmap.CompressFormat.JPEG 表示以JPEG压缩算法进行图像压缩,压缩后的格式可以是".jpg"或者".jpeg",是一种有损压缩
Bitmap.CompressFormat.PNG 表示以PNG压缩算法进行图像压缩,压缩后的格式可以是".png",是一种无损压缩
Bitmap.CompressFormat.WEBP 表示以WebP压缩算法进行图像压缩,压缩后的格式可以是".webp",是一种有损压缩,质量相同的情况下,WebP格式图像的体积要比JPEG格式图像小40%。美中不足的是,WebP格式图像的编码时间“比JPEG格式图像长8倍”

例如:

  fun bitmapToByteArray(bitmap: Bitmap): ByteArray {
        val baos = ByteArrayOutputStream()
        bitmap.compress(Bitmap.CompressFormat.JPEG, 100, baos)
        return baos.toByteArray()
    }

三、Bitmap 的保存和转换

前面介绍了如何创建一个 Bitmap,当我们拿到一个 Bitmap 对象后,通常还有有以下操作:

1. 将 Bitamap 转换为 byte 数组

  fun bitmapToByteArray(bitmap: Bitmap): ByteArray {
        val baos = ByteArrayOutputStream()
        bitmap.compress(Bitmap.CompressFormat.JPEG, 100, baos)
        return baos.toByteArray()
    }

2. 将 Bitamap 保存为 文件

   fun bitmapToFile(bitmap: Bitmap, file: File): Boolean {
        val baos = ByteArrayOutputStream()
        val fileOutputStream = FileOutputStream(file)

        return try {
            bitmap.compress(Bitmap.CompressFormat.JPEG, 100, baos)
            fileOutputStream.write(baos.toByteArray())
            true
        } catch (e: Exception) {
            e.printStackTrace()
            false
        } finally {
            baos.close()
            fileOutputStream.close()
        }
    }

四、Bitmap 的文件大小

说到 Bitmap 大小这一块的时候,我们一定要先搞清楚几个概念:

  • Bitmap 原始的文件大小

  • 把一个 Bitamp 通过压缩保存到本地的文件大小

  • Bitmap 加载到内存中占用的内存大小

注意:通常情况下,这三个值不相等!

我们以一张 宽高为 1080 * 1920 ,图片原始大小为 705 kb 的图片为例(本文均以此图片为例),逐个解释和验证这三个数据:

1. Bitmap 原始的文件大小

这个很好理解,就是图片的自身的大小嘛,没有经过任何处理,通过下图我们可以看到,这张图片的原始大小是 705 kb:

image.png
image.png

注意,如果我们直接在 Android Studio 中打开这张图片的话,上面显示的图片大小是 721.96 kb,这个为什么不是 705 kb呢?
我个人的理解是,这个大小并不是图片本身的大小,而是 图片本身的大小 + 在 Android Studio 中占用的一些信息
就好比在 Window 的截图中的大小指的是图片本身,下面还有一个占用空间指的是在 Windows 中这张图片占用的磁盘空间大小

下面我们通过代码验证一下:

  1. 把图片放到工程的 assets 目录下
  2. 通过下面代码加载图片,然后打印出图片的大小:
val bytes = assets.open("pic.jpg").readBytes()
log("原始文件大小 :${bytes.size / 1024} kb")

日志输出如下:
原始文件大小 :705 kb

2. Bitamp 通过压缩保存到本地的文件大小

通过上面的验证,我们知道,这张图片的原始大小是 705 kb。如果我们把这张图片保存到手机上,那么它的大小还会是 705 kb 么?

把这张图片保存到手机上,分两种情况:

1). 直接拿到图片的输入流或者说 byte 数组,然后保存到本地

  val bytes = assets.open("pic.jpg").readBytes()
  log("assets 中读取的大小 : ${bytes.size / 1024} kb")

  val file = File(getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "pic.jpg")
  if (!file.exists()) {
       file.createNewFile()
     }

  FileUtils.writeToFile(bytes, file)
  log("保存到本地的图片大小 ${file.readBytes().size / 1024} kb")


   // FileUtil 类中的 writeToFile 方法:
  fun writeToFile(data: ByteArray, file: File) {
      val fileOutputStream = FileOutputStream(file)
        try {
            fileOutputStream.write(data)
        } catch (e: Exception) {
            e.printStackTrace()
        } finally {
            fileOutputStream.close()
        }
    }

日志输出如下:

assets 中读取的大小 : 705 kb
保存到本地的图片大小 705 kb

然后我们再验证一下保存的图片信息:

image.png

2.) 创建一个 Bitmap,然后保存到本地

  val bytes = assets.open("pic.jpg").readBytes()
  log("assets 中读取的大小 : ${bytes.size / 1024} kb")

  val bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
  val baos = ByteArrayOutputStream()
  bitmap.compress(Bitmap.CompressFormat.JPEG, 100, baos)  //压缩图片并将数据存储到 ByteArrayOutputStream 中

  val file = File(getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "pic.jpg")
  if (!file.exists()) {
         file.createNewFile()
     }
  val fileOutputStream = FileOutputStream(file)
  fileOutputStream.write(baos.toByteArray())
        
  log("保存到本地的图片大小 : ${file.readBytes().size / 1024} kb")

日志输出如下:

assets 中读取的大小 : 705 kb
保存到本地的图片大小 :  817 kb

再查看一下保存的图片信息:

image.png

到这里,我们就会有疑问了,为什么通过 Bitmap 转换之后图片大小就不一样了呢?

关键就在这一句,

 bitmap.compress(Bitmap.CompressFormat.JPEG, 100, baos)  //压缩图片并将数据存储到 ByteArrayOutputStream 中

当我们需要把一个 Bitmap 对象保存到本地时,需要先将其转换成 byte 数组,这个过程是通过 Bitmap 的 compress 方法完成的。

这个方法中第一个参数代表保存的图片类型,第二个参数代表图片的质量。这个值的范围是 0~100,数值越大图片质量越高,同时保存后的图片大小也越大。

也就是说,当通过这个方法把一张 Bitmap 保存到本地时,第二参数控制了保存的图片质量,同时也就影响了保存图片的大小

3. Bitmap 加载到内存中占用的内存大小

请看第五部分

小结

  • Bitmap 原始的文件大小Bitamp 压缩保存到本地的大小Bitmap 加载到内存中占用的内存大小,这三者是三个不同的概念,且通常这三者并不相等

  • 将 Bitmap 保存到本地时,可以通过 compress 方法的第二个参数控制图片的质量,从而达到控制图片大小的目的。(用于图片压缩,后面会介绍)

五、Bitmap 占用内存的大小

终于到了大家最最关心的点,Bitmap 占用内存的大小!很多时候,我们只是朦朦胧胧的知道,加载大的图片要注意,防止OOM。

但是,加载一张图片到底占用多少内存呢?

如何计算加载一张图片到底占用多少内存

来人,上公式:

总内存 = 宽 × 高 × 色彩空间

把上面的公式再详细描述一下就是:

总内存 = 宽的像素数 × 高的像素数 × 每个像素点占用的大小

这个公式也很好理解,宽 × 高 即图片总共有多少像素点,然后乘 每个像素点占用的大小 就得出了总内存。

Bitmap 中直接提供了相关方法得到图片所占用的内存大小:

  • getAllocationByteCount() // API 19 以后使用
  • getByteCount()

除了系统提供的方法,我们也可以根据上面的公式自己计算。

接下来我们就通过系统提供的方法和我们自己计算来验证一下:

1). 从 assets 目录中加载图片,并计算占用的内存大小:

// 加载图片
val bytes = assets.open("pic.jpg").readBytes()
val bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
ivPic.setImageBitmap(bitmap)

log("占用内存大小: ${Bitmaps.getMemorySize(rawBitmap)} kb \n")
log("计算占用内存大小: ${Bitmaps.calculateMemorySize(rawBitmap)} kb \n")


// 使用系统 api 提供的方法计算
// Bitmaps 中的 getMemorySize 方法
 fun getMemorySize(bitmap: Bitmap, sizeType: SizeType = SizeType.KB): Int {
        val bytes = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {  //Since API 19
            bitmap.allocationByteCount
        } else {
            bitmap.byteCount
        }

        return when (sizeType) {
            SizeType.B -> bytes
            SizeType.KB -> bytes / 1024
            SizeType.MB -> bytes / 1024 / 1024
            SizeType.GB -> bytes / 1024 / 1024 / 1024
        }
 }

// 根据公式手动计算
// Bitmaps 中的 calculateMemorySize方法
  fun calculateMemorySize(bitmap: Bitmap, sizeType: SizeType = SizeType.KB): Int {
        val pixels = bitmap.width * bitmap.height
        val bytes = when (bitmap.config) {
            Bitmap.Config.ALPHA_8 -> pixels * 1
            Bitmap.Config.ARGB_4444 -> pixels * 2
            Bitmap.Config.ARGB_8888 -> pixels * 4
            Bitmap.Config.RGB_565 -> pixels * 2
            else -> pixels * 4
        }

        return when (sizeType) {
            SizeType.B -> bytes
            SizeType.KB -> bytes / 1024
            SizeType.MB -> bytes / 1024 / 1024
            SizeType.GB -> bytes / 1024 / 1024 / 1024
        }
    }

    // 单位的枚举类
    enum class SizeType {
        B,
        KB,
        MB,
        GB
    }

注: Bitmaps 是我自己定义的一个工具类,并不是系统的一个类。源码在文章最下面

2). 计算结果如下:


image.png

我们可以看到,利用系统提供的 api 与 我们自己用公式计算得出的占用内存大小是一样的。

对于这张图片来说,宽高为 1080 * 1920,图片的颜色格式是 ARGB_8888,证明每个像素占用 4 个字节的内存,所以加载它占用的内存就是:

总内存 = 宽 * 高 * 色彩空间 = 1080 * 1920 * 4 = 8294400 byte = 8100 KB = 7.9 MB

注:
1 byte = 8 bit
1 KB = 1024 byte
1 MB = 1024 KB
1 GB = 1024 MB

六、影响 Bitmap 占用内存大小的因素

根据公式:

总内存 = 宽的像素数 × 高的像素数 × 每个像素点占用的大小

可以得出,影响占用内存的大小因素有:

  • 宽高
  • 色彩空间

所以,当我们需要对 Bitamp 加载进行优化的时候,就可以从这两个方面进行着手:

  • 减少 Bitmap 的宽高
  • 使用占用更少内存的色彩模式

除了上面两点,还有一个因素也会影响到 Bitamp 占用的内存大小,它就是 缩放

缩放

1. 什么是缩放

根据前面几部分的介绍,我们知道,加载一张 1080 * 1920 的图片,然后通过 bitmap.getWidth() 和 bitmap.getHeight() 得到的也是 1080 * 1920

如果图片的原始大小是 1080 * 1920,那边加载出来的 Bitmap 对象也一定是 1080 * 1920 么?

答案是否定的。在加载 Bitamp 对象时可以手动设置 inSampleSize 来进行缩放。另外,如果是从 Drawable 目录下加载图片的话,系统会默认地根据图片所在的 Drawable 目录以及手机的 DPI 对加载的图片进行缩放

2. 缩放是如何影响影响占用内存的

当我们对图片进行缩放时,实际上造成的结果是图片宽高的改变,通过上面的公式我们可以知道,宽高改变了,占用的内存也就改变了。

所以图片的缩放对内存的影响本质上还是宽高对占用内存的影响

3. Bitmap 中如何对图片进行缩放

1)、 手动设置缩放参数

当我们创建一个 Bitmap 对象的时候,会有一个可选的 Options 对象,其中的 inSampleSize 参数可以控制缩放的比例,inSampleSize 的值代表 图片的宽度、高度分别变为原来的 1/inSampleSize

比如一张 1080 * 1920 的图片,如果加载时设置了 inSampleSize = 2,证明图片的宽度变为原来的 1/2,高度也变为原来的 1/2,所以得到的 Bitmap 对象的宽高是 540 * 860

根据上面的占用内存的计算公式,它占用的内存大小就变为原来的 1/2 * 1/2 = 1/4

下面我们来验证一下,还是那张图片,在加载的时候设置 inSampleSize = 2 ,然后看一下图片的宽高和占用内存的情况:

 val bytes = assets.open("pic.jpg").readBytes()

 val options = BitmapFactory.Options()
 ptions.inSampleSize = 2
 val bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size, options)

 ivPic.setImageBitmap(bitmap)
 showInfo(bitmap)

结果如下:


image.png

我们可以看到,图片的宽高由原来的 1080 * 1920 变成了 540 * 960,宽高分别变为原来的 1/2,占用内存的大小由原来的 8100 变成了 2025,内存大小变为了原来的 1/2 * 1/2 = 1/4

根据这个特性,我们可以在加载大图的时候进行缩放处理,防止OOM的发生

注意,inSampleSize 的值要求必须大于1,且只能是2的整数倍

2)、 从 Drawable 目录中加载图片的自动缩放

当我们从 assets 目录中或者网络上加载一张图片的时候,默认情况下得到的 Bitmap 对象的宽高是与原图片的宽高一致的。比如前面的我们举的例子,宽高都是 1080 * 1920

如果我们从 Drawable 目录下加载图片的话,系统会根据图片所在的目录以及手机的DPI对图片进行缩放

下面是手机 dpi 与 Drawable 目录的对应关系图:

DPI 分辨率 系统dpi 基准比例 对应Drawable目录
ldpi 240x320 120 0.75 drawable-ldpi(低密度)
mdpi 320x480 160 1 drawable-mdpi(中等密度)
hdpi 480x800 240 1.5 drawable-hdpi(高密度)
xhdpi 720x1280 320 2 drawable-xhdpi(超高密度)
xxhdpi 1080x1920 480 3 drawable-xxhdpi(超超高密度)
xxxhdpi 2160 x3840 640 4 drawable-xxxhdpi(超超超高密度)

Drawable 目录的选择流程

当我们从 Drawable 目录中加载一张图片的时候:

  1. 比如在一个中等分辨率的手机上,Android 就会选择d rawable-mdpi 文件夹下的图片,文件夹下有这张图就会优先被使用,在这种情况下,图片是不会被缩放的

  2. 但是如果没有在 drawable-mdpi 的文件夹下找到相应图片的话,
    Android 系统会首先从更高一级的 drawable-hdpi 文件夹中查找,
    如果找到图片资源就进行缩放处理(缩小),显示在屏幕上

  3. 如果 drawable-hdpi 文件夹下也没有的话,就依次往 drawable-xhdp i文件夹、drawable-xxhdpi 文件夹、
    drawable-xxxhdpi 文件夹、drawable-nodpi 文件夹中寻找

  4. 如果更高密度的文件夹里都没有找到,就往更低密度的文件夹 drawable-ldpi 文件夹下查找。如果找到图片资源就进行缩放处理(放大),显示在屏幕上

  5. 如果都没找到,最终会在默认的drawable文件夹中寻找,如果默认的drawable文件夹中也没有那就会报错啦

Drawable 缩放规则小结

  • 如果图片所在的文件夹 dpi 刚好是手机屏幕密度所对应的文件夹(比如:手机 dpi 为 xxhdpi,图片在 drawable-xxhdpi 文件夹中),
    则该图片不会被压缩

  • 如果图片所在目录 dpi 低于匹配目录,那么该图片被认为是为低密度设备需要的,现在要显示在高密度设备上,图片会被放大,宽和高,以及占用的内存都会变大

注意:如果图片本身就比较大,而又放在了密度较低的文件夹中,
加载时会导致占用内存变得非常大,导致OOM

  • 如果图片所在目录 dpi 高于匹配目录,那么该图片被认为是为高密度设备需要的,现在要显示在低密度设备上,图片会被缩小,宽和高,以及占用的内存都会变小

  • 如果图片所在目录为 drawable-nodpi,则无论设备 dpi 为多少,保留原图片大小,不进行缩放

验证

以我的手机为例,屏幕分辨率是 1080 * 1920,DPI 是 480,对应的 Drawable 目录是 drawable-xxhdpi(超超高密度)

  1. 把图片拷贝到 drawable-xxhdpi 目录下,然后加载图片并显示其信息
 val bitmap = BitmapFactory.decodeResource(resources, R.drawable.pic)

ivPic.setImageBitmap(bitmap)
showInfo(bitmap)  //显示图片信息

根据上面的介绍的规则,我们加载图片所对应的 Drawable 与我们的手机 DPI 相匹配,所以图片不会进行缩放

image.png
  1. 把图片放拷贝到 drawable-xxxhdpi 目录下(高于手机DPI),然后加载图片并显示其信息,此时图片会被缩小
image.png
  1. 把图片放拷贝到 drawable-xhdpi 目录下(低于手机DPI),然后加载图片并显示其信息,此时图片会被放大
image.png

注意:
在做测试的时候,要保证同时只有一个 drawable 文件夹中存在需要加载的那张图片

4. 小结

  • 总内存 = 宽的像素数 × 高的像素数 × 每个像素点占用的大小
  • 由以上公式可以知道影响内存占用大小的因素是 宽高和色彩空间
  • 加载 一个 Bitamap 可以通过设置 inSampleSize 的值控制加载得到的图片的大小
  • 从 Drawable 目录中加载图片时,系统会根据手机 DPI 和 Drawable 目录对图片进行缩放

七、Bitmap 的加载优化与压缩

1. 质量压缩

    /**
     * 将图片 [bitmap] 压缩到指定大小 [targetSize] 以内 ,单位是 kb
     * 这里的大小指的是 “文件大小”,而不是 “内存大小”
     **/
   fun compressQuality(bitmap: Bitmap, targetSize: Int, declineQuality: Int = 10): ByteArray {

        val baos = ByteArrayOutputStream()

        bitmap.compress(Bitmap.CompressFormat.JPEG, 100, baos)
        log("压缩前文件大小:${baos.toByteArray().size / 1024} kb")

        var quality = 100
        while ((baos.toByteArray().size / 1024) > targetSize) {
            baos.reset()
            quality -= declineQuality
            bitmap.compress(Bitmap.CompressFormat.JPEG, quality, baos)
        }

        log("压缩后文件大小:${baos.toByteArray().size / 1024} kb")

        return baos.toByteArray()
    }
  1. 质量压缩不会减少图片的像素,它是在保持像素的前提下改变图片的位深及透明度,来达到压缩图片的目的
  2. 压缩后图片的长,宽,像素都不会改变,那么 bitmap 所占内存大小是不会变的
  3. 由于图片的质量变低了,所以压缩后图片的大小会变小
  4. 质量压缩 png 格式这种图片没有作用,因为 png 是无损压缩


    image.png

2. 采样率压缩


  /**
   * 将图片 [byteArray] 压缩到 宽度小于 [targetWidth]、高度小于 [targetHeight]
   *
   **/
  fun compressInSampleSize(byteArray: ByteArray, targetWidth: Int, targetHeight: Int): ByteArray {

        val options = BitmapFactory.Options()
        options.inJustDecodeBounds = true
        BitmapFactory.decodeByteArray(byteArray, 0, byteArray.size, options)

        var inSampleSize = 1
        while (options.outWidth / inSampleSize > targetWidth || options.outHeight / inSampleSize > targetHeight) {
            inSampleSize *= 2
        }

        options.inJustDecodeBounds = false
        options.inSampleSize = inSampleSize
        val bitmap = BitmapFactory.decodeByteArray(byteArray, 0, byteArray.size, options)

        val compressedByreArray = bitmapToByteArray(bitmap)

        log("压缩前文件大小 :${byteArray.size / 1024} kb")
        log("采样率 :$inSampleSize ")
        log("压缩后文件大小 :${compressedByreArray.size / 1024} kb")

        return compressedByreArray
    }
  1. 采样率压缩其原理是缩放 bitmap 的尺寸
  2. 压缩后图片的 宽度、高度以及占用的内存都会变小,文件大小也会变小(指压缩后保存到本地的文件)
  3. 采样率 inSampleSize 代表 宽度、高度变为原来的几分之一,
    比如 inSampleSize 为 2,代表 宽度、高度都变为原来的 1/2,占用的内存就会变为原来的 1/4
  4. 采样率 inSampleSize 只能为 2 的整次幂,比如:2、4、8、16 ...
  5. 由于 inSampleSize 只能为 2 的整次幂,所以无法精确控制大小
image.png

3. 缩放压缩

    /**
     * 将图片 [bitmap] 压缩到指定宽高范围内
    **/
    fun compressScale(bitmap: Bitmap, targetWidth: Int, targetHeight: Int): Bitmap {
        return try {
            val scale = Math.min(targetWidth * 1.0f / bitmap.width, targetHeight * 1.0f / bitmap.height)

            val matrix = Matrix()
            matrix.setScale(scale, scale)

            val scaledBitmap = Bitmap.createScaledBitmap(bitmap, (bitmap.width * scale).toInt(), (bitmap.height * scale).toInt(), true)

            val rawBytes = bitmapToByteArray(bitmap)
            val scaledBytes = bitmapToByteArray(scaledBitmap)
            log("压缩前文件大小 :${rawBytes.size / 1024} kb")
            log("缩放率 :$scale ")
            log("压缩后文件大小 :${scaledBytes.size / 1024} kb")

            scaledBitmap
        } catch (e: Exception) {
            e.printStackTrace()
            bitmap
        }
    }
  1. 放缩法压缩使用的是通过矩阵对图片进行缩放
  2. 缩放后图片的 宽度、高度以及占用的内存都会变小,文件大小也会变小(指压缩后保存到本地的文件,原始文件不会改变)
image.png

4. 色彩模式压缩(RGB565)

    /**
     * 将图片格式更改为 Bitmap.Config.RGB_565,减少图片占用的内存大小
    **/
    fun compressRGB565(byteArray: ByteArray): Bitmap {

        return try {
            val options = BitmapFactory.Options()
            options.inPreferredConfig = Bitmap.Config.RGB_565
            val compressedBitmap = BitmapFactory.decodeByteArray(byteArray, 0, byteArray.size, options)

            log("压缩前文件大小 :${byteArray.size / 1024} kb")
            log("压缩后文件大小 :${byteArray.size / 1024} kb")
            compressedBitmap
        } catch (e: Exception) {
            e.printStackTrace()
            BitmapFactory.decodeByteArray(ByteArray(0), 0, 0)
        }
    }
  1. 由于图片的存储格式改变,与 ARGB_8888 相比,每个像素的占用的字节由 8 变为 4 , 所以图片占用的内存也为原来的一半
  2. 图片的宽高不发生变化
  3. 如果图片不包含透明信息的话,可以使用此方法进行压缩
image.png

八、Bitmap 的其他操作

1. 旋转

    /**
     * 旋转
     *
     * 注意:如果 [degree] 不是90的倍数的话,会导致旋转后图片变成"斜的",
     * 然而此时计算图片的宽高时仍然是按照水平和竖直方向计算,所以会导致最终旋转后的图片变大
     * 如果进行多次旋转的话,最终会出现OMM
     */
    fun rotate(bitmap: Bitmap, degree: Float): Bitmap {
        val matrix = Matrix()
        matrix.postRotate(degree)
        return Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, false)
    }

2. 镜像

    /**
     * 水平镜像
     */
    fun mirrorX(bitmap: Bitmap): Bitmap {
        val matrix = Matrix()
        matrix.setScale(-1f, 1f)
        return Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, false)
    }

    /**
     * 竖直镜像
     */
    fun mirrorY(bitmap: Bitmap): Bitmap {
        val matrix = Matrix()
        matrix.setScale(1f, -1f)
        return Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, false)
    }

3. 裁切

  /**
     * 从图片中间位置裁剪出一个宽高为的 [width] [height]图片
     */
    fun crop(bitmap: Bitmap, width: Int, height: Int): Bitmap {
        return if (bitmap.width < width || bitmap.height < height) {
            bitmap
        } else {
            Bitmap.createBitmap(bitmap, (bitmap.width - width) / 2, (bitmap.height - height) / 2, width, height)
        }
    }

    /**
     * 从图片中间位置裁剪出一个半径为 [radius] 的圆形图片
     */
    fun cropCircle(bitmap: Bitmap, radius: Int): Bitmap {

        val realRadius: Int = if (bitmap.width / 2 < radius || bitmap.height / 2 < radius) {
            Math.min(bitmap.width, bitmap.height) / 2
        } else {
            radius
        }

        val src = crop(bitmap, realRadius * 2, realRadius * 2)
        val circle = Bitmap.createBitmap(src.width, src.height, Bitmap.Config.ARGB_8888)

        val canvas = Canvas(circle)
        canvas.drawARGB(0, 0, 0, 0)
        val paint = Paint()
        paint.isAntiAlias = true

        canvas.drawCircle((circle.width / 2).toFloat(), (circle.height / 2).toFloat(), realRadius.toFloat(), paint)

        val rect = Rect(0, 0, circle.width, circle.height)
        paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_IN)
        canvas.drawBitmap(src, rect, rect, paint)

        return circle
    }

九、 总结

  1. Bitmap 的颜色配置,以及不同格式占用内存的大小
  2. 注意区分 原始图片大小、Bitmap 对象的大小(宽、高)、Bitmap 占用内存的大小、将 Bitmap 保存成文件的大小
  3. Bitmap 占用内存:总内存 = 宽的像素数 × 高的像素数 × 每个像素点占用的大小
  4. Bitmap 的缩放和从 Drawable 目录中加载图片的规则
  5. Bitmap 的几种压缩方法和各自的特点

相关代码

https://github.com/smashinggit/Study

注:此工程包含多个module,本文所用代码均在 bitmap module 下

注:由于本人水平有限,所以难免会有理解偏差或者使用不正确的问题。如果小伙伴们有更好的理解或者发现有什么问题,欢迎留言批评指正~

参考文章:

玩转Android Bitmap
怎样计算Bitmap的内存占用和Bitmap加载优化
Android 适配(drawable文件夹)图片适配