1. 程式人生 > >Android之帶你從原始碼解析Bitmap佔用記憶體正確的計算公式

Android之帶你從原始碼解析Bitmap佔用記憶體正確的計算公式

Bitmap

前言

做移動端應用開發的朋友或多或少為Bitmap頭疼過,茶不思飯不想就為了想出一個高效載入Bitmap的方法,為什麼會有這樣的情況呢?

毫無疑問,太尼瑪吃記憶體了這玩意,Android手機本來記憶體就這麼點,而且還是多個應用共享,稍微不注意給你送個驚喜,來個OutofMemoryError,不要太酸爽哦,所以作為移動軟體開發者,解決好Bitmap是一個必須跨過去的坑,那麼今天就來聊聊它

Bitmap簡介

既然要將Bitmap解決掉,就需要先了解它的神祕史

Bitmap從字面意思理解是點陣圖,點陣圖又叫點陣圖或畫素圖,計算機螢幕上的圖是由螢幕上的發光點(即畫素)構成的,每個點用二進位制資料來描述其顏色與亮度等資訊,這些點是離散的,類似於點陣。多個畫素點的色彩組合就形成了影象,稱之為點陣圖。

點陣圖在放大到一定程度時會發現它是由一個個小方格組成的,這些小方格被稱為畫素點,一個畫素是影象中最小的影象元素。在處理點陣圖影象時,所編輯的是畫素而不是物件或形狀,它的大小和質量取決於影象中的畫素點的多少,每平方英寸中所含畫素越多,影象越清晰,顏色之間的混和也越平滑。計算機儲存點陣圖實際上是儲存影象的各個畫素的位置和顏色資料等資訊,所以影象越清晰,說明畫素越多,相應的儲存容量也越大

在Android中一個Bitmap物件其實是對點陣圖的抽象,它可以從檔案系統,資原始檔夾,網路等獲取,作用物件可以是JPG圖片,也可以是PNG圖片等,Bitmap物件不僅包括畫素點,還有長寬等資訊

畫素儲存方式

Bitmap內部有一個列舉類Config,它描述了畫素的儲存方式, 這會影響質量(顏色深度)以及顯示透明/半透明顏色的能力。
提供了四種儲存方式

  • ARGB_8888:四個通道即A,R,G,B,其中A指半透明的alpha,RGB是顏色通道,每個通道以八位精度儲存;這種方式下每個畫素佔用四個位元組,可以提供最高質量的圖片,但是最耗記憶體
  • ARGB_4444:通道同上,但是每個通道以四位儲存;這種方式下每個畫素佔用兩個位元組,該模式儲存圖片質量差,失真明顯,但是耗費記憶體小且擁有Alpha通道
  • RGB_565:只有RGB三個通道,R通道以五位精度儲存,G通道以六位精度儲存,B通道以5位精度儲存;這種方式每個畫素以兩個位元組儲存;當使用不需要高保真的不透明Bitmap時,此配置可能很有用;為了獲得更好的結果,應該應用抖動屬性
  • ALPHA_8:只有A通道,每個畫素佔用一個位元組記憶體,不過只有透明度,不儲存顏色資訊

ARGB_4444失真嚴重,基本不用;ALPHA_8使用場景特殊,比如設定遮蓋效果等;不需要設定透明度,RGB_565是個不錯的選擇;既要設定透明度,對圖片質量要求又高,就用ARGB_8888

ARGB_4444在API13中已棄用,從KITKAT(API19)開始,使用此配置建立的任何點陣圖都將預設使用ARGB_8888建立

圖片壓縮格式

Bitmap內部有一個列舉類CompressFormat,給我們指定了三種Bitmap壓縮格式

  • JPEG:全稱Joint Photographic Expert Group,即聯合照片專家組;檔案後輟名為".jpg"或".jpeg";是一種有失真壓縮格式,壓縮比率通常在10:1到40:1之間,壓縮比越大,品質就越低
  • PNG:全稱Portable Network Graphics,即行動式網路圖形;檔案字尾名為“.png”;是一種無失真壓縮格式,支援高級別無損耗壓縮和alpha 通道透明度,主要用於小圖示,透明背景等;但是其壓縮比沒有jpeg大,且色彩複雜情況下壓縮後文件較大
  • WEBP:由Google推出的新格式,同時提供了有失真壓縮與無失真壓縮;無失真壓縮,相同質量的webp比PNG小大約26%;有失真壓縮,相同質量的webp比JPEG小25%-40%,但是WebP格式影象的編碼時間比JPEG格式影象長8倍; 支援GIF

