Android 图片内存优化

在应用中有很多图片需要显示,这种图片可以是多种格式,如 JPEG、PNG 或者 WEBP,这种图片文件并不是很大,为了使文件变小,以提高传输速度,都要经过不同的压缩算法把位图压缩成不同的格式,但要在Android中显示图片,就和图片文件的大小无关了,在Android设备上显示前需要把图片解码成位图格式,而位图格式只和位图的属性相关,压缩格式仅仅是减小了文件大小。

因此,图片占用的内存在整个应用中是非常突出的,特别是 Dalvik 版本虚拟机在 GC 后不能压缩内存,也就是说,当对象被释放时,留下的空间不是连续的。如果需要显示更大的一张图片,虽然剩余内存大于图片所需的内存,但由于剩余内存并不是连续的,仍然会导致位图的内存不够触发 GC,从其他位置释放空间。如图
在这里插入图片描述
前面讲过,Android 中的一张图片解码成位图格式后,占用的内存只和位图的质量和大小相关,位图的主要属性有:图片长度、图片宽度、单位像素占用的字节数。一张图片(BitMap)占用的内存=图片长度×图片宽度×单位像素占用的字节数(图片长度和图片宽度的单位是像素)。如果从应用资源中加载(Drawable),图片占用的内存也应该和屏幕密度有一定关系。

在 Android 默认情况下,当图片文件解码成位图时,会被处理成 32bit/像素。红色、绿色、蓝色和透明通道各 8bit,即使是没有透明通道的图片,如 JEPG 格式是没有透明通道的,但然后会处理成 32bit 位图,这样分配的 32bit 中的 8bit 透明通道数据是没有任何用处的,这完全没有必要,并且在这些图片被屏幕渲染之前,它们首先要被作为纹理传送到 GPU,这意味着每一张图片会同时占用 CPU 内存和 GPU 内存。接下来介绍减少图片内存开销的常用技术。

设置位图格式

Android 提供了多种位图格式,根据开发者或产品需求,可以选择不同的格式解码图片,安卓支持的像素格式如图
在这里插入图片描述
最高的是 RGB_8888,也就是系统默认的位图格式,其他几种都减小了位图通道位,可以减少内存开销并提升图片显示的性能。
当然,这种空间的节省是要付出视觉质量受损的代价的,比如从 RGB_8888 改成使用RGB_565,会损失较多的图片数据,但不是不能用,根据不同场景可以选择不同的规格。

除了大图模式,一般都可以使用,并且几乎看不出差别。只要满足以下其中一点就可以考虑使用 RGB_565:

  • 显示局部图片,比如列表中的小图片。
  • 小屏幕手机或者对图片质量要求不高的场景,可以使用 RGB_565,但实际上是,根据应用开发经验,不需要 Alpha 通道。

如果需要更小的格式,但又需要透明通道,可以尝试 ARGB_4444 图像格式。它减少了一半的数据,但保留了透明通道,视觉差异变化较大,一般用于用户头像,特别是圆角的头像,可以尝试使用 ARGB_4444 看看效果。

Aplha_8 格式主要用于 Alpha 通道模板,相当做一个染色。图像要渲染两次,虽然减少内存,但增加了绘制的开销。

值得注意的是,在 Android 的基本文件结构中不支持 PNG、JPEG 和 WEBP 格式,因此,需要解码后的位图功能,通过设置inPreferredConfig参数来实现不同的位图规格,代码如下:

BitmapFactory.Options options = new BitmapFactory.Options();
options.inPreferredConfig = Bitmap.Config.RGB_565;

设置位图规格并不是减少内存的唯一方法,特别是在现在的应用中,图片场景非常多,为了更高效地显示图片,Android 提供了一系列的接口来减少图片对堆内存空间的占用。

inSampleSize

