Android memory leak analysis ideas and case analysis

analysis of idea

Memory leak refers to a phenomenon in the Android process that some objects are no longer used, but are referenced by some objects with a longer life cycle, resulting in the memory resources they occupy cannot be recycled by GC, and the memory usage continues to increase; memory leaks are It is a common factor that causes our application performance to decline and freeze. The core idea to solve such problems can be summarized in the following two steps:

  1. Simulate the operation path of memory leaks, observe changes in application Heap memory, and determine the approximate location of the problem;
  2. Carry out analysis on the specific location, find the complete reference chain of the leaked object pointing to the GC Root, and control the memory leak from the source.
Analysis tool: Android Stuido Profiler

There are two commonly used memory analysis tools in Profiler: memory curve graph and Heap Dump; memory curve can observe the memory usage status in real time and assist us in dynamic analysis of memory;

When a memory leak occurs, the typical phenomenon of the memory curve is that it presents a staircase shape, and once it rises, it is difficult to fall; for example, after the Activity leaks, the memory usage of the page will rise all the way after repeatedly opening and closing the page, and after clicking the trash can icon for manual GC, the usage cannot be reduced. To the level before opening the Activity, there is a high probability of a memory leak.

At this time, we can manually dump the memory distribution in the application heap memory at this time for static analysis:

Description of various indicators in the UI:

  1. Allocations: The number of instances of this class in the heap memory;
  2. Native Size: The memory occupied by Native objects referenced by all instances of this class
  3. Shallow Size: The actual memory usage of all instances of this class, excluding the memory usage of the objects they refer to;
  4. Retained Size: Shallow SizeDifferent from , this number represents the memory footprint of all instances of the class and all objects referenced by it;

With the help of a picture, you can have a more intuitive impression of these attributes:

As shown in the figure above, the red dot represents the memory size , the blue Shallow Sizedot represents The amount of wasted memory space. Because memory leaks often form a "chain effect", starting from the leaked object, all objects and Native resources referenced by the object cannot be recycled, resulting in a decrease in memory usage efficiency.Native SizeRetained SizeRetained Size

In addition, Leaksrepresents the number of possible memory leak instances; click on a class in the list to view the instance details of the class; in the Instance list represents the depthshortest GC Rootcall chain depth reached by the instance, which Referencecan be seen intuitively in the stack in the right column of Figure 1 With the complete call chain, you can then trace all the way back to find the most suspicious references, analyze the cause of the leak based on the code, and prescribe the right medicine to cure the problem.

Next, we will analyze a few cases where we encountered some typical memory leaks in our projects:

Case analysis

Case 1: BitmapBinder memory leak

When it comes to cross-process Bitmap transmission scenarios, we adopt a BitmapBindermethod; because Intent supports us to pass in a custom Binder, we can use Binder to implement Intent transmission of Bitmap objects:

// IBitmapBinder AIDL文件 
import android.graphics.Bitmap; 
interface IBitmapInterface { 
    Bitmap getIntentBitmap(); 
}

However, Activity1after passing a Bitmap BitmapBinderto using Activity2, two serious memory leaks occurred:

  1. Return after jumping and Activity1cannot be recycled during finish;
  2. When jumping repeatedly, Bitmapand Binderobjects will be created repeatedly and cannot be recycled;

First analyze the Heap Dump:

This is a "multi-instance" memory leak, that is, every time finish Activity1is opened, an Activity object will be added and left in the Heap and cannot be destroyed; it is common in scenarios such as internal class references and static array references (such as listener lists); according to Profiler Given the reference chain, we find BitmapExtthis class:

suspend fun Activity.startActivity2WithBitmap() {
    val screenShotBitmap = withContext(Dispatchers.IO) { 
        SDKDeviceHelper.screenShot() 
    } ?: return
    startActivity(Intent().apply {
        val bundle = Bundle()
        bundle.putBinder(KEY_SCREENSHOT_BINDER, object : IBitmapInterface.Stub() {
            override fun getIntentBitmap(): Bitmap {
                return screenShotBitmap
            }
        }) 
        putExtra (INTENT_QUESTION_SCREENSHOT_BITMAP, bundle)
    })
}

