背景:怎么在onCreate()中获取View的宽高?

在某些场景下,需求咱们在ActivityonCreate()中获取View的宽高,假如直接经过getMeasuredHeight()、getMeasuredWidth()去获取,得到的值都是0

2022-11-14 16:56:42.604  E/TTT: onCreate: width->0, height->0

为什么是这样呢?由于onCreate()回调履行时,View还没有经过onMeasure()、onLayout()、onDraw(),所以此时是获取不到View的宽高的。经过下面几种办法能够在onCreate()中获取到View的宽高:

  • ViewTreeObserver
  • View.post()
  • 经过MeasureSpec自行丈量宽高

具体能够参见:ViewTreeObserver使用总结及取得View高度的几种办法。别的,用postDelay()推迟一段时间也能获取View的宽高,但这种办法不够优雅,具体推迟多长时间是不知道的,因此postDelay()这种办法先不考虑。

本文要点来讨论View.post完成原理,别的几种办法不是本文要点,咱们可自行查找查看。经过学习本文,能够处理下面的几个问题:

  • View.post() 是怎么拿到宽高的?
  • 一个Activity对应一个Window,那么Window加载View的流程又是怎样的?

View.post()原理

先把定论贴出来,后边再具体剖析:

  • View.post(Runnable)履行时,会依据View当前状态履行不同的逻辑:当View还没有履行丈量、布局、制作时,View.post()会将Runnable使命放入一个使命行列中以待后续履行;反之,当View已经履行了丈量、制作后,Runnable使命会直接经过AttachInfo中的Handler履行(UI线程中的Handler)。总归View.post()能够确保提交的使命是在View丈量、制作之后履行,所以能够得到正确的宽高
  • 当前View只有在依附到View树之后,调用View.post()中的使命才有机会履行;反之只是new一个View实例,并未关联到View树的话,那么该View.post()中的Runnable使命永远都不会得到履行

下面来剖析View.post()的源码完成,文中的源码根据API 30~

 // View.java
 public boolean post(Runnable action) {
     //1
     final AttachInfo attachInfo = mAttachInfo;
     if (attachInfo != null) {
         return attachInfo.mHandler.post(action);
     }
     //2、 Postpone the runnable until we know on which thread it needs to run.
     // Assume that the runnable will be successfully placed after attach.
     //推迟runnable履行,确保View attach到Window之后才会履行
     getRunQueue().post(action);
     return true;
  }

能够看到post()办法中,主要是两块逻辑,1里面,假如mAttachInfo不为空,直接调用其内部的Handler发送并履行Runnable使命;不然履行2中的getRunQueue().post(action)。针对上面两种情况,咱们逐步剖析,各个击破。

1针对1处,在View.java类中查找AttachInfo赋值的当地,按mAttachInfo关键字查找,一共有2个当地赋值

//View.java
void dispatchAttachedToWindow(AttachInfo info, int visibility) {
    //1、mAttachInfo赋值
    mAttachInfo = info;
    //2、 履行之前挂起的一切使命,这儿的使命是经过 getRunQueue().post(action)挂起的使命。
    if (mRunQueue != null) {
        mRunQueue.executeActions(info.mHandler);
        mRunQueue = null;
    }
    //3、回调View的onAttachedToWindow办法,该办法在onResume之后,View制作之前履行
    onAttachedToWindow();
    //......其他......
}
void dispatchDetachedFromWindow() {
    //4、mAttachInfo在Window detach View的时候置为空
    mAttachInfo = null;
    //......其他......
}

其中给mAttachInfo赋值的当地是在View#dispatchAttachedToWindow()中,这儿咱们先记住该办法是在View要履行丈量、制作时调用,下一节会具体介绍;一起2处会把之前View.post()中挂起的Runnbale使命取出并经过AttachInfo.Handler发送并履行,由于Android是根据音讯模型运转的,所以能够确保这些Runnable使命是在View丈量、制作之后履行的,终究在View.post{}中获取正确的宽高。

2、回到View.post()的2处,来看getRunQueue().post(action)里的流程:

// View.java
private HandlerActionQueue getRunQueue() {
   if (mRunQueue == null) {
        mRunQueue = new HandlerActionQueue();
    }
    return mRunQueue;
}

getRunQueue()中返回了一个HandlerActionQueue,假如该目标为空会对其进行初始化,持续看HandlerActionQueue类:

//HandlerActionQueue.java
public class HandlerActionQueue {
    private HandlerAction[] mActions;
    private int mCount;
    public void post(Runnable action) {
        postDelayed(action, 0);
    }
    public void postDelayed(Runnable action, long delayMillis) {
        final HandlerAction handlerAction = new HandlerAction(action, delayMillis);
        synchronized (this) {
            if (mActions == null) {
                mActions = new HandlerAction[4];
            }
            mActions = GrowingArrayUtils.append(mActions, mCount, handlerAction);
            mCount++;
        }
    }
    // 将Runnable、delay时间合并到HandlerAction中
    private static class HandlerAction {
        final Runnable action;
        final long delay;
        public HandlerAction(Runnable action, long delay) {
            this.action = action;
            this.delay = delay;
        }
    }
    ...
}