其中JPEG有一個升級版JPEG2000,其壓縮率比JPEG高約30%左右,同時支援有損和無失真壓縮。JPEG2000格式有一個極其重要的特徵在於它能實現漸進傳輸,即先傳輸影象的輪廓,然後逐步傳輸資料,不斷提高影象質量,讓影象由朦朧到清晰顯示

Bitmap記憶體計算

本文基於API24

釋放記憶體

Bitmap包括畫素以及長寬,畫素位數等資訊,其中畫素是載入一張Bitmap最佔記憶體的地方,長寬和畫素位數等是描述資訊

我們知道Android記憶體是分為Java堆記憶體和native記憶體,Android會限制每個應用能使用的最大記憶體,這是堆記憶體和native記憶體總和;在Android2.3(API10)及以前,Bitmap本身存放在Dalvik堆記憶體中,但是最佔記憶體的畫素級資料(pixel data)是存放在native記憶體空間中,這樣虛擬機器無法進行垃圾回收,必須手動呼叫Bitmap.recycle方法,這樣很容易誘發記憶體洩漏;從Android3.0開始,Android將Bitmap本身和畫素級資料一起儲存在了Dalvik堆記憶體中,在載入一張Bitmap時,可以從Monitor直觀的看出來已開闢記憶體在增加

那我們應該怎麼釋放Bitmap所佔記憶體呢?釋放對Bitmap的引用是最正確的做法,那是否還需要呼叫recycle呢?我們看官方的解釋

釋放與此Bitmap關聯的native物件,並清除對畫素資料的引用。 這不會同步釋放畫素資料; 如果沒有其他引用,它只是允許它被GC。 bItmap標記為“dead”,這意味著如果呼叫getPixels()或setPixels(),它將丟擲異常,並且不會繪製任何內容。 此操作無法撤消,因此只有在確定Bitmap沒有進一步用途時才應呼叫此操作。 這是一個高階呼叫,通常不需要呼叫,因為正常的GC程序將在沒有更多對此Bitmap的引用時釋放此記憶體

所以正常情況下是不需要呼叫了,前提是你的應用中沒有保留對此Bitmap的引用了

獲取Bitmap所佔記憶體

Android各個版本獲取Bitmap記憶體方法不一樣

  • int getAllocationByteCount ():API19(Android4.4)及以後,返回用於儲存此Bitmap畫素的已分配記憶體的大小,該值在Bitmap的生命週期內不會改變;如果重新使用它來解碼較小尺寸的其他Bitmap,或者通過手動重新配置,這可能會大於getByteCount()的結果,比如呼叫了reconfigure(int,int,Config),setWidth(int),setHeight(int),setConfig(Bitmap.Config)和BitmapFactory.Options.inBitmap。 如果未以這種方式修改點陣圖,則此值將與getByteCount()返回的值相同
  • int getByteCount ():API12及以後,返回可用於儲存此Bitmap畫素的最小位元組數。從KITKAT(API19)開始,此方法的結果不再用於確定點陣圖的記憶體使用情況。 請參閱getAllocationByteCount()
  • getRowBytes()*getHeight():其中getRowBytes返回Bitmap每行所佔畫素的位元組數,注意這是儲存在native記憶體中的;從KITKAT(API19)開始,此方法不應用於計算點陣圖的記憶體使用情況,請參閱getAllocationByteCount();所以這個結果與getHeight()相乘就是Bitmap所佔的總記憶體

現在Android 都更新到9.0了,4.4及以下版本市場佔有率不足5%,所以盡情使用getAllocationByteCount方法獲取Bitmap畫素所佔記憶體吧

在這裡插入圖片描述

