深度详解 View.post() 为何能够获取到 View 的宽高值?

2023-11-03

1. 简介

翻看之前的博客,深度解析源码 onCreate() 和 onResume() 中获取不到View的宽高值?在文章中通过分析源码解析了获取不到 View 宽高值的原因,在文章结尾处留了一个问题,是打算后面继续分析解读的,但是却给忘了,欠下的总归是要弥补的,因此这里来补上,本文就来深度详解 View.post() 为何能够获取到 View 的宽高值?

1.1 问题描述

首先,承接之前文章提出的问题,下面三处打印输出的结果是什么呢?带着问题思考一下,然后猜测一下输出结果,之后我们再带着问题去探寻源码。

class TestViewPostActivity : Activity() {
    private val TAG: String = TestViewPostActivity::class.java.simpleName
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_view_post)
        // 打印输出日志 1
        btnViewPost.apply {
            Log.e(TAG, "打印输出日志 1---> onCreate() 获取View的测量宽度:" + this.width)
        }
        btnViewPost.post(Runnable {
            // 打印输出日志 2
            Log.e(TAG, "打印输出日志 2---> onCreate() 中 通过 post 方法获取View的测量宽度:" + btnViewPost.width)
        })
    }

    override fun onResume() {
        super.onResume()
        // 打印输出日志 3
        btnViewPost.apply {
            Log.e(TAG, "打印输出日志 3---> onResume() 获取View的测量宽度:" + this.width)
        }
    }
}

1.2 结果展示

来看一下输出的日志结果,看看给你的答案一致不?
结果展示
从图上的打印输出可以看到,onCreate() 和 onResume() 方法中是获取不到 View 的测量值的,在 onCreate() 方法中通过 View # post() 方法可以获取到 View 的测量值。

和你的答案一致不?如果一致且能说出所以然,说明你对 Activity 的生命周期流程、View 绘制流程、以及 View 是如何关联到 Window 的流程都有了一定的理解。不一致且心中疑惑不解的,没关系,下面就来一起学习吧!

2. 源码分析

关于 onCreate() 和 onResume() 方法中获取不到 View 的测量值,可参考深度解析源码 onCreate() 和 onResume() 中获取不到View的宽高值?

这里简述一下,从 Activity 的启动到界面显示出来的过程中,View 绘制流程的开始时机是在 ActivityThreadhandleResumeActivity() 方法,在该方法首先完成 Activity 生命周期 ** Activity # onResume()** 方法回调,然后开始 View 绘制任务。也就是说 View 绘制流程要在 Activity # onResume() 方法之后,但绝大部分业务是在 Activity # onCreate() 方法,比如要获取某个 View 的实际宽高值,由于 View 的绘制任务还未开始,所以就无法正确获取到。

关于 View # post() 方法为何能够获取到 View 的宽高值,带着问题探寻源码吧…

2.1 View # post() 方法添加任务

public class View implements Drawable.Callback, KeyEvent.Callback,
        AccessibilityEventSource {
    ......
    AttachInfo mAttachInfo;
    ......
    // 添加待执行的 Runnable 到消息队列中,该 Runnable 将在用户界面线程(UI主线程)上运行
    public boolean post(Runnable action) {
        final AttachInfo attachInfo = mAttachInfo;
        if (attachInfo != null) {
            return attachInfo.mHandler.post(action);
        }

        // 加入当前 View 的等待队列,保存待执行对象 Runnable,直到我们知道它需要在哪个线程上执行
        // 注意注释:假定待执行对象将放置在 attach 之后执行,实际上也确实是,注释很有用哈
        getRunQueue().post(action);
        return true;
    }
    ......
}

执行流程如下:

  1. 获取 mAttachInfo 信息,并判断其值是否为空,如不为空,则获取其内部的 mHandler 对象,并通过获取到的 mHandler 对象的 post() 方法将待执行的 Runnable 添加到其内部的 MessageQueen 中等待执行。
  2. 如果为空,则将待执行的 Runnable 加入当前 View 的待执行队列,保存待执行 Runnable 对象,直到找到它需要在哪个线程上执行。注意注释:注释很有用哈,假定待执行对象将放置在 attach 之后执行,实际上也确实是,后面会分析。

通过上述分析可知,mAttachInfo 实例是否有值对流程走向起到决定性作用,那么问题来了哦,此时 mAttachInfo 实例到底有没有值呢?

先给出结论,下文具体分析,mAttachInfoView 中的一个类型为 AttachInfo 的成员变量,每一个被添加到 Window 中的 View 都会持有一个 AttachInfo 实例。该 AttachInfo 实例是在 ViewRootImpl 的构造方法中构建的,在 ViewRootImpl # performTraversals() 方法中经过一些判断赋值后,由 View # dispatchAttachedToWindow() 方法将 AttachInfo 对象传递给 View 并赋值给 mAttachInfo。由于在深度解析源码 onCreate() 和 onResume() 中获取不到View的宽高值文章中分析过,在 Activity # onCreate() 方法中,View 还未被添加到 Window 中,也没有开始执行 View 的测量、布局及绘制流程等,所以此时的 mAttachInfo 还未被赋值。

2.2 HandlerActionQueue.post() 方法添加任务

由于 mAttachInfo 此时还是空值,跟踪查看 getRunQueue().post(action) 方法,首先来看一下 getRunQueue() 方法,代码如下:

