Activity作为android视图的承载者,拥有完整的生命周期,那我们到底在那个生命周期后能够通过
View.getHeight
或者View.getMeasureHeight
获取准确的值呢?不至于总是获取到的值为0呢?为何我们通过View.post
发送的runnable
肯定会在界面绘制完成以及activity的window关联windowmanager后才会执行呢?带着这几个问题来追踪一下源码一探究竟;
- 先来看一下View.post实现:
public boolean post(Runnable action) {
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
return attachInfo.mHandler.post(action);// 如果attachinfo不为null 直接将该runnable发送给主线程的handler
}
getRunQueue().post(action); // 获取 HandlerActionQueue 将该runnable添加到该队列
return true;
}
现在有两个问题:
- attachInfo 是何时赋值 如果此值不为null 表明View已经绘制完成可以直接获取宽高
- HandlerActionQueue 到底是如何执行该runnable的
先来分析第二个问题,HandlerActionQueue.java
public class HandlerActionQueue {
private HandlerAction[] mActions; // 我们通过View.post发送的所有runnable包装成HandlerAction的存储数组
private int mCount;
public void post(Runnable action) { // 发送事件
postDelayed(action, 0);
}
public void postDelayed(Runnable action, long delayMillis) {
final HandlerAction handlerAction = new HandlerAction(action, delayMillis); // runnable包装成HandlerAction 该类是一个内部类 就在代码下方
synchronized (this) {
if (mActions == null) {
mActions = new HandlerAction[4]; // 初始化存储数组 默认容量为4
}
mActions = GrowingArrayUtils.append(mActions, mCount, handlerAction); 将包装的HandlerAction存储起来
mCount++;
}
}
... // 此处省略部分不重要的代码
public void executeActions(Handler handler) { // 通过外部传入的handler 遍历 将HandlerAction 数组中的runnable放到该handler的messgequeue中,其实这个handler就是主线程的handler ,下面会分析该方法的调用时机
synchronized (this) {
final HandlerAction[] actions = mActions;
for (int i = 0, count = mCount; i < count; i++) {
final HandlerAction handlerAction = actions[i];
handler.postDelayed(handlerAction.action, handlerAction.delay);
}
mActions = null;
mCount = 0;
}
}
... 此处再省略部分代码
private static class HandlerAction { // 该类就是View.post的runable的包装类
final Runnable action;
final long delay;
public HandlerAction(Runnable action, long delay) {
this.action = action;
this.delay = delay;
}
public boolean matches(Runnable otherAction) {
return otherAction == null && action == null
|| action != null && action.equals(otherAction);
}
}
}
既然View.post
发送的runnable
存储在HandlerActionQueue
中,那就看HandlerActionQueue.executeActions(Handler handler)何时调用,在View中搜索该方法的调用时机:
void dispatchAttachedToWindow(AttachInfo info, int visibility) {
mAttachInfo = info; // 赋值attachinfo
... 省略部分代码
// Transfer all pending runnables.
if (mRunQueue != null) {
mRunQueue.executeActions(info.mHandler); // 将HandlerActionQueue中的runnablre添加到mHandler的MessageQueue中 该mHandler是attachinfo的成员变量 也就是ui线程的handler
mRunQueue = null;
}
performCollectViewAttributes(mAttachInfo, visibility);
onAttachedToWindow(); // view与window关联完成
... 省略部分代码
}
由此看出dispatchAttachedToWindow该方法的调用时机对于view发送的runnable至关重要,其实该方法的调用是由ViewRootImp的performTraversals
private void performTraversals() {
// cache mView since it is used so much below...
final View host = mView; // 该view代表DecorView 是在setView时赋值的
if (mViewLayoutDirectionInitial == View.LAYOUT_DIRECTION_INHERIT) {
host.setLayoutDirection(config.getLayoutDirection());
}
host.dispatchAttachedToWindow(mAttachInfo, 0); // 分发attachinfo信息到host的所有子view 让decorview的子view都拥有attachinfo
mAttachInfo.mTreeObserver.dispatchOnWindowAttachedChange(true);
dispatchApplyInsets(host);
} else {
desiredWindowWidth = frame.width();
desiredWindowHeight = frame.height();
if (desiredWindowWidth != mWidth || desiredWindowHeight != mHeight) {
if (DEBUG_ORIENTATION) Log.v(mTag, "View " + host + " resized to: " + frame);
mFullRedrawNeeded = true;
mLayoutRequested = true;
windowSizeMayChange = true;
}
}
// Non-visible windows can't hold accessibility focus.
if (mAttachInfo.mWindowVisibility != View.VISIBLE) {
host.clearAccessibilityFocus();
}
// Execute enqueued actions on every traversal in case a detached view enqueued an action
getRunQueue().executeActions(mAttachInfo.mHandler); // 执行缓存的view发送的runnable
...
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); // 测量
...
performLayout(lp, mWidth, mHeight); // 布局
...
performLayout(lp, mWidth, mHeight); // 绘制
...
}
...
此处虽然调用dispatchOnWindowAttachedChange
同时也调用了 getRunQueue().executeActions
说明此时将View.post的事件加入到了Messagequeue,那就看performTraversals是不是在这些消息加入Messagequeue前加入了绘制界面的Message,跟踪代码得出一下调用流程:
ViewRootImpl.setView () -> ViewRootImpl.requestLayout() -> ViewRootImpl.scheduleTraversals ()
关键来了,ViewRootImpl.scheduleTraversals ()
这就是执行界面绘制的方法:
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true; // 表明绘制中 避免调用绘制
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier(); // 关键 插入了同步分割栏标记 从此刻开始messagequeue只获取message加了sync标记的message
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null); // 执行 mTraversalRunnable
if (!mUnbufferedInputDispatch) {
scheduleConsumeBatchedInput();
}
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}
可以看出在mTraversalRunnable执行执行前加入了同步分隔栏标记,同步分割栏是起什么作用的呢?它就像一个卡子,卡在消息链表中的某个位置,当消息循环不断从消息链表中摘取消息并进行处理时,一旦遇到这种“同步分割栏”,那么即使在分割栏之后还有若干已经到时的普通Message,也不会摘取这些消息了。请注意,此时只是不会摘取“普通Message”了,如果队列中还设置有“异步Message”,那么还是会摘取已到时的“异步Message”的。在Android的消息机制里,“普通Message”和“异步Message”也就是这点儿区别啦,也就是说,如果消息列表中根本没有设置“同步分割栏”的话,那么“普通Message”和“异步Message”的处理就没什么大的不同了。
既然在mTraversalRunnable执行前加入了同步分隔栏,那这个mTraversalRunnable应该就是个异步消息了:
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
这个mTraversalRunnable的执行体就是 doTraversal();那既然我们上面猜测mTraversalRunnable这个任务是异步消息,那就跟一下Choreographer.java
代码:
mChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null)
- > postCallbackDelayed
- > postCallbackDelayedInternal
调用关系终止与 postCallbackDelayedInternal
,看一下该方法的代码:
private void postCallbackDelayedInternal(int callbackType,
Object action, Object token, long delayMillis) {
if (DEBUG_FRAMES) {
Log.d(TAG, "PostCallback: type=" + callbackType
+ ", action=" + action + ", token=" + token
+ ", delayMillis=" + delayMillis);
}
synchronized (mLock) { // 同步锁
final long now = SystemClock.uptimeMillis();
final long dueTime = now + delayMillis;
mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);
if (dueTime <= now) {
scheduleFrameLocked(now);
} else {
Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
msg.arg1 = callbackType;
msg.setAsynchronous(true); // 重点 果然设置执行doTraversal()这会消息体的消息标记为异步消息
mHandler.sendMessageAtTime(msg, dueTime);
}
}
}
看到这里应该了解到主线程的加入同步分隔栏之后将界面绘制Message设置成异步消息,就是为了先进行界面的绘制,而在doTraversal()中执行逻辑如下:
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier); // 以为已经执行到了绘制界面Message了所以移除了同步分隔栏
if (mProfile) {
Debug.startMethodTracing("ViewAncestor");
}
performTraversals(); // 开始绘制
if (mProfile) {
Debug.stopMethodTracing();
mProfile = false;
}
}
}
可以看到是不是终于绕回来了, performTraversals()
;这个 前面贴出来了有四个主要功能:
-
host.dispatchAttachedToWindow(mAttachInfo, 0) | getRunQueue().executeActions(mAttachInfo.mHandler);
// 执行缓存的view发送的runnable 添加到主线程的MessageQueue 该队列的Message都是同步消息 当设置同步分隔栏时是不会被执行 除非移除后才会执行 -
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
// 测量 -
performLayout(lp, mWidth, mHeight);
// 布局 -
performLayout(lp, mWidth, mHeight);
// 绘制
现在大体思路应该很清楚了:
ViewRootImpl.setView ()
->ViewRootImpl.requestLayout()
-> ViewRootImpl.scheduleTraversals ()
-> ViewRootImpl.doTraversals () // MessageQueue的异步消息
-> host.dispatchAttachedToWindow(mAttachInfo, 0) && getRunQueue().executeActions(mAttachInfo.mHandler) && mesure &&layout && draw // 添加View.post的同步消息到MessgaeQueue
终于清楚了为啥View.post中能获取到View的宽高了 ,那 ViewRootImpl.setView是何时调用呢?查看下篇 Activity启动到View的展示流程