Phenomenon of the problem
Triggering scenarios
- The last application Activity exits;
- activity
android:launchMode="singleInstance"
; - 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?
- 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.
- rewrite
onBackPressed()
Method, called directlyfinishAfterTransition()
, butonBackPressedDispatcher
And so on function is invalid; - 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