Android Bitmap图片优化分析

大憨熊 提交于 2020-07-27 23:07:23

图片移动开发中占据中举足轻重的地位, 一个好的应用不仅能够给用户提供”实用“的功能,早期的android 页面Ui简单,但随着Android系统不断的升级发展, 界面越来越丰富,用户对界面要求越来越高,UI小姐姐们不经需要设计出精致的界面元素,其中不乏一些好看的图片,但是随着手机性能提升(分辨率,cpu主频,内存等),图片质量也越来越大,拍个照动不动就3M,4M,8M, 大家都知道,android 应用创建进程时候,会分配一个固定的内存大小,准确的说话是 google原生OS的默认值是16M,但是各个厂家的系统会对这个值进行修改,如果我们应用直接将这些大图直接加载到内存中,很快内存就会耗尽,最终出现OOM异常,所以图片的处理对于一个稳定,具备丰富界面的应用来说非常重要,今天我们就来聊一聊Bitmap,在开发过程中就把”图片“给优化一番,保证我们项目在线上稳定,流畅运行。

  • Bitmap

Bitmap图像处理的最重要类之一。用它可以获取图像文件信息,进行图像颜色变换、剪切、旋转、缩放等操作,并可以指定格式保存图像文件



如图,bitmap在sdk中算是元老级的人物了,从api1中就已经有了,可见其重要性。

继承关系就不解释了,实现了Parcelable 具备在内存中传递的特性。


bitmap中有两个重要的内部类 CompressFormat 以及 Config

下面分别介绍一下这两个类

  • CompressFormat 

 CompressFormat 是用来设置压缩方式的,是个枚举类,内部提供了三种图片压缩方式类型,

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

这里有的同志会问,这是压缩格式啊,具体怎么操作压缩,Bitmap为我们提供了一个压缩方法供开发者使用,我们来顺便看看Bitmap都有什么方法,如下:


第一个方法就是compress()方法, 没错就是这么就这方法,一共有三个参数

  1. format :👆上面已经说明了,表示压缩格式;
  2. quality : 压缩质量,取值0-100,0表示最低画质压缩,100表示最高画质压缩,对于PNG压缩格式来说,该参数可以忽略,对于WEBP格式来说,小于100为有损压缩格式,会对画质产生直接影响, 等于100时候采用的是无损压缩格式,画质是不会有改变,但是图片大小得到很好压缩;
  3. stream : 将压缩后的图片写到指定的输出流中;

返回值:boolean 返回true表示成功压缩到输出流中,然后可以通过Bitmap.Factory从相应的输入流中解析出来bitmap信息,


从官网介绍可知, 该方法在图片压缩过程中可能消耗较长时间,建议放在子线程中操作,至于为什么大家可以看看源码, 源码中会调用一个nativeCompress 的Native 方法,也就是压缩处理是放在底层处理的;

  • Config

表示位图的像素的存储格式,会影响Bitmap真实图片的透明度以及图片质量;

  1. Bitmap.Config.ALPHA_8:颜色信息只由透明度组成,占8位。

  1. Bitmap.Config.ARGB_4444:颜色信息由透明度与R(Red),G(Green),B(Blue)四部分组成,每个部分都占4位,总共占16位。

  2. Bitmap.Config.ARGB_8888:颜色信息由透明度与R(Red),G(Green),B(Blue)四部分组成,每个部分都占8位,总共占32位。是Bitmap默认的颜色配置信息,也是最占空间的一种配置。

  3. Bitmap.Config.RGB_565:颜色信息由R(Red),G(Green),B(Blue)三部分组成,R占5位,G占6位,B占5位,总共占16位。

android 系统默认存储位图方式ARGB_8888, 4个通道组成,每个通道8位,分表代表透明度和RGB颜色值, 也就是表示一个位图像素占用4个字节(1个byte8个bit位),

同理:采用 Bitmap.Config.RGB_565 存储,单像素占用内存大小仅有2byte,也就是说一张图片采用ARGB_565格式相对于默认的ARGB_8888内存减少一半,所以通过改变bitmap像素存储方式也是图片内存优化的重要渠道,这个后面会讲到;

  • BitmapFactory 

 创建位图对象从不同的来源,包括文件、流, 和字节数组。

