【Android Q】 IRequestFinishCallback$Stub内存泄露问题

1,869 阅读2分钟

问题现象

触发场景

  1. 应用最后一个Activity退出;
  2. activity android:launchMode="singleInstance";
  3. 其他Task stack清空的场景;

问题原因

在LeakCanary中已经给出了答案,在Android Qandroid.app.IRequestFinishCallback$Stub,导致内存泄漏;
具体看下Android Q (Api 29) 的 Activity 源码:

public void onBackPressed() {
    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() {
                        mHandler.post(() -> finishAfterTransition());
                    }
                });
    } catch (RemoteException e) {
        finishAfterTransition();
    }
}

这个内存泄漏小学生都知道:mHandler.post(() -> finishAfterTransition());

Android R 的Bugfix

Android R (Api 30)的中解决方案,和众人皆知的Handler内存泄漏解决办法一样,改为静态内部类,引用改为弱引用;

private static final class RequestFinishCallback extends IRequestFinishCallback.Stub {
    private final WeakReference<Activity> mActivityRef;
    RequestFinishCallback(WeakReference<Activity> activityRef) {
        mActivityRef = activityRef;
    }
    @Override
    public void requestFinish() {
        Activity activity = mActivityRef.get();
        if (activity != null) {
            activity.mHandler.post(activity::finishAfterTransition);
        }
    }
}

public void onBackPressed() {
    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();
    }
}

非Android R如何解决?

  1. 不解决,只有Activity指定了SingleInstance或者关闭应用最后一个Activity才会出现内存泄漏,所以内存泄漏的场景很少,并且影响不大的情况下,可以选择放弃修复;
  2. 重写onBackPressed()方法,直接调用finishAfterTransition(),但是onBackPressedDispatcher等功能就失效了;
  3. 我的解决方案,注释里有具体方案描述:
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 中只有fallbackOnBackPressed是Runnable
                // 这样可以避免代码混淆带来的影响
                OnBackPressedDispatcher::class.java.declaredFields.find {
                    it.type.isAssignableFrom(Runnable::class.java)
                }
            }
        }
    }

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

    private fun fixAndroidQMemoryLeak() {
        if (Build.VERSION.SDK_INT != Build.VERSION_CODES.Q) return
        fallbackOnBackPressedField?.runCatching {
            isAccessible = true
            // 缓存默认的 fallbackOnBackPressed,如果不是TaskRoot还可以直接用它
            fallbackOnBackPressed = get(onBackPressedDispatcher) as? Runnable
            if (fallbackOnBackPressed != null) {
                // 替换默认的 fallbackOnBackPressed
                set(onBackPressedDispatcher, Runnable {
                    onFallbackOnBackPressed()
                })
            }
            fallbackOnBackPressedField?.isAccessible = false
        }?.onFailure {
            fallbackOnBackPressedField?.isAccessible = false
        }
    }

    /**
     * FragmentActivity等都是利用[mOnBackPressedDispatcher]addCallback完成Fragment pop,
     * 并且会优先于[OnBackPressedDispatcher]#mFallbackOnBackPressed执行
     */
    @RequiresApi(Build.VERSION_CODES.Q)
    private fun onFallbackOnBackPressed() {
        // 如果不是TaskRoot,不存在内存泄漏,执行原有函数就可以
        if (!isTaskRoot) {
            fallbackOnBackPressed?.run()
            return
        }
        // actionBar#collapseActionView 属于私有函数,所以就不要在Activity中使用[android.app.ActionBar]
        // if (actionBar != null && actionBar.collapseActionView()) return
        if (!fragmentManager.isStateSaved && fragmentManager.popBackStackImmediate()) return
        finishAfterTransition()
    }
}