BitmapExtThere is a global extension method of Activity startActivity2WithBitmap, which creates a Binder, throws the obtained screenshot Bitmap into it, and wraps it in the Intent and sends it to Activity2; obviously there is an IBitmapInterfaceanonymous inner class here, and it seems that the leak occurs from here. ;

But I have two questions. One is that this inner class is written in the method. When the method ends, won't the inner class reference in the method stack be cleared? Second, this internal class does not reference Activity, right?

To understand these two points, we need to decompile the Kotlin code into Java and take a look:

@Nullable
public static final Object startActivity2WithBitmap(@NotNull Activity $this$startActivity2WithBitmap, boolean var1, @NotNull Continuation var2) {
    ...
    Bitmap var14 = (Bitmap)var10000;
    if (var14 == null) {
        return Unit.INSTANCE;
    } else {
        Bitmap screenShotBitmap = var14;
        Intent var4 = new Intent();
        int var6 = false;
        Bundle bundle = new Bundle();
        // 内部类创建位置:
        bundle.putBinder("screenShotBinder", (IBinder)(new BitmapExtKt$startActivity2WithBitmap$$inlined$apply$lambda$1($this$startActivity2WithBitmap, screenShotBitmap)));
        var4.putExtra("question_screenshot_bitmap", bundle);
        Unit var9 = Unit.INSTANCE;
        $this$startActivity2WithBitmap.startActivity(var4);
        return Unit.INSTANCE;
    }
}

// 这是kotlin compiler自动生成的一个普通类:
public final class BitmapExtKt$startActivity2WithBitmap$$inlined$apply$lambda$1 extends IBitmapInterface.Stub {
    // $FF: synthetic field
    final Activity $this_startActivity2WithBitmap$inlined; // 引用了activity
    // $FF: synthetic field
    final Bitmap $screenShotBitmap$inlined;

    BitmapExtKt$startActivity2WithBitmap$$inlined$apply$lambda$1(Activity var1, Bitmap var2) {
        this.$this_startActivity2WithBitmap$inlined = var1;
        this.$screenShotBitmap$inlined = var2;
    }
    @NotNull
    public Bitmap getIntentBitmap() {
        return this.$screenShotBitmap$inlined;
    }
}

In the Java file compiled by Kotlin Compiler, IBitmapInterfacethe anonymous inner class is replaced by a normal class BitmapExtKt$startActivity2WithBitmap$$inlined$apply$lambda$1, and this normal class holds the Activity. The reason for this situation is that in order to use the variables in the method normally inside the class, Kotlin writes the input parameters of the method and all the variables created above the internal class code into the member variables of the class; therefore, the Activity is This class reference; in addition, the life cycle of Binder itself is longer than Activity, so memory leaks occur.

The solution is to directly declare a normal class to bypass the "optimization" of Kotlin Compiler and remove the reference to Activity.

class BitmapBinder(private val bitmap: Bitmap): IBitmapInterface.Stub() {
    override fun getIntentBitmap( ) = bitmap
}

// 使用:
bundle.putBinder(KEY_SCREENSHOT_BINDER, BitmapBinder(screenShotBitmap))

Next, the problem is that Bitmap and Binder will be created repeatedly and cannot be recycled. The memory phenomenon is as shown in the figure. Every time it jumps and closes, the memory will increase a little, like a ladder; it cannot be released after GC;

In the heap, 2560x1600, 320densityit can be inferred from the Bitmap size that these are screenshot Bitmap objects that cannot be recycled and are held by Binder; however, looking at the reference chain of Binder, we did not find any references related to our application;

We speculate that Binder should be referenced by the Native layer with a long life cycle, which is related to the implementation of Binder, but we have not found an effective method to recycle Binder;

One solution is to reuse Binder to ensure that Binder is not re-created every time Activity2 is opened; in addition, change the BitmapBinderBitmap to a weak reference, so that even if the Binder cannot be recycled, the Bitmap can be recycled in time. After all, the Bitmap is the memory Big house.

object BitmapBinderHolder {
    private var mBinder: BitmapBinder? = null // 保证全局只有一个BitmapBinder

    fun of(bitmap: Bitmap): BitmapBinder {
        return mBinder ?: BitmapBinder(WeakReference(bitmap)).apply { mBinder = this }
    }
}

class BitmapBinder(var bitmapRef: WeakReference<Bitmap>?): IBitmapInterface.Stub() {
    override fun getIntentBitmap() = bitmapRef?.get()
}