public class View implements Drawable.Callback, KeyEvent.Callback,
        AccessibilityEventSource {
	......
    // 待执行的任务队列,用于保存 post() 方法添加到 View 的待执行任务
    private HandlerActionQueue mRunQueue;
    ......
    private HandlerActionQueue getRunQueue() {
        if (mRunQueue == null) {
            mRunQueue = new HandlerActionQueue();
        }
        return mRunQueue;
    }
    ......
}

该方法返回的是 HandlerActionQueue 类型的队列,当 View 还没有关联到 Handler 时,将添加到 View 的待执行对象 Runnable 加入到该等待队列,待合适的时机交给 Handler 来处理。

继续来看 HandlerActionQueue # post() 方法,代码如下:

public class HandlerActionQueue {
	......
	// 默认长度为 4 的HandlerAction 数组
    private HandlerAction[] mActions; 
	......
    public void post(Runnable action) {
    	// 调用内部的 postDelayed() 方法,延迟时间为 0 
        postDelayed(action, 0);
    }

    public void postDelayed(Runnable action, long delayMillis) {
    	// 将传入的待执行对象 Runnable 封装成 HandlerAction 待执行的任务
        final HandlerAction handlerAction = new HandlerAction(action, delayMillis);

        synchronized (this) {
            if (mActions == null) {
            	// 数组为空,则构建一个长度为 4 的 HandlerAction 数组
                mActions = new HandlerAction[4];
            }
            // 将封装的 HandlerAction 对象保存在 mActions 数组中
            mActions = GrowingArrayUtils.append(mActions, mCount, handlerAction);
            // mActions 数组下标加 1
            mCount++;
        }
    }
    ......
}

将传入的待执行对象 Runnable 封装成 HandlerAction 待执行的任务,HandlerAction 内部持有待执行的 Runnable 对象和延迟执行的时间。并将封装的 HandlerAction 对象保存在长度为 4HandlerAction 数组 mActions 中,该数组用于保存添加的待执行任务。

分析完流程的第2部分,只是将 View # post() 方法添加的待执行对象 Runnable,封装成 HandlerAction 待执行任务,保存在 HandlerAction 数组 mActions 中,没有执行任务的入口。

2.3 探究 AttachInfo 的由来

现在回头来分析流程的第1部分,mAttachInfo 已被赋值的情况,在 2.1 View # post() 方法添加任务 中有分析,mAttachInfoView 被添加到 Window 后,在创建 ViewRootImpl 实例对象时,在 ViewRootImpl 构造函数中创建的 mAttachInfo 实例对象,然后通过 ViewRootImpl # performTraversals() 方法中经过一些判断赋值后,由 View # dispatchAttachedToWindow() 方法将 AttachInfo 对象传递给 View 并赋值给 mAttachInfo

时序图如下所示:

时序图

2.3.1 AttachInfo 类

在详细分析流程前,先来查看一下 AttachInfo 这个内部类,代码如下:

public class View implements Drawable.Callback, KeyEvent.Callback,
        AccessibilityEventSource {
	......
	@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
    AttachInfo mAttachInfo;
    ......
    final static class AttachInfo {
    	......
        AttachInfo(IWindowSession session, IWindow window, Display display,
                ViewRootImpl viewRootImpl, Handler handler, Callbacks effectPlayer,
                Context context) {
            mSession = session;
            mWindow = window;   
            mWindowToken = window.asBinder();
            mDisplay = display; // View 显示相关信息
            mViewRootImpl = viewRootImpl; // View 与 Window 之间的桥梁,很重要的一个类
            mHandler = handler; // ViewRootHandler 用来处理 View 的刷新、隐藏等消息事件
            mRootCallbacks = effectPlayer;
            mTreeObserver = new ViewTreeObserver(context);
        }
    }
    ......
}

AttachInfoView 中的一个 final 修饰的静态内部类,通过其构造函数可以看到,其内部存储了当前 View Hierachy 控件树所绑定的 Window 的各种有用的信息,并且会派发给 View Hierachy 控件树中的每一个 View,保存在每个 View 自己的 mAttachInfo 变量中。

注意: 其持有的 mHandlerViewRootHandler 对象,用来处理需要在 UI 主线程执行的操作,如:来自于系统进程 WMSView 刷新、隐藏等消息事件。

2.3.2 ViewRootImpl 类

通过前面的分析可知,mAttachInfo 实例对象是在 ViewRootImpl 构造函数中创建的,继续看一下 ViewRootImpl 类的构造方法,代码如下:

public final class ViewRootImpl implements ViewParent,
        View.AttachInfo.Callbacks, ThreadedRenderer.DrawCallbacks,
        AttachedSurfaceControl {
	......
	boolean mFirst;
	// 依附于创建 ViewRootImpl 实例的线程,即 UI 主线程
	// 用于将某些必须在 UI 主线程进行的操作安排在主线程中执行
	final ViewRootHandler mHandler = new ViewRootHandler();
	......
	public ViewRootImpl(Context context, Display display) {
        this(context, display, WindowManagerGlobal.getWindowSession(),
                false /* useSfChoreographer */);
    }

	public ViewRootImpl(@UiContext Context context, Display display, IWindowSession session,
            boolean useSfChoreographer) {
        mContext = context;
        // mWindowSession 是从 WindowManagerGlobal 获取的一个 IWindowSession.Stub 的 Binder 代理类的实例
        // 用于 ViewRootImpl 和 WMS 进行跨进程通信的
        mWindowSession = session;
        mDisplay = display;
        ......
        // 保存当前线程到 mThread,这个赋值操作体现了创建ViewRootImpl的线程如何成为UI主线程
        // 在ViewRootImpl处理来自控件树的请求时(如请求重新布局,请求重绘,改变焦点等),会检
        // 查发起请求的thread与这个mThread是否相同。倘若不同则会拒绝这个请求并抛出一个异常
        mThread = Thread.currentThread();
		......
        // 创建一个 W 类型的实例,W 是 IWindow.Stub 的 Binder 代理类。
        // 将在 WMS 中作为新窗口的 ID,以及接收来自 WMS 的回调
        mWindow = new W(this);
        ......
        // View 第一次被添加到 Window 时,标志位 mFirst 置为 true
        mFirst = true;
        mPerformContentCapture = true; // also true for the first time the view is added
        mAdded = false;
        mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, this,
                context);
		......
    }
    ......
}