上圖統計資料時間為2018年6月28號,由Google統計針對全世界所有的Android手機佔比情況;從這點也可以看出在新建專案的時候將minSdkVersion設定為19(Android 4.4)是個不錯的選擇

計算所佔記憶體

在講到計算Bitmap記憶體的時候會涉及到DisplayMetrics的兩個變數,如下:

  • density:The logical density of the display. This is a scaling factor for the Density Independent Pixel unit, where one DIP is one pixel on an approximately 160 dpi screen (for example a 240x320, 1.5”x2” screen), providing the baseline of the system’s display. Thus on a 160dpi screen this density value will be 1; on a 120 dpi screen it would be .75; etc.
    This value does not exactly follow the real screen size (as given by xdpi and ydpi, but rather is used to scale the size of the overall UI in steps based on gross changes in the display dpi. For example, a 240x320 screen will have a density of 1 even if its width is 1.8”, 1.3”, etc. However, if the screen resolution is increased to 320x480 but the screen size remained 1.5”x2” then the density would be increased (probably to 1.5).
  • densityDpi:The screen density expressed as dots-per-inch.

這裡可以簡單理解:

densityDpi的意思是螢幕上每英寸有多少點,注意不是畫素點,可以理解為絕對螢幕密度,比如160dpi的螢幕的densityDpi值就是160

density是相對螢幕密度,一個DIP(裝置獨立畫素)在160dpi螢幕上等於一個畫素,我們以160dpi為基準線,density的值即為相對於160dpi螢幕的相對螢幕密度。比如,160dpi螢幕的density值為1, 320dpi螢幕的density值為2

在 DisplayMetrics 當中,這兩個的關係是線性的

density 1 1.5 2 3 3.5 4
densityDpi 160 240 320 480 560 640

舉例

一張實際畫素大小是412*412的圖片,執行在ZTE BV0800手機上,手機density是3,densityDpi是480

使用如下API載入圖片並獲取相關引數

Bitmap value = BitmapFactory.decodeResource(getResources(),R.mipmap.app_logo);
int height = value.getHeight();
int width = value.getWidth();
int size = value.getAllocationByteCount();
  • 放在-hdpi目錄下,載入這張圖片獲取到的height=824,width=824,size=2715904
  • 放在-xxhdpi目錄下,載入這張圖獲取到的height=412,width=412,size=678976
  • 放在-xxxhdpi目錄下,載入這張圖獲取到的height=309,width=309,size=381924

很奇怪對吧,同一張圖片只是放在不同的目錄居然會佔用不同的記憶體,那具體什麼原理呢?我們從原始碼來追溯下

Bitmap.getAllocationByteCount

public final int getAllocationByteCount() {
        if (mBuffer == null) {
            return getByteCount();
        }
        return mBuffer.length;
    }

這個mBuffer 是個int陣列,用於Bitmap的備份緩衝,當進行reconfigure(int,int,Config),setWidth(int),setHeight(int),setConfig(Bitmap.Config)等操作時,這個就會有值,並且會大於getByteCount()值

預設情況下會走到getByteCount()

Bitmap.getByteCount

public final int getByteCount() {
        return getRowBytes() * getHeight();
}

這個方法是返回可用於儲存Bitmap畫素的最小位元組數,最大可達46,340 x 46,340,其中getHeight方法會返回Bitmap物件的mHeight例項域,也就是圖片的高度(單位為px),而getRowBytes方法返回的是圖片的畫素寬度與色彩深度的乘積

繼續追蹤

Bitmap.getRowBytes

public final int getRowBytes() {
        if (mRecycled) {
            Log.w(TAG, "Called getRowBytes() on a recycle()'d bitmap! This is undefined behavior!");
        }
        return nativeRowBytes(mNativePtr);
}

private static native int nativeRowBytes(long nativeBitmap);

到這裡就需要進入native層了

這個方法定義在/frameworks/base/core/jni/android/graphics/Bitmap.cpp

Bitmap.Bitmap_rowBytes

static jint Bitmap_rowBytes(JNIEnv* env, jobject, jlong bitmapHandle) {
    LocalScopedBitmap bitmap(bitmapHandle);
    return static_cast<jint>(bitmap->rowBytes());
}
class LocalScopedBitmap {
public:
    explicit LocalScopedBitmap(jlong bitmapHandle)
            : mBitmapWrapper(reinterpret_cast<BitmapWrapper*>(bitmapHandle)) {}

    BitmapWrapper* operator->() {
        return mBitmapWrapper;
    }

    void* pixels() {
        return mBitmapWrapper->bitmap().pixels();
    }

    bool valid() {
        return mBitmapWrapper && mBitmapWrapper->valid();
    }

private:
    BitmapWrapper* mBitmapWrapper;
};

class BitmapWrapper {
public:
    BitmapWrapper(Bitmap* bitmap)
        : mBitmap(bitmap) { }

    void freePixels() {
        mInfo = mBitmap->info();
        mHasHardwareMipMap = mBitmap->hasHardwareMipMap();
        mAllocationSize = mBitmap->getAllocationByteCount();
        mRowBytes = mBitmap->rowBytes();
        mGenerationId = mBitmap->getGenerationID();
        mIsHardware = mBitmap->isHardware();
        mBitmap.reset();
        
	size_t rowBytes() const {
        if (mBitmap) {
            return mBitmap->rowBytes();
        }
        return mRowBytes;
    }

	void getSkBitmap(SkBitmap* outBitmap) {
        assertValid();
        mBitmap->getSkBitmap(outBitmap);
    }
    
private:
    sk_sp<Bitmap> mBitmap;
    SkImageInfo mInfo;
    bool mHasHardwareMipMap;
    size_t mAllocationSize;
    size_t mRowBytes;
    uint32_t mGenerationId;
    bool mIsHardware;
};

這裡的LocalScopedBitmap其實是對BitmapWrapper這樣一個物件做了封裝,然後BitmapWrapper類內部維護了SkBitmap,Java中的Bitmap在native層其實是一個SKBitmap物件

這個類可以去這裡看skia/src/core/SkBitmap.cpp

SkBitmap.cpp

int SkBitmap::ComputeBytesPerPixel(SkBitmap::Config config) {
    int bpp;
    switch (config) {
        case kNo_Config:
            bpp = 0;   // not applicable
            break;
        case kA8_Config:
        case kIndex8_Config:
            bpp = 1;
            break;
        case kRGB_565_Config:
        case kARGB_4444_Config:
            bpp = 2;
            break;
        case kARGB_8888_Config:
            bpp = 4;
            break;
        default:
            SkDEBUGFAIL("unknown config");
            bpp = 0;   // error
            break;
    }
    return bpp;
}

size_t SkBitmap::ComputeRowBytes(Config c, int width) {
    return SkColorTypeMinRowBytes(SkBitmapConfigToColorType(c), width);
}
inline size_t SkColorTypeMinRowBytes(SkColorType ct, int width) {
    return width * SkColorTypeBytesPerPixel(ct);
}

記憶體計算公式

為什麼ARGB_888佔4個位元組,就是在上面函式定義的,看到這裡基本上差不多可以得出一個結論了:

Bitmap所佔記憶體 計算公式 = bitmapWidth * bitmapHeight * ColorType

這裡面的ColorType跟圖片的畫素儲存方式有關,比如ARGB_8888方式儲存,一個畫素佔用4個位元組,那我們計算下上面那種圖片,它本身實際畫素寬度高度是 412 * 412;載入Bitmap預設的型別是ARGB_8888,那麼所佔記憶體就是
412 * 412 * 4 = 678976

可以看出跟上面給出的第二種結果相同,但是放在其它兩個目錄後的所佔記憶體就變了,為什麼呢?

還記得上面說的手機兩個引數和目錄名稱嗎,這不是隨便瞎說的,因為Bitmap所佔用記憶體絕不僅僅只跟圖片本身(畫素點數和色彩格式)有關,還跟具體裝置的螢幕引數(density和densityDpi)和所在目錄有關

當前手機是確定的,引數density是3,densityDpi是480,現在只有具體放在哪個目錄是可以變的,其實放在哪個目錄是對應相應densityDpi和density的裝置的,對應關係如下

  • ldpi(低)~120dpi
  • mdpi(中)~160dpi
  • hdpi(高)~240dpi
  • xhdpi(超高)~320dpi
  • xxhdpi(超超高)~480dpi
  • xxxhdpi(超超超高)~640dpi

在這裡插入圖片描述
具體可以到官網檢視支援多種螢幕

瞭解了這些對應關係後,那這些值是怎麼決定Bitmap佔用的記憶體的呢?我們從載入方法中去一探究竟

BitmapFactory.decodeResource

public static Bitmap decodeResource(Resources res, int id) {
        return decodeResource(res, id, null);
}

public static Bitmap decodeResource(Resources res, int id, Options opts) {
        Bitmap bm = null;
        InputStream is = null; 
        
        try {
            final TypedValue value = new TypedValue();
            is = res.openRawResource(id, value);
            bm = decodeResourceStream(res, value, is, null, opts);
        } catch (Exception e) {
        } finally {
            try {
                if (is != null) is.close();
            } catch (IOException e) {
                // Ignore
            }
        }

        if (bm == null && opts != null && opts.inBitmap != null) {
            throw new IllegalArgumentException("Problem decoding into existing bitmap");
        }

        return bm;
    }

這裡只是通過openRawResource獲取資源流,然後呼叫decodeResourceStream方法對流進行解碼和適配後返回Bitmap,那到底是怎麼處理記憶體的,進入此方法再看

BitmapFactory.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);
    }
  • 第一步,例項化Options

    public Options() {
      inDither = false;
      inScaled = true;
      inPremultiplied = true;
    }
    

    這些配置屬性稍後再講

  • 第二步,配置Options的兩個屬性inDensity和inTargetDensity,這兩個屬性是影響記憶體開闢的重要因素,那這兩個屬性表示啥呢?
    inDensity:資源所在目錄的densityDpi(對應關係在上面)
    inTargetDensity:當前裝置螢幕的densityDpi

  • 第三步,呼叫decodeStream方法

