由于 Snackbar 导致的内存泄漏

Memory Leak due to Snackbar

我刚刚将 CanaryLeak 添加到我的项目中以查看我的应用程序中是否存在任何内存泄漏并注意到,由于 Snackbar,我的一个片段中确实存在泄漏。

我正在 onCreateView 中创建一个 Snackbar,并在 onDestroyView 中将其设置为 null。但是每次旋转屏幕时都会发生内存泄漏。


    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup 
    container, Bundle savedInstanceState) {

        View view = inflater.inflate(R.layout.fragment_backend, container, 
        false);

        Activity parentActivity = getActivity();
        if (parentActivity != null) {
            mConnectSnackbar = 
            Snackbar.make(parentActivity.findViewById(R.id.nav_host_fragment), 
            "Connect", Snackbar.LENGTH_INDEFINITE);

            mConnectSnackbar.setAction(getString(R.string.connect), v -> 
            startActivity(new Intent(Settings.ACTION_WIRELESS_SETTINGS)));
        }

        return view;
    }

    @Override
    public void onDestroyView() {
        super.onDestroyView();
        mConnectSnackbar.setAction("Connect", null);
        mConnectSnackbar.dismiss();
        mConnectSnackbar = null;
    }

当我清除对操作和 Snackbar 本身的引用时,应该没有任何内存泄漏的原因。但是我无法弄清楚,原因可能是什么,来自 Canary Leak 的堆转储也无济于事。 由于对 nav_host_fragment 的引用,我怀疑这可能是我造成的,但我不知道它是否属实以及如何解决它。

非常感谢您的帮助。

编辑 1:

添加了 Leak Trace 并删除了 hprof 文件。

┬
├─ android.view.accessibility.AccessibilityManager
│    Leaking: NO (a class is never leaking)
│    GC Root: System class
│    ↓ static AccessibilityManager.sInstance
│                                  ~~~~~~~~~
├─ android.view.accessibility.AccessibilityManager
│    Leaking: UNKNOWN
│    ↓ AccessibilityManager.mTouchExplorationStateChangeListeners
│                           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
├─ android.util.ArrayMap
│    Leaking: UNKNOWN
│    ↓ ArrayMap.mArray
│               ~~~~~~
├─ java.lang.Object[]
│    Leaking: UNKNOWN
│    ↓ array Object[].[4]
│                     ~~~
├─ androidx.core.view.accessibility.AccessibilityManagerCompat$TouchExplorationStateChangeListenerWrapper
│    Leaking: UNKNOWN
│    ↓ AccessibilityManagerCompat$TouchExplorationStateChangeListenerWrapper.mListener
│                                                                            ~~~~~~~~~
├─ com.google.android.material.snackbar.BaseTransientBottomBar$SnackbarBaseLayout
│    Leaking: UNKNOWN
│    ↓ BaseTransientBottomBar$SnackbarBaseLayout.this[=11=]
│                                                  ~~~~~~
╰→ com.google.android.material.snackbar.Snackbar$SnackbarLayout
​     Leaking: YES (View.mContext references a destroyed activity)
​     mContext instance of android.view.ContextThemeWrapper, wrapping activity com.twaice.twaice.MainActivity with mDestroyed = true
​     View#mParent is null
​     View#mAttachInfo is null (view detached)
​     View.mWindowAttachCount = 0

我想我已经设法找到了解决方案,尽管我不明白其中的原因。如前所述,我在 onCreateView 中创建了 Snackbar,但稍后一些其他逻辑决定是否显示 Snackbar。

现在解决方案:内存泄漏仅在创建但从未显示的 Snackbar 时存在。因此,我没有在 onCreateView 中创建 Snackbar,而是将以下代码放在应该显示 Snackbar 时调用的函数中:

if (mConnectSnackbar == null) {
        Activity parentActivity = getActivity();
        if (parentActivity != null) {
            mConnectSnackbar = 
            Snackbar.make(parentActivity.findViewById(R.id.nav_host_fragment), 
            "Connect", Snackbar.LENGTH_INDEFINITE);

            mConnectSnackbar.setAction(getString(R.string.connect), v -> 
            startActivity(new Intent(Settings.ACTION_WIRELESS_SETTINGS)));
            mConnectSnackbar.show()
        }
} else {
       mConnectSnackbar.show();
}

自从我添加此更改后,未发生内存泄漏。但是我真的不明白为什么在 show() 没有被调用时会发生这种情况。如果有人能解释为什么会发生这种情况,我将不胜感激。

这是 material-components-android 库中的内存泄漏。我刚刚提交了一个问题:https://github.com/material-components/material-components-android/issues/497

只有在创建了小吃店但从未按照问题所述显示时才会发生此泄漏:

In Material Library 1.0.0, when a BaseTransientBottomBar .SnackbarBaseLayout instance is created, it registers a TouchExplorationStateChangeListener which it then unregisters onDetachedFromWindow(). If the SnackbarBaseLayout is created but never attached (which happens), then it never gets detached. When the underlying context (an activity) gets destroyed, the TouchExplorationStateChangeListener is kept in memory by AccessibilityManager, holding on to its outer class SnackbarBaseLayout which itself holds on to its context, a destroyed activity. Effectively SnackbarBaseLayout is leaking destroyed activities and the entire view hierarchy.

好消息是此代码不再存在于 1.1.0 版本中,因此泄漏已经消失,但不幸的是 1.1.0 仍处于 alpha 版本中。

注意:在以后的帖子中,考虑提供 LeakCanary 输出的文本泄漏跟踪,这对于解决内存泄漏很有用。