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


    本文已同步发表于我的微信公众号,搜索 代码说 即可关注,欢迎与我沟通交流。

    背景:如何在onCreate()中获取View的宽高?

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

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

    为什么是这样呢?因为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;
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    可以看到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;
        //......其他......
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    其中给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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    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;
            }
        }
        ...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    HandlerActionHandlerActionQueue静态内部类,该类保存了要执行的Runnable任务及其delay时间

    getRunQueue().post(action)最终调用了HandlerActionQueue#post(),内部继续调用自身的postDelay()方法,该方法将Runnable任务保存在了HandlerAction数组中,所以getRunQueue().post(Runnable)只是将Runnable任务进行保存,以待后续执行。

    Window加载View流程

    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);
                    }
                }
       }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    重点是调用了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;
           }
     }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    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);
     }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    可以看到是在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) {
             ...
           }
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    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();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    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()));
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    方法内部又会通过循环遍历调用了各个子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.");
          }
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    可以看到在requestLayout()中,如果当前调用的线程不是 ViewRootImpl 的构造方法中初始化的线程就会在checkThread()中抛出异常

    通过上一节的学习,我们知道ViewRootImpl是在ActivityThread#handleResumeActivity()中初始化的,那么如果在onCreate()里新起子线程去更新UI,自然就不会抛异常了,因为此时还没有执行checkThread()去检查线程的合法性。如:

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

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

    资料

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

  • 相关阅读:
    PostgreSQL之SQL高级特性
    科技云报道:混合办公的B面:安全与效率如何兼得?
    k8s存储卷 PV和PVC
    网络基础(网络层)
    Python教程:电子邮件SMTP发送邮件
    大工22春《施工组织课程设计》离线作业模板及要求【标准答案】
    NFT 的技术结构解释
    【Vue实战】基于Vue的九宫格在线抽奖附源代码
    天宇优配|机构动向 港股强势上涨 机构看好反弹持续性
    人工智能在汽车业应用的五项挑战
  • 原文地址:https://blog.csdn.net/u013700502/article/details/127741428