public static Bitmap decodeStream(InputStream is, Rect outPadding, Options opts) {
       
        if (is == null) {
            return null;
        }

        Bitmap bm = null;

        try {
            if (is instanceof AssetManager.AssetInputStream) {
                final long asset = ((AssetManager.AssetInputStream) is).getNativeAsset();
                bm = nativeDecodeAsset(asset, outPadding, opts);
            } else {
                bm = decodeStreamInternal(is, outPadding, opts);
            }

            if (bm == null && opts != null && opts.inBitmap != null) {
                throw new IllegalArgumentException("Problem decoding into existing bitmap");
            }

            setDensityFromOptions(bm, opts);
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_GRAPHICS);
        }

        return bm;
    }

接下來又要進入native層了 ,這個方法定義在/frameworks/base/core/jni/android/graphics/BitmapFactory.cpp

BitmapFactory.cpp

static jobject nativeDecodeStream(JNIEnv* env, jobject clazz, jobject is, jbyteArray storage,
        jobject padding, jobject options) {

    jobject bitmap = NULL;
    std::unique_ptr<SkStream> stream(CreateJavaInputStreamAdaptor(env, is, storage));

    if (stream.get()) {
        std::unique_ptr<SkStreamRewindable> bufferedStream(
                SkFrontBufferedStream::Create(stream.release(), SkCodec::MinBufferedBytesNeeded()));
        SkASSERT(bufferedStream.get() != NULL);
        bitmap = doDecode(env, bufferedStream.release(), padding, options);
    }
    return bitmap;
}

