Android Bitmap變遷與原理解析(4.x-8.x-++)

App開發不可避免的要和圖片打交道,由于其占用內存非常大,管理不當很容易導致內存不足,最后OOM,圖片的背后其實是Bitmap,它是Android中最能吃內存的對象之一,也是很多OOM的元兇,不過,在不同的Android版本中,Bitmap或多或少都存在差異,尤其是在其內存分配上,了解其中的不用跟原理能更好的指導圖片管理。先看Google官方文檔的說明:

On Android 2.3.3 (API level 10) and lower, the backing pixel data for a Bitmap is stored in native memory. It is separate from the Bitmap itself, which is stored in the Dalvik heap. The pixel data in native memory is not released in a predictable manner, potentially causing an application to briefly exceed its memory limits and crash. From Android 3.0 (API level 11) through Android 7.1 (API level 25), the pixel data is stored on the Dalvik heap along with the associated Bitmap. In Android 8.0 (API level 26), and higher, the Bitmap pixel data is stored in the native heap.

大意就是: 2.3之前的像素存儲需要的內存是在native上分配的,并且生命周期不太可控,可能需要用戶自己回收。 2.3-7.1之間,Bitmap的像素存儲在Dalvik的Java堆上,當然,4.4之前的甚至能在匿名共享內存上分配(Fresco采用),而8.0之后的像素內存又重新回到native上去分配,不需要用戶主動回收,8.0之后圖像資源的管理更加優秀,極大降低了OOM。Android 2.3.3已經屬于過期技術,不再分析,本文主要看4.x之后的手機系統。

Android 8.0前后Bitmap內存增長曲線直觀對比

Bitmap內存分配一個很大的分水嶺是在Android 8.0,可以用一段代碼來模擬器Bitmap無限增長,最終OOM,或者Crash退出。通過在不同版本上的表現,期待對Bitmap內存分配有一個直觀的了解,示例代碼如下:

   @onClick(R.id.increase)
       void increase{
         Map<String, Bitmap> map = new HashMap<>();
         for(int i=0 ; i<10;i++){
           Bitmap bitmap = BitmapFactory.decodeResource(getResources(),                         R.mipmap.green);
            map.put("" + System.currentTimeMillis(), bitmap);
            }
        }

Nexus5 Android 6.0的表現

不斷的解析圖片,并持有Bitmap引用,會導致內存不斷上升,通過Android Profiler工具簡單看一下上圖內存分配狀況,在某一個點內存分配情況如下:

1526644329066.jpg

簡單總結下內存占比

內存 大小
Total 211M
Java內存 157.2M
native內存 3.7M
Bitmap內存 145.9M(152663617 byte)
Graphics內存(一般是Fb對應的,App不需要考慮) 45.1M(152663617 byte)

從上表可以看到絕大數內存都是由Bitmap,并且位于虛擬機的heap中,其實是因為在6.0中,bitmap的像素數據都是以byte的數組的形式存在java 虛擬機的heap中。內存無限增大,直到OOM崩潰的時候,內存狀況入下:

1526641659822.jpg
內存 大小
Total 546.2M
Java內存 496.8M
native內存 3.3M
Graphics內存(一般是Fb對應的,App不需要考慮) 45.1M

可見,增長的一直是Java堆中的內存,也就是Bitmap在Dalvik棧中分配的內存,等到Dalvik達到虛擬機內存上限的時候,在Dalvik會拋出OOM異常:

1526641743077.jpg

可見,對于Android6.0,Bitmap的內存分配基本都在Java層。然后,再看一下Android 8.0的Bitmap分配。

Nexus6p Android 8.0 的表現

In Android 8.0 (API level 26), and higher, the Bitmap pixel data is stored in the native heap.

從官方文檔中我們知道,Android8.0之后最大的改進就是Bitmap內存分配的位置:從Java堆轉移到了native堆棧,直觀分配圖如下

