Memory leaks caused by internal classes
Internal classes are ubiquitous in the development process. There are two common ways to use them: member internal classes, static internal classes and anonymous internal classes (and a local internal class which seems to be used little, so it is not introduced). Their code formats are as follows:
- Static inner class
public class ExplicitClassActivity extends Activity{
...
private static class CorrectInnerClass{
}
...
}
- Membership inner class:
public class ExplicitClassActivity extends Activity{
...
private class ErrorInnerClass{
}
...
}
- Anonymous inner class:
public class ExplicitClassActivity extends Activity{
...
AsyncTask task = new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
SystemClock.sleep(Contacts.LONG_TIME);
return null;
}
}.execute();
...
}
Among the three internal classes mentioned above, both non-static internal classes and anonymous internal classes hold references to external classes, so they are prone to memory leaks.
PS: Why do non-static internal classes hold references to external classes? Because the compiler handles non-static internal classes in this way:
1. The compiler automatically adds a member variable to the inner class. The member variable has the same type as the outer class. This member variable refers to the reference of the outer class object.
2. The compiler automatically adds a parameter to the construction method of the inner class. The type of the parameter is the type of the outer class. The member variable added in 1 is used to assign the parameter inside the construction method.
3. When the constructor of the inner class is called to initialize the inner class object, the reference of the outer class is passed in by default.
Memory leaks caused by non-static internal classes
Memory leaks caused by non-static internal classes also depend on static member variables. Memory leaks occur only when the type of a static member variable is a non-static internal class:
public class ExplicitClassActivity extends Activity{
private static Object errorClass = new ErrorInnerClass();
private Object correctClass = new ErrorInnerClass();
private class ErrorInnerClass{
}
}
In the above example, errorClass, as a static member, always holds references to external activities during the application runtime, resulting in memory leaks. Although correctClass also holds a reference to Activity, it synchronizes with Activity's life cycle and does not cause memory leaks.
Memory leaks caused by anonymous internal classes
Anonymous internal classes cause memory leaks in Android for two reasons:
1. Define static members using anonymous inner classes, and hold external class references throughout the application runtime
2. The life cycle of anonymous internal classes is not synchronized with that of external classes, and external class references will always be held during the life cycle of internal classes.
Both are common in Android development:
public class ImplicitClassActivity extends SimpleInfoActivity implements View.OnClickListener {
private static SampleClass errorClass;
private SampleClass correctClass;
...
@Override
public void onClick(View v) {
}
private void sample1(boolean isError) {
if (isError)
findViewById(R.id.tv_info).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
}
});
else
findViewById(R.id.tv_info).setOnClickListener(this);
}
private void sample2(boolean isError) {
Thread thread = null;
if (isError)
thread = new Thread(new Runnable() {
@Override
public void run() {
SystemClock.sleep(Contacts.LONG_TIME);
}
});
else
thread = new Thread(new MyRunnable());
thread.start();
}
private void sample3(boolean isError) {
if (isError)
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
SystemClock.sleep(Contacts.LONG_TIME);
return null;
}
}.execute();
else
new MyAsyncTask().execute();
}
private void sample4(boolean isError) {
if (isError)
errorClass = new SampleClass() {
@Override
protected void sample() {
}
};
else
correctClass = new SampleClass() {
@Override
protected void sample() {
}
};
}
private static class MyRunnable implements Runnable {
@Override
public void run() {
}
}
private static class MyAsyncTask extends AsyncTask<Void, Void, Void> {
@Override
protected Void doInBackground(Void... params) {
SystemClock.sleep(Contacts.LONG_TIME);
return null;
}
}
}
In the above example, four scenarios are given. In each scenario, the code executed when isError==true may cause memory leak. The else judgment is the corresponding evasion scheme.
Leakage point | Causes of Leakage | Action time |
---|---|---|
sample1 | OnClickListener holds external class references | Before OnClickListener was destroyed |
sample2 | Thread holds external class references | Before Thread was destroyed |
sample3 | AsyncTask holds external class references | Before AsyncTask was destroyed |
sample4 | ErorClass holds external class references | App Running Phase |
To avoid memory leaks caused by anonymous internal classes, we can follow the following guidelines:
1. Variables of anonymous internal classes cannot be set to static variables
2. Try not to directly create an anonymous inner class, but to implement it in a more secure way (refer to MyAsyncTask and MyRunnable)
3. Injecting setListener-related methods to create interface implementations is best done by letting external classes implement the relevant interfaces, and then directly passing in references to external classes.
Memory leaks caused by Handler
The memory leak caused by Handler is also a frequent occurrence in Android development. We often use Handler to achieve cross-threaded communication. Its occurrence scenario overlaps with the previous two occurrence scenarios, but has its additional characteristics. So we will take Handler as a separate example:
public class HandlerActivity extends SimpleInfoActivity {
private static Handler errorHandler;
private final SafeHandler safeHandler = new SafeHandler(this);
...
private void sample1(boolean isError) {
if (isError)
errorHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
}
};
else
errorHandler = new SafeHandler(this);
}
private void sample2(boolean isError) {
if (isError) {
safeHandler.postDelayed(new Runnable() {
@Override
public void run() {
}
}, Contacts.LONG_TIME);
}else
safeHandler.postDelayed(safeRunnable, Contacts.LONG_TIME);
}
private void sample3(boolean isError) {
safeHandler.sendEmptyMessageDelayed(0, Contacts.LONG_TIME);
}
@Override
protected void onDestroy() {
super.onDestroy();
if (!isError()) {
safeHandler.removeCallbacks(safeRunnable);
safeHandler.removeMessages(0);
}
}
private static class SafeRunnable implements Runnable {
@Override
public void run() {
SystemClock.sleep(Contacts.LONG_TIME);
}
}
private SafeRunnable safeRunnable=new SafeRunnable();
private static class SafeHandler extends Handler {
private final WeakReference<HandlerActivity> activityReference;
public SafeHandler(HandlerActivity activity) {
activityReference = new WeakReference<>(activity);
}
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
HandlerActivity activity = activityReference.get();
if (activity != null) {
//TODO:
}
}
}
}
Handler may cause memory leaks for many reasons, and some of the resulting memory leaks are still time-bound, only in a certain period of time will be manifested in the memory heap, so even with tools such as MAT inspection may also be incomplete, let's take a look at the above examples to see what may be leaked.
Before explaining the memory leak in the example, let's look at the implementation mechanism of Handler.
As mentioned earlier, Handler is used for cross-process communication, and its principle is not complicated.
When the application starts, Android first opens a main thread (that is, UI thread). In the UI thread, time-consuming operations cannot be performed. Otherwise, fake death or even forced closure will occur.
Hadler created in the main thread runs in the main thread. Subthreads can send messages to the MessageQueue in the Handler by getting an instance of the Handler. At the same time, according to the incoming delayMillis, they can calculate the time uptime Millis that the current message needs to be executed, and then pass messages and uptime Millis into the MessageQueue (MessageQueue corresponds to Message according to the incoming delayMillis). Execution time adds Message to MessageQueue:
Handler.java fragment
public final boolean sendMessageDelayed(Message msg, long delayMillis)
{
if (delayMillis < 0) {
delayMillis = 0;
}
return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
}
private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
msg.target = this;
if (mAsynchronous) {
msg.setAsynchronous(true);
}
return queue.enqueueMessage(msg, uptimeMillis);
}
The Looper of the main thread loops wirelessly, removing messages from the Message Queue and executing them.
Looper.java Source fragment
public static void loop() {
...
for (;;) {
Message msg = queue.next(); // might block
...
msg.target.dispatchMessage(msg);
...
msg.recycleUnchecked();
}
}
}
Message is destroyed after execution:
Message.java Source fragment
void recycleUnchecked() {
...
target = null;
callback = null;
...
}
Through the above way, Handler can easily achieve cross-process communication.
Now let's look at the special aspects of Handler's memory leak:
First, take a look at the enqueueMessage method in Handler.java, which contains this section
msg.target = this; //This code allows Message to hold a reference to Handler
This reference is destroyed by the loop() method in Looper.java:
msg.recycleUnchecked(); //This method sets the holdings of callback and handler to null
Therefore, during the period of message sending to message destroying, message will always hold references to Handler and Runable, so there is a risk of memory leak. When the latency is very short, such a memory leak problem is difficult to detect by the detection tools (MAT, Leak Canary and Android Studio are all difficult to detect such leaks, the three tools will be introduced later).
In conjunction with the Handler Activity example given above, there are memory leaks in our example, including:
Leakage point | Causes of Leakage | Action time |
---|---|---|
sample1 | ErorHandler holds a reference to Activity | App Survival Period |
sample2 | Msg holds references to Handler and Runable; Runable holds references to Activity | Before Message is destroyed |
sample3 | Msg holds a reference to Handler | Before Message is destroyed |
To avoid memory leaks caused by Handler, the following points need to be observed:
1. Call Handler. remote methods in time when you no longer need to use Handler (refer to the onDestroy method in the example)
2. Don't directly use new Handler(), use a safer implementation (see SafeHandler in the example)
3. When using Handler's post method, do not directly use a new Runable, but use a safer implementation (refer to SafeRunnable in the example)
4. Avoid using static to modify Handler as much as possible