前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Carson带你学Android:为什么view.post()能保证获取到view的宽高?

Carson带你学Android:为什么view.post()能保证获取到view的宽高?

作者头像
Carson.Ho
发布2021-12-06 16:46:35
3850
发布2021-12-06 16:46:35
举报
文章被收录于专栏:Android知识分享

前言

为什么view.post()能保证获取到view的宽高?本文将手把手带你深入源码了解view.post() 原理。

Carson带你学Android系列文章 Carson带你学Android:学习方法 Carson带你学Android:四大组件 Carson带你学Android:自定义View Carson带你学Android:异步-多线程 Carson带你学Android:性能优化 Carson带你学Android:动画

背景

  • 业务需求代码开始时机一般是在:Activity的生命周期onCreate()
  • 视图View 绘制时机:Activity的生命周期onResume()之后 (注:ActivityThread 的 handleResumeActivity()执行顺序:先回调 Activity 生命周期 onResume() - 再开始 View 的绘制任务)

矛盾

  • 业务需求代码需获取宽高的时机 跟 View的绘制时机 存在时序问题
  • 一般来说,业务需求代码开始时就需要获取View的相关信息(如宽、高),但:View 绘制时机在Activity.onResume()之后,即在Activity.onCreate()之后 = 业务需求代码开始后。

解决方案

将需执行的任务传入到View.post() 。这个操作大家一定很熟悉。那么 其内部原理是什么呢?

结论

以Handler为基础,View.post() 将传入任务的执行时机调整到View 绘制完成之后。下面我将从源码的角度进行分析。

源码解析

我们直接从view.post()入手:

代码语言:javascript
复制
public boolean post(Runnable action) {
    
    // 仅贴出关键代码
    // ...
    
    // 判断AttachInfo是否为null
    final AttachInfo attachInfo = mAttachInfo;

    // 过程1:若不为null,直接调用其内部Handler的post
    if (attachInfo != null) {
        
        return attachInfo.mHandler.post(action);
    }

    // 过程2:若为null,则加入当前View的等待队列
    getRunQueue().post(action); 
    // getRunQueue() 返回的是 HandlerActionQueue
    // action代表传入的要执行的任务
    // 即调用了HandlerActionQueue.post() ->> 分析1
    return true;
}

此处分成两个过程讲解:

  1. 当AttachInfo不为null时,直接调用其内部Handler的post;
  2. 当AttachInfo为null时,则将任务加入当前View的等待队列中。

此处为了方便理解,我会先讲解过程2

过程2:当AttachInfo为null时,则将任务加入当前View的等待队列中。

代码语言:javascript
复制
public boolean post(Runnable action) {
    
    // 仅贴出关键代码
    // ...
    
    // 判断AttachInfo是否为null
    final AttachInfo attachInfo = mAttachInfo;

    // 过程1:若不为null,直接调用其内部Handler的post
    if (attachInfo != null) {
        
        return attachInfo.mHandler.post(action);
    }

    // 过程2:若为null,则加入当前View的等待队列
    getRunQueue().post(action); 
    // getRunQueue() 返回的是 HandlerActionQueue
    // action代表传入的要执行的任务
    // 即调用了HandlerActionQueue.post() ->> 分析1
    return true;
}

/**
  * 分析1:HandlerActionQueue.post()
  */
public void post(Runnable action) {
    // ...
    postDelayed(action, 0); 
    // ->> 分析2 
}

/**
  * 分析2
  */
public void postDelayed(Runnable action, long delayMillis) {
    // 1. 将传入的任务runnable封装成HandlerAction  ->>分析3
    final HandlerAction handlerAction = new HandlerAction(action, delayMillis);

    synchronized (this) {
        // 2. 将要执行的HandlerAction 保存在 mActions 数组中
        if (mActions == null) {
            mActions = new HandlerAction[4];
        }

        mActions = GrowingArrayUtils.append(mActions, mCount, handlerAction);
        mCount++;
    }
}

/**
  * 分析3:HandlerAction 表示一个待执行的任务
  * 内部持有要执行的 Runnable 和延迟时间
  */
private static class HandlerAction {
    // post的任务
    final Runnable action;
    // 延迟时间
    final long delay;