接下來呼叫doDecode,這個方法有300多行,這裡就給出一些重點部分,這個方法很重要,因為我們通過BitmapFactory.Options設定的一些屬性是怎麼載入Bitmap的都在這個方法體現

static jobject doDecode(JNIEnv* env, SkStreamRewindable* stream, jobject padding, jobject options) {
    
    std::unique_ptr<SkStreamRewindable> streamDeleter(stream);

    // 設定選項引數預設值
    int sampleSize = 1;
    bool onlyDecodeSize = false;
    SkColorType prefColorType = kN32_SkColorType;
    bool isHardware = false;
    bool isMutable = false;
    float scale = 1.0f;
    bool requireUnpremultiplied = false;
    jobject javaBitmap = NULL;
    sk_sp<SkColorSpace> prefColorSpace = nullptr;

    // 根據開發者設定的選項引數從新賦值
    if (options != NULL) {
        sampleSize = env->GetIntField(options, gOptions_sampleSizeFieldID);
        // 糾正sampleSize值
        if (sampleSize <= 0) {
            sampleSize = 1;
        }
				//是否只解碼圖片大小
        if (env->GetBooleanField(options, gOptions_justBoundsFieldID)) {
            onlyDecodeSize = true;
        }

        // 初始化
        env->SetIntField(options, gOptions_widthFieldID, -1);
        env->SetIntField(options, gOptions_heightFieldID, -1);
        env->SetObjectField(options, gOptions_mimeFieldID, 0);
        env->SetObjectField(options, gOptions_outConfigFieldID, 0);
        env->SetObjectField(options, gOptions_outColorSpaceFieldID, 0);

        jobject jconfig = env->GetObjectField(options, gOptions_configFieldID);
        prefColorType = GraphicsJNI::getNativeBitmapColorType(env, jconfig);
        jobject jcolorSpace = env->GetObjectField(options, gOptions_colorSpaceFieldID);
        prefColorSpace = GraphicsJNI::getNativeColorSpace(env, jcolorSpace);
        isHardware = GraphicsJNI::isHardwareConfig(env, jconfig);
        isMutable = env->GetBooleanField(options, gOptions_mutableFieldID);
        requireUnpremultiplied = !env->GetBooleanField(options, gOptions_premultipliedFieldID);
        javaBitmap = env->GetObjectField(options, gOptions_bitmapFieldID);

        if (env->GetBooleanField(options, gOptions_scaledFieldID)) {
        		//這是在java層BitmapFactory.decodeResourceStream中設定的density
            const int density = env->GetIntField(options, gOptions_densityFieldID);
            //這是在java層BitmapFactory.decodeResourceStream中設定的targetDensity
            const int targetDensity = env->GetIntField(options, gOptions_targetDensityFieldID);
            //這個沒有配置
            const int screenDensity = env->GetIntField(options, gOptions_screenDensityFieldID);
            if (density != 0 && targetDensity != 0 && density != screenDensity) {
            	//根據targetDensity和density得到一個縮放值,用於Canvas繪製所用
                scale = (float) targetDensity / density;
            }
        }
    }

    if (isMutable && isHardware) {
        doThrowIAE(env, "Bitmaps with Config.HARWARE are always immutable");
        return nullObjectReturn("Cannot create mutable hardware bitmap");
    }

    // 建立編解碼器.
    NinePatchPeeker peeker;
    std::unique_ptr<SkAndroidCodec> codec(SkAndroidCodec::NewFromStream(
            streamDeleter.release(), &peeker));
    if (!codec.get()) {
        return nullObjectReturn("SkAndroidCodec::NewFromStream returned null");
    }

    // 不允許將ninepatch解碼為565.在過去,解碼到565會抖動,我們不想預先抖動ninepatch ,
    //因為我們知道它們會被拉伸。我們不再抖動565解碼,但我們繼續阻止ninepatch解碼到565,以保持舊的行為
    if (peeker.mPatch && kRGB_565_SkColorType == prefColorType) {
        prefColorType = kN32_SkColorType;
    }

    // 確定輸出大小
    SkISize size = codec->getSampledDimensions(sampleSize);
		//圖片原始寬高
    int scaledWidth = size.width();
    int scaledHeight = size.height();
    bool willScale = false;

    // 如有必要,精細縮放步驟.
    if (needsFineScale(codec->getInfo().dimensions(), size, sampleSize)) {
        willScale = true;
        scaledWidth = codec->getInfo().width() / sampleSize;
        scaledHeight = codec->getInfo().height() / sampleSize;
    }

    // 設定解碼colorType
    SkColorType decodeColorType = codec->computeOutputColorType(prefColorType);
    sk_sp<SkColorSpace> decodeColorSpace = codec->computeOutputColorSpace(
            decodeColorType, prefColorSpace);

    // 如果開發者只需要大小,設定選項並返回
    if (options != NULL) {
        jstring mimeType = encodedFormatToString(
                env, (SkEncodedImageFormat)codec->getEncodedFormat());
        if (env->ExceptionCheck()) {
            return nullObjectReturn("OOM in encodedFormatToString()");
        }
        env->SetIntField(options, gOptions_widthFieldID, scaledWidth);
        env->SetIntField(options, gOptions_heightFieldID, scaledHeight);
        env->SetObjectField(options, gOptions_mimeFieldID, mimeType);

        jint configID = GraphicsJNI::colorTypeToLegacyBitmapConfig(decodeColorType);
        if (isHardware) {
            configID = GraphicsJNI::kHardware_LegacyBitmapConfig;
        }
        jobject config = env->CallStaticObjectMethod(gBitmapConfig_class,
                gBitmapConfig_nativeToConfigMethodID, configID);
        env->SetObjectField(options, gOptions_outConfigFieldID, config);

        env->SetObjectField(options, gOptions_outColorSpaceFieldID,
                GraphicsJNI::getColorSpace(env, decodeColorSpace, decodeColorType));

        if (onlyDecodeSize) {
            return nullptr;
        }
    }

    // 將上面計算的縮放比列進一步精確
    if (scale != 1.0f) {
        willScale = true;
        scaledWidth = static_cast<int>(scaledWidth * scale + 0.5f);
        scaledHeight = static_cast<int>(scaledHeight * scale + 0.5f);
    }

    ......

    SkBitmap outputBitmap;
    if (willScale) {
        // 根據縮放後的寬高除以圖片真實寬高,得到canvas縮放比例
        const float sx = scaledWidth / float(decodingBitmap.width());
        const float sy = scaledHeight / float(decodingBitmap.height());

        // 為outputBitmap設定分配器
        SkBitmap::Allocator* outputAllocator;
        if (javaBitmap != nullptr) {
            outputAllocator = &recyclingAllocator;
        } else {
            outputAllocator = &defaultAllocator;
        }

        SkColorType scaledColorType = decodingBitmap.colorType();
        // 如果alphaType是kUnpremul並且影象具有alpha,則顏色可能不正確,因為Skia尚不支援繪製到/來自未預乘的點陣圖
        outputBitmap.setInfo(
                bitmapInfo.makeWH(scaledWidth, scaledHeight).makeColorType(scaledColorType));
        if (!outputBitmap.tryAllocPixels(outputAllocator)) {
            // 因為OOM失敗
            return nullObjectReturn("allocation failed for scaled bitmap");
        }

        SkPaint paint;
        paint.setBlendMode(SkBlendMode::kSrc);
        paint.setFilterQuality(kLow_SkFilterQuality); // bilinear filtering
        SkCanvas canvas(outputBitmap, SkCanvas::ColorBehavior::kLegacy);
        canvas.scale(sx, sy);
        canvas.drawBitmap(decodingBitmap, 0.0f, 0.0f, &paint);
    } else {
        outputBitmap.swap(decodingBitmap);
    }

    ......

    // 返回Java Bitmap
    return bitmap::createBitmap(env, defaultAllocator.getStorageObjAndReset(),
            bitmapCreateFlags, ninePatchChunk, ninePatchInsets, -1);
}

