Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Understanding ViewTreeObserver leak

I am using LeakCanary 1.3.1-SNAPSHOT. I found a leak concerning ViewTreeObserver.OnScrollChangedListener setup and I fixed it like in the following code:

  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);
  }

However LeakCanary still report it as a leak, any idea why?

* com.couchsurfing.mobile.ui.events.detail.EventDetailsScreen$Presenter has leaked:
* GC ROOT android.view.inputmethod.InputMethodManager$1.this$0 (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$1.this$0 (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$1
|   this$0 = android.view.inputmethod.InputMethodManager [id=0x130239c0]
|   mDescriptor = java.lang.String [id=0x6f5e3f38]
|   mObject = -1601862176
|   mOwner = android.view.inputmethod.InputMethodManager$1 [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$1 [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$0 = 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$3 [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$1 [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$1 [id=0x12fa1950]
|   [1] = null
|   [2] = null
|   [3] = null
|   [4] = null
|   [5] = null
|   [6] = null
|   [7] = null
|   [8] = null
|   [9] = null
|   [10] = null
|   [ 
like image 369
Niqo Avatar asked May 14 '15 21:05

Niqo


People also ask

What is a viewtreeobserver in Android?

A view tree observer is used to register listeners that can be notified of global changes in the view tree. Java documentation for android.view.ViewTreeObserver.

What is the use of view tree observer?

A view tree observer is used to register listeners that can be notified of global changes in the view tree. Such global events include, but are not limited to, layout of the whole tree, beginning of the drawing pass, touch mode change....

When to register a callback in a view tree?

Register a callback to be invoked when the global layout state or the visibility of views within the view tree changes Register a callback to be invoked when the view tree is about to be drawn Register a callback to be invoked when a view has been scrolled.


2 Answers

Try to change your code that removes the listener to run before the View is actually detached from the window, like this:

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

The reason is that after being detached from a window, getViewTreeObserver() returns a different instance (the "floating tree observer"), so you are not gonna remove your listener from the same object where you added it.


UPDATE

Since you are using a ViewTreeObserver of a child view, the behavior is slightly more complex and one possible solution would involve adding an OnAttachStateChangeListener to your scrollView and add/remove your OnScrollChangedListener from there.

Anyway in regard of the reason why there was a leak: getViewTreeObserver() is not going to return the same instance after the View has been detached from the window. Calling removeOnScrollChangedListener() may have no effect, keeping your original OnScrollChangedListener still attached to the old ViewTreeObserver, and so leaking your Context.

like image 192
dmarcato Avatar answered Nov 23 '22 08:11

dmarcato


Indeed the correct solution is by @dmarcato I did the following which stopped leaking my webviews.

getViewTreeObserver().addOnScrollChangedListener(mListener);

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

                }

                @Override
                public void onViewDetachedFromWindow(View v) {
                    getViewTreeObserver().removeOnScrollChangedListener(mListener);
                }
            });
like image 38
Szabolcs Becze Avatar answered Nov 23 '22 08:11

Szabolcs Becze