Android 深入理解View.post() 、Window加载View原理

news2025/1/9 16:26:19

文章目录

      • 背景:如何在onCreate()中获取View的宽高?
      • View.post()原理
      • Window加载View流程
        • setContentView()
        • ActivityThread#handleResumeActivity()
        • 总结
      • 扩展
        • Window、Activity及View三者之间的关系
        • 是否可以在子线程中更新UI
      • 资料

背景:如何在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()是如何拿到宽高的?
  • 一个Activity对应一个Window,那么Window加载View的流程又是怎样的?

View.post()原理

先把结论贴出来,后面再详细分析:

  • View.post(Runnable)执行时,会根据View当前状态执行不同的逻辑:当View还没有执行测量、布局、绘制时,View.post()会将Runnable任务放入一个任务队列中以待后续执行;反之,当View已经执行了测量、绘制后,Runnable任务会直接通过AttachInfo中的Handler执行
  • View.post()能够保证提交的任务是在View测量、绘制之后执行,所以可以得到正确的宽高
  • 只有View关联到ViewRootImpl,或者说View必须依附到View树之后,View.post()中的任务才会生效,否则View.post()中的任务永远都不会执行

下面分析下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先来看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()中挂起的任务取出并通过AttachInfo.Handler执行,因为Android是基于消息模型运行的,所以这些任务能够保证都是在经过测量、绘制之后执行,即能正确的获取各自View的宽高。

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

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

getRunQueue()中初始化了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;
        }
    }
    ...
}

HandlerAction内部保存了要执行的Runnable任务及其delay时间

HandlerActionQueue#post()又调用了内部的postDelay()方法将Runnable任务保存在了HandlerAction数组中,getRunQueue().post(action)只是将Runnable任务进行保存,并不会执行

Window加载View流程

Window添加View

setContentView()

ActivityonCreate()里调用setContentView()之后,实际上是将操作委托给了PhoneWindow,如上面UML类图所示,我们在setContentView()里通过layoutId生成的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()执行过后才会将任务取出来执行。

总结

  • 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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/21715.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

m认知无线电信号检测算法matlab仿真,能量检测,循环平稳检测以及自相关检测

目录 1.算法概述 2.仿真效果预览 3.MATLAB部分代码预览 4.完整MATLAB程序 1.算法概述 频谱感测是认知无线电的一项关键技术。我们将频谱感知作为一个分类问题&#xff0c;提出一种基于深度学习分类的感知方法。我们归一化接收信号功率以克服噪声功率不确定性的影响。我们使…

postgresql源码学习(49)—— MVCC⑤-cmin与cmax 同事务内的可见性判断

一、 难以理解的场景 postgresql源码学习&#xff08;十九&#xff09;—— MVCC④-可见性判断 HeapTupleSatisfiesMVCC函数_Hehuyi_In的博客-CSDN博客 在前篇的可见性判断中有个一直没想明白的问题 —— 本事务插入的数据&#xff0c;什么场景可能会出现去查询获取快照后插入…

路面坑洼检测中的视觉算法

3D道路成像和路面坑洼检测的经典工作综述。论文链接&#xff1a;https://arxiv.org/pdf/2204.13590.pdf 计算机视觉算法在3D道路成像和路面坑洼检测中的应用已有二十多年的历史。这里先介绍了用于2D和3D道路数据采集的传感系统&#xff0c;包括摄像机、激光扫描仪和微软Kinect…

汉兰达汽车发动机怠速抖动故障诊断方案设计

目录 一、课题简介 1 1.1课题基本内容 1 1.2课题解决的主要问题 1 1.3课题设计思路 1 二、毕业设计成果 2 2.1汉兰达汽车发动机怠速抖动故障现象描述 2 2.2 汉兰达汽车发动机怠速抖动故障原因分析 2 2.3汉兰达汽车发动机怠速抖动故障诊断与排除 6 2.4维修结论与建议 12 三、毕业…

java sleep yield join区别

1、sleep&#xff1a;让出CPU调度&#xff0c;Thread类的方法&#xff0c;必须带一个时间参数。会让当前线程休眠进入阻塞状态并释放CPU&#xff08;阿里面试题 Sleep释放CPU&#xff0c;wait 也会释放cpu&#xff0c;因为cpu资源太宝贵了&#xff0c;只有在线程running的时候&…

高效正则匹配工具

很多人都用过正则&#xff0c;但文章或许会给你一种全新的认识(思考) 以下内容适合高效率正则匹配&#xff08;比较适合正则匹配场景较多的情况&#xff09; 效率提升精华&#xff1a;本地缓存减少编译次数&#xff08;对effective java的思考&#xff0c;以及对数据库连接中…

Java中的装包(装箱)和拆包(装包)

装箱和拆箱 在Java的学习中&#xff0c;我们有的时候会设计装箱和拆箱的概念&#xff08;也就是常说的装包和拆包&#xff09;&#xff0c;这篇博客将详细讲解一下装箱和拆箱的概念及其用途。 装箱&#xff08;装包&#xff09;&#xff1a;将基本数据类型转换成包装类类型 拆…

