Android Bitmap那些事
在平時的開發中,Bitmap是我們接觸最多的話題之一,因為它時不時地就來個OOM,讓我們猝不及防。因此有必要來一次徹底的學習,搞清楚Bitmap的一些本質。
本文主要想講清楚兩點內容:
- Bitmap到底佔多大記憶體
- Bitmap複用的限制
OK,開始之前先介紹下解碼圖片時的控制類BitmapFactory.Options
。
BitmapFactory.Options類解析
Options
是BitmapFactory
從輸入源中decode Bitmap的控制引數,其主要屬性可以參見原始碼,此處給出一些常用屬性的用法。
inMutable
若為true,則返回的Bitmap是可變的,可以作為Canvas的底層Bitmap使用。
若為false,則返回的Bitmap是不可變的,只能進行讀操作。
如果要修改Bitmap,那就必須返回可變的bitmap,例如:修改某個畫素的顏色值(setPixel)
inJustDecodeBounds、outWidth、outHeight
獲取Bitmap的寬度和高度最好的方式:
若inJustDecodeBounds為true,則不會把bitmap載入到記憶體(實際是在Native層解碼了圖片,但是沒有生成Java層的Bitmap),只是獲取該bitmap的原始寬(outWidth)和高(outHeight)。例如:
1 2 3 4 5 6 |
BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true |
這裡若inJustDecodeBounds
為true,則outWidth
表示沒有經過Scale的Bitmap的原始寬高(即我們通過圖片編輯軟體看到的寬高),否則則為載入到記憶體後,真實的Bitmap寬高(經過Scale之後的寬高)。
outMimeType
表示被載入圖片的格式,例如:若載入png格式的圖片,則為
image/png
image/jpeg
。
inPurgeable、inInputShareable
從API21開始,這兩個屬性被廢棄了。
在API19及以下,則表示以下含義:
若inPurgeable為true,則表示BitmapFactory建立的用於儲存Bitmap Pixel的記憶體空間,可以在系統記憶體不足時被回收。
當APP需要再次訪問Bitmap的Pixel時(例如:繪製Bitmap或是呼叫getPixel),系統會再次呼叫BitmapFactory decode方法重新生成Bitmap的Pixel陣列。
inInputShareable表示是否進行深拷貝,與inPurgeable結合使用,inPurgeable為false時,該引數無意義。
若為true,share a reference to the input data (inputstream, array, etc.) ,即淺拷貝。
若為false,must make a deep copy.即深拷貝。
inPreferredConfig
表示一個畫素需要多大的儲存空間:
預設為ARGB_8888: 每個畫素4位元組. 共32位。
Alpha_8: 只儲存透明度,共8位,1位元組。
ARGB_4444: 共16位,2位元組。
RGB_565:共16位,2位元組,只儲存RGB值。
inBitmap
Android在API11新增的屬性,用於重用已有的Bitmap,這樣可以減少記憶體的分配與回收,提高效能。但是使用該屬性存在很多限制:
在API19及以上,存在兩個限制條件:
- 被複用的Bitmap必須是Mutable。違反此限制,不會丟擲異常,且會返回新申請記憶體的Bitmap。
- 被複用的Bitmap的記憶體大小(通過Bitmap.getAllocationByteCount方法獲得,API19及以上才有)必須大於等於被載入的Bitmap的記憶體大小。違反此限制,將會導致複用失敗,丟擲異常IllegalArgumentException(Problem decoding into existing bitmap)
在API11 ~ API19之間,還存在額外的限制:
- 被複用的Bitmap的寬高必須等於被載入的Bitmap的原始寬高。(注意這裡是指原始寬高,即沒進行縮放之前的寬高)
- 被載入Bitmap的Options.inSampleSize必須明確指定為1。
- 被載入Bitmap的Options.inPreferredConfig欄位設定無效,因為會被被複用的Bitmap的inPreferredConfig值所覆蓋(不然,所佔記憶體可能就不一樣了)
關於inBitmap
屬性,後面會進行詳細的介紹,此處不再贅述。
inScaled、inDensity、inTargetDensity、inScreenDensity
這幾個屬性值控制瞭如何對Bitmap進行縮放
,以及決定了bitmap的密度density
。
inScaled
表示是否進行縮放:
若為true,且inDensity和inTargetDensity不為0,那麼縮放因子等於(inTargetDensity/inDensity),並且bitmap的density等於inTargetDensity。
若為false,則不會進行縮放,並且bitmap的density等於inDensity(不為0的前提下),否則就是系統預設密度(160)。
inScaled
屬性預設為true,
當從Drawable資原始檔夾載入圖片資源時(通過BitmapFactory.decodeResource方法載入),inDensity
預設初始化為圖片所在資料夾對應的密度,而inTargetDensity
則初始化為當前系統密度。
當從SD卡 or 二進位制流載入圖片資源時,這兩個屬性都預設為0(即不會對圖片資源進行縮放),需要我們根據實際情況進行設定,一般把inTargetDensity
設定為當前系統密度,inDensity
則需要根據圖片實際尺寸和需求進行設定了。
inSampleSize
主要用於獲取Bitmap的縮圖,例如:inSampleSize=2,那麼bitmap的寬度和高度為原來尺寸的1/2。畫素總數則為原來的1/4。Any value <= 1 is treated the same as 1.
看了下程式碼,在Native層解碼生成SKBitmap
的畫素資料時,會根據圖片原始寬高除以inSampleSize,得到縮圖的寬高。
Bitmap的記憶體佔用
首先要明確一點,圖片在記憶體和檔案系統中的大小是兩個不同的概念。這裡我們主要考慮Bitmap佔用記憶體的大小。(檔案系統中的大小是單獨的話題,後續會進行介紹)
決定Bitmap佔用記憶體大小的關鍵因素有以下幾點:
- 圖片的原始寬高(即我們在圖片編輯軟體中看到的寬高)
- 解碼圖片時的Config配置(即每個畫素佔用幾個位元組)
- 解碼圖片時的縮放因子(即
inTargetDensity/inDensity
)
所以Bitmap的記憶體計算公式時
1
|
originWidth * originHeight * (inTargetDensity/inDensity) * (inTargetDensity/inDensity) * 每畫素佔用位元組數
|
其實,Bitmap在API12提供了getByteCount
方法獲取佔用記憶體,如下所示:
1 2 3 4 |
public final int getByteCount() { //getHeight()表示Bitmap的寬度(單位px) return getRowBytes() * getHeight(); } |
其中getRowBytes()會呼叫到Native層處理,其實就是表示一行畫素所佔的記憶體大小,即width * 每畫素佔用位元組數
。
在API19提供了getAllocationByteCount
方法獲取實際佔用的記憶體,如下所示:
1 2 3 4 5 6 7 |
public final int getAllocationByteCount() { //mBuffer表示儲存Bitmap畫素資料的位元組陣列。 if (mBuffer == null) { return getByteCount(); } return mBuffer.length; } |
mBuffer.length
實際獲取的就是用來儲存Bitmap畫素資料的位元組陣列的長度。(若是通過複用其他Bitmap來解碼圖片,那麼這個位元組陣列儲存新Bitmap的畫素資料時,可能並沒有用完)
一般情況下,兩者是相同的。但若是通過複用Bitmap來解碼圖片,那麼前者表示新解碼圖片佔用記憶體的大小(並非實際記憶體大小),後者表示被複用Bitmap真實佔用的記憶體大小(即mBuffer的長度)。
來看個例子吧:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
//首先以原尺寸載入圖片a,這裡圖片a和圖片b的尺寸相同 BitmapFactory.Options options = new BitmapFactory.Options(); options.inMutable = true; options.inDensity = 160; options.inTargetDensity = 160; Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.a, options); Log.i(TAG, "bitmap = " + bitmap); Log.i(TAG, "bitmap.size = " + bitmap.getByteCount()); Log.i(TAG, "bitmap.allocSize = " + bitmap.getAllocationByteCount()); //然後複用a圖片,解碼b圖片。 options.inBitmap = bitmap; //注意這裡解碼得到的圖片寬高為原始尺寸的一半 options.inDensity = 160; options.inTargetDensity = 80; options.inMutable = true; options.inSampleSize = 1; Bitmap bitmapAIO = BitmapFactory.decodeResource(getResources(), R.drawable.b, options); Log.i(TAG, "bitmapAIO = " + bitmapAIO); Log.i(TAG, "bitmap.size = " + bitmap.getByteCount()); Log.i(TAG, "bitmap.allocSize = " + bitmap.getAllocationByteCount()); Log.i(TAG, "bitmapAIO.size = " + bitmapAIO.getByteCount()); Log.i(TAG, "bitmapAIO.allocSize = " + bitmapAIO.getAllocationByteCount()); 輸出: bitmap = [email protected]9fb5d09 bitmap.size = 8294400 bitmap.allocSize = 8294400 bitmapAIO = [email protected]9fb5d09 bitmap.size = 2073600 bitmap.allocSize = 8294400 bitmapAIO.size = 2073600 bitmapAIO.allocSize = 8294400 |
從上述demo,可以得出:
- Bitmap複用成功了,因為bitmap和bitmapAIO是相同的物件。
- 圖片a佔用記憶體8294400,圖片b(寬和高各縮小一半)佔用記憶體2073600,正好是圖片a所佔記憶體的1/4。
- getByteCount方法返回當前圖片應當所佔記憶體大小,getAllocationByteCount返回被複用Bitmap真實佔用記憶體大小。
縮放因子和Bitmap複用限制的由來
在上述計算Bitmap佔用記憶體的公式中,有一個縮放因子,決定了對原始圖片Scale多少倍。下面我們看看Android API18和API19兩個版本是如何處理對原始圖片的縮放操作的(其實就是處理inTargetDensity/inDensity)。同時也從原始碼層面上,驗證下上文提及的不同Android版本對Bitmap複用限制的差異。
因為通過BitmapFactory
解碼圖片的方法很多,這裡我們選擇從Drawable資料夾解碼的方法decodeResource
來進行分析。
Android4.3 API18
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
decodeResource(Resources res, int id, Options opts) { Bitmap bm = null; InputStream is = null; final TypedValue value = new TypedValue(); is = res.openRawResource(id, value); //關鍵的程式碼這裡 bm = decodeResourceStream(res, value, is, null, opts); //這裡很重要,說明若是Bitmap複用失敗,會丟擲異常,並返回null值,所以在複用Bitmap時要特別留意。 if (bm == null && opts != null && opts.inBitmap != null) { throw new IllegalArgumentException("Problem decoding into existing bitmap"); } return bm; } |
下面繼續看decodeResourceStream
方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
decodeResourceStream(Resources res, TypedValue value, InputStream is, Rect pad, Options opts) { if (opts == null) { opts = new Options(); } //正常情況下,opts.inDensity會被賦值為圖片所在Drawable檔案表示的螢幕密度。 if (opts.inDensity == 0 && value != null) { final int density = value.density;//圖片所在Drawable資料夾決定了density if (density == TypedValue.DENSITY_DEFAULT) { opts.inDensity = DisplayMetrics.DENSITY_DEFAULT;//特殊情況下,inDensity為預設值160 } else if (density != TypedValue.DENSITY_NONE) { opts.inDensity = density; } } //正常情況下,opts.inTargetDensity會被賦值為手機系統密度 if (opts.inTargetDensity == 0 && res != null) { opts.inTargetDensity = res.getDisplayMetrics().densityDpi; } //這裡是關鍵程式碼,繼續往下走 return decodeStream(is, pad, opts); } |
下面繼續看decodeStream
方法(極度精簡之後的):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
boolean finish = true; //若是沒有複用已有Bitmap,則走這個邏輯,此時會在native層處理對圖片的縮放 if (opts == null || (opts.inScaled && opts.inBitmap == null)) { float scale = 1.0f; int targetDensity = 0; if (opts != null) { final int density = opts.inDensity; targetDensity = opts.inTargetDensity; if (density != 0 && targetDensity != 0) { scale = targetDensity / (float) density;//求出縮放因子 } } //通過native方法解碼圖片,並在native層完成對圖片的縮放 bm = nativeDecodeStream(is, tempStorage, outPadding, opts, true, scale); //設定解碼出的Bitmap的密度 if (bm != null && targetDensity != 0) bm.setDensity(targetDensity); finish = false; } else { //若複用已有Bitmap來解碼圖片,那麼就不支援在native層對圖片進行縮放 bm = nativeDecodeStream(is, tempStorage, outPadding, opts); } //這裡很重要,說明若是Bitmap複用失敗,會丟擲異常,並返回null值,所以在複用Bitmap時要特別留意。 if (bm == null && opts != null && opts.inBitmap != null) { throw new IllegalArgumentException("Problem decoding into existing bitmap"); } //若沒有在native層完成對圖片的縮放,則會通過finishDecode方法在java層完成對圖片的縮放(顯然這種方式會重新分配記憶體)。 return finish ? finishDecode(bm, outPadding, opts) : bm; |
decodeStream
方法很關鍵,從中可以獲取幾點關鍵資訊:
- 若是直接解碼圖片(不復用已有圖片,即opts.inBitmap為null),那麼通過native方法解碼圖片時,支援把縮放因子作為引數傳遞到native層,並且後續在java層直接返回了native層解碼出來的Bitmap。說明在native層處理了對圖片的縮放。
- 若是通過複用已有Bitmap來解碼圖片,那麼通過native方法解碼圖片時,就不支援傳遞縮放因子引數了,並且後續在java層,通過
finishDecode
方法完成了對圖片的縮放。說明若複用已有Bitmap解碼圖片,則不支援在native層對圖片進行縮放處理,需要在java層單獨對圖片進行縮放處理。簡單概括下:Android API18及之前的版本,不支援在native層同時使用Bitmap複用和進行縮放處理。(API18及之後是可以的,下面會介紹)
下面我們先看下,finishDecode
方法是怎麼在java層完成對Bitmap的縮放處理的,再看下native層的解碼方法。
首先看下finishDecode
方法,如下所示(精簡之後):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
finishDecode(Bitmap bm, Rect outPadding, Options opts) { final int density = opts.inDensity; if (density == 0) { //檢查引數是否合法 return bm; } bm.setDensity(density); final int targetDensity = opts.inTargetDensity; //檢查是否需要進行縮放處理 if (targetDensity == 0 || density == targetDensity || density == opts.inScreenDensity) { return bm; } if (opts.inScaled || isNinePatch) { float scale = targetDensity / (float) density;//計算縮放因子 if (scale != 1.0f) { final Bitmap oldBitmap = bm; //這裡很關鍵哈,根據縮放因子,在原圖的基礎上,直接建立了一個新的Bitmap,並且把原圖給回收了。 bm = Bitmap.createScaledBitmap(oldBitmap, Math.max(1, (int) (bm.getWidth() * scale + 0.5f)), Math.max(1, (int) (bm.getHeight() * scale + 0.5f)), true); if (bm != oldBitmap) oldBitmap.recycle(); } //設定縮放後的Bitmap的密度 bm.setDensity(targetDensity); } return bm; } |
finishDecode
方法很簡單,就是根據縮放因子,在原來Bitmap的基礎上,又新建了一個Bitmap,然後把原圖回收了。這裡新建立的Bitmap就是最終的Bitmap。這種方式會造成在java層重新分配記憶體,顯然不是很好。所以在Android4.4之後,都是在native層完成對Bitmap的縮放處理的。(也就是Android4.4之後,同時支援複用已有Bitmap和在native層對原圖進行縮放處理,後面進行介紹)。
下面看下native層的解碼方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 |
static jobject doDecode(JNIEnv* env, SkStream* stream, jobject padding, jobject options, bool allowPurgeable, bool forcePurgeable = false, bool applyScale = false, float scale = 1.0f) { int sampleSize = 1; bool isMutable = false; bool willScale = applyScale && scale != 1.0f; //獲取java類Options中的配置 prefConfig = GraphicsJNI::getNativeBitmapConfig(env, jconfig); isMutable = env->GetBooleanField(options, gOptions_mutableFieldID); //希望被複用的java層Bitmap javaBitmap = env->GetObjectField(options, gOptions_bitmapFieldID); //可見,在native層不支援既進行縮放又進行Bitmap複用。 if (willScale && javaBitmap != NULL) { return nullObjectReturn("Cannot pre-scale a reused bitmap"); } //獲取圖片解碼器,png,jpeg等圖片格式都有不同的實現類 SkImageDecoder* decoder = SkImageDecoder::Factory(stream); decoder->setSampleSize(sampleSize); //java堆記憶體分配器 JavaPixelAllocator javaAllocator(env); SkBitmap* bitmap; bool useExistingBitmap = false; if (javaBitmap == NULL) { bitmap = new SkBitmap;//若沒有複用的Bitmap,則建立一個新的SkBitmap } else { if (sampleSize != 1) { //可見sampleSize必須為1 return nullObjectReturn("SkImageDecoder: Cannot reuse bitmap with sampleSize != 1"); } //獲取被複用的native層Bitmap bitmap = (SkBitmap*) env->GetIntField(javaBitmap, gBitmap_nativeBitmapFieldID); // only reuse the provided bitmap if it is immutable if (!bitmap->isImmutable()) { //可見只有可變的bitmap才能被複用 useExistingBitmap = true; // options.config,會被被複用的bitmap的配置所替代 prefConfig = bitmap->getConfig(); } else { ALOGW("Unable to reuse an immutable bitmap as an image decoder target."); bitmap = new SkBitmap; } } SkBitmap* decoded; if (willScale) { //有縮放,說明沒有bitmap複用,直接建立新的 decoded = new SkBitmap; } else { decoded = bitmap; } //解碼得到原始尺寸的Bitmap if (!decoder->decode(stream, decoded, prefConfig, decodeMode, javaBitmap != NULL)) { return nullObjectReturn("decoder->decode returned false"); } int scaledWidth = decoded->width(); int scaledHeight = decoded->height(); //計算縮放後的Bitmap的寬度和高度 if (willScale && mode != SkImageDecoder::kDecodeBounds_Mode) { //這裡在計算縮放後的寬高時,有個精度問題 scaledWidth = int(scaledWidth * scale + 0.5f); scaledHeight = int(scaledHeight * scale + 0.5f); } // update options (if any) if (options != NULL) { //這裡主要是把Bitmap的寬度,高度和圖片型別設定到java層的Options裡面 env->SetIntField(options, gOptions_widthFieldID, scaledWidth); env->SetIntField(options, gOptions_heightFieldID, scaledHeight); env->SetObjectField(options, gOptions_mimeFieldID,getMimeTypeString(env, decoder->getFormat())); } // if we're in justBounds mode, return now (skip the java bitmap) //可見若java層Options.inJustDecodeBounds為true,則就直接返回了,不在生成java層的bitmap if (mode == SkImageDecoder::kDecodeBounds_Mode) { return NULL; } if (willScale) { //計算出縮放因子 const float sx = scaledWidth / float(decoded->width()); const float sy = scaledHeight / float(decoded->height()); SkBitmap::Config config = decoded->config(); switch (config) { case SkBitmap::kNo_Config: case SkBitmap::kIndex8_Config: case SkBitmap::kRLE_Index8_Config: config = SkBitmap::kARGB_8888_Config; break; default: break; } //這裡操作的時bitmap變數哈,設定縮放後的寬高 bitmap->setConfig(config, scaledWidth, scaledHeight); bitmap->setIsOpaque(decoded->isOpaque()); //根據新的寬高,重新分配儲存畫素資料的空間 if (!bitmap->allocPixels(&javaAllocator, NULL)) { return nullObjectReturn("allocation failed for scaled bitmap"); } bitmap->eraseColor(0); SkPaint paint; paint.setFilterBitmap(true); //這裡把解碼出來的原始尺寸的decoded Bitmap draw到縮放(sx, sy)倍後的bitmap上,最終完成了對原始圖片的縮放 SkCanvas canvas(*bitmap); canvas.scale(sx, sy); canvas.drawBitmap(*decoded, 0.0f, 0.0f, &paint); } //若複用了bitmap,那麼返回被複用的java層bitmap(也就是說複用成功後,返回的Bitmap就是被複用的Bitmap,只不過底層的畫素資料都發生了改變) if (useExistingBitmap) { // If a java bitmap was passed in for reuse, pass it back return javaBitmap; } // now create the java bitmap //否則,根據縮放後的native層bitmap(SkBitmap)生成java層Bitmap並返回(這裡是呼叫到了java層Bitmap的私有建構函式) return GraphicsJNI::createBitmap(env, bitmap, javaAllocator.getStorageObj(), isMutable, ninePatchChunk, layoutBounds, -1); } |
OK,通過上述程式碼,我們瞭解到以下幾點資訊:
- Android 4.3在native層不支援即複用已有Bitmap,又進行縮放。
- 若縮放因子為1,那麼在進行Bitmap複用時(假設滿足複用條件),會直接在被複用Bitmap上進行解碼操作(主要是修改被複用Bitmap的畫素資料資訊),同時返回到java層的就是被複用的Bitmap。(即如果被複用的Bitmap == 返回的被載入的Bitmap,那麼說明覆用成功了)。
- 若縮放因子大於1,且沒有Bitmap複用,那麼首先會解碼生成一個圖片原始寬高的
SkBitmap
,然後在再根據縮放因子,通過繪製的方式,把原始寬高的Bitmap會繪製到經過Scale之後的SkCanvas
上,以得到一個縮放後的SkBitmap
,最後呼叫java層bitmap的構造方法,建立java層的Bitmap,然後返回到放大呼叫處。
上面我們提出了幾點在Android4.4之前複用Bitmap的限制,在doDecode
方法中基本都得到了驗證。唯獨對寬高必須相等的限制沒有見到。其實這個限制是在解碼得到原始寬高的Bitmap時進行的,即上面程式碼中的decoder->decode
方法中。這裡的decoder解碼器(SkImageDecoder是父型別)根據解碼不同格式的圖片,是不同的實現類物件。下面我們看一下SkImageDecoder_libpng
類的實現(png格式的解碼器)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
bool SkPNGImageDecoder::onDecode(SkStream* sk_stream, SkBitmap* decodedBitmap, Mode mode) { const int sampleSize = this->getSampleSize(); //origWidth和origHeight表示被解碼圖片的原始寬高 SkScaledBitmapSampler sampler(origWidth, origHeight, sampleSize); //decodedBitmap->getPexels() 嘗試獲取點陣圖的畫素緩衝區首地址,如果取出來為 NULL,說明這個bitmap是新建立的,因為new SkBitmap只建立物件,不分配儲存畫素的緩衝區,否則就說明這個是可以複用的點陣圖。 decodedBitmap->lockPixels(); void* rowptr = (void*) decodedBitmap->getPixels(); bool reuseBitmap = (rowptr != NULL); decodedBitmap->unlockPixels(); //若是複用已有的點陣圖,那麼這裡就會進行寬高必須相等的判斷。 if (reuseBitmap && (sampler.scaledWidth() != decodedBitmap->width() || sampler.scaledHeight() != decodedBitmap->height())) { // Dimensions must match return false; } //若不是複用已有Bitmap,那麼就需要設定SkBitmap的寬、高、config等配置資訊。 if (!reuseBitmap) { decodedBitmap->setConfig(config, sampler.scaledWidth(),sampler.scaledHeight(), 0); } //若是Options.inJustDecodeBounds為true,那麼到此就結束了,因為上面寬和高已經計算出來了。 if (SkImageDecoder::kDecodeBounds_Mode == mode) { return true; } //若不是複用已有Bitmap,則說明還沒有儲存畫素資料的記憶體空間,因此這裡需要進行記憶體分配 if (!reuseBitmap) { if (!this->allocPixelRef(decodedBitmap,SkBitmap::kIndex8_Config == config ? colorTable : NULL)) { return false; } } //後續程式碼應該就是真正去解碼png格式圖片,生成畫素資料的核心邏輯了,這塊暫時不進行介紹 |
上述程式碼就是解碼png格式圖片的關鍵程式碼,裡面印證了複用Bitmap時,寬高必須相等的說法。
通過上述程式碼的分析,我們可以有以下(反覆強調)結論:
- 若是直接解碼圖片(不復用已有圖片,即opts.inBitmap為null),則是在naive層處理對原圖的縮放操作。
- 若是通過複用已有Bitmap來解碼圖片,則是在java層處理對原圖的縮放操作(finishDecode方法)。
Android4.4 API19
Android從API19開始,放寬了對Bitmap複用的限制。下面我們看下API19對Bitmap複用的兩個限制是在哪裡實現的,以及該版本是如何對原圖進行縮放的。
從decodeResource -> decodeResourceStream
方法的實現和API18類似,這裡不再贅述,差異點出現在decodeStream
方法。如下所示(精簡後):
1 2 3 4 5 6 7 8 9 10 11 |
Bitmap decodeStream(InputStream is, Rect outPadding, Options opts) { //最終呼叫到native層進行解碼 Bitmap bm = decodeStreamInternal(is, outPadding, opts); } //這裡很重要,說明若是Bitmap複用失敗,會丟擲異常,並返回null值,所以在複用Bitmap時要特別留意。 if (bm == null && opts != null && opts.inBitmap != null) { throw new IllegalArgumentException("Problem decoding into existing bitmap"); } //該方法做了finishDecode方法除了縮放Bitmap之外所有的事 setDensityFromOptions(bm, opts); return bm; |
API19版本的decodeStream和API18相比,最明顯的就是刪除了Java層縮放Bitmap的邏輯(finishDecode方法),因此可以確定對Bitmap的縮放處理都是在native層處理的。下面我們繼續看下native方法的實現(極度精簡):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 |
static jobject doDecode(JNIEnv* env, SkStreamRewindable* stream, jobject padding, jobject options, bool allowPurgeable, bool forcePurgeable = false) { SkBitmap::Config prefConfig = SkBitmap::kARGB_8888_Config;//預設conifg配置 bool isMutable = false; float scale = 1.0f; jobject javaBitmap = NULL; sampleSize = env->GetIntField(options, gOptions_sampleSizeFieldID); if (optionsJustBounds(env, options)) { mode = SkImageDecoder::kDecodeBounds_Mode;//是否只獲取圖片的寬高 } if (options != NULL) { sampleSize = env->GetIntField(options, gOptions_sampleSizeFieldID); if (optionsJustBounds(env, options)) { mode = SkImageDecoder::kDecodeBounds_Mode; } // initialize these, in case we fail later on env->SetIntField(options, gOptions_widthFieldID, -1); env->SetIntField(options, gOptions_heightFieldID, -1); env->SetObjectField(options, gOptions_mimeFieldID, 0); //獲取一些java層Options的屬性值 jobject jconfig = env->GetObjectField(options, gOptions_configFieldID); prefConfig = GraphicsJNI::getNativeBitmapConfig(env, jconfig); isMutable = env->GetBooleanField(options, gOptions_mutableFieldID); //獲取被複用的java層bitmap javaBitmap = env->GetObjectField(options, gOptions_bitmapFieldID); //若java層options.inScaled為true,則進行縮放處理 if (env->GetBooleanField(options, gOptions_scaledFieldID)) { const int density = env->GetIntField(options, gOptions_densityFieldID); const int targetDensity = env->GetIntField(options, gOptions_targetDensityFieldID); const int screenDensity = env->GetIntField(options, gOptions_screenDensityFieldID); //這裡很關鍵,縮放因子是targetDensity / density if (density != 0 && targetDensity != 0 && density != screenDensity) { scale = (float) targetDensity / density; } } } const bool willScale = scale != 1.0f; SkImageDecoder* decoder = SkImageDecoder::Factory(stream);//建立解碼器 //為解碼器設定一些屬性 decoder->setSampleSize(sampleSize); SkBitmap* outputBitmap = NULL; unsigned int existingBufferSize = 0; if (javaBitmap != NULL) { //獲取被複用的native層Skbitmap outputBitmap = (SkBitmap*) env->GetIntField(javaBitmap, gBitmap_nativeBitmapFieldID); if (outputBitmap->isImmutable()) { //API19還存在的限制,被複用的bitmap必須是可變的 ALOGW("Unable to reuse an immutable bitmap as an image decoder target."); javaBitmap = NULL; outputBitmap = NULL; } else { //獲取被複用bitmap儲存畫素資料的緩衝區大小,即有多少記憶體可以被複用(作為後續判斷是否可被複用的依據)。 existingBufferSize = GraphicsJNI::getBitmapAllocationByteCount } } //若沒有可被複用的bitmap,那就直接建立一個。 SkAutoTDelete<SkBitmap> adb(outputBitmap == NULL ? new SkBitmap : NULL); if (outputBitmap == NULL) outputBitmap = adb.get(); //Java堆畫素記憶體分配器 JavaPixelAllocator javaAllocator(env); //迴圈複用的畫素記 |