第4章-View的工作原理读书笔记

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/willway_wang/article/details/85016896

1 初识 ViewRoot 和 DecorView

1.1 ViewRootImpl 类的作用是什么?

  1. ViewRoot 对应于 ViewRootImpl 类,是连接 WindowManager 和 DecorView 的纽带。之所以这样说,是因为在 ViewRootImpl 的构造函数中初始化了 mWindowSession = WindowManagerGlobal.getWindowSession(); 对象,而在 ViewRootImpl 的 setView 方法中,先调用 requestLayout(),然后调用 mWindowSession 的 addToDisplay 方法,将 View 显示了出来。
  2. View 的三大流程都是通过 ViewRoot 来完成的。具体来说,是从 ViewRootImpl 的 performTraversals() 方法开始的。performTraversals() 的工作流程如下:

measure 用来测量 View 的宽和高;
layout 用来确定 View 在父容器中的放置位置,即确定 View 的四个顶点的坐标和实际的 View 的宽/高;
draw 负责将 View 绘制在屏幕上。

1.2 ViewRootImpl 和 DecorView 如何建立关联?

在 ActivityThread 类中,
performLaunchActivity 方法里,会创建 Activity 对象,并调用 Activity 对象的 attach 方法,在 attach 方法中,创建了 PhoneWindow 对象;
handleResumeActivity 方法里,获取到 DecorView,并将 DecorView 添加到 Window 中,具体查看 WindowManagerGlobal 类的 addView 方法:

// 创建了 ViewRootImpl 对象
root = new ViewRootImpl(view.getContext(), display);
// 将 ViewRootImpl 对象和 DecorView view 对象建立关联
root.setView(view, wparams, panelParentView);

2 理解 MeasureSpec

2.1 说一说对于 MeasureSpec 的理解?

从类上看,MeasureSpec 类是 View 类的内部类,它封装了由父容器传递给子元素的布局要求,这个值在很大程度上决定了一个 View 的尺寸规格;
从值上看,MeasureSpec 代表一个 32 位 int 值,高 2 位代表 SpecMode(指测量模式),低 30 位代表 SpecSize(指在某种测量模式下的规格大小)。

public static class MeasureSpec {
    private static final int MODE_SHIFT = 30;
    // 对应的二进制是 11000000 00000000 00000000 00000000,作用是打包和解包 MeasureSpec 值。
    private static final int MODE_MASK  = 0x3 << MODE_SHIFT;
    // 对应的二进制是 00000000 00000000 00000000 00000000,父容器不对 View 有任何限制,要多大给多大,这种情况一般用于系统内部,表示一种测量的状态。
    public static final int UNSPECIFIED = 0 << MODE_SHIFT;
    // 对应的二进制是 01000000 00000000 00000000 00000000,父容器已经检测出 View 所需要的精确大小,View 的最终大小就是 SpecSize 所指定的值。对应于 LayoutParams 中的 match_parent 和具体的数值两种情况。
    public static final int EXACTLY     = 1 << MODE_SHIFT;
    // 对应的二进制是 10000000 00000000 00000000 00000000,父容器指定了一个可用大小即 SpecSize,View 的大小不能大于这个值,具体是什么值要看不同 View 的具体实现,对应于 LayoutParams 中的 wrap_content。
    public static final int AT_MOST     = 2 << MODE_SHIFT;
   
    public static int makeMeasureSpec(int size, int mode) {
        if (sUseBrokenMakeMeasureSpec) {
            return size + mode;
        } else {
            // (size & ~MODE_MASK) 表示取出 size 这个 int 值的低 30 位
            // (mode & MODE_MASK) 表示取出 mode 这个 int 值的高 2 位
            // (size & ~MODE_MASK) | (mode & MODE_MASK) 表示打包一个 MeasureSpec 值,也是 int 型。
            return (size & ~MODE_MASK) | (mode & MODE_MASK);
        }
    }
  
    public static int getMode(int measureSpec) {
        // 从 MeasureSpec 值中解包出 SpecMode 这个 int 值
        return (measureSpec & MODE_MASK);
    }
 
    public static int getSize(int measureSpec) {
    	// 从 MeasureSpec 值中解包出 SpecSize 这个 int 值
        return (measureSpec & ~MODE_MASK);
    }
    static int adjust(int measureSpec, int delta) {
        final int mode = getMode(measureSpec);
        if (mode == UNSPECIFIED) {
            // No need to adjust size for UNSPECIFIED mode.
            return makeMeasureSpec(0, UNSPECIFIED);
        }
        int size = getSize(measureSpec) + delta;
        if (size < 0) {
            Log.e(VIEW_LOG_TAG, "MeasureSpec.adjust: new size would be negative! (" + size +
                    ") spec: " + toString(measureSpec) + " delta: " + delta);
            size = 0;
        }
        return makeMeasureSpec(size, mode);
    }