调用 ViewRootImpl 的构造方法来构建实例时,保存 ContextIWindowSession 等信息,并使用这些信息创建 View.AttachInfo 实例对象。

ViewRootImpl 实现了 ViewParent 接口,作为整个 View Hierachy 控件树的根部,View 测量、布局及绘制等都由 ViewRootImpl 触发。另一方面,它是 WindowManagerGlobal 工作的实际实现者,还需要负责与 WMS 交互通信以调整 Window 的位置大小,以及对来自 WMS 的事件(如 Window 尺寸改变等)作出相应的处理,所以说这个类很重要。

2.3.3 ActivityThread # handleResumeActivity() 方法

结合上面的时序图来探索 View 是如何添加到当前页面 ActivityWindow 中的,其添加时机在 ActivityThreadhandleResumeActivity() 方法中,并且在 Activity # onResume() 方法回调之后,代码如下:

public final class ActivityThread extends ClientTransactionHandler {
	......
	@Override
    public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,
            String reason) {
		......
 		// 调用 performResumeActivity() 方法,流程最后回调到 Activity # onResume() 方法
        final ActivityClientRecord r = performResumeActivity(token, finalStateRequest, reason);
		......
        final Activity a = r.activity;
		......
        if (r.window == null && !a.mFinished && willBeVisible) {
        	// 获取当前 Activity 的 Window,这里获取到的是实现类 PhoneWindow
            r.window = r.activity.getWindow();
            // 获取 PhoneWindow 的顶层 View - DecorView
            View decor = r.window.getDecorView();
            decor.setVisibility(View.INVISIBLE);
            // 获取当前 Activity 的 WindowManager,WindowManager 接口是 ViewManager 接口的子类
            // WindowManager 接口的实现类是 WindowManagerImpl
            ViewManager wm = a.getWindowManager();
            WindowManager.LayoutParams l = r.window.getAttributes();
            a.mDecor = decor;
            if (a.mVisibleFromClient) {
                if (!a.mWindowAdded) {
                    a.mWindowAdded = true;
                    // WindowManager 添加顶层的 DecorView,实际调用的是 WindowManagerImpl 来添加
                    wm.addView(decor, l);
                }
                ......
            }
        }
		......
    }
}

获取当前 ActivityWindow 实现类 PhoneWindow,然后获取 PhoneWindow 的顶层 View,这里指的是 DecorView。通过当前 ActivityWindowManager 实现类 WindowManagerImpl 添加获取到的顶层 DecorView

一般情况下,每个 Activity 都有一个关联的 Window 对象,由 WindowManager 负责管理。Window 本身更倾向于一个抽象的概念,它具体的实体以 View 的形式存在。PhoneWindowWindow 抽象类的唯一实现类,其内部包含一个 DecorView 对象,用来承载添加到 Window 中的 View。通过 Activity # setContentView() 方法设置的布局将添加到 DecorView 的布局 ID 为 content 的容器中,以此构建一个包含多个 ViewView Hierachy 控件树。

2.3.4 WindowManagerImpl # addView() 方法

public final class WindowManagerImpl implements WindowManager {
    @UnsupportedAppUsage
    private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();
    private final Context mContext;
    private final Window mParentWindow;

    private IBinder mDefaultToken;
    ......
    @Override
    public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
   		// 如果没有父 Window 或者没有设置 Token,则使用默认的 Token
        applyDefaultToken(params);
        // 继续调用 WindowManagerGlobal # addView() 方法添加 DecorView
        mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
    }
}

WindowManagerImplWindowManager 接口的实现类,虽是接口的实现类,但功能实现都委托给 WindowManagerGlobal 来完成。所以继续通过 WindowManagerGlobal 的 addView() 方法添加 DecorView,继续查看代码流程。

2.3.5 WindowManagerGlobal # addView() 方法

public final class WindowManagerGlobal {
	......
	public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) {
        ......// 空值检查
        ViewRootImpl root;
        View panelParentView = null;

        synchronized (mLock) {
            ......
            // 创建 ViewRootImpl 实例对象
            root = new ViewRootImpl(view.getContext(), display);
            view.setLayoutParams(wparams);
            // 将作为窗口的 DecorView、布局参数以及新建的 ViewRootImpl 以相同的索引值保存在三个数组中
            mViews.add(view);
            mRoots.add(root);
            mParams.add(wparams);
            try {
            	// 调用 ViewRootImpl 实例对象的 setView() 方法把 DecorView 传给 ViewRootImpl
                root.setView(view, wparams, panelParentView);
            } catch (RuntimeException e) {
                // BadTokenException or InvalidDisplayException, clean up.
                if (index >= 0) {
                    removeViewLocked(index, true);
                }
                throw e;
            }
        }
    }
    ......
}

创建 ViewRootImpl 实例对象,并调用该对象的 setView() 方法把 DecorView 传给 ViewRootImpl,继续查看代码流程。

