Android中Bitmap内存优化

Bitmap作为位图,需要读入一张图片每一个像素点的数据,其主要占用内存的地方也正是这些像素数据。对于像素数据总大小,我们可以猜想为:像素总数量 × 每个像素的字节大小,而像素总数量在矩形屏幕表现下,应该是:横向像素数量 × 纵向像素数量,结合得到:

Bitmap内存占用 ≈ 像素数据总大小 = 横向像素数量 × 纵向像素数量 × 每个像素的字节大小

单个像素的字节大小

单个像素的字节大小由Bitmap的一个可配置的参数Config来决定。

Bitmap中,存在一个枚举类Config,定义了Android中支持的Bitmap配置:

| Config | 占用字节大小(byte) | 说明 |

| — | — | — |

| ALPHA_8 (1) | 1 | 单透明通道 |

| RGB_565 (3) | 2 | 简易RGB色调 |

| ARGB_4444 (4) | 4 | 已废弃 |

| ARGB_8888 (5) | 4 | 24位真彩色 |

| RGBA_F16 (6) | 8 | Android 8.0 新增(更丰富的色彩表现HDR) |

| HARDWARE (7) | Special | Android 8.0 新增 (Bitmap直接存储在graphic memory)注1 |

**注1:**关于Android 8.0中新增的这个配置,stackoverflow已经有相关问题,可以关注下。

之前我们分析到,Bitmap的decode实际上是在native层完成的,因此在native层也存在对应的Config枚举类。