    public static String toString(int measureSpec) {
        int mode = getMode(measureSpec);
        int size = getSize(measureSpec);
        StringBuilder sb = new StringBuilder("MeasureSpec: ");
        if (mode == UNSPECIFIED)
            sb.append("UNSPECIFIED ");
        else if (mode == EXACTLY)
            sb.append("EXACTLY ");
        else if (mode == AT_MOST)
            sb.append("AT_MOST ");
        else
            sb.append(mode).append(" ");
        sb.append(size);
        return sb.toString();
    }
}

2.2 对于顶级 View(即 DecorView)和普通 View 来说,MeasureSpec 的转换过程有什么不同?

对于 DecorView,其 MeasureSpec 由窗口的尺寸和其自身的 LayoutParams 来共同确定,可以查看;
对于普通 View,其 MeasureSpec 由父容器的 MeasureSpec 和自身的 LayoutParams 来共同确定。

2.3 普通 View 的 MeasureSpec 的创建规则


这个图只是 getChildMeasureSpec 这个方法的表格方式而已。

3 View的工作流程

3.1 View 类和 ViewGroup 类的区别和联系是什么?

View 类是一个具体类,也就是说它没有任何抽象方法;
ViewGroup 类继承于 View 类,是一个抽象类,包含一个抽象方法:

protected abstract void onLayout(boolean changed, int l, int t, int r, int b);

3.2 简要说一下 View 的工作流程

View 的工作流程主要是指 measure、layout、draw 这三大流程,即测量、布局和绘制。measure是确定 View 的测量宽/高,layout 是确定 View 的最终宽/高和四个顶点的位置,draw 是将 View 绘制到屏幕上。

3.3 简要说一下 measure 过程

如果是一个原始的 View,那么通过 measure 方法就完成了测量过程;
如果是一个 ViewGroup,除了完成自己的 measure 过程以外,还会遍历去调用所有子元素的 measure 方法,各个子元素再递归去执行 measure 过程。

3.4 View 的子类可以重写 View 类的 measure 方法吗?

不可以,这是因为 View 类中的 measure 方法是一个 final 类型的方法。

public final void measure(int widthMeasureSpec, int heightMeasureSpec)

3.5 View 类的 getSuggestedMinimumWidth 方法的逻辑是什么?

看一下这个方法:

protected int getSuggestedMinimumWidth() {
    return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}

如果 View 没有设置背景,那么会取 mMinWidth,对应的是 xml 中 android:minWidth 的值;
如果 View 设置了背景,那么会取 mMinWidth 和 mBackground.getMinimumWidth() 的最大值,后者返回的是 Drawable 的固有宽度,或者说原始宽度。

3.6 为什么直接继承 View 的自定义控件需要重写 onMeasure 方法并设置 wrap_content 时的自身大小?

如果不设置的话,那么在布局中使用 wrap_content 就相当于使用 match_parent。给 View 指定一个默认的内部宽/高,并在 wrap_content 时设置此宽/高就可以了。可以查看 TextView、ImageView、ProgressBar、Space 等类的处理。

3.7 为什么 ViewGroup 不像 View 一样对其 onMeasure 方法做统一的实现呢?

因为不同的 ViewGroup 子类有不同的布局特性,这导致它们的测量细节各不相同,比如 LinearLayout 和 RelativeLayout 这两者的布局特性显然不同,因此 ViewGroup 无法统一实现。

3.8 为什么说在 onLayout 方法中去获取 View 的测量宽/高是一个好的习惯?

正常情况下,View 的 measure 过程完成以后,通过 getMeasuredWidth/getMeasuredHeight 方法就可以正确地获取到 View 的测量宽/高;但是,在某些极端情况下,系统可能需要多次 measure 才能确定最终的测量宽/高,在这种情形下,在 onMeasure 方法中拿到的测量宽/高很可能是不准确的。

3.9 在 Activity 中,如何获取某个 View 的宽/高?

  1. 使用 Activity/View 的 onWindowFocusChanged 回调方法;
  2. 使用 view.post(runnable);
  3. 使用 ViewTreeObserver;
  4. 使用 view.measure(int widthMeasureSpec, int heightMeasureSpec)。

3.10 简要说一下 layout 过程

如果是 View 的话,那么在它的 layout 方法中就确定了自身的位置,layout 过程就结束了;
如果是 ViewGroup 的话,那么在它的 layout 方法中只是确定了 ViewGroup 自身的位置,要确定子元素的位置,就需要重写 onLayout 方法;在 onLayout 方法中,会调用子元素的 layout 方法,子元素在它的 layout 方法中确定自己的位置,这样一层一层地传递下去完成整个 View 树的 layout 过程。

