解决两个问题:
1:view post为什么能获取宽高?
2:子线程执行时为什么可以更新主线程UI?
Android开发中,在Acivity的onCreate方法中通过控件的getMeasureHeight/getHeight或者getMeasureWidth/getWidth方法获取到的宽高大小都是0,这个问题比较常见,因为在onCreate方法执行时,View还没有measure,比较常见的方式是使用View.post方法获取,最近在看View.post子线程不执行的问题,顺便一起看看这个实现的原理。
view.post(new Runnable() {
@Override
public void run() {
view.getHeight();
}
});
从上篇文章中可知,post方法在7.0之前和之后实现方式有差异,因此会出现低版本中子线程执行View.post方法时,可能会不执行的问题,上篇文章已经分析过了,本篇文章以Android 7.0之后的源码为解析基础吧,原理上大同小异。
看下post源码(以Android 7.0源码为例):
13842 public boolean post(Runnable action) {
13843 final AttachInfo attachInfo = mAttachInfo;
13844 if (attachInfo != null) {
13845 return attachInfo.mHandler.post(action);
13846 }
13847
13848 // Postpone the runnable until we know on which thread it needs to run.
13849 // Assume that the runnable will be successfully placed after attach.
13850 getRunQueue().post(action);
13851 return true;
13852 }
在这里会判断attachInfo是否为空,如果不为null时,直接取出mAttachInfo中存放的Handler对象去post Runnable任务, mAttachInfo是在dispatchAttachedToWindow时赋值的,这些结论跟上篇文章都一致。主要看下mHandler是谁的,以及dispatchAttachedToWindow又是在哪里调用的。
在Activity的onCreate方法中执行view.post方法,这个时候dispatchAttachedToWindow没有执行,但依然能拿到宽高,也就是说执行的getRunQueue().post(action)方法。先看这种情况,解释清楚这种情况,mHandler,dispatchAttachedToWindow调用时机就都捋清了。
看下具体代码:
13803 private HandlerActionQueue getRunQueue() {
13804 if (mRunQueue == null) {
13805 mRunQueue = new HandlerActionQueue();
13806 }
13807 return mRunQueue;
13808 }
继续往下看:
30public class HandlerActionQueue {
31 private HandlerAction[] mActions;
32 private int mCount;
33
34 public void post(Runnable action) {
35 postDelayed(action, 0);
36 }
37
38 public void postDelayed(Runnable action, long delayMillis) {
39 final HandlerAction handlerAction = new HandlerAction(action, delayMillis);
40
41 synchronized (this) {
42 if (mActions == null) {
43 mActions = new HandlerAction[4];
44 }
45 mActions = GrowingArrayUtils.append(mActions, mCount, handlerAction);
46 mCount++;
47 }
48 }
在post方法内部滴哦啊用了postDelayed方法,方法内部将Runnable和long作为参数创建了HandlerAction对象,放到mActions数组,看下HandlerAction:
113 private static class HandlerAction {
114 final Runnable action;
115 final long delay;
116
117 public HandlerAction(Runnable action, long delay) {
118 this.action = action;
119 this.delay = delay;
120 }
121
122 public boolean matches(Runnable otherAction) {
123 return otherAction == null && action == null
124 || action != null && action.equals(otherAction);
125 }
126 }
127}
数据结构比较简单,到这里逻辑比较明朗了,View.post(runnable)会传到HandlerActionQueue封装并缓存起来,HandlerActionQueue默认大小为4,支持子增长的的数组。
缓存起来的Runnable任务何时执行呢?就是HandlerActionQueue executeActions方法,看下实现:
82 public void executeActions(Handler handler) {
83 synchronized (this) {
84 final HandlerAction[] actions = mActions;
85 for (int i = 0, count = mCount; i < count; i++) {
86 final HandlerAction handlerAction = actions[i];
87 handler.postDelayed(handlerAction.action, handlerAction.delay);
88 }
89
90 mActions = null;
91 mCount = 0;
92 }
93 }
从代码可以看出,缓存到HandlerActionQueue的任务最重还是通过handler执行的,看下handler是在传入的:
发现有两个地方:
第一个地方:
15366 void dispatchAttachedToWindow(AttachInfo info, int visibility) {
15367 mAttachInfo = info;
15385 // Transfer all pending runnables.
15386 if (mRunQueue != null) {
15387 mRunQueue.executeActions(info.mHandler);
15388 mRunQueue = null;
15389 }
第二个地方:
1436 private void performTraversals() {
1552 // Execute enqueued actions on every traversal in case a detached view enqueued an action
1553 getRunQueue().executeActions(mAttachInfo.mHandler);
从源码看,performTraversals中也会调用dispatchAttachedToWindow方法,从代码可以看应该是执行的DecorView的dispatchAttachedToWindow,但我们是调用任意view的post方法,再继续看:
先看下View的dispatchAttachedToWindow:
15366 void dispatchAttachedToWindow(AttachInfo info, int visibility) {
15367 mAttachInfo = info;
15368 if (mOverlay != null) {
15369 mOverlay.getOverlayView().dispatchAttachedToWindow(info, visibility);
15370 }
接着会执行到ViewGroup的dispatchAttachedToWindow,
2950 @Override
2951 void dispatchAttachedToWindow(AttachInfo info, int visibility) {
2956 final int count = mChildrenCount;
2957 final View[] children = mChildren;
2958 for (int i = 0; i < count; i++) {
2959 final View child = children[i];
2960 child.dispatchAttachedToWindow(info,
2961 combineVisibility(visibility, child.getVisibility()));
2962 }
2963 final int transientCount = mTransientIndices == null ? 0 : mTransientIndices.size();
2964 for (int i = 0; i < transientCount; ++i) {
2965 View view = mTransientViews.get(i);
2966 view.dispatchAttachedToWindow(info,
2967 combineVisibility(visibility, view.getVisibility()));
2968 }
2969 }
从源码中可以看出,这个一个从DecorView不断调用子View的dispatchAttachedToWindow过程,而每个View的mAttachInfo正是dispatchAttachedToWindow时赋值的,也就是说不管是哪个View执行的post方法,最终执行的都是mAttachInfo.mHandler对象,看下mAttachInfo的创建:
410 public ViewRootImpl(Context context, Display display) {
431 mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, this);
3700 final ViewRootHandler mHandler = new ViewRootHandler();
从源码中看,构造方法中没有指定Looper,因此mHandler绑定的是当前线程的Looper,而ViewRootImpl相关操作均是在主线程中执行,因此绑定的是主线程的Looper。这也就解释了为什么在子线程中执行View.post方法依然能更新UI。
还有一个疑惑:dispatchAttachedToWindow调用时机是ViewRootImpl.performTraversals,但performMeasure,performLayout,performDraw都是在执行流的后面,为什么在View.post中就能拿到view的宽高呢?
经查阅资料发现,跟Android系统的消息机制有关系,performTraversals会先执行dispatchAttachedToWindow,这个时候会将任务post到主线程的MessageQueue等待执行,然后performTraversals方法会继续执行,完全执行完后,Looper再去消费下一个Message,这个时候才有可能会拿到post的Runnable,因此Runnabel操作实际是在performMeasure操作后才执行的,宽高自然就取到了。
总结:
1:View.post方法内部分两种情况处理,当View dispatchAttachedToWindow时,直接通过mAttachInfo.mHandler将Runnable post到主线程的MessageQUeue等待执行,当没有dispatchAttachedToWindow时,会将Runnable缓存起来,并在dispatchAttachedToWndow时,将Runnable post至主线程等待执行。
2:一个Activity上所有的View的mAttachInfo均来源于ViewRootImpl的mAttachInfo,mAttachInfo.mHandler都是绑定的主线程的Looper,因此无论在主线程还是子线程执行View.post方法,都支持UI更新操作。
3:View.post中可以获取View的宽高,是因为performTraversals中执行dispatchAttachedToWindow时,会将Runnable post至主线程等待执行,然后继续执行performMeasure,performLayout,performDraw方法,因此View.post的Runnable实际执行的时机时在performMeasure之后,所以才可以拿到View 的宽高。