View.post()为什么能准确拿到View的宽高

简介: Android View.post

起因:之前一群里的哥们问 Handler.post()  为什么会在 ActivityonResume() 之后执行,我找了一遍之后并没有找到原因,后来从这个问题我想起其他的问题 view.post() 为什么在 view.post() 之后为什么可以准确的获取到 view的宽高。

疑问🤔️:View.post()为什么会准确的获取到View的宽高?

public boolean post(Runnable action) {
  //注释1:判断attachInfo如果不为空 直接调用attachInfo内部Handler.post()方法
      //这样就有一个问题attachInfo在哪里赋值?这个问题先存疑。
        final AttachInfo attachInfo = mAttachInfo;
        if (attachInfo != null) {
            return attachInfo.mHandler.post(action);
        }
        //注释2:此时attachInfo是空的
        getRunQueue().post(action);
        return true;
    }
复制代码

存疑1:attachInfo在哪里赋值?

我们点进去看一下 getRunQueue().post(action); 是何方神圣。

private HandlerActionQueue getRunQueue() {
        if (mRunQueue == null) {
            mRunQueue = new HandlerActionQueue();
        }
        return mRunQueue;
    }
复制代码

上面代码很简单,那么接下来就需要看看 HandlerActionQueue() 是什么玩意了。

public class HandlerActionQueue {
    private HandlerAction[] mActions;
    private int mCount;
   //注释1
    public void post(Runnable action) {
        postDelayed(action, 0);
    }
   //注释2
    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++;
        }
    }
    public void removeCallbacks(Runnable action) {}
   //假装不知道这个是执行方法
    public void executeActions(Handler handler) {}
    public int size() {}
    public Runnable getRunnable(int index) {}
    public long getDelay(int index) {}
    private static class HandlerAction {}
}
复制代码

先看注释1 可以看到这个post()方法 其实就是getRunQueue().post(action); 它内部调用了注释2方法 也就是postDelayed()注释2简单看了里面的代码 发现大概逻辑只是对 我们postrunnable进行缓存起来,那么我们从哪里真正执行这个runnable呢? 我发现 我们一路跟进来直到postDelayed()方法时并没有看到对应的执行方法那么我们就放大招了

影·奥义!时光回溯。

我们看看getRunQueue()时 返回的mRunQueue在哪里调用的就好了。

//不会上传图片...大小不懂得控制 所以用代码块 
    private HandlerActionQueue getRunQueue() {
        if (mRunQueue == null) {
            mRunQueue = new HandlerActionQueue();
        }
        return mRunQueue;
    }
   void dispatchAttachedToWindow(AttachInfo info, int visibility) {
           mAttachInfo = info;
   //忽略的一些代码
  if (mRunQueue != null) {
            mRunQueue.executeActions(info.mHandler);
            mRunQueue = null;
        }
   }
复制代码

通过检查对mRunQueue的引用 可以看到view代码内只有这两个方法有所使用,那么第一个方法我们以及看过了 所以我们重点看下第二个方法。

从上面第二个方法 可以得知mAttachInfodispatchAttachedToWindow(AttachInfo info, int visibility)内的info赋值 那么 info又在哪里生成?先继续看下去。

通过对dispatchAttachedToWindow() 的调用关系可以发现以下方法会调用

private void performTraversals() {
  // cache mView since it is used so much below...
        final View host = mView;
  /.../
      //注释1
  host.dispatchAttachedToWindow(mAttachInfo, 0);
  /.../
            performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);//执行测量
            performLayout(lp, mWidth, mHeight);//执行布局
            performDraw();//执行绘制
 }
复制代码

可以看到 host内会调用这个方法,并且将mAttachInfo作为参数传入,而这个host是一个DecorView

为什么是DecorView? 我们可以反推回去验证 通过源码我们得知 hostViewRootImpl 中的mView的成员变量 对mView检查赋值的地方 可以看到以下代码:

public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
        synchronized (this) {
        if (mView == null) {
                mView = view;//对mView 赋值
                /.../
                }
        }
}
复制代码

那什么时候能调用到setView()呢? 可以看到setView() 并不是静态方法,所以要调用并需要引用实例才可以。

那么我们可以看看构造方法

public ViewRootImpl(Context context, Display display) {
        mContext = context;
        /.../
        //注释1
        mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, this,context);
    }
复制代码

可以看到注释1处的代码我们能够得知 mAttachInfo是在ViewRootImpl构造器中创建出来的。

我们再对构造器查看调用 可以发现再 WindowManagerGlobaladdView()方法 会创建ViewRootImpl

public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) {
            /.../
           root = new ViewRootImpl(view.getContext(), display);
     }
复制代码

WindowManagerGlobaladdView()方法 又被WindowManagerImpladdView()调用

@Override
    public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
        applyDefaultToken(params);
        mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
    }
复制代码

再对这个方法进行调用检查可以看到