    public HandlerAction(Runnable action, long delay) {
        this.action = action;
        this.delay = delay;
    }
   // ...
}
// 回到分析原处

结论:

  1. 将传入的任务封装成HandlerAction对象
  2. 创建一个默认长度为4的 HandlerAction数组,用于保存通过post()添加的任务

注:此时只是保存了通过post()添加的任务,并没执行。

过程1:当AttachInfo不为null时,直接调用其内部Handler的post()

代码语言:javascript
复制
public boolean post(Runnable action) {
    
    // ...
    
    // 判断AttachInfo是否为null
    final AttachInfo attachInfo = mAttachInfo;

    // 过程1:若不为null,直接调用其内部Handler的post ->>分析1
    if (attachInfo != null) {
        return attachInfo.mHandler.post(action);
    }

    // 过程2:若为null,则加入当前View的等待队列
    getRunQueue().post(action); 
    return true;
}

/**
  * 分析1:AttachInfo的赋值过程 -> dispatchAttachedToWindow()
  * 注:AttachInfo持有当前渲染线程Handler
  */
  void dispatchAttachedToWindow(AttachInfo info, int visibility) {

    // ...

    // 给当前View赋值AttachInfo,此时同一个ViewRootImpl内的所有View共用同一个AttachInfo
    mAttachInfo = info;

    //  mRunQueue,即 前面说的 HandlerActionQueue
    // 其内部保存了当前View.post的任务
    if (mRunQueue != null) {
        mRunQueue.executeActions(info.mHandler);
        // 执行使用View.post的任务,post到渲染线程的Handler中
        // ->> 分析2
    }

    // 在Activity的onResume()中调用,但是在View绘制流程之前
    onAttachedToWindow();
    
    ListenerInfo li = mListenerInfo;
    final CopyOnWriteArrayList<OnAttachStateChangeListener> listeners =
            li != null ? li.mOnAttachStateChangeListeners : null;
    if (listeners != null && listeners.size() > 0) {
        for (OnAttachStateChangeListener listener : listeners) {
            // 通知所有监听View已经onAttachToWindow的客户端,即view.addOnAttachStateChangeListener();
            // 但此时View还没有开始绘制,不能正确获取测量大小或View实际大小
            listener.onViewAttachedToWindow(this);
        }
    }

    // ... 
}

/**
  * 分析2:HandlerActionQueue.executeActions()
  */
  public void executeActions(Handler handler) {
    synchronized (this) {
        
        final HandlerAction[] actions = mActions;
        // 任务队列
        // mActions即为前面过程1 保存了通过post()添加的任务 的数组

        // 遍历所有任务
        for (int i = 0, count = mCount; i < count; i++) {
            final HandlerAction handlerAction = actions[i];
            // 发送到Handler中,等待执行
            handler.postDelayed(handlerAction.action, handlerAction.delay);
        }

        // 此时不再需要后续的post,将被添加到AttachInfo中
        mActions = null;
        mCount = 0;
    }
}
// ->> 回到分析原处

结论

在AttachInfo被赋值时(即不为null),就会遍历 前面过程1保存了通过post()添加的任务 的数组,将每个任务发送到handler中等待执行。

下面,我们继续看,AttachInfo的赋值过程 -> dispatchAttachedToWindow()是什么时候被调用的。

答:dispatchAttachedToWindow()调用时机是在 View 绘制流程的开始阶段,即 ViewRootImpl.performTraversals()

代码语言:javascript
复制
/**
  * 基础:
  * 1. AttachInfo的创建是在ViewRootImpl的构造方法中
  * 2. 同一个 View Hierachy 树结构中所有View共用一个AttachInfo
  */
  mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, this,context);

/**
  * dispatchAttachedToWindow()调用时机 = View绘制流程开始
  * 即ViewRootImpl.performTraversals()
  */
    private void performTraversals() {

        // ...

        // mView是DecorView
        // host的类型是 DecorView(继承自 FrameLayout)
        // 每个Activity都有一个关联的 Window(当前窗口),每个窗口内部又包含一个 DecorView 对象(描述窗口的xml视图布局)
        final View host = mView;

        // 调用DecorVIew的dispatchAttachedToWindow()
        // ->> 分析1
        host.dispatchAttachedToWindow(mAttachInfo, 0);
       
       getRunQueue().executeActions(mAttachInfo.mHandler);

       // 开始View绘制流程的测量、布局、绘制阶段
       performMeasure();
       performLayout();
       performDraw();
       ...

    }

