了解 ViewTreeObserver 泄漏

Understanding ViewTreeObserver leak

我正在使用 LeakCanary 1.3.1-SNAPSHOT。 我发现了一个关于 ViewTreeObserver.OnScrollChangedListener 设置的漏洞,我在下面的代码中修复了它:

  private ViewTreeObserver.OnScrollChangedListener scrollViewChangeListener;

  @Override protected void onFinishInflate() {
      super.onFinishInflate();
      ButterKnife.inject(this);
      scrollViewChangeListener = new ViewTreeObserver.OnScrollChangedListener() {
      @Override public void onScrollChanged() {
        EventDetailsView.this.onScrollChanged(scrollView.getScrollY());
      }
    };
      scrollView.getViewTreeObserver()
        .addOnScrollChangedListener(scrollViewChangeListener);
  }
  @Override public void onDetachedFromWindow() {
      super.onDetachedFromWindow();
      scrollView.getViewTreeObserver().removeOnScrollChangedListener(scrollViewChangeListener);
  }

但是 LeakCanary 仍然将其报告为泄漏,知道为什么吗?

* com.couchsurfing.mobile.ui.events.detail.EventDetailsScreen$Presenter has leaked:
* GC ROOT android.view.inputmethod.InputMethodManager.this[=11=] (anonymous class extends com.android.internal.view.IInputMethodClient$Stub)
* references android.view.inputmethod.InputMethodManager.mCurRootView
* references com.android.internal.policy.impl.PhoneWindow$DecorView.mAttachInfo
* references android.view.View$AttachInfo.mTreeObserver
* references android.view.ViewTreeObserver.mOnScrollChangedListeners
* references android.view.ViewTreeObserver$CopyOnWriteArray.mData
* references java.util.ArrayList.array
* references array java.lang.Object[].[0]
* references com.couchsurfing.mobile.ui.events.detail.EventDetailsView.this[=11=] (anonymous class implements android.view.ViewTreeObserver$OnScrollChangedListener)
* references com.couchsurfing.mobile.ui.events.detail.EventDetailsView.presenter
* leaks com.couchsurfing.mobile.ui.events.detail.EventDetailsScreen$Presenter instance* Reference Key: 69d0a429-ae27-48fc-a8e0-033c920dd07c
* Device: LGE google Nexus 5 hammerhead
* Android Version: 5.1 API: 22 LeakCanary: 1.3.1-SNAPSHOT
* Durations: watch=5032ms, gc=165ms, heap dump=2932ms, analysis=29907ms* Details:
* Instance of android.view.inputmethod.InputMethodManager
|   this[=11=] = android.view.inputmethod.InputMethodManager [id=0x130239c0]
|   mDescriptor = java.lang.String [id=0x6f5e3f38]
|   mObject = -1601862176
|   mOwner = android.view.inputmethod.InputMethodManager [id=0x13112da0]
* Instance of android.view.inputmethod.InputMethodManager
|   static $staticOverhead = byte[] [id=0x6fe25d29;length=240;size=256]
|   static CONTROL_START_INITIAL = 256
|   static CONTROL_WINDOW_FIRST = 4
|   static CONTROL_WINDOW_IS_TEXT_EDITOR = 2
|   static CONTROL_WINDOW_VIEW_HAS_FOCUS = 1
|   static DEBUG = false
|   static DISPATCH_HANDLED = 1
|   static DISPATCH_IN_PROGRESS = -1
|   static DISPATCH_NOT_HANDLED = 0
|   static HIDE_IMPLICIT_ONLY = 1
|   static HIDE_NOT_ALWAYS = 2
|   static INPUT_METHOD_NOT_RESPONDING_TIMEOUT = 2500
|   static MSG_BIND = 2
|   static MSG_DUMP = 1
|   static MSG_FLUSH_INPUT_EVENT = 7
|   static MSG_SEND_INPUT_EVENT = 5
|   static MSG_SET_ACTIVE = 4
|   static MSG_SET_USER_ACTION_NOTIFICATION_SEQUENCE_NUMBER = 9
|   static MSG_TIMEOUT_INPUT_EVENT = 6
|   static MSG_UNBIND = 3
|   static NOT_AN_ACTION_NOTIFICATION_SEQUENCE_NUMBER = -1
|   static PENDING_EVENT_COUNTER = java.lang.String [id=0x6f5bb948]
|   static REQUEST_UPDATE_CURSOR_ANCHOR_INFO_NONE = 0
|   static RESULT_HIDDEN = 3
|   static RESULT_SHOWN = 2
|   static RESULT_UNCHANGED_HIDDEN = 1
|   static RESULT_UNCHANGED_SHOWN = 0
|   static SHOW_FORCED = 2
|   static SHOW_IMPLICIT = 1
|   static TAG = java.lang.String [id=0x6f5a76e0]
|   static sInstance = android.view.inputmethod.InputMethodManager [id=0x130239c0]
|   mActive = true
|   mBindSequence = 1523
|   mClient = android.view.inputmethod.InputMethodManager [id=0x13112da0]
|   mCompletions = null
|   mCurChannel = android.view.InputChannel [id=0x1304a850]
|   mCurId = java.lang.String [id=0x1325dd80]
|   mCurMethod = com.android.internal.view.IInputMethodSession$Stub$Proxy [id=0x1304a840]
|   mCurRootView = com.android.internal.policy.impl.PhoneWindow$DecorView [id=0x12eac000]
|   mCurSender = android.view.inputmethod.InputMethodManager$ImeInputEventSender [id=0x12c72850]
|   mCurrentTextBoxAttribute = android.view.inputmethod.EditorInfo [id=0x133036c0]
|   mCursorAnchorInfo = null
|   mCursorCandEnd = 0
|   mCursorCandStart = 0
|   mCursorRect = android.graphics.Rect [id=0x13112d40]
|   mCursorSelEnd = 0
|   mCursorSelStart = 0
|   mDummyInputConnection = android.view.inputmethod.BaseInputConnection [id=0x13112dc0]
|   mFullscreenMode = false
|   mH = android.view.inputmethod.InputMethodManager$H [id=0x13112de0]
|   mHasBeenInactive = false
|   mIInputContext = android.view.inputmethod.InputMethodManager$ControlledInputConnectionWrapper [id=0x13113310]
|   mLastSentUserActionNotificationSequenceNumber = -1
|   mMainLooper = android.os.Looper [id=0x12c76be0]
|   mNextServedView = com.couchsurfing.mobile.ui.drawer.DrawerView [id=0x131f8c00]
|   mNextUserActionNotificationSequenceNumber = 1
|   mPendingEventPool = android.util.Pools$SimplePool [id=0x13110fe0]
|   mPendingEvents = android.util.SparseArray [id=0x13112d80]
|   mRequestUpdateCursorAnchorInfoMonitorMode = 0
|   mServedConnecting = false
|   mServedInputConnection = null
|   mServedInputConnectionWrapper = null
|   mServedView = com.couchsurfing.mobile.ui.drawer.DrawerView [id=0x131f8c00]
|   mService = com.android.internal.view.IInputMethodManager$Stub$Proxy [id=0x13110fc0]
|   mTmpCursorRect = android.graphics.Rect [id=0x13112d20]
|   mViewToScreenMatrix = android.graphics.Matrix [id=0x13110fd0]
|   mViewTopLeft = int[] [id=0x13112d60;length=2;size=24]
* Instance of com.android.internal.policy.impl.PhoneWindow$DecorView
|   mActionMode = null
|   mActionModePopup = null
|   mActionModeView = null
|   mBackgroundFallback = com.android.internal.widget.BackgroundFallback [id=0x12fdd8e0]
|   mBackgroundPadding = android.graphics.Rect [id=0x12ffd9a0]
|   mBarEnterExitDuration = 250
|   mChanging = false
|   mDefaultOpacity = -1
|   mDownY = 0
|   mDrawingBounds = android.graphics.Rect [id=0x12ffd980]
|   mFeatureId = -1
|   mFrameOffsets = android.graphics.Rect [id=0x12ffd9e0]
|   mFramePadding = android.graphics.Rect [id=0x12ffd9c0]
|   mHideInterpolator = android.view.animation.PathInterpolator [id=0x12ffdb00]
|   mLastBottomInset = 144
|   mLastHasBottomStableInset = true
|   mLastHasTopStableInset = true
|   mLastRightInset = 0
|   mLastTopInset = 75
|   mLastWindowFlags = -2122252032
|   mMenuBackground = null
|   mNavigationColorViewState = com.android.internal.policy.impl.PhoneWindow$ColorViewState [id=0x12ff2c70]
|   mNavigationGuard = null
|   mRootScrollY = 0
|   mShowActionModePopup = null
|   mShowInterpolator = android.view.animation.PathInterpolator [id=0x12ffda60]
|   mStatusColorViewState = com.android.internal.policy.impl.PhoneWindow$ColorViewState [id=0x12ff2c40]
|   mStatusGuard = null
|   mWatchingForMenu = false
|   this[=11=] = com.android.internal.policy.impl.PhoneWindow [id=0x12db9e00]
|   mForeground = null
|   mForegroundBoundsChanged = true
|   mForegroundGravity = 119
|   mForegroundInPadding = true
|   mForegroundPaddingBottom = 0
|   mForegroundPaddingLeft = 0
|   mForegroundPaddingRight = 0
|   mForegroundPaddingTop = 0
|   mForegroundTintList = null
|   mForegroundTintMode = null
|   mHasForegroundTint = false
|   mHasForegroundTintMode = false
|   mMatchParentChildren = java.util.ArrayList [id=0x12ffd960]
|   mMeasureAllChildren = false
|   mOverlayBounds = android.graphics.Rect [id=0x12ffd940]
|   mSelfBounds = android.graphics.Rect [id=0x12ffd920]
|   mAnimationListener = null
|   mCachePaint = null
|   mChildAcceptsDrag = false
|   mChildCountWithTransientState = 0
|   mChildTransformation = null
|   mChildren = android.view.View[] [id=0x130064c0;length=12]
|   mChildrenCount = 3
|   mCurrentDrag = null
|   mCurrentDragView = null
|   mDisappearingChildren = null
|   mDragNotifiedChildren = null
|   mFirstHoverTarget = null
|   mFirstTouchTarget = null
|   mFocused = android.widget.LinearLayout [id=0x12eac800]
|   mGroupFlags = 2375763
|   mHoveredSelf = false
|   mInvalidateRegion = null
|   mInvalidationTransformation = null
|   mLastTouchDownIndex = 0
|   mLastTouchDownTime = 137539724
|   mLastTouchDownX = 605.0
|   mLastTouchDownY = 1177.0
|   mLayoutAnimationController = null
|   mLayoutCalledWhileSuppressed = false
|   mLayoutMode = 0
|   mLayoutTransitionListener = android.view.ViewGroup [id=0x12fdd850]
|   mLocalPoint = null
|   mNestedScrollAxes = 0
|   mOnHierarchyChangeListener = null
|   mPersistentDrawingCache = 2
|   mPreSortedChildren = null
|   mSuppressLayout = false
|   mTempPoint = float[] [id=0x12c0a220;length=2;size=24]
|   mTransition = null
|   mTransitioningViews = null
|   mVisibilityChangingChildren = null
|   mAccessibilityCursorPosition = -1
|   mAccessibilityDelegate = null
|   mAccessibilityTraversalAfterId = -1
|   mAccessibilityTraversalBeforeId = -1
|   mAccessibilityViewId = -1
|   mAnimator = null
|   mAttachInfo = android.view.View$AttachInfo [id=0x12c4fcc0]
|   mAttributes = null
|   mBackground = android.graphics.drawable.ColorDrawable [id=0x13014f80]
|   mBackgroundRenderNode = android.view.RenderNode [id=0x12c73740]
|   mBackgroundResource = 0
|   mBackgroundSizeChanged = false
|   mBackgroundTint = null
|   mBottom = 1920
|   mCachingFailed = false
|   mClipBounds = null
|   mContentDescription = null
|   mContext = com.couchsurfing.mobile.ui.MainActivity [id=0x12db9c80]
|   mCurrentAnimation = null
|   mDrawableState = null
|   mDrawingCache = null
|   mDrawingCacheBackgroundColor = 0
|   mFloatingTreeObserver = null
|   mGhostView = null
|   mHasPerformedLongPress = false
|   mID = -1
|   mInputEventConsistencyVerifier = null
|   mKeyedTags = null
|   mLabelForId = -1
|   mLastIsOpaque = true
|   mLayerPaint = null
|   mLayerType = 0
|   mLayoutInsets = null
|   mLayoutParams = android.view.WindowManager$LayoutParams [id=0x12f1f7e0]
|   mLeft = 0
|   mLeftPaddingDefined = true
|   mListenerInfo = android.view.View$ListenerInfo [id=0x13109940]
|   mMatchIdPredicate = null
|   mMatchLabelForPredicate = null
|   mMeasureCache = android.util.LongSparseLongArray [id=0x13400120]
|   mMeasuredHeight = 1920
|   mMeasuredWidth = 1080
|   mMinHeight = 0
|   mMinWidth = 0
|   mNestedScrollingParent = null
|   mNextFocusDownId = -1
|   mNextFocusForwardId = -1
|   mNextFocusLeftId = -1
|   mNextFocusRightId = -1
|   mNextFocusUpId = -1
|   mOldHeightMeasureSpec = 1073743744
|   mOldWidthMeasureSpec = 1073742904
|   mOutlineProvider = android.view.ViewOutlineProvider [id=0x6fcd7240]
|   mOverScrollMode = 1
|   mOverlay = null
|   mPaddingBottom = 0
|   mPaddingLeft = 0
|   mPaddingRight = 0
|   mPaddingTop = 0
|   mParent = android.view.ViewRootImpl [id=0x13313400]
|   mPendingCheckForLongPress = null
|   mPendingCheckForTap = null
|   mPerformClick = null
|   mPrivateFlags = 25201976
|   mPrivateFlags2 = 1611867680
|   mPrivateFlags3 = 4
|   mRecreateDisplayList = false
|   mRenderNode = android.view.RenderNode [id=0x12ffd880]
|   mResources = android.content.res.Resources [id=0x12c078e0]
|   mRight = 1080
|   mRightPaddingDefined = true
|   mScrollCache = null
|   mScrollX = 0
|   mScrollY = 0
|   mSendViewScrolledAccessibilityEvent = null
|   mSendViewStateChangedAccessibilityEvent = null
|   mSendingHoverAccessibilityEvents = false
|   mStateListAnimator = null
|   mSystemUiVisibility = 0
|   mTag = null
|   mTempNestedScrollConsumed = null
|   mTop = 0
|   mTouchDelegate = null
|   mTouchSlop = 24
|   mTransformationInfo = android.view.View$TransformationInfo [id=0x1349e7c0]
|   mTransientStateCount = 0
|   mTransitionName = null
|   mUnscaledDrawingCache = null
|   mUnsetPressedState = null
|   mUserPaddingBottom = 0
|   mUserPaddingEnd = -2147483648
|   mUserPaddingLeft = 0
|   mUserPaddingLeftInitial = 0
|   mUserPaddingRight = 0
|   mUserPaddingRightInitial = 0
|   mUserPaddingStart = -2147483648
|   mVerticalScrollFactor = 0.0
|   mVerticalScrollbarPosition = 0
|   mViewFlags = 402655360
|   mWindowAttachCount = 1
* Instance of android.view.View$AttachInfo
|   mAccessibilityFetchFlags = 0
|   mAccessibilityFocusDrawable = null
|   mAccessibilityWindowId = 2147483647
|   mApplicationScale = 1.0
|   mCanvas = null
|   mContentInsets = android.graphics.Rect [id=0x13364ee0]
|   mDebugLayout = false
|   mDisabledSystemUiVisibility = 0
|   mDisplay = android.view.Display [id=0x12f75b50]
|   mDisplayState = 2
|   mDrawingTime = 137551407
|   mForceReportNewAttributes = false
|   mGivenInternalInsets = android.view.ViewTreeObserver$InternalInsetsInfo [id=0x13364f40]
|   mGlobalSystemUiVisibility = 0
|   mHandler = android.view.ViewRootImpl$ViewRootHandler [id=0x13364d00]
|   mHardwareAccelerated = true
|   mHardwareAccelerationRequested = true
|   mHardwareRenderer = android.view.ThreadedRenderer [id=0x13323dc0]
|   mHasNonEmptyGivenInternalInsets = false
|   mHasSystemUiListeners = true
|   mHasWindowFocus = true
|   mHighContrastText = false
|   mIWindowId = null
|   mIgnoreDirtyState = false
|   mInTouchMode = true
|   mInvalidateChildLocation = int[] [id=0x13370060;length=2;size=24]
|   mKeepScreenOn = false
|   mKeyDispatchState = android.view.KeyEvent$DispatcherState [id=0x13364fc0]
|   mOverscanInsets = android.graphics.Rect [id=0x13364ec0]
|   mOverscanRequested = false
|   mPanelParentWindowToken = null
|   mPendingAnimatingRenderNodes = null
|   mPoint = android.graphics.Point [id=0x133582f0]
|   mRecomputeGlobalAttributes = false
|   mRootCallbacks = android.view.ViewRootImpl [id=0x13313400]
|   mRootView = com.android.internal.policy.impl.PhoneWindow$DecorView [id=0x12eac000]
|   mScalingRequired = false
|   mScrollContainers = java.util.ArrayList [id=0x13364fa0]
|   mSession = android.view.IWindowSession$Stub$Proxy [id=0x13358290]
|   mSetIgnoreDirtyState = true
|   mStableInsets = android.graphics.Rect [id=0x13364f20]
|   mSystemUiVisibility = 0
|   mTempArrayList = java.util.ArrayList [id=0x133701a0]
|   mTmpInvalRect = android.graphics.Rect [id=0x133700c0]
|   mTmpLocation = int[] [id=0x13370080;length=2;size=24]
|   mTmpMatrix = android.graphics.Matrix [id=0x133582d0]
|   mTmpOutline = android.graphics.Outline [id=0x13370180]
|   mTmpRectList = java.util.ArrayList [id=0x13370120]
|   mTmpTransformLocation = float[] [id=0x133700a0;length=2;size=24]
|   mTmpTransformRect = android.graphics.RectF [id=0x133700e0]
|   mTmpTransformRect1 = android.graphics.RectF [id=0x13370100]
|   mTmpTransformation = android.view.animation.Transformation [id=0x13370140]
|   mTransparentLocation = int[] [id=0x13370040;length=2;size=24]
|   mTreeObserver = android.view.ViewTreeObserver [id=0x133656c0]
|   mTurnOffWindowResizeAnim = false
|   mUnbufferedDispatchRequested = false
|   mUse32BitDrawingCache = true
|   mViewRequestingLayout = null
|   mViewRootImpl = android.view.ViewRootImpl [id=0x13313400]
|   mViewScrollChanged = false
|   mViewVisibilityChanged = false
|   mVisibleInsets = android.graphics.Rect [id=0x13364f00]
|   mWindow = android.view.ViewRootImpl$W [id=0x13364e80]
|   mWindowId = null
|   mWindowLeft = 0
|   mWindowToken = android.view.ViewRootImpl$W [id=0x13364e80]
|   mWindowTop = 0
|   mWindowVisibility = 0
* Instance of android.view.ViewTreeObserver
|   mAlive = true
|   mOnComputeInternalInsetsListeners = null
|   mOnDrawListeners = null
|   mOnEnterAnimationCompleteListeners = null
|   mOnGlobalFocusListeners = null
|   mOnGlobalLayoutListeners = android.view.ViewTreeObserver$CopyOnWriteArray [id=0x1315a300]
|   mOnPreDrawListeners = android.view.ViewTreeObserver$CopyOnWriteArray [id=0x13345760]
|   mOnScrollChangedListeners = android.view.ViewTreeObserver$CopyOnWriteArray [id=0x12fd3220]
|   mOnTouchModeChangeListeners = java.util.concurrent.CopyOnWriteArrayList [id=0x133a1420]
|   mOnWindowAttachListeners = null
|   mOnWindowFocusListeners = null
|   mOnWindowShownListeners = null
|   mWindowShown = false
* Instance of android.view.ViewTreeObserver$CopyOnWriteArray
|   mAccess = android.view.ViewTreeObserver$CopyOnWriteArray$Access [id=0x12fa1960]
|   mData = java.util.ArrayList [id=0x12fd3240]
|   mDataCopy = null
|   mStart = false
* Instance of java.util.ArrayList
|   static $staticOverhead = byte[] [id=0x6fcffb29;length=16;size=32]
|   static MIN_CAPACITY_INCREMENT = 12
|   static serialVersionUID = 8683452581122892189
|   array = java.lang.Object[] [id=0x13094a40;length=12]
|   size = 1
|   modCount = 1
* Array of java.lang.Object[]
|   [0] = com.couchsurfing.mobile.ui.events.detail.EventDetailsView [id=0x12fa1950]
|   [1] = null
|   [2] = null
|   [3] = null
|   [4] = null
|   [5] = null
|   [6] = null
|   [7] = null
|   [8] = null
|   [9] = null
|   [10] = null
|   [ 

尝试在视图实际与 window 分离之前将移除侦听器的代码更改为 运行,如下所示:

@Override public void onDetachedFromWindow() {
    scrollView.getViewTreeObserver().removeOnScrollChangedListener(scrollViewChangeListener);
    super.onDetachedFromWindow();
}

原因是在从 window、getViewTreeObserver() return 分离后是一个不同的实例("floating tree observer"),所以您不会删除您的侦听器来自您添加它的同一个对象。


更新

由于您使用的是子视图的 ViewTreeObserver,因此行为稍微复杂一些,一种可能的解决方案是将 OnAttachStateChangeListener 添加到 scrollView 和 add/remove 你的 OnScrollChangedListener 来自那里。

无论如何,关于泄漏的原因:getViewTreeObserver() 不会 return 在 View 与 [=38= 分离后的同一实例].调用 removeOnScrollChangedListener() 可能没有效果,保持原来的 OnScrollChangedListener 仍然附加到旧的 ViewTreeObserver,因此泄漏你的 Context.

用 Wea​​kReference 包装 OnScrollChangedListener 的变量访问,或者您可以从 onDestroyView 调用 removeOnScrollChanged()。在 onDetach 中删除监听器可能为时已晚,您的视图可能会被系统从视图树中删除

确实正确的解决方案是@dmarcato 我做了以下停止泄漏我的网络视图。

getViewTreeObserver().addOnScrollChangedListener(mListener);

webview.addOnAttachStateChangeListener(new OnAttachStateChangeListener() {
                @Override
                public void onViewAttachedToWindow(View v) {

                }

                @Override
                public void onViewDetachedFromWindow(View v) {
                    getViewTreeObserver().removeOnScrollChangedListener(mListener);
                }
            });