App development inevitably has to deal with images, which take up a large amount of memory, so it is easy to run out of memory due to improper management. Finally, in OOM, the image behind is actually Bitmap, which is one of the most memory-eating objects in Android and the culprit of many OOM versions. However, in different Android versions, Bitmaps are more or less different, especially when it comes to memory allocation, and understanding how they work is a better guide to image management. Let’s take a look at Google’s official documentation:

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. 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.

The memory required for pixel storage prior to 2.3 is allocated in Native, and the life cycle is not very controllable and may need to be recycled by users themselves. Between 2.3 and 7.1, Bitmap pixels are stored in Dalvik’s Java heap. Of course, pixels prior to 4.4 can even be allocated in anonymous shared memory (Fresco adopted), while pixel memory after 8.0 is allocated back to Native without the user’s initiative to recycle. After 8.0, image resource management is more excellent, greatly reducing OOM. Android 2.3.3 is an outdated technology and will not be analyzed any more. This article will focus on mobile phone systems after 4.x.

Intuitive comparison of Bitmap memory growth curves before and after Android 8.0

A big watershed for Bitmap memory allocation is in Android 8.0, where you can use a code emulator to grow Bitmap indefinitely, eventually OOM, or Crash out. Through the performance in different versions, expect to have an intuitive understanding of Bitmap memory allocation, sample code is as follows:

   @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);
			}
	    }
Copy the code

Nexus5 Android 6.0 performance

Constantly parsing images and holding Bitmap references can cause memory to increase. Using the Android Profiler tool to take a quick look at the memory allocation above, at one point the memory allocation is as follows:

Just to summarize the memory ratio

memory The size of the
Total 211M
Java memory 157.2 M
Native memory 3.7 M
Bitmap memory 145.9m (152663617 byte)
Graphics memory (usually Fb, App does not need to be considered) 45.1M (152663617 byte)

As you can see from the table above, most of the memory is made up of bitmaps and is placed in the HEAP of the VIRTUAL machine, actually because in 6.0 Bitmap pixel data is stored in the Heap of the Java virtual machine in the form of byte arrays. The memory size increases indefinitely until OOM crashes.

memory The size of the
Total 546.2 M
Java memory 496.8 M
Native memory 3.3 M
Graphics memory (usually Fb, App does not need to be considered) 45.1 M

It can be seen that the increase is always the memory in the Java heap, that is, the memory allocated by Bitmap in Dalvik stack. When Dalvik reaches the upper limit of vm memory, Dalvik will throw an OOM exception:

As you can see, for Android6.0, Bitmap memory allocation is mostly in the Java layer. Then, take a look at the Bitmap allocation for Android 8.0.

Nexus6p Android 8.0 performance

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

As we know from the official documentation, one of the biggest improvements since Android8.0 is where Bitmap memory is allocated: from the Java heap to the native stack, as shown below

memory The size of the
Total 1.2 G
Java memory 0G
Native memory 1.1 G
Graphics memory (usually Fb, App does not need to be considered) 0.1 G

Obviously, the increase of Bitmap memory is basically in the native layer. With the infinite growth of Bitmap memory, the App will eventually fail to allocate memory from the system, which will eventually cause a crash. Take a look at the memory occupied during the crash:

memory The size of the
Total 1.9 G
Java memory 0G
Native memory 1.9 G
Graphics memory (usually Fb, App does not need to be considered) 0.1 G

It can be seen that the memory usage of an APP is up to 1.9G, and almost all of it is native memory, which is actually the biggest optimization made by Google in 8.0. We know that Java VIRTUAL machines generally have an upper limit, but since Android can run multiple apps at the same time, the upper limit is usually not too high. In the case of the nexus6p, this is the typical configuration

. Dalvik vm. Heapstartsize = 8 m dalvik vm. Heapgrowthlimit = 192 m dalvik vm. Heapsize = 512 m dalvik vm. Heaptargetutilization = 0.75 dalvik.vm.heapminfree=512k dalvik.vm.heapmaxfree=8mCopy the code

If largeheap is not enabled in AndroidManifest, the Java heap will crash when the memory reaches 192M, which is a serious waste of resources for 4G phones. An ios APP can use almost all available memory (excluding system expenses). After 8.0, Android is also moving in this direction, and the best place to start is Bitmap, because it’s a huge memory hog. After the image memory is transferred to Native, the image processing of an APP can not only use most of the system memory, but also reduce the Java layer memory usage and OOM risk. However, in the case of unlimited memory growth, APP will also crash, but this kind of crash is not OOM crash, Java virtual machine will not catch, according to reason, should belong to Linux OOM. The difference with Android6.0 can be seen from the crash Log:

In this case, the Crash is not controlled by the Java VIRTUAL machine, and the process dies directly. In fact, the same effect will be achieved if the memory is allocated in native on a mobile phone running Android6.0. That is to say, the memory in native does not affect the OOM of the Java virtual machine.

Android 6.0 Analog Native MEMORY OOM

In direct native memory allocation, and do not release, the simulation code is as follows:

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)); }}Copy the code

Only malloc, not free, in this case Android6.0 memory growth is as follows:

memory The size of the
Total 750m
Java memory 1.9 m
Native memory 703M
Graphics memory (usually Fb, App does not need to be considered) 44.1 M

The Total memory is 750m, exceeding the memory limit of Nexus5 Android6.0 Dalvik VM, but the APP does not crash. It can be seen that the increase of Native memory does not lead to OOM of Java VM. In the Native layer, OOM is when the system memory runs out:

It can be seen that for the 6.0 system, an APP can also use up all the memory of the system. The following is the principle of Bitmap memory allocation and why there is such a big difference between 8.0 and 8.0.

Bitmap memory allocation principle

Principles of Bitmap memory allocation before 8.0

As you can see from the Bitmap’s list of members, there is a byte[] mBuffer in the Bitmap, which is used to store pixel data, obviously in the Java Heap

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

Next, analyze by manually creating a Bitmap: bitmap.java

public static Bitmap createBitmap(int width, int height, Config config) {
    return createBitmap(width, height, config, true);
}
Copy the code

The creation of Java layer bitmaps eventually leads to the native layer: 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)); <! Key points - 1 pixel memory allocation - > Bitmap * nativeBitmap = GraphicsJNI: : allocateJavaPixelRef (env, & Bitmap, NULL); if (! nativeBitmap) { return NULL; }... <! Jbyte * addr = (jbyte*) env->CallLongMethod(gVMRuntime, gVMRuntime_addressOf, arrayObj); . <! Android ::Bitmap* wrapper = new Android ::Bitmap(env, arrayObj, (void*) addr, info, rowBytes, ctable); wrapper->getSkBitmap(Bitmap); Bitmap->lockPixels(); return wrapper; }Copy the code

Here only look at the key 1, pixel memory allocation: GraphicsJNI: : allocateJavaPixelRef can be can be seen from the function name, is in the Java layer distribution, to go in, is:

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(); <! -- Keypoint 1, create Java layer byte data, 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; }Copy the code

Since we only care about memory allocation, we also only look at key point 1. Here, we actually create Java layer Byte [] in the Native layer and use this byte[] as a pixel storage structure, and then build Java Bitmap object in the Native layer. Pass the generated byte[] to the Bitmap.java object:

jobject GraphicsJNI::createBitmap(JNIEnv* env, android::Bitmap* bitmap, int bitmapCreateFlags, jbyteArray ninePatchChunk, jobject ninePatchInsets, int density) { ... <! -- Key point 1, build Java Bitmap object, 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; }Copy the code

The above is the pre-8.0 memory allocation, in fact, 4.4 and the pre-8.0 memory allocation is even more chaotic, let’s take a look at the post-8.0 Bitmap principle.

What are the new features of Bitmap memory allocation after 8.0

In the bitmap. Java class, the original private byte[] mBuffer member has been removed and replaced by the private final Long mNativePtr. Details are as follows:

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

I mentioned before that the memory allocation after 8.0 is native. What is the specific performance of the code? The process is basically similar to that before 8.0, with differences in native allocation:

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); . <! Points - 1, native layer to create the bitmap, and distribution of native memory - > sk_sp < bitmap > nativeBitmap = bitmap: : allocateHeapBitmap (& bitmap); if (! nativeBitmap) { return NULL; }... return createBitmap(env, nativeBitmap.release(), getPremulBitmapCreateFlags(isMutable)); }Copy the code

How does allocateHeapBitmap allocate memory

static sk_sp<Bitmap> allocateHeapBitmap(size_t size, const SkImageInfo& info, size_t rowBytes) { <! Void * addr = calloc(size, 1); void* addr = calloc(size, 1); if (! addr) { return nullptr; } <! --> return sk_sp<Bitmap>(new Bitmap(addr, size, info, rowBytes)); }Copy the code

It can be seen that after 8.0, Bitmap pixel memory is allocated by directly calling Calloc in the Native layer, so its pixels are allocated on the Native heap. This is why Bitmap memory consumption after 8.0 can grow indefinitely until the system memory is exhausted. There is no reason for the Java OOM.

Bitmap memory reclamation mechanism after 8.0

NativeAllocationRegistry is a mechanism introduced in Android 8.0 to assist automatic reclamation of native memory. When Java objects are reclaimed due to GC, The NativeAllocationRegistry can assist in recovering native memory applied for by Java objects. Take Bitmap as an example, as follows:

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(); <! - support recycling native memory - > NativeAllocationRegistry registry = new NativeAllocationRegistry (Bitmap. Class. GetClassLoader (), nativeGetNativeFinalizer(), nativeSize); registry.registerNativeAllocation(this, nativeBitmap); if (ResourcesImpl.TRACE_FOR_DETAILED_PRELOAD) { sPreloadTracingNumInstantiatedBitmaps++; sPreloadTracingTotalBitmapsSize += nativeSize; }}Copy the code

Of course, this function also needs Java virtual machine support, have the opportunity to re-analyze.

Prior to Android 4.4, Bitmap could also allocate memory in Native (pseudo)

A good example of this is Fresco. Fresco used this feature to improve image processing performance prior to 5.0, but it was not mature enough and was scrapped after 5.0. The two properties related to this feature are inPurgeable and inInputShareable in BitmapFactory.Options, which are not analyzed. Outdated technology, equal to garbage, interested, can analyze their own.

 		 /**
         * @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;
Copy the code

conclusion

  • Bitmap pixel data prior to 8.0 is basically stored in the Java Heap
  • Bitmap pixel data after 8.0 is basically stored in the Native heap
  • 4.4 inInputShareable and inPurgeable can be used to make Bitmap native layer allocation (obsolete)

Android Bitmap (4. X-8. X)

For reference only, welcome correction