看官方文档介绍,如下介绍了从字节数组、指定路径,系统资源、二进制流等方式创建Bitmap, 当然有的方法需要一些特殊参数,例如通过字节数组方式需要指定解析的起始偏移位置,长度等,还有一个参数是 BitmapFactory.Option , 它也是我们图片优化的重要手段;


BitmapFactort.Options

这个是什么鬼呢,  很重要, 加载bitmap的配置类,如下其内部属性



我们大概先说一下几个重要属性

  • insampleSize :采样率,默认1表示无缩放,等于2表示宽高缩放2倍,总大小缩小4倍;
  • inBitmap  :
  • inJustDecodeBound : 如果设置为true,不获取图片,不分配内存,但会返回图片的高度宽度信息;
  • inMutable :是否图片内容可变,如果Bitmap复用的话,需要设置为true;

好了,Bitmap的api我们就讲到这里,因为我们今天不是主要讲解他的用法,为了给接下来的知识做一个铺垫,简单减少了bitmap的知识点,我们接下来回归”正题“

Bitmap 占用内存分析 

Bitmap 用来描述一张图片的长、宽、颜色等信息。通常情况下,我们可以使用 BitmapFactory 来将某一路径下的图片解析为 Bitmap 对象。

当一张图片加载到内存后,具体需要占用多大内存呢?

getAllocationByteCount 探索 我们可以通过 Bitmap.getAllocationByteCount() 方法获取 Bitmap 占用的字节大小,比如以下代码:

image.png

上图中 rodman 是保存在 res/drawable-xhdpi 目录下的一张 600*600,大小为 65Kb 的图片。打印结果如下:

I/Bitmap ( 5673): bitmap size is 1440000

解释

默认情况下 BitmapFactory 使用 Bitmap.Config.ARGB_8888 的存储方式来加载图片内容,而在这种存储模式下,每一个像素需要占用 4 个字节。因此上面图片 rodman 的内存大小可以使用如下公式来计算:

宽 * 高 * 4 = 600 * 600 * 4 = 1440000复制代码

屏幕自适应 但是如果我们在保证代码不修改的前提下,将图片 rodman 移动到(注意是移动,不是拷贝)res/drawable-hdpi 目录下,重新运行代码,则打印日志如下:

I/Bitmap  ( 6047): bitmap size is 2560000复制代码

可以看出我们只是移动了图片的位置,Bitmap 所占用的空间竟然上涨了 77%。这是为什么呢?

实际上 BitmapFactory 在解析图片的过程中,会根据当前设备屏幕密度和图片所在的 drawable 目录来做一个对比,根据这个对比值进行缩放操作。具体公式为如下所示:

缩放比例 scale = 当前设备屏幕密度 / 图片所在 drawable 目录对应屏幕密度 Bitmap 实际大小 = 宽 * scale * 高 * scale * Config 对应存储像素数 在 Android 中,各个 drawable 目录对应的屏幕密度分别为下:

111.png

我运行的设备是 Nexus 4,屏幕密度为 320。如果将 rodman 放到 drawable-hdpi 目录下,最终的计算公式如下:

rodman 实际占用内存大小 = 600 * (320 / 240) * 600 * (320 / 240) * 4 = 2560000复制代码

assets 中的图片大小 我们知道,Android 中的图片不仅可以保存在 drawable 目录中,还可以保存在 assets 目录下,然后通过 AssetManager 获取图片的输入流。那这种方式加载生成的 Bitmap 是多大呢?同样是上面的 rodman.png,这次将它放到 assets 目录中,使用如下代码加载:

image (1).png

最终打印结果如下:

I/Bitmap  ( 5673): bitmap size is 1440000复制代码

可以看出,加载 assets 目录中的图片,系统并不会对其进行缩放操作。

Bitmap 加载优化 上面的例子也能看出,一张 65Kb 大小的图片被加载到内存后,竟然占用了 2560000 个字节,也就是 2.5M 左右。因此适当时候,我们需要对需要加载的图片进行缩略优化。

修改图片加载的 Config

修改占用空间少的存储方式可以快速有效降低图片占用内存。比如通过 BitmapFactory.Options 的 inPreferredConfig 选项,将存储方式设置为 Bitmap.Config.RGB_565。这种存储方式一个像素占用 2 个字节,所以最终占用内存直接减半。如下:

