Android踩坑经验-View.post获取宽高及子线程调用更新UI原理解析

解决两个问题:
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方法,再继续看:
Android踩坑经验-View.post获取宽高及子线程调用更新UI原理解析_第1张图片
先看下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 的宽高。

你可能感兴趣的:(Android踩坑经验,Android系统)