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

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

  1. 设计师切图,图片本身的分辨率;
  2. 图片所放文件夹代表的 密度 dpi;
  3. 手机自身的屏幕密度;
  4. 经过系统缩放得到的最终加载到手机上图片的密度和占用的内存。

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

  1. 将一张分辨率为 720x1080 的图片放到 xxhdpi 或者 hdpi ,同放在 xhdpi 标准文件夹下,对于同一台手机占用内存大小是否有变化?
  2. 同一张分辨率为 720x1080 的图片被不同屏幕分辨率的手机加载,BitmapFactory 的成员变量 inDensity、 inScreenDensity、 inTargetDensity 会怎样变化?这些值又是怎样被赋值的,又是怎样进行缩放的?
  3. 使用 decodeResource() 和 decodeStream() 有什么区别?
  4. Options 的 inDensity、 inTargetDensity 和 输出的 Bitmap 的 mDensity 有什么关系?Bitmap 的 mWidth、 mHeight 与 Options 的 outputWidth、 outputHeight 有什么关系?
  5. 这些同计算 Bitmap 内存占用大小的 长宽有什么关系?

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

一、DisplayMetrics和Bitmap及其相关类

DisplayMetrics

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


屏幕密度对照表.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三者之间的关系,如下图所示:


Bitmap、BitmapFactory、Options关系类图.png

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

ImageView 设置图片

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

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

总结:由上可知,ImageView设置本地图片会先生成 Bitmap 再将 Bitmap 转成 Drawable,最终通过 setImageDrawable() 设置;
【所以这步是否可以看做使用 setImageDrawable 会跳过读取和解码 Bitmap 操作,为最优设置本地图片方式呢?
—— 需测试内存占用情况方可验证。】

Bitmap创建流程

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

Bitmap创建方法.png

总结:

  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 目录下的图片,会用到 <code>decodeResource</code>获取 Bitmap,该方法可以直接看上面提到的 decodeResourceStream() 方法源码,通过源码可知:

  • 在读取资源时,使用 openRawResource 方法,然后会对 TypedValue 进行赋值,其中包含了原始资源的 density 等信息,也即是文件夹代表的density;
  • 调用 decodeResourceStream 对原始资源进行解码和适配,实际是原始资源 density 到 设备屏幕 density 的映射。

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


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

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


资源解码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

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

  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
如有误,请指正!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容