Phenomenon of the problem

Triggering scenarios

  1. The last application Activity exits;
  2. activity android:launchMode="singleInstance";
  3. Other scenarios where the Task Stack is empty;

Question why

In LeakCanary has given the answer in the Android Q Android. The app. IRequestFinishCallback $Stub, causing the memory leak; Android Q (Api 29)

public void onBackPressed(a) {
    if(mActionBar ! =null && mActionBar.collapseActionView()) {
        return;
    }
    FragmentManager fragmentManager = mFragments.getFragmentManager();
    if(! fragmentManager.isStateSaved() && fragmentManager.popBackStackImmediate()) {return;
    }
    if(! isTaskRoot()) {// If the activity is not the root of the task, allow finish to proceed normally.
        finishAfterTransition();
        return;
    }
    try {
        // Inform activity task manager that the activity received a back press
        // while at the root of the task. This call allows ActivityTaskManager
        // to intercept or defer finishing.
        ActivityTaskManager.getService().onBackPressedOnTaskRoot(mToken,
                new IRequestFinishCallback.Stub() {
                    public void requestFinish(a) { mHandler.post(() -> finishAfterTransition()); }}); }catch(RemoteException e) { finishAfterTransition(); }}Copy the code

Mhandler.post (() -> finishAfterTransition());

Android R Bugfix

The solution for Android R (Api 30), as well as the well-known Handler memory leak solution, is to change to static inner classes and reference to weak references;

private static final class RequestFinishCallback extends IRequestFinishCallback.Stub {
    private final WeakReference<Activity> mActivityRef;
    RequestFinishCallback(WeakReference<Activity> activityRef) {
        mActivityRef = activityRef;
    }
    @Override
    public void requestFinish(a) {
        Activity activity = mActivityRef.get();
        if(activity ! =null) { activity.mHandler.post(activity::finishAfterTransition); }}}public void onBackPressed(a) {
    if(mActionBar ! =null && mActionBar.collapseActionView()) {
        return;
    }
    FragmentManager fragmentManager = mFragments.getFragmentManager();
    if(! fragmentManager.isStateSaved() && fragmentManager.popBackStackImmediate()) {return;
    }
    if(! isTaskRoot()) {// If the activity is not the root of the task, allow finish to proceed normally.
        finishAfterTransition();
        return;
    }
    try {
        // Inform activity task manager that the activity received a back press
        // while at the root of the task. This call allows ActivityTaskManager
        // to intercept or defer finishing.
        ActivityTaskManager.getService().onBackPressedOnTaskRoot(mToken,
                new RequestFinishCallback(new WeakReference<>(this)));
    } catch(RemoteException e) { finishAfterTransition(); }}Copy the code

How to solve non-Android R?

  1. Memory leaks occur only when the Activity specifies SingleInstance or closes the last Activity of the application. Therefore, memory leaks are rare and can be abandoned if the impact is not significant.
  2. rewriteonBackPressed()Method, called directlyfinishAfterTransition(), butonBackPressedDispatcherAnd so on function is invalid;
  3. My solution is described in the notes:
abstract class BaseActivity : AppCompatActivity() {
    private var fallbackOnBackPressed: Runnable? = null

    companion object {
        @JvmStatic
        val fallbackOnBackPressedField by lazy {
            if(Build.VERSION.SDK_INT ! = Build.VERSION_CODES.Q) {null
            } else {
                // OnBackPressedDispatcher only fallbackOnBackPressed is Runnable
                // This avoids code obfuscation
                OnBackPressedDispatcher::class.java.declaredFields.find {
                    it.type.isAssignableFrom(Runnable::class.java)
                }
            }
        }
    }

    override fun onCreate(savedInstanceState: Bundle?). {
        super.onCreate(savedInstanceState)
        fixAndroidQMemoryLeak()
    }

    private fun fixAndroidQMemoryLeak(a) {
        if(Build.VERSION.SDK_INT ! = Build.VERSION_CODES.Q)returnfallbackOnBackPressedField? .runCatching { isAccessible =true
            // Cache the default fallbackOnBackPressed, which can be used if it is not TaskRoot
            fallbackOnBackPressed = get(onBackPressedDispatcher) as? Runnable
            if(fallbackOnBackPressed ! =null) {
                // Replace the default fallbackOnBackPressed
                set(onBackPressedDispatcher, Runnable { onFallbackOnBackPressed() }) } fallbackOnBackPressedField? .isAccessible =false}? .onFailure { fallbackOnBackPressedField? .isAccessible =false}}/** * FragmentActivity/mOnBackPressedDispatcher addCallback /** * * and will take precedence over [OnBackPressedDispatcher]#mFallbackOnBackPressed to execute */
    @RequiresApi(Build.VERSION_CODES.Q)
    private fun onFallbackOnBackPressed(a) {
        // If it is not TaskRoot, there is no memory leak, just execute the original function
        if(! isTaskRoot) { fallbackOnBackPressed? .run()return
        }
        // ActionBar #collapseActionView is a private function, so do not use android.app.actionbar in the Activity view
        // if (actionBar ! = null && actionBar.collapseActionView()) return
        if(! fragmentManager.isStateSaved && fragmentManager.popBackStackImmediate())return
        finishAfterTransition()
    }
}
Copy the code