Memory leaks caused by internal classes

Keywords: Android Java Fragment

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

Posted by mdemetri2 on Wed, 19 Jun 2019 13:44:01 -0700