美文网首页
View.post为何能够准确获取View的宽高

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

作者: luweicheng24 | 来源:发表于2019-03-28 11:40 被阅读0次

    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的宽高

          本文链接:https://www.haomeiwen.com/subject/aatzvqtx.html