这个方法在日常开发中是经常用到的,例如在子线程中我们需要更新UI,可以通过post
一个runnable
,在run
方法中去绘制UI
,或者我们需要在Activity
的onCreate
中获取一个View
的宽高时,也会通过该方法在run
方法中获取这个View
的宽高信息。本文基于Android 9.0的源码进行分析。
- 首先,先从第一个问题开始分析,为什么我们在子线程调用
View
的post
方法,在run
里面就可以更新UI
了呢?先看下这个post
方法的定义:
<View.java>
public boolean post(Runnable action) {
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
// 这个mHandler是主线程handler,直接进行线程切换
return attachInfo.mHandler.post(action);
}
// 将runnable临时存储到队列中
// Postpone the runnable until we know on which thread it needs to run.
// Assume that the runnable will be successfully placed after attach.
getRunQueue().post(action);
return true;
}
-
这里面的
AttachInfo
是一个存储了View
的信息,在整个View
视图的树状结构中,都是共同引用同一个AttachInfo
对象。这个在此暂不展开分析。 -
如果
attachInfo
不为空,则会直接调用attachInfo
中的mHandler
变量进行post
操作,这里提前说明下:这个mHandler
就是主线程的Handler
,因此可以在run
方法中直接进行UI
操作了。 -
如果
attachInfo
为空时,则进入下面的getRunQueue().post(action)
。getRunQueue
方法会返回一个HandlerActionQueue
队列,这个队列从注释就能看出,只是作为一个临时缓存的容器,缓存的runnable
会在适当的时机去执行。
<View.java>
// 获取runnable缓存队列
private HandlerActionQueue getRunQueue() {
if (mRunQueue == null) {
mRunQueue = new HandlerActionQueue();
}
return mRunQueue;
}
<HandlerActionQueue.java>
/**
* Class used to enqueue pending work from Views when no Handler is attached.
*
* @hide Exposed for test framework only.
*/
public class HandlerActionQueue {
private HandlerAction[] mActions;
private int mCount;
// 将runnable进行缓存,等待时机进行执行
public void post(Runnable action) {
postDelayed(action, 0);
}
public void postDelayed(Runnable action, long delayMillis) {
final HandlerAction handlerAction = new HandlerAction(action, delayMillis);
synchronized (this) {
if (mActions == null) {
mActions = new HandlerAction[4];
}
mActions = GrowingArrayUtils.append(mActions, mCount, handlerAction);
mCount++;
}
}
...
// 执行runnable
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];
handler.postDelayed(handlerAction.action, handlerAction.delay);
}
mActions = null;
mCount = 0;
}
}
...
// 存储了runnable对象和delay延时
private static class HandlerAction {
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);
}
}
}
- 那这个
executeActions
方法具体是在什么时机执行的呢?一步步向上溯源,我们在View
中找到这么一个方法:
<View.java>
void dispatchAttachedToWindow(AttachInfo info, int visibility) {
...
// Transfer all pending runnables.
if (mRunQueue != null) {
// 此处会将缓存的runnable交给ViewRootImpl的mHandler进行处理
mRunQueue.executeActions(info.mHandler);
// 移交后置空
mRunQueue = null;
}
performCollectViewAttributes(mAttachInfo, visibility);
// 熟悉的回调
onAttachedToWindow();
...
}
- 继续向上查找
dispatchAttachedToWindow
的调用地方,发现ViewGroup
中重写方法中通过super
调用该方法。
分析过View
的绘制流程的都知道ViewRootImpl
是整个视图结构的顶级管理,自然会猜测是它在主动调用该方法。果然在performTraversals
方法中,会调用DecorView
的dispatchAttachedToWindow
方法。此时调用,说明View
已经和Window
进行关联,mAttachInfo
也已经实例化了,其内部的主线程mHandler
(主线程创建的)也实例化了,因此可以将run
方法中的代码post
到主线程执行,自然也就可以刷新UI
了。
<ViewRootImpl.java>
private void performTraversals() {
// cache mView since it is used so much below...
...
if (mFirst) {
mFullRedrawNeeded = true;
mLayoutRequested = true;
final Configuration config = mContext.getResources().getConfiguration();
if (shouldUseDisplaySize(lp)) {
// NOTE -- system code, won't try to do compat mode.
Point size = new Point();
mDisplay.getRealSize(size);
desiredWindowWidth = size.x;
desiredWindowHeight = size.y;
} else {
desiredWindowWidth = mWinFrame.width();
desiredWindowHeight = mWinFrame.height();
}
// We used to use the following condition to choose 32 bits drawing caches:
// PixelFormat.hasAlpha(lp.format) || lp.format == PixelFormat.RGBX_8888
// However, windows are now always 32 bits by default, so choose 32 bits
mAttachInfo.mUse32BitDrawingCache = true;
mAttachInfo.mHasWindowFocus = false;
mAttachInfo.mWindowVisibility = viewVisibility;
mAttachInfo.mRecomputeGlobalAttributes = false;
mLastConfigurationFromResources.setTo(config);
mLastSystemUiVisibility = mAttachInfo.mSystemUiVisibility;
// Set the layout direction if it has not been set before (inherit is the default)
if (mViewLayoutDirectionInitial == View.LAYOUT_DIRECTION_INHERIT) {
host.setLayoutDirection(config.getLayoutDirection());
}
// 1======>是在这里调用的,这个host是指向mView的引用,mView本质上就是根布局DecorView
// 调用该方法实质上是将view.post的runnable移交给mHandler,加入消息队列进行分发处理
host.dispatchAttachedToWindow(mAttachInfo, 0);
mAttachInfo.mTreeObserver.dispatchOnWindowAttachedChange(true);
dispatchApplyInsets(host);
} else {
...
// 2======>View的宽高测量
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
...
mIsInTraversal = false;
}
- 回到问题2,前面
View.post
到ViewRootImpl.performTraversals
的流程都是一样的,但是有个问题,看上面代码的注释,host.dispatchAttachedToWindow(mAttachInfo, 0);
这句代码执行是在performMeasure
之前,按理来说应该此时应该也是获取不到宽高的。
这里就涉及到Handler
的消息队列,其消息队列是同步的(本场景),也就是说是阻塞的。我们先看下performTraversals
是在哪里调用的:
<ViewRootImpl.java>
void doTraversal() {
if (mTraversalScheduled) {
...
// 此处调用
performTraversals();
...
}
}
final class TraversalRunnable implements Runnable {
@Override
public void run() {
// 该runnable中run方法调用了doTraversal
doTraversal();
}
}
final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
// 此处触发mTraversalRunnable
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
if (!mUnbufferedInputDispatch) {
scheduleConsumeBatchedInput();
}
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}
- 往上查找,找到是
Choreographer#postCallback
触发了整个绘制流程,Choreographer
的postCallback
方法中也是通过一个FrameHandler
进行发送消息处理。这个FrameHandler
实例化时在其构造方法中传入了一个Looper
。
再看看Choreographer
的实例化,发现传进来的looper
正是主线程的Looper
对象,说明Choreographer
中的mHandler
和ViewRootImpl.mHandler
都是主线程Handler
。因此在消息队列中,View.post
的消息是在Choreographer.postCallback
消息之后,所以会先调用一次performTraversals -> performMeasure
完成测量后,再处理View.post
的消息,此刻View
就能获取到宽高信息了。
<Choreographer.java>
// 也是通过post消息处理
public void postCallback(int callbackType, Runnable action, Object token) {
postCallbackDelayed(callbackType, action, token, 0);
}
public void postCallbackDelayed(int callbackType,
Runnable action, Object token, long delayMillis) {
...
postCallbackDelayedInternal(callbackType, action, token, delayMillis);
}
private void postCallbackDelayedInternal(int callbackType,
Object action, Object token, long delayMillis) {
... // handler发送消息处理
Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
msg.arg1 = callbackType;
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, dueTime);
...
}
}
// 这个也是主线程handler
private final FrameHandler mHandler;
// 构造方法传入looper对象
private Choreographer(Looper looper, int vsyncSource) {
mLooper = looper;
mHandler = new FrameHandler(looper);
...
}
private static final ThreadLocal<Choreographer> sThreadInstance =
new ThreadLocal<Choreographer>() {
@Override
protected Choreographer initialValue() {
// 对象是主线程实例化的,所以这个是主线程looper
Looper looper = Looper.myLooper();
if (looper == null) {
throw new IllegalStateException("The current thread must have a looper!");
}
Choreographer choreographer = new Choreographer(looper, VSYNC_SOURCE_APP);
if (looper == Looper.getMainLooper()) {
mMainInstance = choreographer;
}
return choreographer;
}
};
...
// Thread local storage for the SF choreographer.
private static final ThreadLocal<Choreographer> sSfThreadInstance =
new ThreadLocal<Choreographer>() {
@Override
protected Choreographer initialValue() {
// 对象是主线程实例化的,所以这个是主线程looper
Looper looper = Looper.myLooper();
if (looper == null) {
throw new IllegalStateException("The current thread must have a looper!");
}
return new Choreographer(looper, VSYNC_SOURCE_SURFACE_FLINGER);
}
};
- 总结一下整个流程:
-
Choreographer.postCallback
触发ViewRootImpl
的TraversalRunnable.run
方法 -
TraversalRunnable
在其run
方法中执行doTraversal -> scheduleTraversals
-
scheduleTraversals
中执行DecorView
的dispatchAttachedToWindow
,将View.post
的runnable
拿过来给mHandler
进行处理(加入到主线程消息队列) -
scheduleTraversals
继续往下执行performMeasure
,完成宽高测量 -
mHandler
取出后续消息,处理View.post
中的runnable
,在此刻View
的宽高已经赋值,因此可以获取到宽高信息了。
网友评论