文中的源代码版本为api23
之所以会出现这篇文章,是因为最近用到了这个方法。 最开始我用的是Activity.runOnUiThread
和Handler.post
这两个方法,但是发现获取到的视图尺寸为0,而只有View.post
才能得到我想要的结果,这让我觉得自己对View.post
这个方法的理解还不充分,因此写下这篇文章做个记录。
1 Handler.post
Handler
应该是大家非常熟悉的类了,其post
方法会将Runnable
对象包装成一个Message
对象,放到消息队列中等待被执行。对于该方法就不展开讲了。 在构造Handler
的时候我们可以传入Looper
对象来指定Handler
在什么线程工作。如果使用默认构造函数,则取决于创建Handler
时所在的线程。
2 Activity.runOnUiThread
public final void runOnUiThread(Runnable action) {
//当前线程如果不是主线程则通过mHandler
//将action抛到主线程消息队列中,等待被执行
if (Thread.currentThread() != mUiThread) {
mHandler.post(action);
} else {
//如果当前线程就是主线程
//那么直接调用run方法执行
action.run();
}
}
复制代码
相对于使用Handler
可以指定线程,Activity.runOnUiThread
方法只能在主线程任务。同时该方法还做了判断,如果当前线程就是主线程,那么立刻执行Runnable.run
方法执行任务。
3 View.post
我们先来看看该方法的源码
public boolean post(Runnable action) {
//mAttachInfo是在触发View.dispatchAttachedToWindow
//方法时被赋值,它由ViewRootImpl负责创建
//ViewRootImpl以及其管理的所有View、ChildView都共享这一
//AttachInfo实例
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
//mHandler也有ViewRootImpl负责创建
//它的实际类型为ViewRootHandler
//工作在主线程
return attachInfo.mHandler.post(action);
}
//getRunQueue返回的实际类型是RunQueue
ViewRootImpl.getRunQueue().post(action);
return true;
}
//RunQueue.java
void post(Runnable action) {
postDelayed(action, 0);
}
void postDelayed(Runnable action, long delayMillis) {
//将action封装成一个HandlerAction对象
HandlerAction handlerAction = new HandlerAction();
handlerAction.action = action;
handlerAction.delay = delayMillis;
synchronized (mActions) {
//mActions实际类型为ArrayList<HandlerAction>
mActions.add(handlerAction);
}
}
复制代码
可以看到View.post
方法有两个分支
- 当
mAttachInfo
字段不为空,则使用一个Handler
直接将任务抛到主线程任务队列中等待执行 - 当
mAttachInfo
字段为空,则将任务保存在RunQueue
中
我们先不讨论mAttachInfo
何时为空何时不为空的问题。
RunQueue
中的任务在什么时候会被执行呢? 答案在ViewRootImpl.performTraversals
中
private void performTraversals() {
//...
getRunQueue().executeActions(mAttachInfo.mHandler);
//...
}
//RunQueue.java
//executeActions的逻辑很简单
//遍历mActions中的所有HandlerAction
//然后使用传入的Handler将这些任务
//抛到消息队列中
void executeActions(Handler handler) {
synchronized (mActions) {
final ArrayList<HandlerAction> actions = mActions;
final int count = actions.size();
for (int i = 0; i < count; i++) {
final HandlerAction handlerAction = actions.get(i);
handler.postDelayed(handlerAction.action, handlerAction.delay);
}
actions.clear();
}
}
复制代码
performTraversals
方法我们知道是视图树执行重绘的核心方法,平时我们在调用View.requestLayout
方法后,最终都会触发该方法。该方法执行结束,那么所有视图的尺寸就差不多都出来了。若此时在该方法内post
一个消息,那么当这个消息被执行时肯定是可以拿到视图尺寸的。
3.1 复盘
基于这样的理解,我们再回过头来解决本文要解决的核心问题:为什么View.post方法可以用来获取视图尺寸
通常我们再Activity.onCreate
方法中使用View.post
public class DemoActivity extends Activity{
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.actv_demo);
view.post(new Runnable(){
public void run(){
Log.d("size",view.getMeasuredHeight()+":"+view.getMeasuredWidth());
}
});
}
}
复制代码
那么此时view
中的的mAttachInfo
是否有值呢?答案是否定的。
前面提到过mAttachInfo
字段是在触发View.dispatchAttachedToWindow
方法时被赋值的
//View.java
void dispatchAttachedToWindow(AttachInfo info, int visibility) {
mAttachInfo = info;
//...
}
复制代码
那么什么时候会触发dispatchAttachedToWindow
呢?答案在ViewRootImpl.performTraversals
中。
private void performTraversals() {
//...
//当前的ViewRootImpl首次执行performTraversals
//时,mFirst为true
if (mFirst) {
//...
//host为DecorView,其本质是FrameLayout的派生类
//dispatchAttachedToWindow会一层层传递到
//视图树最低部的视图
host.dispatchAttachedToWindow(mAttachInfo, 0);
//...
}else{
//...
}
//...
//无论是否是第一次
//这句代码都会执行
getRunQueue().executeActions(mAttachInfo.mHandler);
//...
}
复制代码
那么ViewRootImpl
首次执行performTraversals
方法(也即是ViewRootImpl.scheduleTraversals
方法首次被调用)是在什么时候呢?
答案在ActivityThread.handleResumeActivity
中。
final void handleResumeActivity(IBinder token,
boolean clearHide, boolean isForward, boolean reallyResume) {
//...
//Activity.onResume方法在performResumeActivity
//方法执行的过程中被触发
ActivityClientRecord r = performResumeActivity(token, clearHide);
if (r != null) {
//...
if (r.window == null && !a.mFinished && willBeVisible) {
//...
if (a.mVisibleFromClient) {
a.mWindowAdded = true;
//第一次触发scheduleTraversals
//wm的实际类型为WindowManagerImpl
wm.addView(decor, l);
}
// If the window has already been added, but during resume
// we started another activity, then don't yet make the
// window visible.
} else if (!willBeVisible) {
//...
}
//...
}else{
//...
}
}
复制代码
handleResumeActivity
方法最终会通过WindowManager.addView
触发scheduleTraversals
,这个调用流程会比较长: WindowManagerImpl.addView
-> WindowManagerGlobal.addView
-> ViewRootImpl.setView
-> ViewRootImpl.requestLayout
-> ViewRootImpl.scheduleTraversals
scheduleTraversals
会在主线程消息队列中放一个任务,当这个任务执行时就会触发performTraversals
。
结论是View.mAttachInfo
这个字段是在Activity.onResume
执行完毕之后在未来的某个消息周期才会被赋值的,也就是说,当我们在Activity.onCreate
中调用View.post
时,任务会被放到RunQueue
中。
因此,我们可以使用View.post
方法在Activity.onCreate
中获取视图尺寸。
4 总结
还是上张图会比较清晰
Handler.post
方法获取视图尺寸的操作发生在视图尺寸计算操作之前,因此获取到的视图尺寸为0。
在主线程中调用
Activity.runOnUiThread
执行任务,使得获取视图尺寸的操作立刻被执行,遭遇视图尺寸计算,因此获取到的视图尺寸也是0。
View.post
由于获取视图尺寸的时机在视图尺寸计算之后,因此可以正常获取到视图尺寸。