It turns out that it is Dialog that causes memory leaks in Android

It turns out that it is Dialog that causes memory leaks in Android

1. Memory leak bugs surge

Recently, I detected some memory leaks when I was doing mokey testing on the App. During the test the day before yesterday, I received 4 such bug tickets in a flash, and I was so upset that I felt like thousands of alpacas were running towards me.

Memory leak on the login page??!! The author's code is so perfect and impeccable, how could there be so many leaks?

What is Activity leak: Activity in Android represents a page and has a life cycle. After the life cycle ends, the Activity object should be reclaimed by the VM at an appropriate time. Leakage means that after the Activity life cycle ends, the VM finds that the Activity has been held and the useless memory has not been reclaimed.

According to past experience, most Activity leaks are caused by the Handler inner class hanging in the thread for a long time. And our App has already considered and handled this. What is the leak?

2. WebView causes memory leaks.

With a skeptical attitude and to prove my innocence, I clicked in one by one and saw a total of three different reference chains. For the sake of subsequent explanation, I gave them a name:

① AuthDialog reference chain

② BrowserFrame reference chain

③ IClipboradDataPaste reference chain

It seems that the situation is a little different this time! Since Monkey tested relatively few models, all the bugs here come from a Samsung GT-I9300@android+4.3 phone.

In order to solve the problem quickly, the author asked other colleagues and StackOverflow and found that three classes, CookieSyncManager, WebView, and WebViewClassic, have been mentioned by many people. They will cause memory leaks! The preliminary conclusions are as follows:

1. CookieSyncManager is a global static singleton. The operating system uses the App's Activity as the Context to construct its instance. We should create this singleton for the system first when the App is started, and use applicationContext so that it does not reference the Activity.

2. When the page (Activity) using WebView exits (onDestory) at the end of its life cycle, you need to actively call WebView.onPause() and WebView.destory() to allow the system to release WebView related resources.

4. WebView memory leaks are well known. It is recommended to start another process to run WebView. Don't want 9998, don't want 9999, we want 100%! Kill the process after WebView is used up, even if there is a leak.

According to the above conclusions, we all believe that this is caused by WebView.

But! Our application's main process LoginActivity does not use WebView at all!!!

3. What should I do if a third-party jar package uses WebView?

According to the above AuthDialog reference chain, the host has targeted a certain SDK:

After looking through the disgusting obfuscated code for a while, I found the following section. The SDK does create a WebView instance, and uses the client program's Activity object as the WebView's Context as follows:

Both c and j are subclasses inherited from WebView in the SDK, and k is the input parameter Activity of the login interface. After the c object is created, it is shaped upward and assigned to j.

There are many examples online showing that directly using Activity as a parameter to construct WebView is very likely to cause Activity leaks.

However, I also saw in the code that the WebView's destroy() method is called to release resources. However, it seems that there is no guarantee that dismiss() will be executed.

The problem is quite troublesome here. SDK is a third-party package for us. We can't let the third-party package not use WebView, or let the third-party package run WebView in another process! Therefore, it is not easy to circumvent it on the App. So I asked the SDK guys to analyze it together.

Finally, we all reached a preliminary consensus that in older versions below Android 4.3, using Activity objects to create WebViews may indeed cause memory leaks. I am very happy to get the strong support of SDK friends. We have made preliminary progress in analyzing the problem together.

4. The knot is not resolved, look through the WebView source code to understand the root cause

However, I still have a serious doubt in my mind (what is it?). So I took the source code of Android 4.3 and looked through it again to find the root cause. I made some notes and drew a rough class diagram for the structure of WebView in the Java layer: Source: http://androidxref.com/4.3_r2.1/ (Please copy the link above and open it in your browser)

The general situation is like this: In the WebView structure, there is a factory class WebViewFactory that provides static methods.

The Android 4.3 (Jelly Bean) version creates a global singleton object WebViewClassic$Factory through the WebViewFactory factory class, and then uses this Factory to create a complete set of implementation codes (XXXClassic): WebViewClassic, CookieManagserClassic, WebViewDatabaseClassic.

