前言
为什么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;
}
此处分成两个过程讲解:
- 当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 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博客
相关系列文章阅读
Carson带你学Android:学习方法
Carson带你学Android:四大组件
Carson带你学Android:自定义View
Carson带你学Android:异步-多线程
Carson带你学Android:性能优化
Carson带你学Android:动画
欢迎关注Carson_Ho的
不定期分享关于安卓开发的干货,追求短、平、快,但却不缺深度。