阅读 284

View的removeCallbacks真正有效吗?

作者:暴走的小青春

背景概述

近期我们app的灰度版本在bugly上报了如下的异常: 屏幕快照 2021-01-23 下午9.27.22.png 看到此类日志,很自然的分析出是页面被destroy了,然后消息队列里还有消息,在执行到此消息时此view为null了,事实上出问题的代码也正式如此,只不过在页面onDestory时我们已经去removeCallbacks了,模拟的代码如下,

class TestFragment : Fragment() {

    val runnable=Runnable{
        Log.i("TestFragment","do runnable")
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        rv_launch_start.post(runnable,300)    
    }

    override fun onDestroyView() {
        rv_launch_start.removeCallbacks(runnable)
        super.onDestroyView()
    }
} 
复制代码

很明显,代码在fragment的onDestroyView已经去removeCallbacks,但还是在一定几率下会执行runnable,那到底是由于啥原因呢?

初步排查

看到这问题,首先看下view.post的源码

public boolean post(Runnable action) {
    final AttachInfo attachInfo = mAttachInfo;
    if (attachInfo != null) {
        return attachInfo.mHandler.post(action);
    }

    // Postpone the runnable until we know on which thread it needs to run.
    // Assume that the runnable will be successfully placed after attach.
    getRunQueue().post(action);
    return true;
}
复制代码
可以看到view的post方法依赖于mAttachInfo这个成员变量,如果为null就加到RunQueue()的队列里去,不为null才加到了handler的队列里去

紧接着看下view.removeCallbacks方法

public boolean removeCallbacks(Runnable action) {
    if (action != null) {
        final AttachInfo attachInfo = mAttachInfo;
        if (attachInfo != null) {
            attachInfo.mHandler.removeCallbacks(action);
            attachInfo.mViewRootImpl.mChoreographer.removeCallbacks(
                Choreographer.CALLBACK_ANIMATION, action, null);
        }
        getRunQueue().removeCallbacks(action);
    }
    return true;
}
复制代码

可以说是和view.post方法遥相呼应,通过判断mAttachInfo为null来判断是否要执行mHandler.removeCallbacks的操作

初步猜想

在看了下这两个方法后便有了如下猜想: 首先代码里没有线程竞争,表示了如果执行了handler.removeCallbacks方法,此runnable肯定会在handler队列中移除,这是毋庸置疑的,那看了如上的代码,唯一一种会发生此问题的原因就是此消息被添加到消息队列中了,然后执行removeCallbacks的时候,此view的mAttachInfo对象为null了,导致没有执行到mHandler的removeCallbacks的方法

public boolean removeCallbacks(Runnable action) {
    if (action != null) {
        final AttachInfo attachInfo = mAttachInfo;
        //此时mAttachInfo为null导致了无法去remove
        if (attachInfo != null) {
            attachInfo.mHandler.removeCallbacks(action);
            attachInfo.mViewRootImpl.mChoreographer.removeCallbacks(
                Choreographer.CALLBACK_ANIMATION, action, null);
        }
        getRunQueue().removeCallbacks(action);
    }
    return true;
}
复制代码

深入分析

思想误区

看了最上面removeCallbacks执行的时机,明显是在onDestroyView调用super之前执行的,怎么mAttachInfo就为null了呢? 其实我们所认为的理所当然很多时候只是没看到事物的另一面,onDestroyView只是fragment回调的一个“钩子”并不是移除的逻辑 在fragment里onDestroyView仅仅就如下所示:

//fragment的onDestroyView方法
@MainThread
@CallSuper
public void onDestroyView() {
    mCalled = true;
}

复制代码

可以看到并没有操作mAttachInfo的逻辑

具体分析

于是我们在view的dispatchDetachedFromWindow中,我们找到了mAttachInfo被置为null的逻辑

void dispatchDetachedFromWindow() {
    AttachInfo info = mAttachInfo;
    if (info != null) {
        int vis = info.mWindowVisibility;
        if (vis != GONE) {
            onWindowVisibilityChanged(GONE);
            if (isShown()) {
                // Invoking onVisibilityAggregated directly here since the subtree
                // will also receive detached from window
                onVisibilityAggregated(false);
            }
        }
    }

    onDetachedFromWindow();
    onDetachedFromWindowInternal();

    InputMethodManager imm = InputMethodManager.peekInstance();
    if (imm != null) {
        imm.onViewDetachedFromWindow(this);
    }

    ListenerInfo li = mListenerInfo;
    final CopyOnWriteArrayList<OnAttachStateChangeListener> listeners =
        li != null ? li.mOnAttachStateChangeListeners : null;
    if (listeners != null && listeners.size() > 0) {
        // NOTE: because of the use of CopyOnWriteArrayList, we *must* use an iterator to
        // perform the dispatching. The iterator is a safe guard against listeners that
        // could mutate the list by calling the various add/remove methods. This prevents
        // the array from being modified while we iterate it.
        for (OnAttachStateChangeListener listener : listeners) {
            listener.onViewDetachedFromWindow(this);
        }
    }

    if ((mPrivateFlags & PFLAG_SCROLL_CONTAINER_ADDED) != 0) {
        mAttachInfo.mScrollContainers.remove(this);
        mPrivateFlags &= ~PFLAG_SCROLL_CONTAINER_ADDED;
    }
    //此处被置为null
    mAttachInfo = null;
    if (mOverlay != null) {
        mOverlay.getOverlayView().dispatchDetachedFromWindow();
    }

    notifyEnterOrExitForAutoFillIfNeeded(false);
}
复制代码

