1. 程式人生 > >Android Bitmap那些事

Android Bitmap那些事

在平時的開發中,Bitmap是我們接觸最多的話題之一,因為它時不時地就來個OOM,讓我們猝不及防。因此有必要來一次徹底的學習,搞清楚Bitmap的一些本質。
本文主要想講清楚兩點內容:

  1. Bitmap到底佔多大記憶體
  2. Bitmap複用的限制

OK,開始之前先介紹下解碼圖片時的控制類BitmapFactory.Options

BitmapFactory.Options類解析

OptionsBitmapFactory從輸入源中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
;
/* 這裡返回的bmp是null */ Bitmap bmp = BitmapFactory.decodeFile(path, options); int width = options.outWidth; int height = options.outHeight;

這裡若inJustDecodeBounds為true,則outWidth表示沒有經過Scale的Bitmap的原始寬高(即我們通過圖片編輯軟體看到的寬高),否則則為載入到記憶體後,真實的Bitmap寬高(經過Scale之後的寬高)。

outMimeType

表示被載入圖片的格式,例如:若載入png格式的圖片,則為
image/png

;若載入jpg格式的圖片,則為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及以上,存在兩個限制條件:

  1. 被複用的Bitmap必須是Mutable。違反此限制,不會丟擲異常,且會返回新申請記憶體的Bitmap。
  2. 被複用的Bitmap的記憶體大小(通過Bitmap.getAllocationByteCount方法獲得,API19及以上才有)必須大於等於被載入的Bitmap的記憶體大小。違反此限制,將會導致複用失敗,丟擲異常IllegalArgumentException(Problem decoding into existing bitmap)

在API11 ~ API19之間,還存在額外的限制:

  1. 被複用的Bitmap的寬高必須等於被載入的Bitmap的原始寬高。(注意這裡是指原始寬高,即沒進行縮放之前的寬高)
  2. 被載入Bitmap的Options.inSampleSize必須明確指定為1。
  3. 被載入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佔用記憶體大小的關鍵因素有以下幾點:

  1. 圖片的原始寬高(即我們在圖片編輯軟體中看到的寬高)
  2. 解碼圖片時的Config配置(即每個畫素佔用幾個位元組)
  3. 解碼圖片時的縮放因子(即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,可以得出:

  1. Bitmap複用成功了,因為bitmap和bitmapAIO是相同的物件。
  2. 圖片a佔用記憶體8294400,圖片b(寬和高各縮小一半)佔用記憶體2073600,正好是圖片a所佔記憶體的1/4。
  3. 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方法很關鍵,從中可以獲取幾點關鍵資訊:

  1. 若是直接解碼圖片(不復用已有圖片,即opts.inBitmap為null),那麼通過native方法解碼圖片時,支援把縮放因子作為引數傳遞到native層,並且後續在java層直接返回了native層解碼出來的Bitmap。說明在native層處理了對圖片的縮放
  2. 若是通過複用已有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,通過上述程式碼,我們瞭解到以下幾點資訊:

  1. Android 4.3在native層不支援即複用已有Bitmap,又進行縮放。
  2. 若縮放因子為1,那麼在進行Bitmap複用時(假設滿足複用條件),會直接在被複用Bitmap上進行解碼操作(主要是修改被複用Bitmap的畫素資料資訊),同時返回到java層的就是被複用的Bitmap。(即如果被複用的Bitmap == 返回的被載入的Bitmap,那麼說明覆用成功了)。
  3. 若縮放因子大於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時,寬高必須相等的說法。

通過上述程式碼的分析,我們可以有以下(反覆強調)結論:

  1. 若是直接解碼圖片(不復用已有圖片,即opts.inBitmap為null),則是在naive層處理對原圖的縮放操作。
  2. 若是通過複用已有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);
//迴圈複用的畫素記