您的位置:首页 > 移动开发 > Android开发

Android性能优化:Bitmap详解&你的Bitmap占多大内存?

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

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

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

3. 手机自身的屏幕密度;

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

我们知道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 中,这两个是线性相关:



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三者之间的关系,如下图所示:



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

ImageView 设置图片

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



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

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

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

Bitmap创建流程

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



总结:

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

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

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

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

5. 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.
*
* <p>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()}.</p>
*/
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.
*
* <p>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 的映射。

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



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



最后通过查阅 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

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

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

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

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

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

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

拓展问题:只切一套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。

参考源码API-26

参考:http://dev.qq.com/topic/591d61f56793d26660901b4e

https://www.tuicool.com/articles/3eMNr2n

如有误,请指正!
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  android Bitmap