HandlerActionHandlerActionQueue的静态内部类,该类保存了要履行的Runnable使命及其delay时间

getRunQueue().post(action)终究调用了HandlerActionQueue#post(),内部持续调用本身的postDelay()办法,该办法将Runnable使命保存在了HandlerAction数组中,所以getRunQueue().post(Runnable)只是将Runnable使命进行保存,以待后续履行。

Window加载View流程

Android | 深入理解View.post()获取宽高、Window加载View原理

从setContentView()开端

业务开发中,最常使用的便是在ActivityonCreate()里调用setContentView()来设置页面布局。实际上调用该办法之后是将操作托付给了PhoneWindow,如上面UML类图所示,咱们在setContentView()里经过layoutId生成的View被增加到了树的顶层 View(也便是DecorView) 中,而此时DecorView还没有增加到PhoneWindow中,也没有履行丈量、布局、制作等一些列流程。

ActivityThread#handleResumeActivity()

真正页面可见是在onResume()之后。具体来说,是在ActivityThread#handleResumeActivity()中,调用了WindowManager#addView()办法将DecorView增加到了WMS中:

 public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,
            String reason) {
        ......
        final Activity a = r.activity;
        if (r.window == null && !a.mFinished && willBeVisible) {
            r.window = r.activity.getWindow();
            View decor = r.window.getDecorView();
            decor.setVisibility(View.INVISIBLE);
            ViewManager wm = a.getWindowManager();
            WindowManager.LayoutParams l = r.window.getAttributes();
            a.mDecor = decor;
            l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
            if (a.mVisibleFromClient) {
                if (!a.mWindowAdded) {
                    a.mWindowAdded = true;
                    //要点看这儿
                    wm.addView(decor, l);
                } else {
                    a.onWindowAttributesChanged(l);
                }
            }
   }

要点是调用了WindowManager.addView(decor, l)WindowManager是一个接口类型,其父类ViewManager也是一个接口类型,ViewManager描绘了View的增加、删除、更新等操作(ViewGroup也完成了此接口)。

WindowManager的真正完成者是WindowManagerImpl,其内部经过托付调用了WindowManagerGlobaladdView()WindowMangerGlobal是一个单例类,一个进程中只有一个WindowMangerGlobal实例目标。来看WindowMangerGlobal#addView()的完成:

//WindowMangerGlobal.java
 public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow, int userId) {
      ViewRootImpl root;
      //1、创立ViewRootImpl
      root = new ViewRootImpl(view.getContext(), display);
      mViews.add(view);
      mRoots.add(root);
      mParams.add(wparams);
      // do this last because it fires off messages to start doing things
      try {
          //2、调用了ViewRootImpl的setView()
          root.setView(view, wparams, panelParentView, userId);
     } catch (RuntimeException e) {
          // BadTokenException or InvalidDisplayException, clean up.
          if (index >= 0) {
              removeViewLocked(index, true);
          }
         throw e;
       }
 }

WindowMangerGlobal#addView()中主要有两步操作:在1处创立了ViewRootImpl,这儿额定看一下ViewRootImpl的结构办法:

 public ViewRootImpl(Context context, Display display, IWindowSession session,
            boolean useSfChoreographer) {
    mContext = context;
    mWindowSession = session;
    mDisplay = display;
    ...
    mWindow = new W(this);
    mLeashToken = new Binder();
    //初始化了AttachInfo
    mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, this,
                context);
 }

能够看到是在ViewRootImpl的结构办法中一起初始化了AttachInfo。回到WindowMangerGlobal#addView()的2处,持续调用了ViewRootImpl#setView()

 public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView,
            int userId) {
        // 1、DecorView中关联的View会履行measure、layout、draw流程
        requestLayout();
        InputChannel inputChannel = null;
        if ((mWindowAttributes.inputFeatures
                        & WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL) == 0) {
            //2、创立InputChannel用于接收接触事情
            inputChannel = new InputChannel();
        }
        try {
            // 3、经过Binder将View增加到WMS中
            res = mWindowSession.addToDisplayAsUser(mWindow, mSeq, mWindowAttributes,
                            getHostVisibility(), mDisplay.getDisplayId(), userId, mTmpFrame,
                            mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
                            mAttachInfo.mDisplayCutout, inputChannel,
                            mTempInsets, mTempControls);
           setFrame(mTmpFrame);
       } catch (RemoteException e) {
         ...
       }
  }

setView()中,1处终究会履行到Viewmeasure、layout、draw流程,2处创立了InputChannel用于接收接触事情,终究在3处经过BinderView增加到了WMS