61526525051_.pic.jpg
內存 大小
Total 1.2G
Java內存 0G
native內存 1.1G
Graphics內存(一般是Fb對應的,App不需要考慮) 0.1G

很明顯,Bitmap內存的增加基本都在native層,隨著Bitmap內存占用的無限增長,App最終無法從系統分配到內存,最后會導致崩潰,看一下崩潰的時候內存占用:

51526524893_.pic.jpg
內存 大小
Total 1.9G
Java內存 0G
native內存 1.9G
Graphics內存(一般是Fb對應的,App不需要考慮) 0.1G

可見一個APP內存的占用驚人的達到了1.9G,并且幾乎全是native內存,這個其實就是Google在8.0做的最大的一個優化,我們知道Java虛擬機一般是有一個上限,但是由于Android同時能運行多個APP,這個上限一般不會太高,拿nexus6p而言,一般是如下配置

dalvik.vm.heapstartsize=8m
dalvik.vm.heapgrowthlimit=192m
dalvik.vm.heapsize=512m
dalvik.vm.heaptargetutilization=0.75
dalvik.vm.heapminfree=512k
dalvik.vm.heapmaxfree=8m

如果沒有在AndroidManifest中啟用largeheap,那么Java 堆內存達到192M的時候就會崩潰,對于現在動輒4G的手機而言,存在嚴重的資源浪費,ios的一個APP幾乎能用近所有的可用內存(除去系統開支),8.0之后,Android也向這個方向靠攏,最好的下手對象就是Bitmap,因為它是耗內存大戶。圖片內存被轉移到native之后,一個APP的圖片處理不僅能使用系統絕大多數內存,還能降低Java層內存使用,減少OOM風險。不過,內存無限增長的情況下,也會導致APP崩潰,但是這種崩潰已經不是OOM崩潰了,Java虛擬機也不會捕獲,按道理說,應該屬于linux的OOM了。從崩潰時候的Log就能看得出與Android6.0的區別:

1526641932348.jpg

可見,這個時候崩潰并不為Java虛擬機控制,直接進程死掉,不會有Crash彈框。其實如果在Android6.0的手機上,在native分配內存,也會達到相同的效果,也就是說native的內存不影響java虛擬機的OOM。

Android 6.0模擬native內存OOM

在直接native內存分配,并且不釋放,模擬代碼如下:

void increase(){
     int size=1024*1024*100;
    char *Ptr = NULL;
    Ptr = (char *)malloc(size * sizeof(char));
    for(int i=0;i<size ;i++) {
      *(Ptr+i)=i%30;
    }
    for(int i=0;i<1024*1024 ;i++) {
       if(i%100==0)
      LOGI(" malloc  - %d" ,*(Ptr+i));
    }
}

只malloc,不free,這種情況下Android6.0的內存增長如下:

image.png
內存 大小
Total 750m
Java內存 1.9m
native內存 703M
Graphics內存(一般是Fb對應的,App不需要考慮) 44.1M

Total內存750m,已經超過Nexus5 Android6.0 Dalvik虛擬機內存上限,但APP沒有崩潰,可見native內存的增長并不會導致java虛擬機的OOM,在native層,oom的時機是到系統內存用盡的時候:

屏幕快照 2018-05-17 下午7.44.53.png

可見對于6.0的系統,一個APP也是能夠耗盡系統所有內存的,下面來看下Bitmap內存分配原理,為什么8.0前后差別這么大。

Bitmap內存分配原理

8.0之前Bitmap內存分配原理

其實,通過Bitmap的成員列表,就能看出一點眉目,Bitmap中有個byte[] mBuffer,其實就是用來存儲像素數據的,很明顯它位于java heap中

public final class Bitmap implements Parcelable {
    private static final String TAG = "Bitmap";
     ...
    private byte[] mBuffer;
     ...
    }

接下來,通過手動創建Bitmap,進行分析:Bitmap.java

public static Bitmap createBitmap(int width, int height, Config config) {
    return createBitmap(width, height, config, true);
}
屏幕快照 2018-05-22 上午11.06.00.png

