Activity中View.dispatchAttachedToWindow 调用时机

4,167 阅读3分钟

如果文章有问题,请及时指出

android 29 源码

dispatchAttachedToWindow()每个view只会调用一次。

一 第一次绘制时

1 ActivityThread.handleResumeActivity()

在ActivityThread.handleResumeActivity()方法中会把DecorView添加到window中。
@Override
public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,
        String reason) {
    .........
      if (a.mVisibleFromClient) {
          if (!a.mWindowAdded) {
              a.mWindowAdded = true;
              // 把DecorView添加到window中
              wm.addView(decor, l);
              } else {
                // The activity will get a callback for this {@link LayoutParams} change
                // earlier. However, at that time the decor will not be set (this is set
                // in this method), so no action will be taken. This call ensures the
                // callback occurs with the decor set.
                a.onWindowAttributesChanged(l);
        }
    }           
}

2 WindowManagerImpl.addView()

3 WindowManagerGlobal.addView()

public void addView(View view, ViewGroup.LayoutParams params,
        Display display, Window parentWindow) {
            .......
    root = new ViewRootImpl(view.getContext(), display);
    // 会调用DecorView.requestLayout()-》ViewRootImpl.requestLayout()
    view.setLayoutParams(wparams);

    mViews.add(view);
    mRoots.add(root);
    mParams.add(wparams);

    // do this last because it fires off messages to start doing things
    try {
        // 这里也会调用ViewRootImpl.requestLayout()
        root.setView(view, wparams, panelParentView);
    } catch (RuntimeException e) {
        // BadTokenException or InvalidDisplayException, clean up.
        if (index >= 0) {
            removeViewLocked(index, true);
        }
        throw e;
    }

}

4 ViewRootImpl.requestLayout()

5 ViewRootImpl.scheduleTraversals()

6 ViewRootImpl.doTraversal();

7 ViewRootImpl.performTraversals();

这里会调用dispatchAttachedToWindow,DecorView对子View都调用dispatchAttachedToWindow(),不管是否可见。
private void performTraversals() {
    .......
    // 第一次调用
    if (mFirst) {
        // 调用DecorView的dispatchAttachedToWindow()
        host.dispatchAttachedToWindow(mAttachInfo, 0);
    }
    performMeasure();
    performLayout(); // 2590行
    mFirst = false; // 2718行
    performDraw();  // 2755行
}    

8 View.dispatchAttachedToWindow()

二 第一次绘制后,添加View也会调用

1 ViewGroup.AddView()

public void addView(View child, int index, LayoutParams params) {
    if (DBG) {
        System.out.println(this + " addView");
    }

    if (child == null) {
        throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");
    }

    // addViewInner() will call child.requestLayout() when setting the new LayoutParams
    // therefore, we call requestLayout() on ourselves before, so that the child's request
    // will be blocked at our level
    // 先调用requestLayout
    requestLayout();
    invalidate(true);
    addViewInner(child, index, params, false);
}

2 ViewGroup.addViewInner

private void addViewInner(View child, int index, LayoutParams params,
        boolean preventRequestLayout) {
    ......
    // FLAG_PREVENT_DISPATCH_ATTACHED_TO_WINDOW只有在onAttachedToWindow中添加子View,时才会设置。
    // 一般不会有。即使在onAttachedToWindow中addView,
    // 也会先执行requestLayout(),再执行dispatchAttachedToWindow
    if (ai != null && (mGroupFlags & FLAG_PREVENT_DISPATCH_ATTACHED_TO_WINDOW) == 0) {
        boolean lastKeepOn = ai.mKeepScreenOn;
        ai.mKeepScreenOn = false;
        // 这里调用dispatchAttachedToWindow
        child.dispatchAttachedToWindow(mAttachInfo, (mViewFlags&VISIBILITY_MASK));
        if (ai.mKeepScreenOn) {
            needGlobalAttributesUpdate(true);
        }
        ai.mKeepScreenOn = lastKeepOn;
    }                                       
}

3 View.dispatchAttachedToWindow()

三 为什么onCreate/onStart/onResume中直接获取View的宽高是不行的,但View.post()就能

首先onCreate/onStart/onResume中不行,是因为这是View还没测量布局。Activity.onResume时,才会请求requestLayout,等待垂直同步信号后,才开始测试布局绘制的。所以在onCreate/onStart/onResume不能获得正确宽高。那么View.post()为什么行?

1 View.post()源码

如果mAttachInfo不等于空,就使用mAttachInfo中handler.post(),这就是往主线程post消息。
如果mAttachInfo为空,就把任务存在一个队列中,每个View都有一个这样的任务队列(记得以前是ViewRootImpl才有的)。
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;
}

/**
 * Returns the queue of runnable for this view.
 *
 * @return the queue of runnables for this view
 */
private HandlerActionQueue getRunQueue() {
    if (mRunQueue == null) {
        mRunQueue = new HandlerActionQueue();
    }
    return mRunQueue;
}

2 dispatchAttachedToWindow中会把任务队列post到主线程中,置空任务队列。

mAttachInfo是在dispatchAttachedToWindow()中赋值的。
View.java

void dispatchAttachedToWindow(AttachInfo info, int visibility) {
    // 对mAttachInfo赋值
    mAttachInfo = info;
    .......
    // Transfer all pending runnables.
    // 会把任务队列中任务使用Handler.post(),重新丢到主线程中。
    // 这里的Handler其实是final ViewRootHandler mHandler = new ViewRootHandler();
    if (mRunQueue != null) {
        mRunQueue.executeActions(info.mHandler);
        mRunQueue = null;
    }    
}

void dispatchDetachedFromWindow() {
     mAttachInfo = null;
}
dispatchAttachedToWindow()方法调用后,View.post()和我们自己定义的Handler.post()没有啥区别。主要是在dispatchAttachedToWindow()方法调用前。
这里主要分析dispatchAttachedToWindow()执行前,View.post()的情况。(其实如果之前后能获得,之后肯定没问题)
分2种情况:
1 第一次绘制前,View就已经add到DecorView
在Activity启动时,在onCreate/onStart/onResume中使用View.post()。在第一次绘制时,会调用View.dispatchAttachedToWindow(),把View中任务队列通过ViewRootHandler,放到主线的消息队列中,这样会在第一次绘制之后执行。所以此时能获得View的宽高。
2 第一次绘制后,View才add到DecorView
addView时,会先requestLayout(),再dispatchAttachedToWindow()。View.requestLayout()最后都会到ViewRootImpl.requestLayout()。ViewRootImpl.requestLayout()往主线程的消息队列中发送同步屏蔽消息,之后的所有同步消息禁止执行,只能执行异步消息(在Choreographer中)。dispatchAttachedToWindow()中往主线发送的消息都不能执行,只有等同步垂直信号来了,开始绘制时,才把同步屏蔽消息删除。但这时已经开始测量布局绘制了,主线程消息队列中任务只能之后执行了。所以这种情况也能获得View的宽高。