App開發不可避免的要和圖片打交道,因爲其佔用內存很是大,管理不當很容易致使內存不足,最後OOM,圖片的背後實際上是Bitmap,它是Android中最能吃內存的對象之一,也是不少OOM的元兇,不過,在不一樣的Android版本中,Bitmap或多或少都存在差別,尤爲是在其內存分配上,瞭解其中的不用跟原理能更好的指導圖片管理。先看Google官方文檔的說明:java
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.linux
大意就是: 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
Bitmap內存分配一個很大的分水嶺是在Android 8.0,能夠用一段代碼來模擬器Bitmap無限增加,最終OOM,或者Crash退出。經過在不一樣版本上的表現,期待對Bitmap內存分配有一個直觀的瞭解,示例代碼以下:ios
@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);
}
}
複製代碼
不斷的解析圖片,並持有Bitmap引用,會致使內存不斷上升,經過Android Profiler工具簡單看一下上圖內存分配情況,在某一個點內存分配狀況以下:數組
簡單總結下內存佔比app
內存 | 大小 |
---|---|
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崩潰的時候,內存情況入下:ide
內存 | 大小 |
---|---|
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分配。工具
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。
在直接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的成員列表,就能看出一點眉目,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.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的緣由。
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虛擬機的支持,有機會再分析。
其實在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;
複製代碼
做者:看書的小蝸牛 Android Bitmap變遷與原理解析(4.x-8.x)
僅供參考,歡迎指正