分為如下幾步:

  • 首先會拿到density和targetDensity這兩個值,然後根據它們計算出縮放比例

    scale = (float) targetDensity / density
    
  • 通過sampleSize值計算出縮小後的寬高

    scaledWidth = codec->getInfo().width() / sampleSize;
    scaledHeight = codec->getInfo().height() / sampleSize;
    
  • 如果scale不等於1,也就是density和targetDensity不相等,那就再從新計算載入的Bitmap的寬高

    scaledWidth = static_cast<int>(scaledWidth * scale + 0.5f);
    scaledHeight = static_cast<int>(scaledHeight * scale + 0.5f);
    
  • 根據計算後的寬高,除以圖片真實寬高,得到canvas寬高縮放比例

        const float sx = scaledWidth / float(decodingBitmap.width());
        const float sy = scaledHeight / float(decodingBitmap.height());
    
  • 最後通過sx和sy將canvas縮放,然後將讀到記憶體的Bitmap畫上去,這樣Bitmap最後也縮放了相應比例

修正後的記憶體計算公式

到此Bitmap記憶體的計算原理就結束了,我們回到上面的例子看看:

一張實際畫素大小是412*412圖片,執行在ZTE BV0800手機上,手機density是3,densityDpi是480

  • 放在-hdpi目錄下,載入這張圖片獲取到的height=824,width=824,size=2715904
  • 放在-xxhdpi目錄下,載入這張圖獲取到的height=412,width=412,size=678976
  • 放在-xxxhdpi目錄下,載入這張圖獲取到的height=309,width=309,size=381924

