美文网首页Android面试必读Android进阶之路Android源码分析
Android:为什么view.post()能保证获取到view

Android:为什么view.post()能保证获取到view

作者: Carson带你学安卓 | 来源:发表于2020-10-26 08:18 被阅读0次

    前言

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


    背景

    • 业务需求代码开始时机一般是在: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;
    }
    

    此处分成两个过程讲解:

    1. 当AttachInfo不为null时,直接调用其内部Handler的post;
    2. 当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;
        }
       // ...
    }
    // 回到分析原处
    

    结论:

    1. 将传入的任务封装成HandlerAction对象
    2. 创建一个默认长度为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绘制任务之后

    接下来推出的文章,我将继续讲解Android的相关知识,感兴趣的读者可以继续关注我的博客哦:Carson_Ho的Android博客


    请点赞!因为你们的赞同/鼓励是我写作的最大动力!

    相关文章阅读
    Android开发:最全面、最易懂的Android屏幕适配解决方案
    Android开发:史上最全的Android消息推送解决方案
    Android开发:最全面、最易懂的Webview详解
    Android开发:JSON简介及最全面解析方法!
    Android四大组件:Service服务史上最全面解析
    Android四大组件:BroadcastReceiver史上最全面解析


    欢迎关注Carson_Ho的简书!

    不定期分享关于安卓开发的干货,追求短、平、快,但却不缺深度

    相关文章

      网友评论

        本文标题:Android:为什么view.post()能保证获取到view

        本文链接:https://www.haomeiwen.com/subject/fhyijktx.html