Java層Bitmap的創建最終還是會走向native層:Bitmap.cpp

 static jobject Bitmap_creator(JNIEnv* env, jobject, jintArray jColors,
                               jint offset, jint stride, jint width, jint height,
                               jint configHandle, jboolean isMutable) {
     SkColorType colorType = GraphicsJNI::legacyBitmapConfigToColorType(configHandle);
      ... 
 
     SkBitmap Bitmap;
     Bitmap.setInfo(SkImageInfo::Make(width, height, colorType, kPremul_SkAlphaType));
        <!--關鍵點1 像素內存分配-->
     Bitmap* nativeBitmap = GraphicsJNI::allocateJavaPixelRef(env, &Bitmap, NULL);
     if (!nativeBitmap) {
         return NULL;
     }
      ... 
     <!--獲取分配地址-->
     jbyte* addr = (jbyte*) env->CallLongMethod(gVMRuntime, gVMRuntime_addressOf, arrayObj);
     ...
     <!--創建Bitmap-->
     android::Bitmap* wrapper = new android::Bitmap(env, arrayObj, (void*) addr,
             info, rowBytes, ctable);
     wrapper->getSkBitmap(Bitmap);
     Bitmap->lockPixels();
     return wrapper;
 }

這里只看關鍵點1,像素內存的分配:GraphicsJNI::allocateJavaPixelRef從這個函數名可以就可以看出,是在Java層分配,跟進去,也確實如此:

android::Bitmap* GraphicsJNI::allocateJavaPixelRef(JNIEnv* env, SkBitmap* bitmap,
                                             SkColorTable* ctable) {
    const SkImageInfo& info = bitmap->info();
    if (info.fColorType == kUnknown_SkColorType) {
        doThrowIAE(env, "unknown bitmap configuration");
        return NULL;
    }

    size_t size;
    if (!computeAllocationSize(*bitmap, &size)) {
        return NULL;
    }

    // we must respect the rowBytes value already set on the bitmap instead of
    // attempting to compute our own.
    const size_t rowBytes = bitmap->rowBytes();
   <!--關鍵點1 ,創建Java層字節數據,作為數據存儲單元-->
    jbyteArray arrayObj = (jbyteArray) env->CallObjectMethod(gVMRuntime,
                                                             gVMRuntime_newNonMovableArray,
                                                             gByte_class, size);
    if (env->ExceptionCheck() != 0) {
        return NULL;
    }
    SkASSERT(arrayObj);
    jbyte* addr = (jbyte*) env->CallLongMethod(gVMRuntime, gVMRuntime_addressOf, arrayObj);
    if (env->ExceptionCheck() != 0) {
        return NULL;
    }
    SkASSERT(addr);
    android::Bitmap* wrapper = new android::Bitmap(env, arrayObj, (void*) addr,
            info, rowBytes, ctable);
    wrapper->getSkBitmap(bitmap);
    // since we're already allocated, we lockPixels right away
    // HeapAllocator behaves this way too
    bitmap->lockPixels();

    return wrapper;
}

由于只關心內存分配,同樣只看關鍵點1,這里其實就是在native層創建Java層byte[],并將這個byte[]作為像素存儲結構,之后再通過在native層構建Java Bitmap對象的方式,將生成的byte[]傳遞給Bitmap.java對象:

jobject GraphicsJNI::createBitmap(JNIEnv* env, android::Bitmap* bitmap,
        int bitmapCreateFlags, jbyteArray ninePatchChunk, jobject ninePatchInsets,
        int density) {
    ...<!--關鍵點1,構建java Bitmap對象,并設置byte[] mBuffer-->
    jobject obj = env->NewObject(gBitmap_class, gBitmap_constructorMethodID,
            reinterpret_cast<jlong>(bitmap), bitmap->javaByteArray(),
            bitmap->width(), bitmap->height(), density, isMutable, isPremultiplied,
            ninePatchChunk, ninePatchInsets);
    hasException(env); // For the side effect of logging.
    return obj;
}