/**
  * 分析1:DecorVIew.dispatchAttachedToWindow()
  * 注:DecorView并无重写该方法,而是在其父类ViewGroup里 
  */
  void dispatchAttachedToWindow(AttachInfo info, int visibility) {
    super.dispatchAttachedToWindow(info, visibility);

    // 子View的数量
    final int count = mChildrenCount;
    final View[] children = mChildren;

    // 遍历所有子View,调用所有子View的dispatchAttachedToWindow() & 为每个子View关联AttachInfo
    // 子View 的 dispatchAttachedToWindow()在前面过程1已经分析过:
    // 即 遍历 前面过程1保存了通过post()添加的任务 的数组,将每个任务发送到handler中等待执行。
    for (int i = 0; i < count; i++) {
        final View child = children[i];
        child.dispatchAttachedToWindow(info,combineVisibility(visibility, child.getVisibility()));
    }
   
    // ...
}

结论

  • 通过View.post()添加的任务是在View绘制任务里 - 开始绘制阶段时添加到消息队列的尾部的;
  • 所以,View.post() 添加的任务的执行是在View绘制任务后才执行,即在View绘制流程结束之后执行
  • 即View.post() 添加的任务能够保证在所有 View绘制流程结束之后才被执行,所以 执行View.post() 添加的任务时可以正确获取到 View 的宽高。

额外延伸

a. 问题描述

若只是创建一个 View & 调用它的post(),那么post的任务会不会被执行?

代码语言:javascript
复制
final View view = new View(this);

    view.post(new Runnable() {
        @Override
        public void run() {
            // ...
        }
    });

b. 答案

不会。主要原因是: 每个View中post() 需执行的任务,必须得添加到窗口视图-执行绘制流程 - 任务才会被post到消息队列里去等待执行,即依赖于dispatchAttachedToWindow ();

若View未添加到窗口视图,那么就不会走绘制流程,post() 添加的任务最终不会被post到消息队列里,即得不到执行。(但会保存到HandlerAction数组里)

上述例子,因为它没有被添加到窗口视图,所以不会走绘制流程,所以该任务最终不会被post到消息队列里 & 执行

c. 解决方案

此时只需要添加将View添加到窗口,那么post()的任务即可被执行

代码语言:javascript
复制
// 因为此时会重新发起绘制流程,post的任务会被放到消息队列里,所以会被执行
contentView.addView(view);

至此,关于view.post()原理讲解完毕

总结

View.post()的原理:以Handler为基础,View.post() 将传入任务添加到 View绘制任务所在的消息队列尾部,从而保证View.post() 任务的执行时机是在View 绘制任务完成之后的。 其中,几个关键点:

1-View.post()实际操作:将view.post()传入的任务保存到一个数组里 / 2-View.post()添加的任务 添加到 View绘制任务所在的消息队列尾部的时机:View 绘制流程的开始阶段,即 ViewRootImpl.performTraversals() 3-View.post()添加的任务执行时机:在View绘制任务之后

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2020/10/26 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 背景
  • 矛盾
  • 解决方案
  • 结论
  • 源码解析
  • 过程2:当AttachInfo为null时,则将任务加入当前View的等待队列中。
    • 结论:
    • 过程1:当AttachInfo不为null时,直接调用其内部Handler的post()
      • 结论
        • 结论
        • 额外延伸
          • a. 问题描述
            • b. 答案
              • c. 解决方案
              • 总结
              相关产品与服务
              消息队列 CMQ 版
              消息队列 CMQ 版(TDMQ for CMQ,简称 TDMQ CMQ 版)是一款分布式高可用的消息队列服务,它能够提供可靠的,基于消息的异步通信机制,能够将分布式部署的不同应用(或同一应用的不同组件)中的信息传递,存储在可靠有效的 CMQ 队列中,防止消息丢失。TDMQ CMQ 版支持多进程同时读写,收发互不干扰,无需各应用或组件始终处于运行状态。
              领券
              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档