再细看来下1处的requestLayout(),其内部会顺次履行 scheduleTraversals() -> doTraversal() -> performTraversals()

//ViewRootImpl.java
private void performTraversals() {
   final View host = mView; //mView对应的是DecorView
   //1、
   host.dispatchAttachedToWindow(mAttachInfo, 0);
   mAttachInfo.mTreeObserver.dispatchOnWindowAttachedChange(true);
   //2、履行View的onMeasure()
   performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
   //......其他代码......
   //3、履行View的onLayout(),可能会履行屡次
   performLayout(lp, mWidth, mHeight);
   //......其他代码......
   //4、履行View的onDraw(),可能会履行屡次
   performDraw();
}

performTraversals()中2、3、4处分别对应View的丈量、布局、制作流程,不再多说;1处hostDecorView(DecorView承继自FrameLayout),终究调用到了ViewGroupdispatchAttachedToWindow()办法:

    // ViewGroup.java
    @Override
    void dispatchAttachedToWindow(AttachInfo info, int visibility) {
        ...
        super.dispatchAttachedToWindow(info, visibility);
        final int count = mChildrenCount;
        final View[] children = mChildren;
        for (int i = 0; i < count; i++) {
            final View child = children[i];
            //遍历调用子View的dispatchAttachedToWindow()同享AttachInfo
            child.dispatchAttachedToWindow(info,
                    combineVisibility(visibility, child.getVisibility()));
        }
    }

办法内部又会经过循环遍历调用了各个子ViewdispatchAttachedToWindow()办法,然后AttachInfo会经过遍历传递到各个子View中去,换句话说:经过dispatchAttachedToWindow(AttachInfo info, int visibility),ViewRootImpl中关联的一切View同享了AttachInfo

回顾一下上一节的View.post()内部完成,View.post()提交的使命必须在AttachInfo != null时,经过AttachInfo内部的Handler发送及履行,此时View已经经过了丈量、布局、制作流程,所以肯定能正确的得到View的宽高;而假如AttachInfo == null时,View.post()中提交的使命会进入使命行列中,直到View#dispatchAttachedToWindow()履行过后才会将使命取出来履行。

剖析到这儿,咱们再回看下上面的UML类图联系,整个Window加载ViewView.post()的原理是不是比较清晰了。

总结

  • WindowManager承继自ViewManager接口,提供了增加、删除、更新View的APIWindowManager能够看作是WMS在客户端的代理类。
  • ViewRootImpl完成了ViewParent接口,其是整个View树的根部,View的丈量、布局、制作以及输入事情的处理都由ViewRootImpl触发;别的,它仍是WindowManagerGlobal的实际工作者,担任与WMS交互通讯以及处理WMS传过来的事情(窗口尺度改动等)。ViewRootImpl的生命从setView()开端,到die()完毕,ViewRootImpl起到了承上启下的作用

扩展

Window、Activity及View三者之间的联系

  • 一个 Activity 对应一个 Window(PhoneWindow)PhoneWindow 中有一个 DecorView,在 setContentView 中会将 layoutId生成的View 填充到此 DecorView 中。
  • Activity看上去像是一个被代理类,内部增加View的操作是经过Window操作的。能够将Activity了解成是WindowView之间的桥梁。

是否能够在子线程中更新UI

回看下ViewRootImpl中的办法:

  //ViewRootImpl.java
  public ViewRootImpl(Context context, Display display, IWindowSession session,
      boolean useSfChoreographer) {
   ...
   mThread = Thread.currentThread();
  }
  @Override
  public void requestLayout() {
      if (!mHandlingLayoutInLayoutRequest) {
          //查看线程的正确性
          checkThread();
          mLayoutRequested = true;
          scheduleTraversals();
      }
  }
  void checkThread() {
      if (mThread != Thread.currentThread()) {
        throw new CalledFromWrongThreadException(
              "Only the original thread that created a view hierarchy can touch its views.");
      }
  }

能够看到在requestLayout()中,假如当前调用的线程不是 ViewRootImpl 的结构办法中初始化的线程就会在checkThread()中抛出反常

经过上一节的学习,咱们知道ViewRootImpl是在ActivityThread#handleResumeActivity()中初始化的,那么假如在onCreate()里新起子线程去更新UI,天然就不会抛反常了,由于此时还没有履行checkThread()去查看线程的合法性。如:

//Activity.java
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    //子线程中更新UI成功
    thread { mTvDelay.text = "子线程中更新UI" }
 }

此时子线程中更新UI成功,定论:只要在ActivityThread#handleResumeActivity()之前的流程中(如onCreate())新起一个子线程更新UI,也是会生效的,不过一般不建议这么操作

材料

【1】WindowManger完成桌面悬浮窗
【2】深入了解WindowManager
【3】直面底层:你真的了解 View.post() 原理吗?
【4】blog.csdn.net/stven_king/…