也就是说执行fragment的onDestroyView之前,会执行view的dispatchDetachedFromWindow方法,从而导致view的mAttachInfo被置为了null,那谁调用了此方法呢? 我们知道view的measure,layout都是由其parent也就是viewGroup所调用的,以此类推,我们便查到了viewgoup的dispatchDetachedFromWindow方法 发现有多处调用view的dispatchDetachedFromWindow方法,分析下来最符合的方法如下:

//viewgroup
private void removeViewInternal(int index, View view) {
    if (mTransition != null) {
        mTransition.removeChild(this, view);
    }

    boolean clearChildFocus = false;
    if (view == mFocused) {
        view.unFocus(null);
        clearChildFocus = true;
    }
    if (view == mFocusedInCluster) {
        clearFocusedInCluster(view);
    }

    view.clearAccessibilityFocus();

    cancelTouchTarget(view);
    cancelHoverTarget(view);

    if (view.getAnimation() != null ||
        (mTransitioningViews != null && mTransitioningViews.contains(view))) {
        addDisappearingView(view);
    } else if (view.mAttachInfo != null) {
        view.dispatchDetachedFromWindow();
    }

    if (view.hasTransientState()) {
        childHasTransientStateChanged(view, false);
    }

    needGlobalAttributesUpdate(false);

    removeFromArray(index);

    if (view.hasUnhandledKeyListener()) {
        decrementChildUnhandledKeyListeners();
    }

    if (view == mDefaultFocus) {
        clearDefaultFocus(view);
    }
    if (clearChildFocus) {
        clearChildFocus(view);
        if (!rootViewRequestFocus()) {
            notifyGlobalFocusCleared(this);
        }
    }

    dispatchViewRemoved(view);

    if (view.getVisibility() != View.GONE) {
        notifySubtreeAccessibilityStateChangedIfNeeded();
    }

    int transientCount = mTransientIndices == null ? 0 : mTransientIndices.size();
    for (int i = 0; i < transientCount; ++i) {
        final int oldIndex = mTransientIndices.get(i);
        if (index < oldIndex) {
            mTransientIndices.set(i, oldIndex - 1);
        }
    }

    if (mCurrentDragStartEvent != null) {
        mChildrenInterestedInDrag.remove(view);
    }
}
复制代码

我们知道当我们removeView的时候会调用removeViewInternal方法

@Override
public void removeView(View view) {
    if (removeViewInternal(view)) {
        requestLayout();
        invalidate(true);
    }
}
复制代码

也就是说在执行了fragmentonDestroyView的之前,已经执行了removeView方法,此时我们知道离真相不远了,只要看下fragment添加和移除的操作就行了 我们知道其操作都是在 FragmentManagerImpl 1.2.0版本 类里进行的 仔细来看下具体移除的逻辑

//fragmentManager
void moveToState(@NonNull Fragment f, int newState) {
   ...
    case Fragment.ACTIVITY_CREATED:
    if (newState < Fragment.ACTIVITY_CREATED) {
        if (isLoggingEnabled(Log.DEBUG)) {
            Log.d(TAG, "movefrom ACTIVITY_CREATED: " + f);
        }
        if (f.mView != null) {
            // Need to save the current view state if not
            // done already.
            if (mHost.onShouldSaveFragmentState(f) && f.mSavedViewState == null) {
                fragmentStateManager.saveViewState();
            }
        }
        FragmentAnim.AnimationOrAnimator anim = null;
        if (f.mView != null && f.mContainer != null) {
            // Stop any current animations:
            f.mContainer.endViewTransition(f.mView);
            f.mView.clearAnimation();
            // If parent is being removed, no need to handle child animations.
            if (!f.isRemovingParent()) {
                if (mCurState > Fragment.INITIALIZING && !mDestroyed
                    && f.mView.getVisibility() == View.VISIBLE
                    && f.mPostponedAlpha >= 0) {
                    anim = FragmentAnim.loadAnimation(mHost.getContext(),
                                                      mContainer, f, false);
                }
                f.mPostponedAlpha = 0;
                // Robolectric tests do not post the animation like a real device
                // so we should keep up with the container and view in case the
                // fragment view is destroyed before we can remove it.
                ViewGroup container = f.mContainer;
                View view = f.mView;
                if (anim != null) {
                    f.setStateAfterAnimating(newState);
                    FragmentAnim.animateRemoveFragment(f, anim,
                                                       mFragmentTransitionCallback);
                }
                container.removeView(view);
                // If the local container is different from the fragment
                // container, that means onAnimationEnd was called, onDestroyView
                // was dispatched and the fragment was already moved to state, so
                // we should early return here instead of attempting to move to
                // state again.
                if (container != f.mContainer) {
                    return;
                }
            }
        }
        // If a fragment has an exit animation (or transition), do not destroy
        // its view immediately and set the state after animating
        if (mExitAnimationCancellationSignals.get(f) == null) {
            destroyFragmentView(f);
        } else {
            f.setStateAfterAnimating(newState);
        }
    }
    ...

}
复制代码

可以看到在执行destroyFragmentView之前container已经移除了fragment所对应的view了,问题也就水落石出了 ps:在fragment的1.1.0和1.0.0版本都是先执行performDestroyView方法,在执行移除的操作的,也就不会有如下问题了 如下: image.png

总结及经验

对于任何版本的升级都有一定的风险性,就像上述framgent库,同样的写法可能在1.0.0和1.1.0版本,没有问题,但是当fragment库升级到1.2.0原来的写法就有缺陷了。 我们有时候分析问题要克服自己的思想误区,才能更直观的分析出问题。

文章分类
Android
文章标签