如果内存中的图片大于屏幕显示出的图片大小,或者大于指定屏幕区域的大小,这些高分辨率图片会导致严重的性能问题。因此,需要改变内存的占用,避免此类问题。这里的问题在于,占据内存中实际未使用的区块,这些图片占用了内存堆中的大量空间,使应用空间变少,然后重置这些图片大小,让它们符合实际显示的大小,既能减小内存的开销,也能提高显示的效率,这样载入内存的图片规格符合实际显示规格,而不是完整的分辨率。

位图功能对象中的 inSampleSize 属性实现了位图的缩放功能,代码如下:

BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = 4;
BitmapFactory.decodeStream(null, null, options);

将这个属性设置为 1 时,可以在不加载完整大小图片的前提下,生成一张只有原始图片部分大小的新图片,如 inSampleSize 为 2 时获得只有 1/2 大小的图片,同理设置为 4 就是 1/4大小的图片。因此,图片大小总会比原始图片小一倍以上。

inSampleSize 是一个很便捷的操作。它会根据设置的数值,每隔相应像素读入一次。

inScaled ,inDensity 和 和 inTargetDensity

虽然 inSampleSize 可以实现图片的缩放,都是指数幂的缩放,如果想更细地缩放图片,就需要使用位图的 inScaled、inDensity 和 inTargetDensity 功能。代码如下:

BitmapFactory.Options options = new BitmapFactory.Options();
options.inScaled = true;
options.inDensity = DisplayMetrics.DENSITY_DEFAULT;
options.inTargetDensity = dstWidth;
BitmapFactory.decodeStream(null, null, options);

当 inScaled 设置为 true 时,系统会按照现有的密度来划分目标密度,通过派生绽放数来应用到位图上,使用这个方法会重设图片大小,并对它应用一个新的过滤。

虽然这些方法都非常好用,并且减少图片显示需要的内存,但因为过多的算法,导致图片显示的过程需要更多的时间开销,如果图片很多的话,就影响到图片的显示效果。最好的方案是结合这两个方法,达到最佳的性能结合,首先使用 insamplsesie 处理图片,转换为接近目标的 2 次幂,然后用 inDensity 和 inTargetdensy 生成最终想要的准确大小,因为inSamplesize 会减少像素的数量,而基于输出密码的需要对像素重新过滤。但获取资源图片的大小,需要设置位图对象的 inJustDecodeBounds 值为 true,然后继续解码图片文件,这样才能生成图片的宽高数据,并允许继续优化图片。整体代码如下:

BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
options.inDensity = DisplayMetrics.DENSITY_DEFAULT;
options.inTargetDensity = DisplayMetrics.DENSITY_DEFAULT;
options.inScreenDensity = DisplayMetrics.DENSITY_DEFAULT;
BitmapFactory.decodeResource(getResources(), imageResId, options);

inBitmap

Android 3.0(API Level 11)引进了 BitmapFactory.Options.inBitmap 字段,如果设置了该属性,那么当使用了带有该 Options 参数的 decode 方法加载内容时,decode 方法会尝试重用一个已经存在的位图。这就意味着位图内存已经被重用了,从而改善了性能,并且没有内存的分配和释放过程。

常见的使用方案可以结合 LruCache 来实现,在 LruCache 移除超出 cache size 的图片时,暂时缓存 Bitmap 到一个软引用集合,需要创建新的 Bitmap 时,可以从这个软引用集合中找到最适合重用的 Bitmap,来重用它的内存区域。

合理地处理图片,降低内存的占用并提高显示速度,在应用中非常重要,但通常来讲一个应用使用图片的地方非常多,特别是很多的 ListView 或者 GridView 等需要显示大量图片的容器,如果都各自管理图片的显示,很难保证整体的质量,并且对总内存占用不可控。因此,在应用中需要一个图片管理模块,把所有的图片加载和管理收到一个模块中管理,并对此管理模块整体优化,使应用更加流畅和稳定。

发布了119 篇原创文章 · 获赞 28 · 访问量 6万+

猜你喜欢

转载自blog.csdn.net/ldxlz224/article/details/100739159