計算公式 = bitmapWidth * bitmapHeight * ColorType(預設是4)


現在從上面的原始碼可以得出新的計算公式 :

(int)(bitmapWidth * targetDensity / density + 0.5f) * (int)(bitmapHeight * targetDensity / density + 0.5f) * ColorType

新公式中的density 指的是資源所在目錄的densityDpi,如下

  • ldpi(低)~120dpi
  • mdpi(中)~160dpi
  • hdpi(高)~240dpi
  • xhdpi(超高)~320dpi
  • xxhdpi(超超高)~480dpi
  • xxxhdpi(超超超高)~640dpi

targetDensity 指的是裝置螢幕的densityDpi = 480

現在來計算下各個目錄的值,見證奇蹟的時刻來了

  • 放在-hdpi目錄下,(int)(412 * 480 / 240 + 0.5f) * (int) (412 * 480 / 240 + 0.5f) * 4 = 824 * 824 * 4 = 2715904
  • 放在-xxhdpi目錄下 (int)(412 * 480 / 480 + 0.5f) * (int) (412 * 480 / 480 + 0.5f) * 4 = 412 * 412 * 4 = 678976
  • 放在-xxxhdpi目錄下 (int)(412 * 480 / 640 + 0.5f) * (int) (412 * 480 / 640 + 0.5f) * 4 = 309 * 309 * 4 = 381924

