Three, Android memory optimization

3. Android memory optimization

Preface

Recently, I am dealing with some memory optimization related issues. This article will make some brief records of memory, and make some records of why memory is optimized and the practice of optimizing memory. Memory optimization is a long-term task and a long way to go.

Reference

Geek time Android development master class memory optimization

WeMobileDev Android memory optimization miscellaneous talk

WeChat Android terminal memory optimization practice

Android performance optimization

Meituan Androdi memory leak automated link analysis component

table of Contents


1. why do we need to optimize memory

Memory RAM is equivalent to the PC's memory, as a memory medium for temporary data storage during the operation of the mobile APP. In 2008, mobile phones had only about 140MB of memory, but now mobile phones, such as Huawei Mate 20 Pro phones, have 8GB of memory. So is there no need to do memory optimization? All such large memory is enough. First of all, from the perspective of the LPDDR RAM (low-power double data rate memory) memory used by mobile phones, the performance of LPDDR4 is twice that of LPDDR3, and LPDDR4X has a lower operating voltage than LPDDR4. You can guarantee that domestic manufacturers can use 8GB memory in mobile phones. Are you using a good LPDDR? Another is that the memory is so large. Your APP has been optimized for memory to reduce the runtime memory. The performance is definitely better. It can prevent some OOM. If the memory is too large, it may be killed by the LMK mechanism and trigger GC. , Summarize the benefits of memory optimization as follows:

  • Avoid frequent triggering of GC, thereby avoiding lag, thereby increasing the user experience, not to the point of uninstalling the APP, and finally creating a good reputation

  • Prevent OOM in the application

  • Avoid the probability of excessive memory being killed by the LMK mechanism


2. Basic knowledge to know before memory optimization

1. The life cycle of Java objects


Created allocates storage space for the object and constructs the object

InUse object is held by strong reference

Invisible is not held by a strong reference, and the object is still alive. (This object may be held by some static variables or JNI loaded under the virtual machine, which will cause memory leaks)

Unreachable GC Root is unreachable, the reference is no longer held by any strong reference

Collected phase

Finalized waiting for recycling

Deallocated


2. Java memory model


There are 8 kinds of similar save and load operations, namely lock, unlock, read, load, use, assign, store, write

Simply put, if you want to transfer a variable from the main memory to the working memory, you must perform read and load operations sequentially. If you want to write a variable back from the working memory to the main memory, you must perform store and write sequentially. operating

3. JVM memory partition


  • The method area is used to store class information, constants, static variables and other data loaded by the virtual machine. The string constant pool is located in the method area

  • The heap is used to store object instances, and the ones created by new are stored in the heap

  • The virtual machine stack is used to implement method calls. Each method call corresponds to a stack frame in the stack. The stack frame contains method related information such as the local variable table, operand stack, and method interface.

  • Local method stack The local method stack is similar to the virtual machine stack, except that the call of the local method is saved. In the implementation of the HotSpot virtual machine, the virtual machine and the local method stack are merged together.

  • The function of the program counter is equivalent to the function of the PC register. If the Java method is currently being executed, it is only the address of the current bytecode instruction. If the execution is a local method, the value is Undefined. It can ensure that the thread is interrupted and resumes execution according to the instruction at the time of interruption.

  • The method area and heap are thread shared memory, and the virtual machine, local method stack, and program counter are independent memories. Each thread only saves its own data.

  • Registers are used to temporarily store instructions, data, and addresses. The internal components and registers of the CPU have very high read and write speeds. The CPU is composed of arithmetic units, controllers, registers, etc., and the devices are connected by an internal bus

  • Direct memory is not part of the data area of the virtual machine when it is running, nor is it the memory area defined in the virtual machine specification, but this part of the memory is also frequently used. And it may also cause OutOfMemoryError to appear

The partition before JDK 1.8 is the above situation, but after JDK 1.8, it has been changed to the following partition. It is explained that the method area before JDK 1.8 is only a logical partition , and does not exist physically independent of the heap . It is located in the permanent generation. So the method area at this time can also be recycled. After Java 1.8 version, Hot Spot removed the permanent generation, used local memory to store class metadata information, and named it metaspace


