为什么view.post()能保证获取到view的宽高?

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

  • 背景
    业务需求代码开始时机一般是在: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()入手:

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;
}

此处分成两个过程讲解:

当AttachInfo不为null时,直接调用其内部Handler的post;
当AttachInfo为null时,则将任务加入当前View的等待队列中。
此处为了方便理解,我会先讲解过程2

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

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;
    }
   // ...
}

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

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

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

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()
/**
  * 基础:
  * 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的任务会不会被执行?

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()的任务即可被执行

// 因为此时会重新发起绘制流程,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绘制任务之后

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容