android内存分析bitmap,Android性能优化:Bitmap详解&你的Bitmap占多大内存?

在开发app时,显示一张本地图片,这张图片在加载时会占用大多内存呢?猜测占用内存大小和以下几个因素有关:

设计师切图,图片本身的分辨率;

图片所放文件夹代表的 密度 dpi;

手机自身的屏幕密度;

经过系统缩放得到的最终加载到手机上图片的密度和占用的内存。

我们知道Android中在加载本地大图时,很容易OOM,主要原因在于加载的Bitmap占用内存太大。接下来将围绕以下几个问题说明如何计算一张Bitmap占用的内存大小。

将一张分辨率为 720x1080 的图片放到 xxhdpi 或者 hdpi ,同放在 xhdpi 标准文件夹下,对于同一台手机占用内存大小是否有变化?

同一张分辨率为 720x1080 的图片被不同屏幕分辨率的手机加载,BitmapFactory 的成员变量 inDensity、 inScreenDensity、 inTargetDensity 会怎样变化?这些值又是怎样被赋值的,又是怎样进行缩放的?

使用 decodeResource() 和 decodeStream() 有什么区别?

Options 的 inDensity、 inTargetDensity 和 输出的 Bitmap 的 mDensity 有什么关系?Bitmap 的 mWidth、 mHeight 与 Options 的 outputWidth、 outputHeight 有什么关系?

这些同计算 Bitmap 内存占用大小的 长宽有什么关系?

在回答这些问题之前,先介绍一下DisplayMetrics和Bitmap及其相关类。

一、DisplayMetrics和Bitmap及其相关类

DisplayMetrics

说明:屏幕密度相关类,可以用于获取屏幕高和宽以及屏幕密度density、每英寸点数densityDpi . 这里,density 数值为 1dp = density px;在 DisplayMetrics 中,这两个是线性相关:

4ba3e63c8cdc?utm_campaign=maleskine&utm_content=note&utm_medium=seo_notes&utm_source=recommendation

屏幕密度对照表.png

Bitmap

说明:Bitmap 在 Android 中指的是一张图片,可以是 png,也可以是 jpg等其他图片格式。

作用:可以获取图像文件信息,对图像进行剪切、旋转、缩放、压缩等操作,并可以指定格式保存图像文件。

Bitmap.Config

说明:Bitmap 格式。除了尺寸外,影响一个图片占用空间还有色彩细节。位图位数越高表示可以存储的颜色信息越多,图像也就越清晰逼真。

ALPHA_8:表示8位Alpha位图,每像素占1byte内存;

RGB_565:表示R为5位,G为6位,B为5位,一共16位,每像素占2byte内存;

ARGB_4444:表示16位位图,每像素占2byte内存(poor quality - Android Deprecated);

ARGB_8888:表示32位ARGB位图,每像素占4byte内存(Recommended)。

BitmapFactory

说明:提供解析Bitmap的静态工厂方法。

BitmapFactory.Options

说明:用于解码Bitmap时的各种参数控制。

几个重要参数:

inBitmap:在解析Bitmap时重用该Bitmap,但是必须相同大小的Bitmap & inMutable = true 才可重用;

inMutable :配置Bitmap是否可更改,如每隔几个像素给Bmp添加一条直线;

inPreferredConfig:Config颜色位数,默认值为Bitmap.Config.ARGB_888;

inDither:是否抖动,默认false(Android Depracated);

inPremultiplied:默认true,一般不改变其值。

inPurgeable:当存储像素内存空间 在系统内存不足时 是否可被回收(Android L Deprecated);

inInputShareable:是否可以共享一个 InputStream (Android L Deprecated);

inPreferQualityOverSpeed:为true时会优先保证 Bitmap 质量,其次是解码速度(Android N Deprecated);

inTempStorage:解码时的临时空间,建议 16K;

inJustDecodeBounds:为true时仅返回 Bitmap 宽高等属性,返回bmp=null,为false时才返回占内存的 bmp;

inSampleSize:表示 Bitmap 的压缩比例,值必须 > 1 & 是2的幂次方。inSampleSize = 2 时,表示压缩宽高各1/2,最后返回原始图1/4大小的Bitmap;

inDensity:表示 Bitmap 像素密度;

inTargetDensity:表示 Bitmap 最终的像素密度;

inScreenDensity:表示当前屏幕的像素密度;

inScaled:默认为true,是否支持缩放,设置为true时,Bitmap将以 inTargetDensity 的值进行缩放;

outputWidth:返回的 Bitmap的宽;

outputHeight:返回的 Bitmap的高。

以一张类图说明Bitmap、BitmapFactory和BitmapFactory.Options三者之间的关系,如下图所示:

4ba3e63c8cdc?utm_campaign=maleskine&utm_content=note&utm_medium=seo_notes&utm_source=recommendation

Bitmap、BitmapFactory、Options关系类图.png

二、ImageView 设置图片 & Bitmap创建流程

ImageView 设置图片

