作者:暴走的小青春
背景概述
近期我们app的灰度版本在bugly上报了如下的异常:
看到此类日志,很自然的分析出是页面被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);
}
}
复制代码
也就是说在执行了fragment
的onDestroyView
的之前,已经执行了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
方法,在执行移除的操作的,也就不会有如下问题了
如下:
总结及经验
对于任何版本的升级都有一定的风险性,就像上述framgent库,同样的写法可能在1.0.0和1.1.0版本,没有问题,但是当fragment库升级到1.2.0原来的写法就有缺陷了。 我们有时候分析问题要克服自己的思想误区,才能更直观的分析出问题。