WebViewClassic is the one that actually implements the various APIs of WebView. WebViewClassic creates and maintains the WebViewCore object.

WebViewCore creates a child thread "WebViewCoreThread", which is a global singleton Thread that will not stop once started! WebViewCore will create, maintain and call BrowserFrame methods in this child thread.

BrowserFrame itself is a Handler subclass belonging to the "WebViewCoreThread" thread. BrowserFrame will be called by the native (c++) layer, and then these calls will be switched to the "WebViewCoreThread" thread for execution, such as refreshing progress or handling screen rotation events.

BrowserFrame also calls CookieSyncManager.createIntance(), which is the only place in the system framework where it is called!

After reading this, the host found that the above mentioned, help the system call in advance

CookieSyncManager.createInstance(contenxt.getApplicationContext()) may have no effect, because the system does this anyway. It is unlikely that the phone manufacturer will modify this.

What is CookieSyncManager? Similarly, it also creates a child thread called "CookieSyncManager", which is also a global singleton and will not stop! This thread will persist the cookies cached in memory syncFromRamToFlash() every 5 minutes.

Here we are more concerned about why the Activity leaks, so the key is to see which class objects hold Activity (Context) references: WebViewClassic, WebViewCore, BrowserFrame.

There are many static singletons and child threads in this structure, which is disgusting. Moreover, the three key classes all hold Activity references. However, we found that the lifecycles of the two objects, WebViewClassic and WebViewCore, are consistent with that of the WebView object. When the Activity is destroyed, the WebView is destroyed, and when the WebView is destroyed, the other two objects are destroyed as well. It's gone like smoke...

There are two lonely child threads still running, and the global static nail house object.

However, BrowserFrame itself is a Handler. If it sends a message to "WebViewCoreThread" because of the call of the native layer, a reference chain can be established:

Thread->MessageQueue->Message->Handler(BrowserFrame)->Activity

OK, what is the question of the host?

5. ***’s doubts

Let's take a look at the reference chain of AuthDialog.

It will be clearer if we change to MAT:

The author found that the CookieSyncManager thread here actually directly referenced the Message object! What the hell is this? Generally, HandlerThread holds a MessageQueue object, and MessageQueue holds the Message queue.

Java Local : A local variable. For example, input parameters, or locally created objects of methods that are still in the stack of a thread. Native stack.

Input or output parameters in native code, for example user-defined JNI code or JVM internal code. Many methods have native parts, and the objects that are handled as method parameters become garbage collection roots. For example, parameters used for file, network, I/O, or reflection operations.

This shows that there is a local variable of Message in the CookieSyncManager thread, and because the thread has not ended, the local variable has not been released. And this Message.obj member references the AuthDialog$3 object.

This is an inner class. The OP found that the naming rule for inner classes after confusion is: name it according to the first occurrence.

There are many inner classes in AuthDialog:

As shown above, AuthDialog$3 in the reference chain in MAT refers to the anonymous inner class OnDismissListener here! Next, let's take a look at what Dialog.setOnDismissListener does:

What the hell! OnDismissListener is assigned to the Message.obj member!

So, the reference chain generated in our mind is as follows:

Thread(main) -> MessageQueue->Message -> obj(OnDismissListener) -> AuthDialog -> Activity

But that's not right, the reference chain we can find has nothing to do with the CookieSyncManager child thread!

Let’s compare again:

The child thread CookieSyncManager got the main thread's Message!! Oh no!! What's going on??? Is this Message incorrectly referenced somewhere? The child thread gets the Java layer object in native through JNI?

Well, I admit that I have studied it for a whole night but have not made any progress. . .

6. It turns out to be this!—Dialog

Note: The following analysis and insights come from an article on Github: "A murder caused by a memory leak"

https://github.com/bboyfeiyu/android-tech-frontier/blob/master/issue-25/%E4%B8%80%E4%B8%AA %E5%86%85%E5%AD%98%E6%B3%84%E6%BC%8F%E5%BC%95%E5%8F%91%E7%9A%84%E8%A1%80%E6%A1%88-Square.md

If you are interested, please ☞☞ copy the link to your browser to open it ☜☜ and read it in detail!