以上就是8.0之前的內存分配,其實4.4以及之前的更亂,下面再看下8.0之后的Bitmap是什么原理。

8.0之后Bitmap內存分配有什么新特點

其實從8.0的Bitmap.java類也能看出區別,之前的 private byte[] mBuffer成員不見了,取而代之的是private final long mNativePtr,也就說,Bitmap.java只剩下一個殼了,具體如下:

public final class Bitmap implements Parcelable {
    ...
    // Convenience for JNI access
    private final long mNativePtr;
    ...
 }

之前說過8.0之后的內存分配是在native,具體到代碼是怎么樣的表現呢?流程與8.0之前基本類似,區別在native分配時:

屏幕快照 2018-05-22 下午1.55.15.png
static jobject Bitmap_creator(JNIEnv* env, jobject, jintArray jColors,
                              jint offset, jint stride, jint width, jint height,
                              jint configHandle, jboolean isMutable,
                              jfloatArray xyzD50, jobject transferParameters) {
    SkColorType colorType = GraphicsJNI::legacyBitmapConfigToColorType(configHandle);
     ...
     <!--關鍵點1 ,native層創建bitmap,并分配native內存-->
    sk_sp<Bitmap> nativeBitmap = Bitmap::allocateHeapBitmap(&Bitmap);
    if (!nativeBitmap) {
        return NULL;
    }
    ...
    return createBitmap(env, nativeBitmap.release(), getPremulBitmapCreateFlags(isMutable));
}

看一下allocateHeapBitmap如何分配內存

static sk_sp<Bitmap> allocateHeapBitmap(size_t size, const SkImageInfo& info, size_t rowBytes) {
    <!--關鍵點1 直接calloc分配內存-->
    void* addr = calloc(size, 1);
    if (!addr) {
        return nullptr;
    }
    <!--關鍵點2 創建native Bitmap-->
    return sk_sp<Bitmap>(new Bitmap(addr, size, info, rowBytes));
}

可以看出,8.0之后,Bitmap像素內存的分配是在native層直接調用calloc,所以其像素分配的是在native heap上, 這也是為什么8.0之后的Bitmap消耗內存可以無限增長,直到耗盡系統內存,也不會提示Java OOM的原因。

8.0之后的Bitmap內存回收機制

NativeAllocationRegistry是Android 8.0引入的一種輔助自動回收native內存的一種機制,當Java對象因為GC被回收后,NativeAllocationRegistry可以輔助回收Java對象所申請的native內存,拿Bitmap為例,入下:

Bitmap(long nativeBitmap, int width, int height, int density,
        boolean isMutable, boolean requestPremultiplied,
        byte[] ninePatchChunk, NinePatch.InsetStruct ninePatchInsets) {
    ...
    mNativePtr = nativeBitmap;
    long nativeSize = NATIVE_ALLOCATION_SIZE + getAllocationByteCount();
    <!--輔助回收native內存-->
    NativeAllocationRegistry registry = new NativeAllocationRegistry(
        Bitmap.class.getClassLoader(), nativeGetNativeFinalizer(), nativeSize);
    registry.registerNativeAllocation(this, nativeBitmap);
   if (ResourcesImpl.TRACE_FOR_DETAILED_PRELOAD) {
        sPreloadTracingNumInstantiatedBitmaps++;
        sPreloadTracingTotalBitmapsSize += nativeSize;
    }
}

當然這個功能也要Java虛擬機的支持,有機會再分析。

Android 4.4之前其實Bitmap也可在native(偽)分配內存