WindowManagerGlobal 是一个 final 修饰的类,没有继承实现任何一个类、接口,是 WindowManager 功能的最终实现者。维护了当前进程中所有已经添加到系统中的 Window 的信息。注意:在一个进程中仅有一个 WindowManagerGlobal 的实例。

2.3.6 ViewRootImpl # setView() 方法

public final class ViewRootImpl implements ViewParent,
        View.AttachInfo.Callbacks, ThreadedRenderer.DrawCallbacks,
        AttachedSurfaceControl {
	......
	public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView,
            int userId) {
        synchronized (this) {
            if (mView == null) {
                mView = view; // 保存传入的 DecorView
				......
                mSoftInputMode = attrs.softInputMode;
                mWindowAttributesChanged = true;
                mAttachInfo.mRootView = view;
                mAttachInfo.mScalingRequired = mTranslator != null;
                mAttachInfo.mApplicationScale =
                        mTranslator == null ? 1.0f : mTranslator.applicationScale;
                ......
                mAdded = true;
				// 在添加到 Window 之前,通过 requestLayout() 方法发起在 UI 主线程上的首次遍历
                // 所谓“遍历”是指 ViewRootImpl 中的核心方法 performTraversal()
                // 这个方法实现对控件树进行测量、布局、向 WMS申请修改窗口属性以及重绘的所有工作
                requestLayout();
                ......
                try {
                    ......
                    // 将窗口添加至到 WMS 中
                    res = mWindowSession.addToDisplayAsUser(mWindow, mWindowAttributes,
                            getHostVisibility(), mDisplay.getDisplayId(), userId,
                            mInsetsController.getRequestedVisibility(), inputChannel, mTempInsets,
                            mTempControls);
					......
                } 
                ......
                // 设置 DecorView 的 parent 为当前 ViewRootImpl
                view.assignParent(this);
            }
        }
    }
    ......
}

将传入的 DecorView 赋值给 mView 保存,调用 ViewRootImpl # requestLayout() 方法,在向 Window 中添加顶层 View 之前,先通过 ViewRootImpl # requestLayout() 方法在 UI 主线程中安排一次“遍历”,所谓“遍历”是指 ViewRootImpl 中的核心方法 performTraversal(),这个方法实现对 View Hierachy 控件树进行测量、布局、向 WMS 申请修改 Window 属性以及重绘的所有工作,继续查看代码流程。

2.3.7 ViewRootImpl # requestLayout() 方法

public final class ViewRootImpl implements ViewParent,
        View.AttachInfo.Callbacks, ThreadedRenderer.DrawCallbacks,
        AttachedSurfaceControl {
	......
    @Override
    public void requestLayout() {
        if (!mHandlingLayoutInLayoutRequest) {
            checkThread();
            mLayoutRequested = true;
            // 调用 scheduleTraversals() 来调度安排 TraversalRunnable 的执行
            scheduleTraversals();
        }
    }
    
    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
    void scheduleTraversals() {
        if (!mTraversalScheduled) {
            mTraversalScheduled = true;
            // 通过 MessageQueue#postSyncBarrier() 为 UI 主线程的 Handler 添加同步屏障消息
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
            // Choreographer 通过 postCallback 提交一个任务,mTraversalRunnable 是待执行的任务回调
            // 有了同步屏障 mTraversalRunnable 在下一次 VSync 信号到来时就会被优先执行
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
            notifyRendererOfFramePending();
            pokeDrawLockIfNeeded();
        }
    }
        
    void doTraversal() {
        if (mTraversalScheduled) {
            mTraversalScheduled = false;
            // 通过 MessageQueue#removeSyncBarrier 方法移除同步屏障消息,传入 postSyncBarrier() 方法的返回值作为参数
            // 标识需要移除哪个屏障,然后将该屏障消息会从队列中移除,以确保消息队列恢复正常操作,否则应用程序可能会挂起
            mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
			......
			// 执行 View 的绘制流程开始渲染页面
            performTraversals();
			......
        }
    }
    ......
    final class TraversalRunnable implements Runnable {
        @Override
        public void run() {
        	// 调用 doTraversal() 方法,执行 TraversalRunnable 任务
            doTraversal();
        }
    }
    final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
}

代码执行流程如下:

  1. ViewRootImpl # requestLayout() 方法中,调用 ViewRootImpl # scheduleTraversals() 方法来调度安排 TraversalRunnable 的执行。
  2. ViewRootImpl # scheduleTraversals() 方法中,通过 MessageQueue # postSyncBarrier() 方法为 UI 主线程Handler 添加同步屏障消息,然后通过 Choreographer # postCallback() 方法提交一个任务,mTraversalRunnable 是待执行的任务回调,有了同步屏障消息 mTraversalRunnable 在下一次 VSync 信号到来时就会被优先执行,详情可参考文章 Handler 之同步屏障机制与 Android 的屏幕刷新机制的解析
  3. 调用 ViewRootImpl # doTraversal() 方法,执行 mTraversalRunnable 任务,首先通过 MessageQueue # removeSyncBarrier() 方法移除同步屏障消息,传入 MessageQueue # postSyncBarrier() 方法的返回值作为参数标识需要移除哪个屏障,然后将该屏障消息会从 MessageQueue 中移除,以确保消息队列恢复正常操作,然后调用 ViewRootImpl # performTraversals() 方法执行 View 的绘制流程开始渲染页面。

友情提示: 参考 Android 屏幕刷新机制之 Choreographer 可深入 Native 层进一步探索 Android 的屏幕刷新机制,笔者将带你深入底层去探索。