一般地,给 ImageView 设置资源图片时,会用到四种方式:setImageResource(), setImageUri(), setImageBitmap(), setImageDrawable。这四种方式有什么区别呢?用一张图来展示:

4ba3e63c8cdc?utm_campaign=maleskine&utm_content=note&utm_medium=seo_notes&utm_source=recommendation

ImageView设置图片四种方法流程.png

总结:由上可知,ImageView设置本地图片会先生成 Bitmap 再将 Bitmap 转成 Drawable,最终通过 setImageDrawable() 设置;

【所以这步是否可以看做使用 setImageDrawable 会跳过读取和解码 Bitmap 操作,为最优设置本地图片方式呢?

—— 需测试内存占用情况方可验证。】

Bitmap创建流程

BitmapFactory 提供了五种方式来创建Bitmap,分别是:decodeFile, decodeResource, decodeByteArray, decodeStream, decodeFileDescription,这里只介绍常见三种方式创建流程如下:

4ba3e63c8cdc?utm_campaign=maleskine&utm_content=note&utm_medium=seo_notes&utm_source=recommendation

Bitmap创建方法.png

总结:

最常用的三个方法:decodeFile, decodeResource, decodeStream,前两个最终调用的是 decodeStream;

**decodeStream, decodeByteArray, decodeFileDescription **这三个内部则调用的是 native 方法来创建 Bitmap的【有种说法,Bitmap是Android中唯一通过 native 方法创建的类】;

decodeResourceStream主要做了两件事:一是对 opts.inDensity 赋值,没有设置默认值 160;二是对 opts.inTargetDensity 赋值,没有赋值为当前设备 densityDpi;

decodeStream主要也做了两件事:一是调用 native 方法解析 Bitmap;二是对解析得到的 Bitmap 调用 setDensityFraomOptions(bmp, opts) 进行设置;

setDensityFraomOptions(bmp, opts)主要做了这样几件事:一是当opts.inDensity != opts.inTargetDensity || opts.inDensity != opts.inScreenDensity && (inScaled = true || isNinePatch) 时,将设置 outputBitmap.mDensity = inTargetDensity;

decodeResourceStream()方法源码如下:

public static Bitmap decodeResourceStream(Resources res, TypedValue value,

InputStream is, Rect pad, Options opts) {

if (opts == null) {

opts = new Options();

}

if (opts.inDensity == 0 && value != null) {

final int density = value.density;

if (density == TypedValue.DENSITY_DEFAULT) {

opts.inDensity = DisplayMetrics.DENSITY_DEFAULT;

} else if (density != TypedValue.DENSITY_NONE) {

opts.inDensity = density;

}

}

if (opts.inTargetDensity == 0 && res != null) {

opts.inTargetDensity = res.getDisplayMetrics().densityDpi;

}

return decodeStream(is, pad, opts);

}

setDensityFromOptions(bmp, opts)源码如下:

private static void setDensityFromOptions(Bitmap outputBitmap, Options opts) {

if (outputBitmap == null || opts == null) return;

final int density = opts.inDensity;

if (density != 0) {

outputBitmap.setDensity(density);

final int targetDensity = opts.inTargetDensity;

if (targetDensity == 0 || density == targetDensity || density == opts.inScreenDensity) {

return;

}

byte[] np = outputBitmap.getNinePatchChunk();

final boolean isNinePatch = np != null && NinePatch.isNinePatchChunk(np);

if (opts.inScaled || isNinePatch) {

outputBitmap.setDensity(targetDensity);

}

} else if (opts.inBitmap != null) {

// bitmap was reused, ensure density is reset

outputBitmap.setDensity(Bitmap.getDefaultDensity());

}

}

三、如何计算Bitmap占用内存大小?

常规方式:

API方法:getByteCount() 获取 - 不准确

粗略方式:

计算公式:图片长 * 宽 * 4bytes/ARG_8888 - 不正确

通读源码得来的方式:

/**

* Returns the minimum number of bytes that can be used to store this bitmap's pixels.

*

*

As of {@link android.os.Build.VERSION_CODES#KITKAT}, the result of this method can

* no longer be used to determine memory usage of a bitmap. See {@link

* #getAllocationByteCount()}.

*/

public final int getByteCount() {

// int result permits bitmaps up to 46,340 x 46,340

return getRowBytes() * getHeight();

}

/**

* Return the number of bytes between rows in the bitmap's pixels. Note that

* this refers to the pixels as stored natively by the bitmap. If you call

* getPixels() or setPixels(), then the pixels are uniformly treated as

* 32bit values, packed according to the Color class.

*

*

As of {@link android.os.Build.VERSION_CODES#KITKAT}, this method

* should not be used to calculate the memory usage of the bitmap. Instead,

* see {@link #getAllocationByteCount()}.

*

* @return number of bytes between rows of the native bitmap pixels.

*/

public final int getRowBytes() {

if (mRecycled) {

Log.w(TAG, "Called getRowBytes() on a recycle()'d bitmap! This is undefined behavior!");

}

return nativeRowBytes(mNativePtr);

}