4. Class loading

  4.1, the timing of class loading

  • From loading to unloading, life cycle -> loading, verification, preparation, analysis, initialization, use, and unloading, a total of seven stages

  • The verification, preparation, and analysis are collectively referred to as connection

  • Passive use -> Referencing the static field of the parent class through the subclass will not cause the subclass to be initialized

  • Passive use -> define the reference class through the array, it will not trigger the initialization of this class

  • Passive use -> Constants are stored in the constant pool of the calling class during the compilation phase, and subsequent references to constants are converted into references to the class's own constant pool

  • The interface also has an initialization process, which is consistent with the class. The compiler will still generate the "<clint>()" class constructor for the interface, which is used to initialize the member variables defined in the interface. When the interface is initialized, it is not required that the parent interface has been initialized, it will only be initialized when the parent interface is used.

 4.2, the process of class loading

  • Loading the Class Loading process

    • Get the binary stream that defines this class through the fully qualified name of a class

    • Convert the static storage structure represented by this byte stream into the runtime data structure of the method area

    • Generate a java.lang.Class object representing this class in memory as the access entry for various data of this class in the method area

  • Verification (the loading phase and the connection phase are interleaved)

    • Ensure that the information contained in the byte stream of the Class file symbolizes the requirements of the current virtual machine and does not endanger the security of the virtual machine itself

    • File format verification (whether the byte stream conforms to the Class file format specification)

      • Whether to start with the magic number 0xCAFEBABE

      • Whether the major and minor version numbers are within the processing range of the current virtual machine

      • Whether there are unsupported constant types in the constants of the constant pool, check the tag flag

      • Are there any constants pointing to non-existent constants or constants of unsigned types among the various index values pointing to constant

      • Is there any unsigned UTF8 encoded data in the constant of CONSTANT_UTF8_info type

      • Whether the various parts of the Class file and the file itself have been deleted or additional information

    • Metadata verification (verification of bytecode description information to ensure that there is no metadata that does not conform to the Java language specification)

      • Does this class have a parent class

      • Whether the parent class of this class inherits a class that is not allowed to be inherited

      • If this class is not an abstract class, whether it has implemented all the methods required to be implemented in its parent class or interface

      • Whether the fields and methods in the class conflict with the parent class

    • Bytecode verification (The main purpose is to confirm that the program semantics are legal and logical through data flow and control flow analysis. This stage is mainly to verify and analyze the method body)

      • Ensure that the data type and instruction code sequence of the operand stack can work together at any time

      • Ensure that jump instructions will not jump to bytecode instructions outside the method body

      • Ensure that the type conversion in the method body is effective

      • For optimization after JDK 1.6, check the status of the "StackMapTable" attribute of the "Code" attribute table. After JDK 1.7, the type check fails and cannot fall back to type inference.

    • Symbol reference verification

      • Whether the fully qualified name described by the string in the symbol reference can find the corresponding class

      • Whether there is a field descriptor that matches the method and the method and field described by the simple name in the specified class

      • Whether the access type of the class, field, and method in the symbol reference can be accessed by the current class

  • Preparation (formally allocate memory for class variables (static modification) and set the initial value of class variables (int i -> 0), the memory used by these variables will be allocated in the method area)

  • Resolution (the process in which the virtual machine replaces symbol references in the constant pool with direct references)

  • initialization

    • The "<clinit>()" method is generated by the compiler automatically collecting the assignment actions of all class variables in the class and combining the statements in the static statement block.

    • The initialization phase is the process of executing the "<clint>()" method of the class constructor.

5. Android memory allocation

In the Android system, the heap is actually a piece of anonymous shared memory, managed by the underlying C library, and still uses the functions malloc and free provided by libc to allocate and release memory. Most static data will be mapped to a shared process. Common static data includes Dalvik Code, app resources, so files, and so on. And in most cases, Android realizes the mechanism that the dynamic RAM area can be shared between different processes by displaying the allocated shared memory area (such as Ashmem or Gralloc).

6. JVM recycling algorithm and recycling mechanism