2.3.8 ViewRootImpl # performTraversals() 方法

    private void performTraversals() {
        // mView 是 DecorView,WindowManager # addView() 方法传入
        final View host = mView;
		mIsInTraversal = true;
		......
		// 创建 ViewRootImpl 实例对象时,在其构造方法中 mFirst 标志位置为 true
		// 此时第一次遍历,View Hierachy 树还未执行测量、布局等操作,也未被添加到 WMS
		// 待执行完毕后,标志位被置为 false,并将测量、布局后的 View Hierachy 树添加到 WMS
        if (mFirst) {
        	......
			// 经一系列条件判断给 AttachInfo 赋值
            mAttachInfo.mWindowVisibility = viewVisibility;
        	// 此时控件树即将第一次被显示在 Window 上
        	// 调用 DecorView 的 dispatchAttachedToWindow() 方法传入 mAttachInfo 实例
        	// 为每个位于 DecorView 中的 View 传递 mAttachInfo 关联信息
        	// 同时调用 View # onAttachedToWindow() 来绑定到 Window
            host.dispatchAttachedToWindow(mAttachInfo, 0);
            ......
        }
		......
        // 已分离 View 加入了一个任务,在每次 performTraversals 时执行其加入的任务
        // 通过 mAttachInfo 持有的 Handler 将消息添加到 UI 主线程的消息队列排队执行
        getRunQueue().executeActions(mAttachInfo.mHandler);
        ......
        // 执行 View 的测量操作
        performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
		......
        final boolean didLayout = layoutRequested && (!mStopped || wasReportNextDraw);
        if (didLayout) {
        	// 执行 View 的布局操作
            performLayout(lp, mWidth, mHeight);
            ......
        }
		......
		// 第一次执行测量、布局等执行完后,标志位 mFirst 置为 false
        mFirst = false;
        ......
        boolean cancelDraw = mAttachInfo.mTreeObserver.dispatchOnPreDraw() || !isViewVisible;
        if (!cancelDraw) {
            ......
            // 执行 View 的绘制操作
            performDraw();
        }
		......
        mIsInTraversal = false;
    }

执行流程如下:

  1. 获取 mView 中保存的 DecorView,由于创建 ViewRootImpl 实例对象时在其构造方法中将 mFirst 标志位置为 true,此时由于是第一次遍历,将调用 DecorView # dispatchAttachedToWindow() 方法并传入 mAttachInfo 实例,为 DecorView 中的每个 View 传递 mAttachInfo 关联信息。注意mAttachInfo 实例也是在构建 ViewRootImpl 实例时创建的。
  2. 执行 View 的测量、布局操作后,将 mFirst 标志位置为 false,对于同一个 View Hierachy 控件树中的 View,后续再调用 ViewRootImpl # performTraversals() 方法时,则不再调用 DecorView # dispatchAttachedToWindow() 方法。
  3. 如果没有取消 View 的绘制,调用 ViewRootImpl # performDraw() 方法执行 View 的绘制,这不是本文的分析重点哈。

2.3.9 ViewGroup # dispatchAttachedToWindow() 方法

由上面的分析可知,host 的实际类型是继承自 FrameLayoutDecorView,即 DecorView 本质上是一个 ViewGroup,且由于 DecorView 以及 FrameLayout 都没有重写 dispatchAttachedToWindow() 方法,因此调用的是其父类 ViewGroup # dispatchAttachedToWindow() 方法,代码如下:

public abstract class ViewGroup extends View implements ViewParent, ViewManager {
	......
    @Override
    @UnsupportedAppUsage
    void dispatchAttachedToWindow(AttachInfo info, int visibility) {
        mGroupFlags |= FLAG_PREVENT_DISPATCH_ATTACHED_TO_WINDOW;
        super.dispatchAttachedToWindow(info, visibility);
        mGroupFlags &= ~FLAG_PREVENT_DISPATCH_ATTACHED_TO_WINDOW;

        final int count = mChildrenCount; // 子 View 的数量
        final View[] children = mChildren; // 包含 ViewGroup 的所有子 View 的数组
        for (int i = 0; i < count; i++) {
        	// 遍历并获取 ViewGroup 的所有子 View
            final View child = children[i];
            // 调用每个子 View # dispatchAttachedToWindow() 方法
            // 为每个子 View 传递 AttachInfo 关联信息
            child.dispatchAttachedToWindow(info,
                    combineVisibility(visibility, child.getVisibility()));
        }
        final int transientCount = mTransientIndices == null ? 0 : mTransientIndices.size();
        for (int i = 0; i < transientCount; ++i) {
            View view = mTransientViews.get(i);
            // 为每个 TransientView 传递 AttachInfo 关联信息
            view.dispatchAttachedToWindow(info,
                    combineVisibility(visibility, view.getVisibility()));
        }
    }
    ......
}

遍历并获取 ViewGroup 的每个子 View,调用 View # dispatchAttachedToWindow() 方法为每个子 View 传递 AttachInfo 关联信息。

2.3.10 View # dispatchAttachedToWindow() 方法