websocket给指定客户端推送消息

业务场景 最近有一个业务场景是要做实时语音转义&#xff0c;考虑到实时性&#xff0c;所以决定采用websocket实现。 业务场景是A客户端(手机)进行语音转义的结果实时同步到B客户端(pc)&#xff0c;这就需要用到websocket将A转义的结果发送给服务端&#xff0c;服务端接收到A…

软件工程经济学复习题答案

1、利润 收入-成本费用 2、资产 流动资产非流动资产 3、显性成本可以用货币计量&#xff0c;是可以在会计的帐目上反映出来的 4、领取什么保险应缴纳个人所得税 商业保险 某企业一项固定资产的原价为8000 000元&#xff0c;预计使用年限为6年&#xff0c;预计净残值为5 0…

[LeetCode周赛复盘] 第 320 场周赛20221120

[LeetCode周赛复盘] 第 320 场周赛20221120 一、本周周赛总结二、 [Easy] 6241. 数组中不等三元组的数目1. 题目描述2. 思路分析3. 代码实现三、[Medium] 6242. 二叉搜索树最近节点查询1. 题目描述2. 思路分析3. 代码实现四、[Hard] 6243. 到达首都的最少油耗1. 题目描述2. 思路…

10_libpcap以及libnet

知识点1【飞秋欺骗】 1、windwos安装飞秋 双击运行 2、ubuntu安装飞秋 sudo apt-get install iptux ubuntu运行飞秋&#xff1a;iptux& 3、飞秋的格式&#xff1a; 版本:包编号:用户名:主机名:命令字:附加消息 飞秋的端口是2425固定的 1表示上线 32表示普通消息 1_i…

(经典dp) hdu 递推求解专题练习

文章目录前言题单hdu2044 一只小蜜蜂...hdu2045 不容易系列之(3)—— LELE的RPG难题hdu2046 骨牌铺方格hdu2047 阿牛的EOF牛肉串hdu2048 神、上帝以及老天爷hdu2049 不容易系列之(4)——考新郎hdu2050 折线分割平面END前言 题单&#xff1a;递推求解专题练习&#xff08;For Be…

华为机试 - 找出经过特定点的路径长度

目录 题目描述 输入描述 输出描述 用例 题目解析 算法源码 题目描述 无 输入描述 输入一个字符串&#xff0c;都是以大写字母组成&#xff0c;每个相邻的距离是 1&#xff0c; 第二行输入一个字符串&#xff0c;表示必过的点。 说明每个点可过多次。 输出描述 经过这…

精益(Lean)与ERP实施

周四、五看完了24小时不停的Lean Global Connection&#xff0c;总觉得要说些什么。 印象最深的有三个地方&#xff1a; 一是John Shook的话&#xff0c;他说Lean是一种Mindset。 这种Mindset是&#xff1a; 一种积极的态度&#xff0c;Problems solving, 把问题和挑战当成是…

Web 性能优化:TLS

个人博客 Web 性能优化&#xff1a;TCP&#x1f3af; Web 性能优化&#xff1a;TLSWeb 性能优化&#xff1a;HTTP “do it, do it work, do it better … and secure ☠️” 随着追逐利益而来的恶意参与者越来越多&#xff0c;当前的 Web 应用&#xff0c;已经从野蛮生长转而…

【通关MySQL】Java的JDBC编程

✨哈喽&#xff0c;进来的小伙伴们&#xff0c;你们好耶&#xff01;✨ &#x1f6f0;️&#x1f6f0;️系列专栏:【通关MySQL】 ✈️✈️本篇内容:Java的JDBC编程。 &#x1f680;&#x1f680;代码存放仓库gitee&#xff1a;MySQL码云存放&#xff01; ⛵⛵作者简介&#xff…

[附源码]java毕业设计-室内田径馆预约管理系统

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

轻量应用服务器和云服务器CVM有什么区别?

腾讯云新推出的轻量应用服务器Lighthouse和原来的CVM云服务器有什么区别&#xff1f;轻量应用服务器Lighthouse是一种易于使用和管理、适合承载轻量级业务负载的云服务器&#xff0c;主要用于Web网站应用&#xff0c;轻量服务器使用及后期运维更加简单方便&#xff1b;云服务器…

RNA-seq 保姆教程:差异表达分析(二)

介绍 RNA-seq 目前是测量细胞反应的最突出的方法之一。RNA-seq 不仅能够分析样本之间基因表达的差异&#xff0c;还可以发现新的亚型并分析 SNP 变异。本教程[1]将涵盖处理和分析差异基因表达数据的基本工作流程&#xff0c;旨在提供设置环境和运行比对工具的通用方法。由于完整…

计算机网络——数据链路层

数据链路层概述 数据链路层在网络体系结构中所处的地位 主机H1给主机H2发送数据时&#xff0c;需要通过路由器R1通过广域网链路转发到路由器R2&#xff0c;R2转发到主机H2。路由器转发只用到网络层及以下各层。【以上涉及数据包按网络体系结果逐层封装解封】 为了简单起见&am…