For details, please refer to "In-Depth Understanding of the Java Virtual Machine Version 2", the following is a brief summary of the recycling mechanism and recycling algorithm

  • There are three areas of memory allocation, young generation Eden Survivor1 Survivor2 (1/8) old generation, permanent generation, first allocated in Eden, and later will be reclaimed according to age calculation or enter the old or immortal generation.

  • Before garbage collection: first determine whether the object is alive

    • Reference counting method, add 1 if it is referenced in one place, and decrease 1 if it is invalid. =0 means that it is impossible to be referenced. If two objects refer to each other, it cannot be recycled.

    • Reachability analysis, by judging that an object is not reachable when there is no reference chain connected to the GC ROOT

  • After judging that the object is unreachable, it will filter to see if it is necessary to execute

  • Finally collected by garbage collection algorithm

    • The two stages of the mark-sweep algorithm, the mark-sweep is insufficient: efficiency problems, and a large number of discontinuous memory fragments will be generated

    • The copy algorithm divides the available memory into two equal pieces according to the allowable capacity, and only uses one of them each time. If it is insufficient, the memory is reduced by half. When there is no large amount of memory recycling, it is too wasteful. (Applicable to the new generation, there is not so much memory to be recycled)

    • Marking-sorting algorithm marks and clears, so that the surviving objects move to one end. (Applicable to old age)

ps: Actually there are about six recycling algorithms. Let s understand them. They are Reference Counting, Mark-Sweep, Copying, Mark-Compact, and Incremental Collection. Incremental Collecting), Generation (Generational Collecting)

7. Android's LMK (Low Memory Killer) mechanism

First understand, the system's process classification, sorted by priority

  • Foreground process

  • Visible process

  • Service process

  • backstage process

  • Empty process (an empty process occupies 10M of memory, so you need to consider whether it is necessary to open a process to handle some business)

The LMK mechanism is that the lower the priority, the easier it is to kill.

8. 4.references

  • Strong reference, long life cycle, never recycling

  • Soft reference, short life cycle, only reclaimed when the memory is insufficient

  • Weak references have a shorter life cycle than soft references. Once a weak reference object is found, no matter whether the current memory space is sufficient, the weak reference will be recycled.

  • Phantom reference, the shortest life cycle, if an object only holds a phantom reference, then it is equivalent to no reference, and may be recycled by the garbage collector at any time


3. the three major problems of memory

1. Memory jitter

The cause is that frequent GC, memory reallocation and reclamation lead to memory instability, which presents a jagged appearance on the memory. The recycling strategy cannot be optimized for us (except for the Great God), and we can only start with reducing GC.

2. Memory leak

After the object is useless, the occupied memory is not reclaimed. why? Because GC Roots holds the reference, or the object is reachable to GC Roots. This will lead to a reduction in available memory, frequent GC, and memory jitter.

Common memory leak scenarios

  • Singleton life cycle is equal to the life cycle of the entire application, if you hold an Activity, it will cause the Activity to be unable to be recycled

  • Anonymous inner class The most common is that Handler holds a reference to Activity, causing memory leaks

  • The resource is not closed File, Bitmap, etc. are closed or recycle, EventBus, etc. are not unregistered after registration

  • The static variables of the class hold large data objects or hold non-static internal class instances

  • ListView does not use the cached View when sliding quickly, resulting in frequent creation of a large number of objects. This is generally no longer a concern. These years, you must use ViewHolder. As for RecyclerView, you can release the image reference operation in the onViewRecycled method of RecyclerViewAdapter.

  • WebView just use a WebView in the application, the memory will not be freed, usually can open a separate process for the WebView, AIDL use to communicate with the main process, when destroyed, directly off the course just fine.

Common optimization solutions

  • SparseArray, SparseInt instead of HashMap

  • Avoid automatic boxing and unboxing of basic data types, such as int 4 bytes, and Integer objects have 16 bytes

  • Use IntDef and StringDef instead of enumerations as much as possible

  • Use the LruCache method to cache large resources, such as preloading

  • Bitmap optimization, if the quality does not need to be good, generally ARGG_565 is sufficient, as mentioned below. inSampleSize, inScaled, inBitmap, etc. can be fully utilized. It should be noted that before Android 4.4, only the same size of Bitmap memory area can be reused. After Android 4.4, any Bitmap memory area can be reused, as long as this piece of memory is larger than the non-memory Bitmap. Big enough

  • When the memory is too low, use the onTrimMemory/onLowMemory method to release the image cache, static cache, etc.

  • Try to use String.valueOf instead of ""+int. The former is more efficient. Use StringBuffer and StringBuilder for string splicing.

  • Pay attention to whether the anonymous inner class Runnable holds a reference to an external class or an Activity, you need to release the Activity reference, and it is forbidden to use the new Runnable anywhere.

3. Memory overflow