@Override
    public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,
            String reason) {
                 if (r.window == null && !a.mFinished && willBeVisible) {
            r.window = r.activity.getWindow();
            View decor = r.window.getDecorView(); //注释1
            decor.setVisibility(View.INVISIBLE);
            ViewManager wm = a.getWindowManager();
            WindowManager.LayoutParams l = r.window.getAttributes();
            a.mDecor = decor;
            l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
            l.softInputMode |= forwardBit;
            if (r.mPreserveWindow) {
                a.mWindowAdded = true;
                r.mPreserveWindow = false;
                ViewRootImpl impl = decor.getViewRootImpl();
                if (impl != null) {
                    impl.notifyChildRebuilt();
                }
            }
            if (a.mVisibleFromClient) {
                if (!a.mWindowAdded) {
                    a.mWindowAdded = true;
                    wm.addView(decor, l); //注释2
                } else {
                    a.onWindowAttributesChanged(l);
                }
            }
        } else if (!willBeVisible) {
            if (localLOGV) Slog.v(TAG, "Launch " + r + " mStartedActivity set");
            r.hideForNow = true;
        }
            }
复制代码

可以在注释1处:会返回一个DecorView 并且在注释2处添加进入。 至此 我们可以得知ViewRootImpl处的mView就是 DecorView

知识点:并且我们可以从上面代码看出在onResumeview 才会被添加在window内并且执行view的测量布局绘制 这就是为什么在onCreate()时获取到view宽高会是0的原因,因为那时view都没有添加进window呢!!!

时间再次穿梭 回到ViewRootImplperformTraversals()

既然已知mView就是DecorView 那么这个DecorView是一个继承于FrameLayoutViewGroup, 我们在DecorViewFrameLayout内没有找到对dispatchAttachedToWindow()方法的处理,就自然而然的来到了ViewGroup处。

@Override
    void dispatchAttachedToWindow(AttachInfo info, int visibility) {
        final int count = mChildrenCount;
        final View[] children = mChildren;
        for (int i = 0; i < count; i++) {
            final View child = children[i];
            //遍历调用 这样最终会回到View的dispatchAttachedToWindow()
            child.dispatchAttachedToWindow(info,
                    combineVisibility(visibility, child.getVisibility()));
        }
    }
复制代码

这时候我们又回到了ViewdispatchAttachedToWindow()方法内的mRunQueue.executeActions(info.mHandler);并点击去看源码

public void executeActions(Handler handler) {
        synchronized (this) {
            final HandlerAction[] actions = mActions;//注释1
            for (int i = 0, count = mCount; i < count; i++) {
                final HandlerAction handlerAction = actions[i];
                handler.postDelayed(handlerAction.action, handlerAction.delay);//注释2
            }
            mActions = null;
            mCount = 0;
        }
    }
复制代码

注释1处 我们可以看到mActions其实是我们用view.post方法时 传入的runnable的存储数组。 而注释2处 就会将runnable 交给handler.post()方法并且添加进这个Handler所持有的Looper内部的MessageQueue中。

至此我们可以大概的进行一次总结了。

总结:

1:View内部的mAttachInfo 会在ViewdispatchAttachedToWindow()方法内赋值 在dispatchDetachedFromWindow()赋值为null,并且mAttachInfo的根本是在 ViewRootImpl的构造器内创建的,所以我们就可以知道当viewattchInfo不为空时 这个 view是已经被添加进窗口内的,如果为null就说明view没有在window内。

2: 我们能通过view.post()正确的获取View的宽高主要得益于Android内的生命周期是被Handler所驱动的,所以当ViewRootImplActivityonResume()生命周期内被创建时,其实主线程的Handler 是在执行处理一个Message的流程中,虽然我们从上面ViewRootImpl内的performTraversals()源码中看到 view缓存的runnable会在performMeasure(), performLayout(),performDraw()这些方法前先被post出去并且添加到MessageQueue链表中,但是这些runnable是属于下一个Message的,而performMeasure(), performLayout(),performDraw()这三个方法是属于本次Message的逻辑,只有本次消息处理完成Handler内部的Looper才会进行下一次消息的处理,最终保证了 View.post()能够正确的拿到View的宽高。


作者:FeanCheng

链接:https://juejin.cn/post/6916024212696236040

来源:掘金

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

目录
相关文章
|
2月前
|
JSON Android开发 数据格式
Android动态添加view设置view大小(宽高)
Android动态添加view设置view大小(宽高)
25 0
|
存储 缓存 索引
RecyclerView 动画原理 | pre-layout,post-layout 与 scrap 缓存的关系
RecyclerView 动画原理 | pre-layout,post-layout 与 scrap 缓存的关系
67 0
|
API uml Android开发
Android | 深入理解View.post()获取宽高、Window加载View原理
深入理解View.post()获取宽高、Window加载View原理
360 0
|
iOS开发
iOS开发 - touchBegan事件判断点击的位置在View上还是在View的子View上
iOS开发 - touchBegan事件判断点击的位置在View上还是在View的子View上
262 0
iOS开发 - touchBegan事件判断点击的位置在View上还是在View的子View上
|
前端开发 容器
View的测量、布局和绘制过程中父View(当前View)和子View的先后顺序
View的测量、布局和绘制过程中,到底是先测量(布局、绘制)父View,还是先测量子View,这篇文章会从源码角度给出答案。
调用View#requestLayout后,哪些View会被影响?
调用View#requestLayout后,哪些View会被影响?
|
消息中间件 XML 存储
面试官:View.post() 为什么能够获取到 View 的宽高 ?
面试官:View.post() 为什么能够获取到 View 的宽高 ?
|
容器
View的绘制过程
View的绘制过程从Activity.setContentView开始经过如下方法: Activity.setContentView—>PhoneWindow.
985 0