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工具簡單看一下上圖內存分配狀況,在某一個點內存分配情況如下:
簡單總結下內存占比
內存 | 大小 |
---|---|
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崩潰的時候,內存狀況入下:
內存 | 大小 |
---|---|
Total | 546.2M |
Java內存 | 496.8M |
native內存 | 3.3M |
Graphics內存(一般是Fb對應的,App不需要考慮) | 45.1M |
可見,增長的一直是Java堆中的內存,也就是Bitmap在Dalvik棧中分配的內存,等到Dalvik達到虛擬機內存上限的時候,在Dalvik會拋出OOM異常:
可見,對于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堆棧,直觀分配圖如下
內存 | 大小 |
---|---|
Total | 1.2G |
Java內存 | 0G |
native內存 | 1.1G |
Graphics內存(一般是Fb對應的,App不需要考慮) | 0.1G |
很明顯,Bitmap內存的增加基本都在native層,隨著Bitmap內存占用的無限增長,App最終無法從系統分配到內存,最后會導致崩潰,看一下崩潰的時候內存占用:
內存 | 大小 |
---|---|
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的區別:
可見,這個時候崩潰并不為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的內存增長如下:
內存 | 大小 |
---|---|
Total | 750m |
Java內存 | 1.9m |
native內存 | 703M |
Graphics內存(一般是Fb對應的,App不需要考慮) | 44.1M |
Total內存750m,已經超過Nexus5 Android6.0 Dalvik虛擬機內存上限,但APP沒有崩潰,可見native內存的增長并不會導致java虛擬機的OOM,在native層,oom的時機是到系統內存用盡的時候:
可見對于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);
}
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分配時:
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)
僅供參考,歡迎指正