image (2).png

打印日志如下:

I/Bitmap  ( 6339): bitmap size is 720000复制代码

另外 Options 中还有一个 inSampleSize 参数,可以实现 Bitmap 采样压缩,这个参数的含义是宽高维度上每隔 inSampleSize 个像素进行一次采集。比如以下代码:

image (3).png

因为宽高都会进行采样,所以最终图片会被缩略 4 倍,最终打印效果如下:

I/Bitmap  ( 6414): bitmap size is 180000   // 170Kb复制代码

Bitmap 复用 

场景描述 如果在 Android 某个页面创建很多个 Bitmap,比如有两张图片 A 和 B,通过点击某一按钮需要在 ImageView 上切换显示这两张图片,

可以使用以下代码实现上述效果:

image (4).png

但是在每次调用 switchImage 切换图片时,都需要通过 BitmapFactory 创建一个新的 Bitmap 对象。当方法执行完毕后,这个 Bitmap 又会被 GC 回收,这就造成不断地创建和销毁比较大的内存对象,从而导致频繁 GC(或者叫内存抖动)。像 Android App 这种面相最终用户交互的产品,如果因为频繁的 GC 造成 UI 界面卡顿,还是会影响到用户体验的。可以在 Android Studio Profiler 中查看内存情况,多次切换图片后,显示的效果如下:

image (5).png

使用 Options.inBitmap 优化 实际上经过第一次显示之后,内存中已经存在了一个 Bitmap 对象。每次切换图片只是显示的内容不一样,我们可以重复利用已经占用内存的 Bitmap 空间,具体做法就是使用 Options.inBitmap 参数。将 getBitmap 方法修改如下:

image (6).png

解释说明:

图中 1 处创建一个可以用来复用的 Bitmap 对象。 图中 2 处,将 options.inBitmap 赋值为之前创建的 reuseBitmap 对象,从而避免重新分配内存。 重新运行代码,并查看 Profiler 中的内存情况,可以发现不管我们切换图片多少次,内存占用始终处于一个水平线状态。

image (7).png

注意:在上述 getBitmap 方法中,复用 inBitmap 之前,需要调用 canUseForInBitmap 方法来判断 reuseBitmap 是否可以被复用。这是因为 Bitmap 的复用有一定的限制:

在 Android 4.4 版本之前,只能重用相同大小的 Bitmap 内存区域; 4.4 之后你可以重用任何 Bitmap 的内存区域,只要这块内存比将要分配内存的 bitmap 大就可以。 canUserForInBitmap 方法具体如下:

image (8).png

细心的你可能也发现了在每次加载之前,除了 inBitmap 参数之外,我还将 Options.inMutable 置为 true,这里如果不置为 true 的话,BitmapFactory 将不会重复利用 Bitmap 内存,并输出相应 warning 日志:

W/BitmapFactory: Unable to reuse an immutable bitmap as an image decoder target.复制代码

Bitmap 缓存

 当需要在界面上同时展示一大堆图片的时候,比如 ListView、RecyclerView 等,由于用户不断地上下滑动,某个 Bitmap 可能会被短时间内加载并销毁多次。这种情况下通过使用适当的缓存,可以有效地减缓 GC 频率保证图片加载效率,提高界面的响应速度和流畅性。

最常用的缓存方式就是 LruCache,基本使用方式如下:

image (11).png

解释说明:

图中 1 处指定 LruCache 的最大空间为 20M,当超过 20M 时,LruCache 会根据内部缓存策略将多余 Bitmap 移除。 

图中 2 处指定了插入 Bitmap 时的大小,当我们向 LruCache 中插入数据时,LruCache 并不知道每一个对象会占用大多内存,因此需要我们手动指定,并且根据缓存数据的类型不同也会有不同的计算方式。 

上面就是今天的内容,讲解类Bitmap的相关基础知识点和优化,Bitmap实际问题的处理远不止这么多,像截屏长图的处理,如果不处理这张”超大图“,应用很容易就崩掉,这里需要用到分片加载, 这里不多说了,大家可自行查阅官方文档学习一下。 



易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!