public class View implements Drawable.Callback, KeyEvent.Callback,
        AccessibilityEventSource {
    ......
	@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
    void dispatchAttachedToWindow(AttachInfo info, int visibility) {
    	// View 赋值 AttachInfo
        mAttachInfo = info;
        if (mOverlay != null) {
            mOverlay.getOverlayView().dispatchAttachedToWindow(info, visibility);
        }
        mWindowAttachCount++;
        ......
  		// mRunQueue 类型是 HandlerActionQueue,内部保存了当前 View.post() 添加的任务
        if (mRunQueue != null) {
        	// 执行 View # post() 添加的待执行任务
            mRunQueue.executeActions(info.mHandler);
            mRunQueue = null;
        }
        performCollectViewAttributes(mAttachInfo, visibility);
        // 回调 View 的 onAttachedToWindow() 方法
   	    // 在 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);
            }
        }
        ......
		// 回调 View 的 onVisibilityChanged() 方法
		// 注意:这时候 View 绘制流程还未真正开始(参见 2.3.8 小节)
        onVisibilityChanged(this, visibility);
		......
    }
    .....
}

该方法中,首先为当前 ViewmAttachInfo 赋值,然后调用 mRunQueue # executeActions() 方法执行队列中保存的待执行任务,注意: mRunQueue 类型是 HandlerActionQueue,其内部保存的是通过 View # post() 方法添加的待执行任务。

2.3.11 HandlerActionQueue # executeActions() 方法

    public void executeActions(Handler handler) {
        synchronized (this) {
        	// 获取保存的任务队列
            final HandlerAction[] actions = mActions;
            // 遍历保存的任务队列
            for (int i = 0, count = mCount; i < count; i++) {
                final HandlerAction handlerAction = actions[i];
                // 通过 ViewRootHandler 将任务入队,等待执行
                // 注意这里是 post 到 UI 主线程的 Handler 中,即 ViewRootHandler 
                handler.postDelayed(handlerAction.action, handlerAction.delay);
            }
			// 置空 mActions 数组
            mActions = null;
            mCount = 0;
        }
    }

遍历 mActions 中保存的所有待执行任务,并通过 ViewRootHandler 的 postDelayed() 方法添加到其 MessageQueen 中排队执行,最后将保存任务的 mActions 置为 null,因为后续通过 View # post() 方法添加的待执行任务将直接添加到 AttachInfo 持有的 ViewRootHandler 中( 因为 mAttachInfo 已被赋值 )。

3. 总结

时序图
经过上面对源码的深入探索,结合时序图,来总结回答一下问题:为何 View # post() 方法能够获取到 View 的宽高值?

经过分析得出结果如下

首先,通过 View # post() 方法添加待执行任务 Runnable,在 View 还未添加到 Window 之前,此时 mAttachInfo 还未被赋值,因此先通过 HandlerActionQueue # post() 方法将待执行的 Runnable 封装成 HandlerAction,然后将封装的 HandlerAction 对象保存在长度为 4HandlerAction 数组 mActions 中,等待时机执行。

其次,在 Activity # onResume() 生命周期方法执行过后,调用 WindowManager # addView() 方法将 View 添加到 Window 中,通过前面结合时序图进行的源码分析可知,在 ViewRootImpl # performTraversals() 方法中,如果 mFirsttrue,即当前 Window 窗口中第一次添加 View注意:这里指的是 DecorView),将调用 DecorView # dispatchAttachedToWindow() 方法,DecorView 继承自 FrameLayout 是一个 ViewGroup,因此这里会继续调用父类 ViewGroup # dispatchAttachedToWindow() 方法,遍历获取每个子 View,并调用 View # dispatchAttachedToWindow() 方法为每个子 View 设置 AttachInfo 关联信息。

最后,在 View # dispatchAttachedToWindow() 方法中,为当前 ViewmAttachInfo 赋值,然后调用 mRunQueue # executeActions() 方法执行 mActions 数组中通过 View # post() 方法添加的待执行任务,这里所谓的执行,其实是通过 mAttachInfo 内持有的 ViewRootHandler 的 postDelayed() 方法将待执行任务添加到 UI 主线程的 MessageQueen 中排队执行(排在 View 的测量、布局和绘制任务的后面)。

注意: 此时 UI 主线程中已经在执行 ViewRootImpl # performTraversals() 方法中 View 的测量、布局和绘制操作,并且移除了 MessageQueen 中的同步屏障消息以恢复消息队列正常的循环,所以通过 View # post() 方法添加的待执行任务是在 View 的测量、布局和绘制操作之后才会执行,因此能够获取到 View 的宽高值。

交流:如有分析错误或者别的理解,还望留言或者私信笔者讨论,共同学习。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