最终通过native源码方法,可得到:一张ARGB_8888 的Bitmap占用内存计算公式:bmpWidth * bmpHeight * 4byte。不是直接使用图片分辨率进行计算,而是界面后 Bitmap 的宽高进行计算。

然而,这样计算并不准确。有几个不同的场景会导致最终计算的结果不正确。

将一张 720x1080 图片分别放在不同分辨率drawable文件夹下,在同一个手机上加载;

也是同一张图片放在指定分辨率的 drawable 文件夹下,在不同手机上加载;

切不同分辨率图片到对应 drawable 文件夹下,在各分辨率设备上加载。

一般,我们读取 drawable 目录下的图片,会用到 decodeResource获取 Bitmap,该方法可以直接看上面提到的 decodeResourceStream() 方法源码,通过源码可知:

在读取资源时,使用 openRawResource 方法,然后会对 TypedValue 进行赋值,其中包含了原始资源的 density 等信息,也即是文件夹代表的density;

调用 decodeResourceStream 对原始资源进行解码和适配,实际是原始资源 density 到 设备屏幕 density 的映射。

这里看一下 资源文件夹代表的密度:

4ba3e63c8cdc?utm_campaign=maleskine&utm_content=note&utm_medium=seo_notes&utm_source=recommendation

资源文件夹密度对照表.png

对照 decodeResourceStream() 源码如何设置 opts.inDensity 逻辑:

4ba3e63c8cdc?utm_campaign=maleskine&utm_content=note&utm_medium=seo_notes&utm_source=recommendation

资源解码Bitmap参数设置流程.png

最后通过查阅 native 源码,得到计算公式:

一张图片对应 Bitmap 占用内存 = mBitmapHeight * mBitmapWidth * 4byte(ARGB_8888);

Native 方法中,mBitmapWidth = mOriginalWidth * (scale = opts.inTargetDensity / opts.inDensity) * 1/inSampleSize,

mBitmapHeight = mOriginalHeight * (scale = opts.inTargetDensity / opts.inDensity) * 1/inSampleSize

现在针对介绍的几种场景,会得到这样的结论:

将一张 720x1080图片放在 drawable-xhdpi 目录下(inDensity = 320),

在 720x1080 手机上加载(inTargetDensity = 320),图片不会被压缩;

在 480x800 手机上加载(inTargetDensity = 240),图片会被压缩 9/16;

在 1080x1920 手机上加载(inTargetDensity = 480),图片会被放大 2.25;

切不通分辨率大小的图片放到对应文件夹下,会根据屏幕获取对应文件夹的图片,就不存在加载图片时压缩和放大(针对标准屏);

拓展问题:只切一套UI图,是否适用?如何选择?

注意,上述计算方式是在通过 decodeResource() 方法获取 Bitmap 的情况下得出,其他几种方式获取Bitmap,最后得到占用内存Size不会跟资源文件目录相关联。

四、问题解答

问题一:一张图片对应 Bitmap 占用内存 = mBitmapHeight * mBitmapWidth * 4byte(ARGB_8888);Native 方法中,mBitmapHeight = mOriginalHeight * (scale = opts.inTargetDensity / opts.inDensity) * 1/inSampleSize ;

由此可知,手机屏幕大小 1280 x 720(inTarget = 320),加载 xxhdpi (inDensity = 480)中的图片 1920 x 1080,scale = 320 / 480,inSampleSize = 1,最终获得的 Bitmap 的图像大小是 :

mBitmapWidth = opts.outWidth = 1080 * (320 / 480) * 1/1 = 720,

mBitmapHeight = opt.outHeight = 1920 * (320 / 480) * 1/1 = 1280,

getAllocatedMemory() = mBitmapWidth * mBitmapHeight * 4 = Bitmap占用内存。

问题三:使用 decodeResource() 和 decodeStream() 有什么区别?

(1)decodeResource() 流程,会先用 TypedValue 保存图片信息,然后会根据条件设置 opts.inDensity = value.inDensity,为0则设置为默认 160dpi; 文件夹代表密度

Opts.inTargetDensity = getDisplayMetrics().densityDpi; 屏幕密度

设置完上述参数后,最终还是会调用 decodeStream() 方法;

(2)decodeStream() native 方法得到 Bitmap后,调用 setDensityFromOptions() 方法来设置 Bitmap.mDensity:

若 opts.inDensity != 0,bitmap.mDensity = opts.inDensity;

若 opts.inTargetDensity != 0 && inDensity != targetDensity && inDensity != screenDensity,继续判断,如果 opts.inScaled || isNinePatch,bitmap.mDensity = targetDensity;

所以,

(1)若使用 decodeResource() 加载本地图片,inDensity 为加载图片所在的文件夹代表的 dpi,inTargetDensity 为目标屏幕密度(or 图片真实像素密度?),

最终 bitmap.mDensity = targetDensity。

(2)若使用 decodeStream() 则不会先记录图片信息,得到bitmap 后,直接调用 setDensityFromOptions() 方法,所以最终 bitmap.mDensity = defaultDensity() = DENSITY_DEVICE。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值