IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 移动开发 -> 深度详解 View.post() 为何能够获取到 View 的宽高值? -> 正文阅读

[移动开发]深度详解 View.post() 为何能够获取到 View 的宽高值?

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 的宽高值。

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

  移动开发 最新文章
Vue3装载axios和element-ui
android adb cmd
【xcode】Xcode常用快捷键与技巧
Android开发中的线程池使用
Java 和 Android 的 Base64
Android 测试文字编码格式
微信小程序支付
安卓权限记录
知乎之自动养号
【Android Jetpack】DataStore
上一篇文章      下一篇文章      查看所有文章
加:2022-07-17 16:34:46  更:2022-07-17 16:36:38 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/22 6:55:47-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码