深度详解 View.post() 为何能够获取到 View 的宽高值? 的相关文章

  • 从队列更新活动的最佳方法

    我有一个LinkedBlockingQueue在我的 生产者 调解者 消费者 模型中的调解者中 Producer 首先更新将 Mediator 添加到 ActivityQueue 中 接下来 消费者 活动在队列中等待 侦听并获取下一个项目
  • 生产中偶尔会发生 android.webkit.WebView 类膨胀错误

    我的应用程序在 Google Play 上 它在大多数设备 数千个用户 上运行良好 但在极少数情况下 占每日活跃用户百分比的一小部分 我得到Error inflating class android webkit WebView当我为我的片
  • Android STFP 库 [关闭]

    Closed 这个问题正在寻求书籍 工具 软件库等的推荐 不满足堆栈溢出指南 help closed questions 目前不接受答案 我想在我的 Android 项目中使用 SFTP 安卓已经有了吗 SFTP 库 还是我必须实现它 I
  • Android Ble GATT_ERROR 133 经常使用三星设备

    我正在研究 BLE 应用程序 我已经使用 Nexus Moto Samsung LG 等不同设备进行了测试 我仅在三星设备中收到 GATT 错误 133 三星 A5 2016 尝试连接 10 次 但只连接了 2 或 3 次 请帮助我 Non
  • Android:如何从输入流创建 9patch 图像?

    我使用下面的代码实例化 9patch 图像并将其设置为按钮的背景 下图显示了不理想的结果 InputStream MyClass class getResourceAsStream images btn default normal 9 p
  • Android 从键盘读取

    我的登录屏幕根本没有文本字段 当用户使用 RFID 扫描仪扫描他的 id 令牌时 我会得到一个 8 个字符长的字符串 其原理与使用键盘相同 只是更快 我希望我的登录活动在用户扫描其令牌时而不是之前执行 有一个聪明的方法来实现这个吗 我不能有
  • Notification.Builder 中 setGroup() 的用途是什么?

    我对目标的理解有些困难setGroup http developer android com reference android app Notification Builder html setGroup java lang String
  • 通过我的应用程序以编程方式插入新联系人,而不使用 Intent

    我正在使用一个应用程序 与手机联系人进行交互 我想将新联系人添加到我的手机联系人列表中 我已经尝试过以下代码 但它不起作用 void addContact Context ctx PreviewContactModel model Arra
  • Android Studio:Android 设备监视器未显示我的设备

    我的真实设备是索尼 Xperia c6502安卓版本4 3 我确定我将其连接到我的计算机然后打开开发者选项 USB调试 on 在 SDK 管理器中 Google USB 驱动程序已安装 I downloaded Xperia Z Drive
  • 调试 Java InterruptedException,即查找原因

    在调试Android应用程序时 有时中断异常发生并使应用程序崩溃 我已经能够在默认异常处理程序上设置断点 但调用堆栈不提供信息 at java util concurrent locks AbstractQueuedSynchronizer
  • Android 导航回到 Activity;不要重新加载父级

    我有一个场景 我单击 ListFragment 并启动一个新的 Activity 如下所示 public void onListItemClick ListView l View v int position long id super o
  • 当编辑文本获得焦点时更改边框颜色

    我想知道当编辑文本聚焦时如何更改它的边框颜色 目前它看起来像这样 我尝试过在SDK中检查源图片 但我无法理解它 我也尝试过使用xml 但无法仅更改边框颜色 如果我找到源图片 我可以在 Photoshop 中编辑以更改颜色 有什么关于如何执行
  • Kotlin 和惯用的书写方式,基于可变值“如果不为空,则...”

    假设我们有这样的代码 class QuickExample fun function argument SomeOtherClass if argument mutableProperty null doSomething argument
  • 取消通知

    我使用Onesignal推送通知 需要取消所有onPause和onResume的通知 NotificationManager notificationManager NotificationManager getApplicationCon
  • 从 Handler.obtainMessage() 获取什么参数

    我正在使用线程来执行一些 BT 任务 我正在尝试向 UI 线程发送消息 以便我可以基于我的 BT 线程执行 UI 工作 为此 我使用处理程序 但我不知道如何检索发送到处理程序的数据 要发送数据 我使用 handler obtainMessa
  • cordova插件条码扫描仪打不开扫描

    我的条形码扫描仪插件有问题 我不是天才 我不太了解如何编写网络应用程序 我使用phonegap和cordova 并且尝试制作一个网络应用程序 在单击链接后扫描条形码 我之前已经使用此命令行安装了该插件 cordova plugin add
  • Android 发布到 facebook 墙,stream.publish 几天来就中断了

    我有很多使用 FB android sdk 发布的应用程序 github com facebook facebook android sdk 我所有使用 FB 的应用程序几天后就停止工作了 这必然是 FB 方面的更改或错误 因为我的应用程序
  • 在 Android ADT Eclipse 插件中滚动布局编辑器

    有谁知道当布局编辑器的内容溢出一个 屏幕 时如何滚动这些内容 我说的是在设计时使用 ADT 布局编辑器 而不是在物理设备上运行时滚动 效果很好 关闭 Android 布局编辑器中的剪辑 切换剪辑 按钮位于 Android 布局编辑器的右上角
  • Android:ANT 构建失败,并显示 google-play-services-lib:“解析为没有项目的 project.properties 文件的路径”

    我正在尝试使用 ANT 构建我的应用程序 但在包含 google play services lib 库项目后 我惨遭失败 Step 1 我在 project properties 文件中设置了对库项目的引用 android library
  • Android UnityPlayerActivity 操作栏

    我正在构建一个 Android 应用程序 其中包含 Unity 3d 交互体验 我已将 Unity 项目导入 Android Studio 但启动时该 Activity 是全屏的 并且不显示 Android 操作栏 我怎样才能做到这一点 整