3.10 layout 方法和 onLayout 方法的区别是什么?

layout 方法是确定 View 本身的位置,即设定 View 的四个顶点的位置,这样就确定了 View 在父容器中的位置;
onLayout 方法是父容器确定子元素的位置,这个方法在 View 中是空实现,因为 View 没有子元素了,在 ViewGroup 中则进行抽象化,它的子类必须实现这个方法。

3.11 View 的 getMeasuredWidth 和 getWidth 这两个方法有什么区别?

  1. 两者的赋值时机不同,测量宽/高的赋值时机要早于最终宽/高。具体来说,View 的测量宽/高形成于 View 的 measure 过程,可以看 measure 方法中的 setMeasuredDimensionRaw 方法:
private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
  mMeasuredWidth = measuredWidth;
  mMeasuredHeight = measuredHeight;
  mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}

而 getMeasuredWidth 获取的就是 mMeasuredWidth 值:

public final int getMeasuredWidth() {
   return mMeasuredWidth & MEASURED_SIZE_MASK;
}

再看一下 getWidth 方法:

public final int getWidth() {
   return mRight - mLeft;
}

而 mRight,mLeft 的赋值是在 layout 过程的 setFrame 方法中,所以 View 的最终宽/高形成于 View 的 layout 过程。

  1. 两者的值多数情况下是相等的,但在某些特殊情况下会不一致。例如有两种特殊情况:
  • 重写 View 的 layout 方法重写 View 的 layout 方法;
  • View 需要多次 measure 才能确定自己的测量宽/高,在前几次的测量过程中,其得出的测量宽/高有可能和最终宽/高不一致,但最终来看,测量宽/高还是会和最终宽/高相同。

3.12 View 的绘制过程的步骤是什么?

  1. 绘制背景(background.draw(canvas););
  2. 绘制自己(onDraw);
  3. 绘制 children(dispatchDraw(canvas));
  4. 绘制装饰(onDrawScrollBars)。

3.13 View 的绘制过程是如何传递的?

View 的绘制过程是通过 dispatchDraw 来传递的。dispatchDraw 会遍历调用子元素的 draw 方法,如此 draw 事件就一层一层传递了下去。dispatchDraw 在 View 类中是空实现的,在 ViewGroup 类中是真正实现的。

3.14 View 类中的 setWillNotDraw 方法的含义及其开发意义是什么?

含义:如果一个 View 不需要绘制任何内容,那么就设置这个标记为 true,系统会进行进一步的优化。默认地,View 没有启用这个标记,但是View 的子类例如 ViewGroup 可以启用这个标记。特别地,如果覆写了 onDraw 方法,就要清除这个标记。
开发意义:当创建的自定义控件继承于 ViewGroup 并且不具备绘制功能时,就可以开启这个标记,便于系统进行后续的优化;当明确知道一个 ViewGroup 需要通过 onDraw 绘制内容时,需要关闭这个标记。
查看 LinearLayout 对这个方法的调用:setWillNotDraw(divider == null);,在有 divider 时才会关闭这个标记,否则是打开的;FrameLayout 是在有 foreground 时关闭这个标记,否则打开这个标记;ScrollView 直接使用 setWillNotDraw(false);;ViewStub 类则使用 setWillNotDraw(true);

4 自定义 View

4.1 自定义 View 的分类

分类 用途 特点
1.继承 View 重写 onDraw 方法 用于实现一些不规则的效果,不方便通过布局的组合方式来达到 需要通过绘制的方式来实现,即重写 onDraw 方法;需要自己支持 wrap_content,处理 padding
2.继承 ViewGroup 派生特殊的 Layout 用于实现自定义的布局 稍微复杂一些,需要合适地处理 ViewGroup 的测量、布局这两个过程,并同时处理好子元素的测量和布局过程
3.继承特定的 View 用于扩展已有的 View 的功能 不需要自己支持 wrap_content 和 padding 等
4.继承特定的 ViewGroup 用于实现几种 View 组合在一起的效果 不要自己处理 ViewGroup 的测量和布局这两个过程,方法2能实现的效果一般方法4也可以实现,但方法2更接近 View 底层

4.2 自定义 View 的注意事项

  • 对于直接继承 View 或者 ViewGroup 的控件,要在 onMeasure 方法中对 wrap_content 做特殊处理;
  • 必要时,让自定义 View 支持 padding;
  • 尽量不要在自定义 View 中使用 Handler,用 View 自己的 post 方法就行;
  • 及时关闭自定义 View 中的线程或者动画,比如在 onDetachedFromWindow 方法中;
  • 自定义 View 带有嵌套滑动时,要处理好滑动冲突。

猜你喜欢

转载自blog.csdn.net/willway_wang/article/details/85016896