OOM, when the program is applying for memory, there is not enough memory allocated to it, causing the program to crash. Memory leaks lead to a decrease in available memory, and in a suitable scenario, it can also lead to memory overflow. In fact, many OOMs are caused by improper image processing. In reality, image loading frameworks such as Glide are generally used, and they are basically handled by third-party libraries.

Use the following command to view the amount of memory allocated by the mobile phone system to the APP

adb shell getprop | grep dalvik.vm.heapsize 

4. memory optimization strategy

Memory optimization, the focus is on image correlation. For optimizations such as so reprogramming/unable to reprogramming, etc., readers who are interested can refer to it on their own. Generally, when accessing third-party frameworks, basic OOM and Native memory leaks can be captured. The key is to prevent them from online memory trends. Determine whether it is necessary to have a high probability of OOM.

Changes in Android Bitmap memory allocation

  • Before Android 3.0, Bitmap objects are placed on the heap, and pixel data is placed on the Native. If you do not manually call recycle, Bitmap Native memory recovery is completely dependent on the finalize callback, and the timing is very uncontrollable.

  • From Android 3.0 to Android 7.0, the Bitmap object and pixel data are placed in the Java heap together, so that even if recycle is not called, the Bitmap memory will be recycled along with the object. However, Bitmap is a big memory consuming user. If it is placed in the Java heap, will the maximum memory of the App be used up immediately, and if it is placed in the heap, it is not optimized well, and there will be frequent GC

  • In Android 8.0, the pixel data is placed in Native, which realizes the rapid release and recycling together with the object. And also increased the hardware bitmap Hardware Bitmap, which reduces the image memory and improves the drawing efficiency

If you are interested, you can learn about the process of putting pictures into Native in the Fresco image library.

 
//  Native Bitmap
Bitmap nativeBitmap = nativeCreateBitmap(dstWidth, dstHeight, nativeConfig, 22);
 
//  Java Bitmap
Bitmap srcBitmap = BitmapFactory.decodeResource(res, id);
 
//  Java Bitmap   Native Bitmap  
mNativeCanvas.setBitmap(nativeBitmap);
mNativeCanvas.drawBitmap(srcBitmap, mSrcRect, mDstRect, mPaint);
 
//  Java Bitmap  
srcBitmap.recycle();
srcBitmap = null  

Next, record some practical optimization strategies

1. Unified image library

Collect the call of pictures, reduce unnecessary picture library, and optimize the package size. Generally, using Glide is sufficient for image-related operations.

The flow chart of Glide loading picture principle is as follows


2. Unified management

Including all image-related interface operations, image caching strategies, etc., can be unified.

Set the configuration of the picture, for example, for pictures that are not demanding, use Bitmap.Config.ARGB_4444. You can apply the following formula to calculate and see how much memory can be reduced

  * desityDip/drawable desityDip  *   * desityDip/drawable desityDip * 4  Bitmap.Config.ARGB_8888   4 
 320  drawable-hdpi  240
  600 * 600  
600 * (320/240) * 600 * (320/240) = 2560000 byte = 2.56M 

3. Avoid unnecessary opening processes

If it is contrary to KPI, forget it. As mentioned on the previous page, an empty process will occupy 10MB of memory. If you do memory optimization, for some operations, there is no need to open the process, but also use the process, and then introduce some process communication pits. Then I have nothing to say. If it can improve the running speed, improve the user experience, facilitate the management of the APP, and facilitate the optimization of the APP, it is worth discussing.

4. Picture detection

When optimizing the package size before, the MCImage that has been called over bytes can also be used to detect large images , but this is based on offline.

Duplicate picture means that the Bitmap pixel data is completely the same, but there are multiple different objects. Generally use the Hprof analysis tool to automatically output the repeated Bitmap image and reference stack. The HAHA library detects duplicate images . The author has limited ability. I didn't do this step in practice. There is a hands-on project in geek time, but for small companies, access to the project is actually not necessary, and some compatibility issues need to be considered.

5. Establish a threaded application memory monitoring system

