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

看書的小蝸牛發表於2018-05-22

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)

僅供參考,歡迎指正

相關文章