美文网首页Android进阶安卓应用层Android
Android性能优化:Bitmap详解&你的Bitma

Android性能优化:Bitmap详解&你的Bitma

作者: Coralline_xss | 来源:发表于2017-12-02 09:59 被阅读1261次

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

    相关文章

      网友评论

      • 路过的人:但是dpi也不能过于太大,因为在那个基础的设计稿切出来的图片本身的宽高就很大,因此在主流上高一个X就ok,如果只针对一种主流的分辨率进行dpi文件适配,另一些相对主流的屏幕可能ui展示上会被放大或者压缩,业界有的公司适配一套有的适配两套,都无所谓
      • 路过的人:抛去图片本身不讲,如果dpi文件夹和屏幕分辨率对等,则图片占用的内存其实是一样的;
        但是如果dpi的文件夹和屏幕分辨率不对等,首先需要把图片转换城对应屏幕分辨率的dpi,因此同一图片放在不同dpi的文件夹下在同一设备上加载的内存是不一样的;计算方式是:图宽/图屏dpi比*图高/图屏dpi比*字节。因此在同一个设备上,从占用内存上来讲图片的DPI应当大于设备dpi的;但是从应用成像的清晰度和主流设备的情况考虑,应该让设计师针对主流机型分辨率的设计稿进行切图( xhdpi / xxhdpi),但是有部分机型会大于xxhdpi,为了开发方便减少包的大小,直接舍弃xhdpi,只适配xxhdpi和xxxhdpi

      本文标题:Android性能优化:Bitmap详解&你的Bitma

      本文链接:https://www.haomeiwen.com/subject/fqyzattx.html