一般使用时,我们并未关注这个配置,在BitmapFactory中,有:

  • Image are loaded with the {@link Bitmap.Config#ARGB_8888} config by default.

*/

public Bitmap.Config inPreferredConfig = Bitmap.Config.ARGB_8888;

因此,Android系统中,默认Bitmap加载图片,使用24位真彩色模式。

Bitmap占用内存大小实例

首先准备了一张800×600分辨率的jpg图片,大小约135k,放置于res/drawable文件夹下:

image

并将其加载到一个200dp×300dp大小的ImageView中,使用BitmapFactory。

Bitmap bitmapDecode = BitmapFactory.decodeResource(getResources(), resId);

imageView.setImageBitmap(bitmapDecode);

打印出相关信息:

图中显示了从资源文件中decode得到的bitmap的长、宽和占用内存大小(byte)等信息。

首先,从数据上可以验证:

17280000 = 2400 * 1800 * 4

这意味着,为了将单张800 * 600 的图片加载到内存当中,付出了近17.28M的代价,即使现在手机运存普遍上涨,这样的开销也是无法接受的,因此,对于Bitmap的使用,是需要非常小心的。好在,目前主流的图像加载库(Glide、Fresco等)基本上都不在需要开发者去关心Bitmap内存占用问题。

先暂时回到Bitmap占用内存的计算上来,对比之前定义的公式和源图片的尺寸数据,我们会发现,这张800 * 600大小的图片,decode到内存中的Bitmap的横纵像素数量实际是:2400 * 1800,相当于缩放了3倍大小。为了探究这缩放来自何处,我们开始跟踪源码:之前提到过,Bitmap的decode过程实际上是在native层完成的,为此,需要从BitmapFactory.cpp#nativeDecodeXXX方法开始跟踪,这里省略其他decode代码,直接贴出和缩放相关的代码如下:

if (env->GetBooleanField(options, gOptions_scaledFieldID)) {

const int density = env->GetIntField(options, gOptions_densityFieldID);

const int targetDensity = env->GetIntField(options, gOptions_targetDensityFieldID);

const int screenDensity = env->GetIntField(options, gOptions_screenDensityFieldID);

if (density != 0 && targetDensity != 0 && density != screenDensity) {

scale = (float) targetDensity / density;

}

}

int scaledWidth = decoded->width();

int scaledHeight = decoded->height();

if (willScale && mode != SkImageDecoder::kDecodeBounds_Mode) {

scaledWidth = int(scaledWidth * scale + 0.5f);

scaledHeight = int(scaledHeight * scale + 0.5f);

}

if (willScale) {

const float sx = scaledWidth / float(decoded->width());

const float sy = scaledHeight / float(decoded->height());

bitmap->setConfig(decoded->getConfig(), scaledWidth, scaledHeight);

bitmap->allocPixels(&javaAllocator, NULL);

bitmap->eraseColor(0);

SkPaint paint;

paint.setFilterBitmap(true);

SkCanvas canvas(*bitmap);

canvas.scale(sx, sy);

canvas.drawBitmap(*decoded, 0.0f, 0.0f, &paint);

}

从上述代码中,我们看到bitmap最终通过canvas绘制出来,而canvas在绘制之前,有一个scale的操作,scale的值由

scale = (float) targetDensity / density;

这一行代码决定,即缩放的倍率和targetDensity和density相关,而这两个参数都是从传入的options中获取到的。这时候,需要回到Java层,看看options这个对象的定义和赋值。

BitmapFactory#Options

Options是BitmapFactory中的一个静态内部类,用于配置Bitmap在decode时的一些参数。

// native层doDecode方法,传入了Options参数

static jobject doDecode(JNIEnv* env, SkStreamRewindable* stream, jobject padding, jobject options)

其内部有很多可配置的参数,下面的类图,列举出了部分常用的参数。

image

我们先关注之前提到的几个密度相关的参数,通过阅读源码的注释,大概可以知道这三个密度参数代表的涵义:

  • inDensity:Bitmap位图自身的密度、分辨率

  • inTargetDensity: Bitmap最终绘制的目标位置的分辨率

  • inScreenDensity: 设备屏幕分辨率

其中inDensity和图片存放的资源文件的目录有关,同一张图片放置在不同目录下会有不同的值:

| density | 0.75 | 1 | 1.5 | 2 | 3 | 3.5 | 4 |

| — | — | — | — | — | — | — | — |

| densityDpi | 120 | 160 | 240 | 320 | 480 | 560 | 640 |

| DpiFolder | ldpi | mdpi | hdpi | xhdpi | xxhdpi | xxxhdpi | xxxxhdpi |

inTargetDensity和inScreenDensity一般来说,很少手动去赋值,默认情况下,是和设备分辨率保持一致。为此,我在手机(红米4,Android 6.0系统,设备dpi 480)上测试加载不同资源文件下的bitmap的参数,结果见下图:

image

以上可以验证几个结论:

  • 同一张图片,放在不同资源目录下,其分辨率会有变化,

  • bitmap分辨率越高,其解析后的宽高越小,甚至会小于图片原有的尺寸(即缩放),从而内存占用也相应减少

  • 图片不特别放置任何资源目录时,其默认使用mdpi分辨率:160

  • 资源目录分辨率和设备分辨率一致时,图片尺寸不会缩放

因此,关于Bitmap占用内存大小的公式,从之前:

Bitmap内存占用 ≈ 像素数据总大小 = 横向像素数量 × 纵向像素数量 × 每个像素的字节大小

可以更细化为:

Bitmap内存占用 ≈ 像素数据总大小 = 图片宽 × 图片高× (设备分辨率/资源目录分辨率)^2 × 每个像素的字节大小

对于本节中最开始的例子,如下:

17,280,000 = 800 * 600 * (480 / 160 )^2 * 4

Bitmap内存优化


图片占用的内存一般会分为运行时占用的运存和存储时本地开销(反映在包大小上),这里我们只关注运行时占用内存的优化。

在上一节中,我们看到对于一张800 * 600 大小的图片,不加任何处理直接解析到内存中,将近占用了17.28M的内存大小。想象一下这样的开销发生在一个图片列表中,内存占用将达到非常夸张的地步。从之前Bitmap占用内存的计算公式来看,减少内存主要可以通过以下几种方式:

  1. 使用低色彩的解析模式,如RGB565,减少单个像素的字节大小

  2. 资源文件合理放置,高分辨率图片可以放到高分辨率目录下

  3. 图片缩小,减少尺寸

第一种方式,大约能减少一半的内存开销。Android默认是使用ARGB8888配置来处理色彩,占用4字节,改用RGB565,将只占用2字节,代价是显示的色彩将相对少,适用于对色彩丰富程度要求不高的场景。

第二种方式,和图片的具体分辨率有关,建议开发中,高分辨率的图像应该放置到合理的资源目录下,注意到Android默认放置的资源目录是对应于160dpi,目前手机屏幕分辨率越来越高,此处能节省下来的开销也是很可观的。理论上,图片放置的资源目录分辨率越高,其占用内存会越小,但是低分辨率图片会因此被拉伸,显示上出现失真。另一方面,高分辨率图片也意味着其占用的本地储存也变大。

第三种方式,理论上根据适用的环境,是可以减少十几倍的内存使用的,它基于这样一个事实:源图片尺寸一般都大于目标需要显示的尺寸,因此可以通过缩放的方式,来减少显示时的图片宽高,从而大大减少占用的内存。

前两种方式,相对比较简单。第三种方式会涉及到一些编码,目前也有很多典型的使用方式,如下:

BitmapFactory.Options options = new BitmapFactory.Options();

options.inPreferredConfig = Bitmap.Config.RGB_565;

options.inJustDecodeBounds = true;

BitmapFactory.decodeResource(getResources(), resId,options);

options.inJustDecodeBounds = false;

options.inSampleSize = BitmapUtil.computeSampleSize(options, -1, imageView.getWidth() * imageView.getHeight());

Bitmap newBitmap = BitmapFactory.decodeResource(getResources(), resId, options);

原理很简单,充分利用了Options类里的参数设置,也可以从native底层源码上看到对应的逻辑。第一次解析bitmap只获取尺寸信息,不生成像素数据,继而比较bitmap尺寸和目标尺寸得到缩放倍数,第二次根据缩放倍数去解析我们实际需要的尺寸大小。

// Apply a fine scaling step if necessary.

if (needsFineScale(codec->getInfo().dimensions(), size, sampleSize)) {

willScale = true;

scaledWidth = codec->getInfo().width() / sampleSize;

scaledHeight = codec->getInfo().height() / sampleSize;

}

上图是使用上述手段优化后的结果,可以看到现在占用的内存大小大约为960KB,从优化后的宽高来看,第三种方式并没有效果。应为目标ImageView尺寸也不小,而inSampleSize的值必须是2的整数幂,因此计算得到的值还是1。

PS: Bitmap内存占用的优化还有一个方式是复用和缓存

不同Android版本时的Bitmap内存模型


我们知道Android系统中,一个进程的内存可以简单分为Java内存和native内存两部分,而Bitmap对象占用的内存,有Bitmap对象内存和像素数据内存两部分,在不同的Android系统版本中,其所存放的位置也有变化。Android Developers上列举了从API 8 到API 26之间的分配方式:

| API级别 | API 10 - | API 11 ~ API 25 | API 26 + |

| — | — | — | — |

| Bitmap对象存放 | Java heap | Java heap | Java heap |

| 像素(pixel data)数据存放 | native heap | Java heap | native heap |

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

img

img

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

学习分享

①「Android面试真题解析大全」PDF完整高清版+②「Android面试知识体系」学习思维导图压缩包

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

以扫码获取!!(备注:Android)**

学习分享

①「Android面试真题解析大全」PDF完整高清版+②「Android面试知识体系」学习思维导图压缩包

[外链图片转存中…(img-LEx7FOQL-1712544770635)]

[外链图片转存中…(img-FWekxTvW-1712544770636)]

[外链图片转存中…(img-oAANCxLv-1712544770636)]

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

  • 19
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值