看到沒有,我的天啊,終於找到正確答案了,現在你有沒有get到Bitmap記憶體的正確計算方式呢

補充

有人可能覺得這個0.5f加了有什麼用呢,這裡也沒體現出來啊,要知道我們這裡的裝置螢幕densityDpi都是很規整的,但是總會有一些不正規廠家出的非正版手機,比如某pin多多上面的一塊錢手機,當然了,這裡只是開玩笑,但是targetDensity / density結果可能是小數,比如小數位是0.5+這種,而圖片的尺寸,都是以 int 型別為單位,如果不加0.5個,對於小數位大於0.5的值就也會直接捨去,影響最終結果的精準性。所以 Android 為了規避這樣的問題,做了個容差值,修正結果

到這裡我們可以知道了一個Bitmap佔用多少記憶體跟以下這些因素有關

  • 圖片本身的畫素寬高
  • 圖片畫素儲存方式或者說色彩格式,比如ARGB_8888,ARGB_4444等
  • 原始檔案存放的資源目錄,比如xhdpi 和 xxhdpi等
  • 使用裝置螢幕的densityDpi

知道了Bitmap記憶體佔用的原理,那麼一個很直接的問題就來了,怎麼降低Bitmap在應用中所佔記憶體或者怎麼高效載入Bitmap呢?這個就不在這篇文章繼續了,太長了,寫的我眼都花了,休息會,放到下一篇文章中解析

劃重點

memory = (int)(bitmapWidth * targetDensity / density + 0.5f) * (int)(bitmapHeight * targetDensity / density + 0.5f) * ColorType

(小提示:Android預設會從與螢幕相匹配的 densityDpi的目錄去尋找圖片資源,如果沒有,就往更高densityDpi的目錄去尋找,開發者切勿放到低densityDpi的目錄,這會放大佔用記憶體)