Here is a brief explanation, the author's conclusion is: using AlertDialog before Android Lollipop may cause memory leaks!

The author found that the life cycle of local variables is different between Dalvik VM and ART/JVM. In DVM, if a thread is in an infinite loop or blocked, the local variables in the thread stack frame will not be recycled unless they are set to null.

The following code uses a blocking queue to illustrate the problem:

The child thread calls loop() in an infinite loop, continuously taking out a MyMessage object from the blocking queue and assigning the object reference to the local variable message. After one while loop, the virtual machine should end the life cycle of the local variable in the while curly braces and release the corresponding MyMessage object in the heap memory. However, DVM did not do this!!

In the VM, each stack frame is a collection of local variables, and the garbage collector is conservative: as long as there is a live reference, it will not be recycled. After each loop, the local variable is no longer accessible, but the local variable still holds a reference to the Message. The interpreter/JIT should theoretically set the reference to null when the local variable is inaccessible, but they did not do so. The reference is still alive and will not be set to null, so it will not be recycled!!

Isn't this the way the Android Handler message mechanism handles this scenario?!

Looper keeps taking the next message Message from the blocking queue MessageQueue and assigning the reference to the local variable msg. Once a loop ends, msg is not set to null, and the corresponding Message object is not recycled, so it leaks.

However, Message has its own recycling mechanism, and is shared by any thread. From the source code above, we can see that after each Message is processed by the Handler, it will be recycle(), all member variables will be cleared, and it will be placed in the recycling pool.

Well, the Message object that has been looped once by the CookieSyncManager child thread's Looper is also recycled and placed in the recycling pool like everyone else. At this time, Dialog is encountered!!

This guy just got the Message from the recycling pool through obtainMessage() (referenced by the local variable of the CookieSyncManager thread), and the Message.obj variable is the OnDismissListener.

After getting it, Dialog actually took it for himself!! He doted on it as a member!

Since Dialog has the mDismissMessage object, it will not be placed in the message queue. It will only copy it each time it is used. Message.obtain(mDismissMessage), so this Message will never return to the recycling pool until the Dialog is destroyed and the mDismissMessage variable is set to null.

However, this Message still occupies the heap memory and is referenced by a "free" child thread local variable msg!! So there is this reference chain:

Thread(CookieSyncManager) -> Message -> AuthDialog$3(OnDismissListener) -> AuthDialog -> Activity

7. Summary of some points

For Android 4.3 and below, or Android versions using DVM

  1. When using WebView, be sure to call destroy()
  2. Consider whether to use applicationContext() to construct a WebView instance
  3. When calling Dialog to set OnShowListener, OnDismissListener, OnCancelListener, pay attention to whether the inner class leaks the Activity object
  4. Try not to hold Message objects yourself.

<<:  How to speed up deep learning on mobile apps? You will know after reading this article

>>:  The most detailed explanation of Toolbar development in history, this is a must-read!

Recommend

What? Did anyone see the “wall-walking technique”?

Produced by: Science Popularization China Author:...

6 tips for attracting new customers in 2020!

During the epidemic, affected by the epidemic, ma...

More than 20% of users are still using third-party Android ROMs

Compared with the closed system of Apple iOS, And...

Zhao Dongxuan: Business model + implementation case analysis

Course Catalog├──1 -Course Study Guide, must read ...

New analysis of Tik Tok’s marketing strategy!

1. Long-term management returns to its essence In...

Short video operation: content positioning and hot spot operation skills

I believe all operators have more or less some un...

These car ads never disappoint.

Claude C. Hopkins, the father of modern advertisi...

Why did the “Lenovo Killer” become the new president of Lenovo Group?

After integrating the recently completed mergers ...

Ningxia issues implementation opinions on carbon peak and carbon neutrality

On January 10, 2022, the Party Committee and Gove...

Changes in NCAP rating standards force changes in automotive technology

NCAP—New Car Assessment Program, by simulating var...

These "foreign specialties" have become domestic products

Recently, Yunnan Plus avocados were put on the sh...

How to anger a copywriter in one sentence?

"It's just writing some words, everyone c...