其實在Android5.0之前,Bitmap也是可以在native分配內存的,一個典型的例子就是Fresco,Fresco為了提高5.0之前圖片處理的性能,就很有效的利用了這個特性,不過由于不太成熟,在5.0之后廢棄,直到8.0重新拾起來(新方案),與這個特性有關的兩個屬性是BitmapFactory.Options中的inPurgeable與inInputShareable,具體的不在分析。過期技術,等于垃圾,有興趣,可以自行分析。

         /**
         * @deprecated As of {@link android.os.Build.VERSION_CODES#LOLLIPOP}, this is
         * ignored.
         *
         * In {@link android.os.Build.VERSION_CODES#KITKAT} and below, if this
         * is set to true, then the resulting bitmap will allocate its
         * pixels such that they can be purged if the system needs to reclaim
         * memory. In that instance, when the pixels need to be accessed again
         * (e.g. the bitmap is drawn, getPixels() is called), they will be
         * automatically re-decoded.
         *
         * <p>For the re-decode to happen, the bitmap must have access to the
         * encoded data, either by sharing a reference to the input
         * or by making a copy of it. This distinction is controlled by
         * inInputShareable. If this is true, then the bitmap may keep a shallow
         * reference to the input. If this is false, then the bitmap will
         * explicitly make a copy of the input data, and keep that. Even if
         * sharing is allowed, the implementation may still decide to make a
         * deep copy of the input data.</p >
         *
         * <p>While inPurgeable can help avoid big Dalvik heap allocations (from
         * API level 11 onward), it sacrifices performance predictability since any
         * image that the view system tries to draw may incur a decode delay which
         * can lead to dropped frames. Therefore, most apps should avoid using
         * inPurgeable to allow for a fast and fluid UI. To minimize Dalvik heap
         * allocations use the {@link #inBitmap} flag instead.</p >
         *
         * <p class="note"><strong>Note:</strong> This flag is ignored when used
         * with {@link #decodeResource(Resources, int,
         * android.graphics.BitmapFactory.Options)} or {@link #decodeFile(String,
         * android.graphics.BitmapFactory.Options)}.</p >
         */
        @Deprecated
        public boolean inPurgeable;

        /**
         * @deprecated As of {@link android.os.Build.VERSION_CODES#LOLLIPOP}, this is
         * ignored.
         *
         * In {@link android.os.Build.VERSION_CODES#KITKAT} and below, this
         * field works in conjuction with inPurgeable. If inPurgeable is false,
         * then this field is ignored. If inPurgeable is true, then this field
         * determines whether the bitmap can share a reference to the input
         * data (inputstream, array, etc.) or if it must make a deep copy.
         */
        @Deprecated
        public boolean inInputShareable;

總結

  • 8.0之前的Bitmap像素數據基本存儲在Java heap
  • 8.0之后的 Bitmap像素數據基本存儲在native heap
  • 4.4可以通過inInputShareable、inPurgeable讓Bitmap的內存在native層分配(已廢棄)

作者:看書的小蝸牛
Android Bitmap變遷與原理解析(4.x-8.x)

僅供參考,歡迎指正

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,572評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,071評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,409評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,569評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,360評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,895評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,979評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,123評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,643評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,559評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,742評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,250評論 5 356
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,981評論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,363評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,622評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,354評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,707評論 2 370

推薦閱讀更多精彩內容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,592評論 25 707
  • HereAndroid的內存優化是性能優化中很重要的一部分,而避免OOM又是內存優化中比較核心的一點。這是一篇關于...
    HarryXR閱讀 3,830評論 1 24
  • 嬉笑怒罵,有你,有我 人間真情,何須言語 美麗,可笑,都想與你分享 哈哈哈哈哈哈哈 想象得到你的開懷大笑 我不得不...
    張九九的娘閱讀 264評論 0 0
  • 將日期時間轉化為字符串 將時間字符串轉換為日期 時間格式 演練 (查看http://blog.csdn.net/c...
    botherbox閱讀 15,745評論 5 11
  • 前段時間,看了一部宮崎駿導演的動漫《起風了》,感觸頗深。片子是描述日本航空之父,零式戰機的開發者掘越二郎年輕時的故...
    故道浮云閱讀 360評論 10 5