View.post为何能够准确获取View的宽高

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;
    }

现在有两个问题:

  1. attachInfo 是何时赋值 如果此值不为null 表明View已经绘制完成可以直接获取宽高
  2. 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();这个 前面贴出来了有四个主要功能:

  1. host.dispatchAttachedToWindow(mAttachInfo, 0) | getRunQueue().executeActions(mAttachInfo.mHandler); // 执行缓存的view发送的runnable 添加到主线程的MessageQueue 该队列的Message都是同步消息 当设置同步分隔栏时是不会被执行 除非移除后才会执行
  2. performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); // 测量
  3. performLayout(lp, mWidth, mHeight); // 布局
  4. 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的展示流程

你可能感兴趣的:(View.post为何能够准确获取View的宽高)