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

简介: 深入理解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

为什么是这样呢?因为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流程

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】https://blog.csdn.net/stven_king/article/details/78775166

相关文章
|
1月前
|
Java Android开发
Android面试题经典之Glide取消加载以及线程池优化
Glide通过生命周期管理在`onStop`时暂停请求,`onDestroy`时取消请求,减少资源浪费。在`EngineJob`和`DecodeJob`中使用`cancel`方法标记任务并中断数据获取。当网络请求被取消时,`HttpUrlFetcher`的`cancel`方法设置标志,之后的数据获取会返回`null`,中断加载流程。Glide还使用定制的线程池,如AnimationExecutor、diskCacheExecutor、sourceExecutor和newUnlimitedSourceExecutor,其中某些禁止网络访问,并根据CPU核心数动态调整线程数。
77 2
|
3天前
|
API Android开发 开发者
Android经典实战之使用ViewCompat来处理View兼容性问题
本文介绍Android中的`ViewCompat`工具类,它是AndroidX库核心部分的重要兼容性组件,确保在不同Android版本间处理视图的一致性。文章列举了设置透明度、旋转、缩放、平移等功能,并提供了背景色、动画及用户交互等实用示例。通过`ViewCompat`,开发者可轻松实现跨版本视图操作,增强应用兼容性。
20 5
|
18天前
|
XML 前端开发 Android开发
Android面试高频知识点(3) 详解Android View的绘制流程
View的绘制和事件处理是两个重要的主题,上一篇《图解 Android事件分发机制》已经把事件的分发机制讲得比较详细了,这一篇是针对View的绘制,View的绘制如果你有所了解,基本分为measure、layout、draw 过程,其中比较难理解就是measure过程,所以本篇文章大幅笔地分析measure过程,相对讲得比较详细,文章也比较长,如果你对View的绘制还不是很懂,对measure过程掌握得不是很深刻,那么耐心点,看完这篇文章,相信你会有所收获的。
39 2
|
1月前
|
SQL 安全 Java
Android经典面试题之Kotlin中object关键字实现的是什么类型的单例模式?原理是什么?怎么实现双重检验锁单例模式?
Kotlin 单例模式概览 在 Kotlin 中,`object` 关键字轻松实现单例,提供线程安全的“饿汉式”单例。例如: 要延迟初始化,可使用 `companion object` 和 `lazy` 委托: 对于参数化的线程安全单例,结合 `@Volatile` 和 `synchronized`
29 6
|
1月前
|
Android开发
Android面试题之自定义View注意事项
在Android开发中,自定义View主要分为四类:直接继承View重写onDraw,继承ViewGroup创建布局,扩展特定View如TextView,以及继承特定ViewGroup。实现时需注意:支持wrap_content通过onMeasure处理,支持padding需在onDraw或onMeasure/onLayout中处理。避免在View中使用Handler,使用post系列方法代替。记得在onDetachedFromWindow时停止线程和动画以防止内存泄漏。处理滑动嵌套时解决滑动冲突,并避免在onDraw中大量创建临时对象。
23 4
|
1月前
|
Android开发
Android面试题之View的invalidate方法和postInvalidate方法有什么区别
本文探讨了Android自定义View中`invalidate()`和`postInvalidate()`的区别。`invalidate()`在UI线程中刷新View,而`postInvalidate()`用于非UI线程,通过消息机制切换到UI线程执行`invalidate()`。源码分析显示,`postInvalidate()`最终调用`ViewRootImpl`的`dispatchInvalidateDelayed`,通过Handler发送消息到UI线程执行刷新。
27 1
|
19天前
|
机器学习/深度学习 人工智能 算法
探索AI在医疗影像分析中的应用探索安卓开发中的自定义View组件
【7月更文挑战第31天】随着人工智能技术的飞速发展,其在医疗健康领域的应用日益广泛。本文将聚焦于AI技术在医疗影像分析中的运用,探讨其如何通过深度学习模型提高诊断的准确性和效率。我们将介绍一些关键的深度学习算法,并通过实际代码示例展示这些算法是如何应用于医学影像的处理和分析中。文章旨在为读者提供对AI在医疗领域应用的深刻理解和实用知识。
22 0
|
1月前
|
前端开发 API Android开发
Android自定义View之Canvas一文搞定
这篇文章介绍了Android自定义View中如何使用Canvas和Paint来绘制图形。Canvas可理解为画布,用于绘制各种形状如文字、点、线、矩形、圆角矩形、圆和弧。常见API包括`drawText()`、`drawPoint()`、`drawLine()`、`drawRect()`等。文章还提到了Canvas的保存、恢复、平移和旋转方法,通过绘制钟表盘的例子展示了如何实际应用。总结关键点:Canvas与Paint结合用于图像绘制,掌握Canvas的基本绘图函数及坐标变换操作是自定义View的关键。
23 0
Android自定义View之Canvas一文搞定
|
26天前
|
消息中间件 调度 Android开发
Android经典面试题之View的post方法和Handler的post方法有什么区别?
本文对比了Android开发中`View.post`与`Handler.post`的使用。`View.post`将任务加入视图关联的消息队列,在视图布局后执行,适合视图操作。`Handler.post`更通用,可调度至特定Handler的线程,不仅限于视图任务。选择方法取决于具体需求和上下文。
26 0
|
1月前
|
消息中间件 前端开发 Android开发
Android面试题自定义View之Window、ViewRootImpl和View的三大流程
Android开发中,View的三大核心流程包括measure(测量)、layout(布局)和draw(绘制)。MeasureSpec类在测量过程中起到关键作用,它结合尺寸大小和模式(EXACTLY、AT_MOST、UNSPECIFIED)来指定View应如何测量。onMeasure方法用于自定义View的测量,布局阶段,ViewGroup调用onLayout确定子元素位置,而draw阶段按照特定顺序绘制背景、内容、子元素和装饰。整个流程始于ViewRootImpl的performTraversals,该方法触发测量、布局和绘制。
24 0