// 使用:
bundle.putBinder(KEY_SCREENSHOT_BINDER, BitmapBinderHolder.of(screenShotBitmap))

Verification: Like the memory map, after a GC, all the Bitmaps created can be recycled normally.

Case 2: Flutter multi-engine scene plug-in memory leak

Many projects use multi-engine solutions to implement Flutter hybrid development. When the Flutter page is closed, in order to avoid memory leaks, not only must , , FlutterViewand FlutterEngineother MessageChannelrelated components be unbound and destroyed in a timely manner, but you also need to pay attention to whether each Flutter plug-in is released normally. operate.

For example, in one of our multi-engine projects, a memory leak was discovered by repeatedly opening and closing a page:

This activity is a secondary page that uses a multi-engine solution and runs on it. FlutterViewIt seems to be a "single instance" memory leak, that is, no matter how many times it is switched on and off, the Activity will only retain one instance and cannot be released in the heap. This is common. The scenario is a reference to a global static variable. This kind of memory leak has a slightly lighter impact on memory than a multi-instance leak. However, if the Activity is large and holds many Fragments and Views, and these related components are leaked together, optimization should also be focused on.

Judging from the reference chain, this is FlutterEnginea memory leak caused by a communication Channel within; when FlutterEngineis created, each plug-in in the engine will create its own MessageChanneland register it FlutterEngine.dartExecutor.binaryMessengerso that each plug-in can communicate with Native independently.

For example, a common plug-in might be written like this:

class XXPlugin: FlutterPlugin {
    private val mChannel: BasicMessageChannel<Any>? = null

    override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { // 引擎创建时回调
        mChannel = BasicMessageChannel(flutterPluginBinding.flutterEngine.dartExecutor.binaryMessenger, CHANNEL_NAME, JSONMessageCodec.INSTANCE)
        mChannel?.setMessageHandler { message, reply ->
            ...
        }
    }

    override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { // 引擎销毁时回调
        mChannel?.setMessageHandler(null)
        mChannel = null
    }
}

You can see that FlutterPluginactually will hold binaryMessengera reference to , and binaryMessengerwill have FlutterJNIa reference to... This series of reference chains will eventually FlutterPluginhold Context, so if the plug-in does not release the reference correctly, a memory leak will inevitably occur.

loggerChannelLet’s take a look at how it is written in the reference chain above :

class LoggerPlugin: FlutterPlugin {
    override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
        LoggerChannelImpl.init(flutterPluginBinding.getFlutterEngine())
    }

    override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
    }
}

object LoggerChannelImpl { // 这是一个单例
    private var loggerChannel: BasicMessageChannel<Any>?= null

    fun init(flutterEngine: FlutterEngine) {
        loggerChannel = BasicMessageChannel(flutterEngine.dartExecutor.binaryMessenger, LOGGER_CHANNEL, JSONMessageCodec.INSTANCE)
        loggerChannel?.setMessageHandler { messageJO, reply ->
            ...
        }
    }
}

In LoggerPlugin.onAttachedToEngine, is FlutterEnginepassed into the singleton LoggerChannelImpland binaryMessengeris held by the singleton, and onDetachedFromEnginethe method has not been destroyed, so it has been referenced by the singleton and the context cannot be released.

This plug-in may not have taken the multi-engine scenario into consideration when designing it. In a single engine, the plug-in is onAttachedToEngineequivalent onDetachedFromEngineto following the life cycle of the application, so there will be no memory leaks; but in a multi-engine scenario, it DartVMwill be used for each engine. The engine allocates isolate, which is somewhat similar to a process; the dart heap memory of isolate is completely independent, so any objects (including static objects) between engines are not interoperable; therefore, respective instances will be created in their own isolates, which makes FlutterEngineeach FlutterPluginWhen creating an engine, the life cycle of the plug-in will be repeated. When an engine is destroyed, the plug-in is not recycled normally and the relevant references are not released in time, resulting in a memory leak Context.FlutterEngine

amendment:

  1. LoggerChannelImplThere is no need to use singleton writing, just replace it with a normal class to ensure that each engine MessageChannelis independent;
  2. LoggerPlugin.onDetachedFromEngineNeed to MessageChannelbe destroyed and made empty;