随机推荐

  • 使用随机森林回归填补缺失值

    文章目录 一 概述 二 实现 1 导入需要的库 2 加载数据集 3 构造缺失值 4 使用0和均值填充缺失值 5 使用随机森林填充缺失值 6 对填充好的数据进行建模 7 评估效果对比 一 概述 现实中收集的数据 几乎不可能是完美无缺的 往往都
  • 合并两个有序数组(给定两个有序整数数组 nums1 和 nums2,将 nums2 合并到 nums1 中,使得 num1 成为一个有序数组。)

    void merge int nums1 int nums1Size int m int nums2 int nums2Size int n 定义 让数组从后往前遍历 int i m 1 int j n 1 int len m n 1 如果
  • Python中安装Beautiful Soup成功后出现No module named 'bs4'

    此篇文章主要用来解决在终端中完成了beautiful soup的成功安装 但是在IDLE中依然出现以下错误 gt gt gt from bs4 import BeautifulSoup Traceback most recent call
  • 我的大学职业规划(大一时的思考)

    我的大学职业规划 文章目录 我的大学职业规划 1 计算机科学与技术专业的发展方向 不仅限于计科 2 大学四年应该做什么 3 学校竞赛与证书考核 4 编程学习的境界 以C 举例 5 考研与就业 考公与参军的抉择 写作时间 2021 5 28
  • 学会这八个技术,你离BAT大厂不远了

    红人榜第七期来咯 本期干货 HTTP 本周最受关注的技术关键词TOP8 往下看吧 在如今这个时间和知识都是碎片化的时代 C站根据C1 C4认证的成长路径 进行知识细化整理 形成系统化的知识图谱 小编根据C1认证的成长路径整理了100篇HTT
  • Linux下Gitee的user和email配置,查看配置信息命令

    Linux下Gitee的user和email配置 查看配置信息命令 查看配置信息 git config l 配置邮箱 git config global user email email 配置用户名 git config global us
  • STM32CUBEMX配置教程(二)时钟等内部参数配置

    STM32CUBEMX配置教程 二 时钟等内部参数配置 基于STM32H743VI 使用STM32CUBEMX两年了 始终觉得这个工具非常的方便 但因为不是经常使用 导致有些要点总是会有些遗忘 因此写下这一系列教程以供记忆 顺便让我这个大萌
  • Python 打造最强表白程序(源码)

    此程序结合数据抓取 微信自动发消息 定时任务 实现一个能每天自动定时给你心爱的 ta 发送 你们相识相恋天数 情话 我爱你的图片 具体的消息如下 每天发送的消息格式如下 message 亲爱的 早上好 今天是你和 Koc 相恋的第 天 今天
  • C++性能测试工具——gperftools的安装

    一 软件安装说明 gperftools的安装有两种方式 一种是源码方式 一种是直接安装模式 这里使用源码安装模式 原因是使用直接安装模式比较简单 安装此软件需要先安装libunwind这个软件 所以这里需要通过源码方式安装libunwind
  • 【机器学习】支持向量机【上】硬间隔

    有任何的书写错误 排版错误 概念错误等 希望大家包含指正 在阅读本篇之前建议先学习 机器学习 拉格朗日对偶性 机器学习 核函数 由于字数限制 分成两篇博客 机器学习 支持向量机 上 硬间隔 机器学习 支持向量机 下 软间隔与核函数 支持向量
  • CSS布局flex布局 对齐 等分 均分 详解

    一切都始于这样一个问题 怎样通过 CSS 简单而优雅的实现水平 垂直同时居中 记得刚开始学习 CSS 的时候 看到float属性不由得感觉眼前一亮 顺理成章的联想到 Word 文档排版中用到的的左对齐 右对齐和居中对齐 然而很快就失望的发现
  • 【leetcode】1143.最长公共子序列

    leetcode 1143 最长公共子序列 题目 思路 代码 复杂度 题目 leetcode原题链接 给定两个字符串 text1 和 text2 返回这两个字符串的最长 公共子序列 的长度 如果不存在 公共子序列 返回 0 一个字符串的 子
  • 如何快速查看并定位网页元素代码

    如何快速查看并定位网页元素代码 目的 可以迅速得找出一个网页中对应元素的html代码 1 首先我们打开一个网页 比如 百度首页 2 打开后我们会看到很多的文字链接以及按钮链接 那么我们找到我们想要查看的元素的文字或者按钮 3 我们这里以 百
  • @Cacheable注解属性介绍

    本文目录 1 value cacheNames 属性 2 key属性 3 keyGenerator 属性 4 cacheManager 属性 5 cacheResolver 属性 6 condition 属性 7 unless 属性 8 s
  • C++导出EXCEL开源库xlslib库使用心得

    使用教程 第一步 下载xlslib库 本文建立在xlslib2 5 0版本基础上 下载地址xlsLib download SourceForge net 第二步 切换到解压文件目录xlslib build msvc2008 打开项目xlsl
  • linux查询jvm运行内存使用情况,在Linux下获取正在运行的JVM的总使用内存

    您可以运行 ps aux grep java 这将显示包含在其推出的字符串java的每个应用程序的内存使用情况 这应该是大多数 如果不是所有的Java应用程序 从我的服务器的输出如下 servername servername ps aux
  • 超过飞飞系列-ZYNQ之FPGA学习2.1Verilog语法

    一 VHDL Verilog C语言区别 VHDL 硬件描述语言 美军开发 相对难 不直观 需要专业培训 欧洲发展较好 Verilog 硬件描述语言 设计群体广泛 资源成熟 中国多采用 并行处理运行 C 软件语言 经过C的单片机程序需取码
  • 简单工厂(Simple Factory)

    文章目录 1 代码示例 2 简单工厂模式的定义 实现意图 工厂模式 通过把创建对象的代码包装起来 做到创建对象的代码与具体的业务逻辑代码相隔离的目的 工厂模式可以细分为 简单工厂模式 工厂方法模式 抽象工厂模式 1 代码示例 include
  • servlet实现图片的上传

    servlet实现图片的上传 我们通常说的上传图片 是将图片上传到服务器上面 本篇以tomcat为例 实现简单的本地图片上传服务器 一 图片的上传需要引入两个jar包 commons fileupload 1 4 jar 下载地址 http
  • 深度详解 View.post() 为何能够获取到 View 的宽高值?

    文章目录 1 简介 1 1 问题描述 1 2 结果展示 2 源码分析 2 1 View post 方法添加任务 2 2 HandlerActionQueue post 方法添加任务 2 3 探究 AttachInfo 的由来 2 3 1 A