This can be done, and I personally find it very practical, the following is a brief description of how to build

  • According to user sampling, collect PSS (via Debug.MemoryInfo), Java heap, and total image memory at regular intervals, and upload them to the server. You can also count the number of GCs. The server needs to set a switch, because the number of GCs will affect For mobile phone performance, it is recommended to sample and test in grayscale.

  • The server calculates the indicator in the following way

    Memory exception rate

     UV   = PSS   400 MB UV/  UV 

    Topping rate

     UI   = JAVA   85%/ UV
     
     
     
    long javaMax = runtime.maxMemory();
    long javaTotal = runtime.totalMemory();
    long javaUsed = javaTotal - runtime.freeMemory();
    //Java   85%
    float proportion = (float) javaUsed/javaMax; 
  • Through the front end to graph the data, for example, through the memory change curve, etc., we can clearly monitor whether there are new memory-related issues

Interested students can see the US group Androdi memory leaks automated link analysis components

6, memory pocket strategy

The main purpose is to select an appropriate scene to kill the process and restart it before the user is not aware of the system exception, so that the application memory usage can return to normal.

Generally from 2 to 5 in the morning, or the main interface exits the background for more than 30 minutes, or the Java Heap size is about to exceed 85% of the maximum allocatable of the current process, you can use this strategy quietly, come on demand, generally small applications, There is no need, no one will use it for a long time. As for the bottoming strategy, you can try to use the Jingdong APP to filter items and check the items for an hour, and you will discover the wonders.


5. memory optimization tools commonly used

Here only introduce the tools used in the analysis, such as top, adb dumpsys meminfo to view the memory information and self-check

You can view the startup time through adb shell am start -W[packageName]/[packageName.MainActivity], and view some memory information through adb shell, dumpsys meminfo [pkg], including the number of current activities

1. Memory Profiler


Here is a detailed introduction to the Android Profiler. The following briefly introduces the meaning of each field in the floating box.

  • Java object memory allocated from Java or Kotlin code

  • Native object memory allocated from C C++ code

  • Graphics graphics buffer queue to the screen display pixels (including GL surface, GL texture) memory used

  • The memory used by the native stack and the Java stack in the Stack application, which is usually related to how many threads the application runs

  • Code is used to process the memory of code and resources (such as dex bytecode, optimized or compiled dex code, so library and fonts)

  • Other system is not sure how to allocate memory

  • The number of Java/Kotlin objects allocated by the Allocated application. This number is not included in the objects allocated by C and C++

  • Total memory size used by Total

Click on the class, the meaning of the class list field that appears is as follows


  • The Shallow Size object itself occupies the memory size, does not include the object it refers to

  • The Shallow Size of the Retained Size object itself + the Shallow Size of the object that can be directly or indirectly referenced by the object can be understood as the sum of the memory that can be reclaimed after the object is GC

  • Mobile phones after Native Size 8.0 will display, mainly reflecting the pixel memory used by Bitmap (after 8.0, it has been transferred to native)

  • The allocated size in the Allocated heap, that is, the size of the memory occupied by the APP timing

The worst thing about using Profiler is that it sometimes leads to APP card or no response. Don't think it is a code problem written by yourself.

When the App is in the A interface, then open the B page, then return to the A page, and perform a few GCs. If the java indicator memory is not restored, then it is very likely that optimization is needed here, and then use the following two box tools to analyze

2. LeakCanary

Principle analysis of Android memory leak detection tool LeakCanary

It is generally used for offline integration to check whether there is a memory leak problem. When analyzing page freezes, the integrated LeakCanary should be removed to avoid errors

3. MAT

Powerful Java Heap analysis ( download link ) tool, generates overall reports, analyzes memory problems, etc. Generally small and medium companies will use MAT, and large companies may use JHT.

Generally, it is also used offline. As for online monitoring, I currently want to achieve it through the fifth point of the above optimization strategy. After analyzing the problem, use these tools offline.

You can use the Android Studio Profiler to obtain the java heap file, and then convert the file into an hprof file. The conversion method is as follows

hprof-conv before.hprof after.hprof //hprof android studio sdk platform-tools  

Then open the converted standard hprof through MAT


Click in the red box below, such as histogram, you can search for some activities, view the call chain, see the source of the memory leak, and also support leak warning

Here are two brief introductions to the use of   MAT    one introduction to the use of MAT two Android memory optimization


Sixth, bit operation

There is an integer array A, in which only one number appears odd number of times, and the others all appear even number of times. Please print this number. The time complexity is O(N) and the space complexity is O(1). Difficulty: Easy

public static int oddApperance1(int[] A, int n) {
	int e = 0;
	for (int i = 0; i < n; i++) {
		e = e ^ A[i];
	}
	return e;
}
 


Note Seven