0%

高级UI-(03)2021.9.16-View的绘制流程--leo老师

View 是什么时候被添加进去的?

我们 setContentView 只是创建了 DecorView ,并且解析 xml 将 View 添加到 DecorView 中了。

为什么在 Activity 的 onCreate 方法中通过 view.post 中就能获取到 各个元素的宽高呢?这是因为所有的 View 操作都是 Handler 的 msg ,在之前的 msg 已经在 layout了,而我们 post 过去的 msg 必然是在 layout 这个 msg 之后。因为高是通过 bottom - top 得到的,所以,还没layout 之前是没有的。

1
2
3
public int getHeight(){
return mBottom - mTop;
}

我们 window.addView 是在 performResume 之后,add 进去之后才会测量,所以我们执行 onResume 的时候,实际上也拿不到 元素的宽高的。

window 相关的各个类的功能:

  • WindowManagerImpl: 确定 view 属于哪个 window,哪个父窗口

  • WindowManagerGlobal : 管理整个进程、所有的 窗口信息,它是进程唯一的。

  • ViewRootImpl 是 WindowManagerGlobal 的实际操作者,只不过ViewRootImpl 只操作自己的 window

DecorView 是什么时候添加到 Window 上的?

handleResumeActivity
–> performResumeActivity
–> r.activity.performResume
–> mInstrumentation.callActivityOnResume
–> wm.addView(decor, l); (WindowManagerImpl.java)
–> WindowManagerGlobal.addView
–> root = new ViewRootImpl(view.getContext(), display);
–> mViews.add(view); // DecorView
mRoots.add(root); // ViewRootImpl
mParams.add(wparams); // WindowManager.LayoutParams
–> root.setView(view, wparams, panelParentView, userId);

接下来执行的过程:

ViewRootImpl.setView
–> requestLayout(); // 请求遍历
–> scheduleTraversals
–> mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
–> 上面的 mTraversalRunnable 中会执行到 doTraversal
–> performTraversals(); // 绘制View ,measure、layout、draw

将 window 添加到 WMS 上之后, DecorView 就会设置自己的 parent 为这个 ViewRootImpl ,所以,其实我们的 view 只需要getParent 得到 mParent ,之后 mParent 再次 getParent ,如此循环(往上遍历 View 树 ),最后肯定会到最顶部的 ViewRootImpl :

--> res = mWindowSession.addToDisplayAsUser // 将窗口添加到WMS上面  WindowManagerService
--> 事件处理
--> view.assignParent(this); //DecorView 设置自己的 parent 为 ViewRootImpl

ViewRootImpl 的构造函数里面有几行代码需要注意:

1
2
3
4
5
6
mThread = Thread.currentThread();//创建它的线程

mDirty = new Rect();//脏区域


mAttachInfo = new View.AttachInfo(xxxx);//保存当前 Window 的信息

预测量

当前View 的 onMeasure 最多会执行多少次? 4 次,因为还可能(只有子 View 是 WrapContent的情况才有)有预测量(measureHierarchy 方法)过程,所以,我们也可以说 wrapContent 就是这样影响性能。预测量过程:

  1. 设置一个值,进行第一次测量

  2. 获取一个状态值,看看是否已经合适了

  3. 如果不合适,更改大小为某个值,进行第二次测量

  4. 如果还是不满意,直接给自己的最大值,进行第3次测量

第 4 步之后,如果还有问题,在预测量之后,还有一次 performMeasure ,所以最后,预测量里面可能最多有 3 次测量,当前 View 可能最多有 4次测量(当然,performMeasure 之后还可能引起父 view 的重新 measure ,这个另说)。

LayoutInflater.inflate () 的时候,如果 没有设置 parent 的话,那么xml 根部布局设置的布局参数无效(宽度、高度等)。因为会根据 parent.generateParams 来确定 xml 的 layoutParams 的,所以如果没有 parent ,inflate 出来就不会 setParams 。 generateLayoutParams 的例子如下:

1
2
3
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new RelativeLayout.LayoutParams(getContext(), attrs);
}

MeasureSpec 是个 32 位的 int 型,低 30 是值,高 2 位是 mode 模式(AT_MOST、EXACTLY、UNSPECIFIED)

performLayout

child.layout

performDraw

我们有个输入框,当点击弹出输入法的时候,这个输入框可能会移动,这是怎么做到的呢?其实就是在 draw() 方法里面执行的 scrollxxx 某个方法实现的。

在 draw 方法里面,会判断是否开启硬件绘制,用 if-else 来分别使用 硬件绘制和软件绘制。

canvas 也是有软硬件绘制区分的。

绘制熟顺序: drawBackground 、onDraw、 drawForground

一般来说,ViewGroup 的 onDraw 是不执行的,只会执行 dispatchDraw ,而 view 则相反; 但是,如果我想让 ViewGroup 的 onDraw 方法执行,有几种方法?

  • setWillNotDraw(false) 设置为false

  • setBackground 设置背景

  • 通过反射改变标志位

面试题?

UI 刷新只能在主线程进行吗?

我们知道出现报错是在 ViewRootImpl 的 requestLayout 的时候,会执行 checkThread ,而这个代码是:

1
2
3
4
5
6
void checkThread() {
if (mThread != Thread.currentThread()) {
throw new CalledFromWrongThreadException(
"Only the original thread that created a view hierarchy can touch its views.");
}
}

意思是,如果 requestLayout 线程和 当初创建 ViewRootImpl 的线程不是同一个,则报错。所以,并不是说只有主线程才能刷新 UI ,而是只有在创建 ViewRootImpl 的线程才能刷新 UI

那如何在子线程刷新UI 呢?

  • 在 ViewRootImpl 创建之前调用,比如在onCreate 中创建线程 然后调用 textview.setText()

  • 利用 Handler

  • 在子线程中创建 ViewRootImpl:可以在子线程创建一个 WindowManager ,然后 addView ,比如我们页面上要有一个悬浮的按钮

在 WindowManager 的 addView 的时候,就会创建 ViewRootImpl 了

最后,放上2 张老师在ppt 上的图片:

requestLayout 的时序图:

requestLayout 的时序图

invalidate 时序图:

invalidate 时序图

谢谢你的鼓励