Case 3: Third-party library Native reference memory leak

A third-party reader SDK is connected to the project. During a memory analysis, it was found that every time the reader is opened, the memory will increase and cannot decrease. From the heap dump file, the Profiler did not indicate that there is a memory leak in the project. , but you can see that there is an Activity in the app heap that has a very large number of instances that cannot be recycled, and the memory usage is large.

Looking at GCRoot References, we found that these activities are not referenced by any known GCRoot:

There is no doubt that this Activity has a memory leak, because the relevant pages have been finished and manually GCed during the operation. Therefore, the reason can only be that the Activity is referenced by an invisible GCRoot.

In fact, Profiler's Heap Dump will only display the GCRoot of the Java heap memory, and the GCRoot in the Native heap will not be displayed in this reference list. So, is it possible that this Activity is held by a Native object?

We used dynamic analysis tools Allocations Recordto look at the references of Java classes in the Native heap, and sure enough we found some reference chains of this Activity:

But unfortunately, the reference chains are all memory addresses, and the class name is not displayed. There is no way to know where the Activity is referenced. I tried it later with LeakCanary. Although it was clearly stated that the memory leak was caused by the reference of the Native layer, there was still Global Variableno Provide specific calling location;

We have to go back to the source code to analyze the possible calling locations. This DownloadActivityis a book download page we made to adapt to the reader SDK; when there are no books locally, the book file will be downloaded first, and then passed to the SDK to open the SDK's own Activity; therefore, the function is to download, DownloadActivityproofread Verify, decompress books, and handle some startup processes of the SDK reader.

According to the general idea, we first checked the download, verification, and decompression codes, and found no doubts. The listener and the like were encapsulated by weak references; therefore, it is speculated that the memory leak is caused by the writing method of the SDK itself.

It is found that when the reader SDK is started, there is a context parameter:

class DownloadActivity {
    ... 
    private fun openBook() {
        ... 
        ReaderApi.getInstance().startReader(this, bookInfo) 
    } 
}

Since the source code of this SDK has been obfuscated, we can only dig into it and startReadertrace the call chain from the method point all the way:

class ReaderApi: void startReader(Activity context, BookInfo bookInfo) 
        ↓ 
class AppExecutor: void a(Runnable var1) 
        ↓ 
class ReaderUtils: static void a(Activity var0, BookViewerCallback var1, Bundle var2) 
        ↓ 
class BookViewer: static void a(Context var0, AssetManager var1) 
        ↓ 
class NativeCpp: static native void initJNI(Context var0, AssetManager var1);

Finally, when we come to the method NativeCppof this class initJNI, we can see that this local method passes our Activity in. The subsequent processing is unknown, but based on the above memory analysis, we can basically conclude that it is because of this method that the reference of the Activity is passed to the Native Long-lived objects are held, causing memory leaks in the Activity.

As for why Native needs to use context, there is no way to analyze it. We can only feed this issue back to the SDK supplier and let them handle it further. The solution is not difficult either:

  1. When the reader is destroyed, the Activity reference is promptly cleared;
  2. startReaderThe method does not need to specify the Activity object, just change the input parameter declaration to Context, and the external parameter can be Application Contextpassed in.

In order to help everyone understand performance optimization more comprehensively and clearly, we have prepared relevant core notes (including underlying logic):https://qr18.cn/FVlo89

Core notes on performance optimization:https://qr18.cn/FVlo89

Startup optimization

, memory optimization,

UI optimization,

network optimization,

Bitmap optimization and image compression optimization : multi-thread concurrency optimization and data transmission efficiency optimization, volume package optimizationhttps://qr18.cn/FVlo89




"Android Performance Monitoring Framework":https://qr18.cn/FVlo89

"Android Framework Learning Manual":https://qr18.cn/AQpN4J

  1. Boot Init process
  2. Start the Zygote process on boot
  3. Start the SystemServer process on boot
  4. Binder driver
  5. AMS startup process
  6. PMS startup process
  7. Launcher startup process
  8. Android four major components
  9. Android system service-Input event distribution process
  10. Android underlying rendering - screen refresh mechanism source code analysis
  11. Android source code analysis in practice

Guess you like

Origin blog.csdn.net/weixin_61845324/article/details/133355082