Android性能優(yōu)化:Bitmap詳解&你的Bitmap占多大內(nèi)存?

在開發(fā)app時,顯示一張本地圖片,這張圖片在加載時會占用大多內(nèi)存呢?猜測占用內(nèi)存大小和以下幾個因素有關(guān):

  1. 設計師切圖,圖片本身的分辨率;
  2. 圖片所放文件夾代表的 密度 dpi;
  3. 手機自身的屏幕密度;
  4. 經(jīng)過系統(tǒng)縮放得到的最終加載到手機上圖片的密度和占用的內(nèi)存。

我們知道Android中在加載本地大圖時,很容易OOM,主要原因在于加載的Bitmap占用內(nèi)存太大。接下來將圍繞以下幾個問題說明如何計算一張Bitmap占用的內(nèi)存大小。

  1. 將一張分辨率為 720x1080 的圖片放到 xxhdpi 或者 hdpi ,同放在 xhdpi 標準文件夾下,對于同一臺手機占用內(nèi)存大小是否有變化?
  2. 同一張分辨率為 720x1080 的圖片被不同屏幕分辨率的手機加載,BitmapFactory 的成員變量 inDensity、 inScreenDensity、 inTargetDensity 會怎樣變化?這些值又是怎樣被賦值的,又是怎樣進行縮放的?
  3. 使用 decodeResource() 和 decodeStream() 有什么區(qū)別?
  4. Options 的 inDensity、 inTargetDensity 和 輸出的 Bitmap 的 mDensity 有什么關(guān)系?Bitmap 的 mWidth、 mHeight 與 Options 的 outputWidth、 outputHeight 有什么關(guān)系?
  5. 這些同計算 Bitmap 內(nèi)存占用大小的 長寬有什么關(guān)系?

在回答這些問題之前,先介紹一下DisplayMetrics和Bitmap及其相關(guān)類。

一、DisplayMetrics和Bitmap及其相關(guān)類

DisplayMetrics

說明:屏幕密度相關(guān)類,可以用于獲取屏幕高和寬以及屏幕密度density、每英寸點數(shù)densityDpi . 這里,density 數(shù)值為 1dp = density px;在 DisplayMetrics 中,這兩個是線性相關(guān):


屏幕密度對照表.png
Bitmap

說明:Bitmap 在 Android 中指的是一張圖片,可以是 png,也可以是 jpg等其他圖片格式。
作用:可以獲取圖像文件信息,對圖像進行剪切、旋轉(zhuǎn)、縮放、壓縮等操作,并可以指定格式保存圖像文件。

Bitmap.Config

說明:Bitmap 格式。除了尺寸外,影響一個圖片占用空間還有色彩細節(jié)。位圖位數(shù)越高表示可以存儲的顏色信息越多,圖像也就越清晰逼真。

  • ALPHA_8:表示8位Alpha位圖,每像素占1byte內(nèi)存;
  • RGB_565:表示R為5位,G為6位,B為5位,一共16位,每像素占2byte內(nèi)存;
  • ARGB_4444:表示16位位圖,每像素占2byte內(nèi)存(poor quality - Android Deprecated);
  • ARGB_8888:表示32位ARGB位圖,每像素占4byte內(nèi)存(Recommended)。
BitmapFactory

說明:提供解析Bitmap的靜態(tài)工廠方法。

BitmapFactory.Options

說明:用于解碼Bitmap時的各種參數(shù)控制。
幾個重要參數(shù):

inBitmap:在解析Bitmap時重用該Bitmap,但是必須相同大小的Bitmap & inMutable = true 才可重用;
inMutable :配置Bitmap是否可更改,如每隔幾個像素給Bmp添加一條直線;
inPreferredConfig:Config顏色位數(shù),默認值為Bitmap.Config.ARGB_888;
inDither:是否抖動,默認false(Android Depracated);
inPremultiplied:默認true,一般不改變其值。
inPurgeable:當存儲像素內(nèi)存空間 在系統(tǒng)內(nèi)存不足時 是否可被回收(Android L Deprecated);
inInputShareable:是否可以共享一個 InputStream (Android L Deprecated);
inPreferQualityOverSpeed:為true時會優(yōu)先保證 Bitmap 質(zhì)量,其次是解碼速度(Android N Deprecated);
inTempStorage:解碼時的臨時空間,建議 16K;
inJustDecodeBounds:為true時僅返回 Bitmap 寬高等屬性,返回bmp=null,為false時才返回占內(nèi)存的 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三者之間的關(guān)系,如下圖所示:


Bitmap、BitmapFactory、Options關(guān)系類圖.png

二、ImageView 設置圖片 & Bitmap創(chuàng)建流程

ImageView 設置圖片

一般地,給 ImageView 設置資源圖片時,會用到四種方式:setImageResource(), setImageUri(), setImageBitmap(), setImageDrawable。這四種方式有什么區(qū)別呢?用一張圖來展示:

ImageView設置圖片四種方法流程.png

總結(jié):由上可知,ImageView設置本地圖片會先生成 Bitmap 再將 Bitmap 轉(zhuǎn)成 Drawable,最終通過 setImageDrawable() 設置;
【所以這步是否可以看做使用 setImageDrawable 會跳過讀取和解碼 Bitmap 操作,為最優(yōu)設置本地圖片方式呢?
—— 需測試內(nèi)存占用情況方可驗證。】

Bitmap創(chuàng)建流程

BitmapFactory 提供了五種方式來創(chuàng)建Bitmap,分別是:decodeFile, decodeResource, decodeByteArray, decodeStream, decodeFileDescription,這里只介紹常見三種方式創(chuàng)建流程如下:

Bitmap創(chuàng)建方法.png

總結(jié):

  1. 最常用的三個方法:decodeFile, decodeResource, decodeStream,前兩個最終調(diào)用的是 decodeStream;
  2. **decodeStream, decodeByteArray, decodeFileDescription **這三個內(nèi)部則調(diào)用的是 native 方法來創(chuàng)建 Bitmap的【有種說法,Bitmap是Android中唯一通過 native 方法創(chuàng)建的類】;
  3. decodeResourceStream主要做了兩件事:一是對 opts.inDensity 賦值,沒有設置默認值 160;二是對 opts.inTargetDensity 賦值,沒有賦值為當前設備 densityDpi;
  4. decodeStream主要也做了兩件事:一是調(diào)用 native 方法解析 Bitmap;二是對解析得到的 Bitmap 調(diào)用 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占用內(nèi)存大小?

常規(guī)方式:
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占用內(nèi)存計算公式:bmpWidth * bmpHeight * 4byte。不是直接使用圖片分辨率進行計算,而是界面后 Bitmap 的寬高進行計算。

然而,這樣計算并不準確。有幾個不同的場景會導致最終計算的結(jié)果不正確。

  • 將一張 720x1080 圖片分別放在不同分辨率drawable文件夾下,在同一個手機上加載;
  • 也是同一張圖片放在指定分辨率的 drawable 文件夾下,在不同手機上加載;
  • 切不同分辨率圖片到對應 drawable 文件夾下,在各分辨率設備上加載。

一般,我們讀取 drawable 目錄下的圖片,會用到 <code>decodeResource</code>獲取 Bitmap,該方法可以直接看上面提到的 decodeResourceStream() 方法源碼,通過源碼可知:

  • 在讀取資源時,使用 openRawResource 方法,然后會對 TypedValue 進行賦值,其中包含了原始資源的 density 等信息,也即是文件夾代表的density;
  • 調(diào)用 decodeResourceStream 對原始資源進行解碼和適配,實際是原始資源 density 到 設備屏幕 density 的映射。

這里看一下 資源文件夾代表的密度:


資源文件夾密度對照表.png

對照 decodeResourceStream() 源碼如何設置 opts.inDensity 邏輯:


資源解碼Bitmap參數(shù)設置流程.png

最后通過查閱 native 源碼,得到計算公式:
一張圖片對應 Bitmap 占用內(nèi)存 = mBitmapHeight * mBitmapWidth * 4byte(ARGB_8888);
Native 方法中,mBitmapWidth = mOriginalWidth * (scale = opts.inTargetDensity / opts.inDensity) * 1/inSampleSize,
mBitmapHeight = mOriginalHeight * (scale = opts.inTargetDensity / opts.inDensity) * 1/inSampleSize

現(xiàn)在針對介紹的幾種場景,會得到這樣的結(jié)論:

  1. 將一張 720x1080圖片放在 drawable-xhdpi 目錄下(inDensity = 320),
    • 在 720x1080 手機上加載(inTargetDensity = 320),圖片不會被壓縮;
    • 在 480x800 手機上加載(inTargetDensity = 240),圖片會被壓縮 9/16;
    • 在 1080x1920 手機上加載(inTargetDensity = 480),圖片會被放大 2.25;
  2. 切不通分辨率大小的圖片放到對應文件夾下,會根據(jù)屏幕獲取對應文件夾的圖片,就不存在加載圖片時壓縮和放大(針對標準屏);

拓展問題:只切一套UI圖,是否適用?如何選擇?

注意,上述計算方式是在通過 decodeResource() 方法獲取 Bitmap 的情況下得出,其他幾種方式獲取Bitmap,最后得到占用內(nèi)存Size不會跟資源文件目錄相關(guān)聯(lián)。

四、問題解答

問題一:一張圖片對應 Bitmap 占用內(nèi)存 = 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占用內(nèi)存。

問題三:使用 decodeResource() 和 decodeStream() 有什么區(qū)別?
(1)decodeResource() 流程,會先用 TypedValue 保存圖片信息,然后會根據(jù)條件設置 opts.inDensity = value.inDensity,為0則設置為默認 160dpi; 文件夾代表密度
Opts.inTargetDensity = getDisplayMetrics().densityDpi; 屏幕密度
設置完上述參數(shù)后,最終還是會調(diào)用 decodeStream() 方法;

(2)decodeStream() native 方法得到 Bitmap后,調(diào)用 setDensityFromOptions() 方法來設置 Bitmap.mDensity:
若 opts.inDensity != 0,bitmap.mDensity = opts.inDensity;
若 opts.inTargetDensity != 0 && inDensity != targetDensity && inDensity != screenDensity,繼續(xù)判斷,如果 opts.inScaled || isNinePatch,bitmap.mDensity = targetDensity;

所以,
(1)若使用 decodeResource() 加載本地圖片,inDensity 為加載圖片所在的文件夾代表的 dpi,inTargetDensity 為目標屏幕密度(or 圖片真實像素密度?),
最終 bitmap.mDensity = targetDensity。

(2)若使用 decodeStream() 則不會先記錄圖片信息,得到bitmap 后,直接調(diào)用 setDensityFromOptions() 方法,所以最終 bitmap.mDensity = defaultDensity() = DENSITY_DEVICE。

參考源碼API-26
參考:http://dev.qq.com/topic/591d61f56793d26660901b4e
???????????https://www.tuicool.com/articles/3eMNr2n
如有誤,請指正!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容