面试博弈:掰扯5分钟View的生命周期

面试就是一次技术的博弈过程,能唬住面试官就是胜利。如果每当面试官提出一个问题,都能掰扯5分钟,想必会给面试官一个不错的印象。同时,我们也可以将面试官的问题向自己擅长的领域引导,进而在面试过程中起到正向的引导作用。
然而,面试过程中经常不知道该说什么,又该从何说起。本文将串一下相关知识点。帮助你轻松凑够5分钟~~
今天的主题就是——View的生命周期。

1. 概览

View的生命周期,其实在面试中并不常被问到,但是却可以牵扯很广的一部分知识,进可引出自定义View ,事件分发机制,退可谈到 Activity ,Fragment 生命周期。可以说是很重要的一部分知识了。
来,开始掰扯~

文不如图,希望一张图可以帮助我们更好的记忆~
在这里插入图片描述

2. View生命周期相关方法

2.1 Constructors() - 构造方法

如果大家写过自定义 View 的话,想必会都会很清楚,View 有四个构造函数。
一般大家都知道第一个构造方法是简单的在代码中new View 的时候调用的,第二个构造方法使用最广泛,是对应的生成 xml 中定义的 View 的时候调用的。
剩下的两个构造方法,大家了解的就比较少了。一般在自定义 View 的时候都会不加思索的按照固定的写法。

  • 第1 种 构造方法
// 从代码创建视图时使用的简单构造函数。
// context视图运行的上下文,它可以通过这个上下文运行访问当前的主题、资源等。
public View(Context context) { ... }
  • 第 2 种 构造方法
    构造函数,该构造函数在从XML扩展视图时调用。当从XML文件构造视图并提供在XML文件中指定的属性时,将调用此方法。这个版本使用默认样式0,所以应用的属性值是上下文主题和给定的AttributeSet中的属性值。
    在添加了所有子元素之后,将调用 onfinishinflation() 方法。
//@param context视图运行的上下文,它可以通过这个上下文运行访问当前主题、资源等。
//@param使用XML标记的属性来扩展视图。
public View(Context context, @Nullable AttributeSet attrs) {
    this(context, attrs, 0);
}
  • 第 3 种 构造方法
    从XML执行填充,并从主题属性应用特定于类的基础样式。View的这个构造函数允许子类在扩展时使用它们自己的基础样式。
    eg. Button类的构造函数将调用这个版本的构造函数,并应用 R.attr.buttonStyle 中的风格。
    这允许主题的按钮样式修改所有的基本视图属性(特别是其背景)以及按钮类的属性。
    相对于第2种,多提供了一种给 View 添加默认属性的方式
public View(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
    this(context, attrs, defStyleAttr, 0);
}
  • 第 4 种 构造方法
    相对第3个构造函数就多了一个 defStyleRes ,其实就是多了一种提供 View 默认属性的一种方式。这种方式更加的简单,直接在代码中传入 R.style.XX 就可以了。如果没有默认值的话就为 0 。这个参数只有 defStyleAttr 为 0 的时候才会生效。
    需要注意的是@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public View(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
    // ...
}

2.2 onFinishInflate()

该方法当View及其子View从XML文件中加载完成后触发调用。
也就是会在Activity中调用setContentView之后就会调用onFinishInflate这个方法,这个方法就代表自定义控件中的子控件映射完成了,然后可以进行一些初始化控件的操作,就可以通过 findViewById 得到控件,得到控件之后进行一些初始化的操作。
当然在这个 方法里面是得不到控件的高宽的 ,控件的高宽是必须在调用了onMeasure方法之后才能得到,而onFinishInflate方法是在setContentView之后、onMeasure之前

2.3 onVisibilityChanged()

该方法在当前View或其父控件 的可见性改变时被调用。如果View状态不可见或者GONE,该方法会第一个被调用。
onVisibilityChanged是否调用,依赖于View是否执行过onAttachedToWindow方法。也就是View是否被添加到Window上。

2.4 onAttachedToWindow()

onAttachToWindow 当View被附着到一个窗口时触发。
在Activity第一次执行完onResume方法后被调用。
即:onCreate -> onStart -> onResume -> onAttachedToWindow

注意:该方法只会调用一次

2.5 onMeasure()

onMeasure 确定View以及其子View尺寸大小时被调用。
自定义 View 中,最重要的三个方法就是 onMeasure(),onLayout(),onDraw() ,所以这部分很重要了。
源码:

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
    // ...
    onMeasure(widthMeasureSpec, heightMeasureSpec);
    //...
}
  • measure()
    measure()这个方法由final来修饰,意味着不能够被子类重写。
    其作用是:测量出一个View的实际大小,而实际性的测量工作,Android系统却并没有帮我们完成,而是交给了onMeasure(),所以我们需要在自定义View的时候按照自己的需求,重写onMeasure方法。
    而子控件又分为view和viewGroup两种情况,具体测量的流程如下面所示:
    在这里插入图片描述
  • onMeasure(int widthMeasureSpec, int heightMeasureSpec)
    两个参数 widthMeasureSpec 和 heightMeasureSpec 均为 int 类型,看名字知道是跟宽和高有关系,但它们其实不是宽和高,而是由宽、高和各自方向上对应的模式来合成的一个值:其中,在int类型的32位二进制位中,31-30这两位表示模式,0~29这三十位表示宽和高的实际值。
    其中模式一共有三种,被定义在View类的内部类 View.MeasureSpec 中:
    ①UNSPECIFIED:表示默认值,父控件没有给子view任何限制。------二进制表示:00
    ②EXACTLY:表示父控件给子view一个具体的值,子view要设置成这些值的大小。------二进制表示:01
    ③AT_MOST:表示父控件个子view一个最大的特定值,而子view不能超过这个值的大小。------二进制表示:10

2.6 onSizeChanged()

onSizeChanged( 当view的大小发生变化时触发 )
该方法在Measure方法之后且测量大小与之前不一样的时候被调用。

2.7 onLayout()

onLayout 在当前View需要为其子View分配尺寸和位置时会被调用。
measure过程结束后,视图的大小就已经测量好了,接下来就是layout的过程了。
为视图及其所有子视图分配大小和位置

  • 这是布局机制的第二阶段。(首先是测量)。在这个阶段,每个父进程调用它的所有子进程的layout来定位它们。这通常是使用存储在measure pass()中的子度量来完成的。
    派生类不应重写此方法。带有子元素的派生类应该重写onLayout。在该方法中,它们应该调用每个子元素的layout。
    /**
     * Assign a size and position to a view and all of its
     * descendants
     *
     * <p>This is the second phase of the layout mechanism.
     * (The first is measuring). In this phase, each parent calls
     * layout on all of its children to position them.
     * This is typically done using the child measurements
     * that were stored in the measure pass().</p>
     *
     * <p>Derived classes should not override this method.
     * Derived classes with children should override
     * onLayout. In that method, they should
     * call layout on each of their children.</p>
     */
    public void layout(int l, int t, int r, int b) {
        // ...

        boolean changed = isLayoutModeOptical(mParent) ?
                setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
            onLayout(changed, l, t, r, b);

            if (shouldDrawRoundScrollbar()) {
                if(mRoundScrollbarRenderer == null) {
                    mRoundScrollbarRenderer = new RoundScrollbarRenderer(this);
                }
            } else {
                mRoundScrollbarRenderer = null;
            }

            mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;

            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnLayoutChangeListeners != null) {
                ArrayList<OnLayoutChangeListener> listenersCopy =
                        (ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
                int numListeners = listenersCopy.size();
                for (int i = 0; i < numListeners; ++i) {
                    listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
                }
            }
        }

        final boolean wasLayoutValid = isLayoutValid();

        mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
        mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;

        //...
    }

layout()方法接收四个参数,分别代表着左、上、右、下的坐标,当然这个坐标是相对于当前视图的父视图而言的。可以看到,这里还把刚才测量出的宽度和高度传到了layout()方法中。

2.8 onDraw(Canvas)

onDraw 该方法用于View渲染内容的细节。
measure和layout的过程都结束后,接下来就进入到draw的过程了。同样,根据名字就可以判断出,在这里才真正地开始对视图进行绘制。
ViewRoot中的代码会继续执行并创建出一个Canvas对象,然后调用View的draw()方法来执行具体的绘制工作。

  • draw()方法内部的绘制过程总共可以分为六步,其中第二步和第五步在一般情况下很少用到,因此这里我们只分析简化后的绘制过程。代码如下所示:
public void draw(Canvas canvas) {
    if (ViewDebug.TRACE_HIERARCHY) {
        ViewDebug.trace(this, ViewDebug.HierarchyTraceType.DRAW);
    }
    final int privateFlags = mPrivateFlags;
    final boolean dirtyOpaque = (privateFlags & DIRTY_MASK) == DIRTY_OPAQUE &&
            (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
    mPrivateFlags = (privateFlags & ~DIRTY_MASK) | DRAWN;
    // Step 1, draw the background, if needed
    int saveCount;
    if (!dirtyOpaque) {
        final Drawable background = mBGDrawable;
        if (background != null) {
            final int scrollX = mScrollX;
            final int scrollY = mScrollY;
            if (mBackgroundSizeChanged) {
                background.setBounds(0, 0,  mRight - mLeft, mBottom - mTop);
                mBackgroundSizeChanged = false;
            }
            if ((scrollX | scrollY) == 0) {
                background.draw(canvas);
            } else {
                canvas.translate(scrollX, scrollY);
                background.draw(canvas);
                canvas.translate(-scrollX, -scrollY);
            }
        }
    }
    final int viewFlags = mViewFlags;
    boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
    boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
    if (!verticalEdges && !horizontalEdges) {
        // Step 3, draw the content
        if (!dirtyOpaque) onDraw(canvas);
        // Step 4, draw the children
        dispatchDraw(canvas);
        // Step 6, draw decorations (scrollbars)
        onDrawScrollBars(canvas);
        // we're done...
        return;
    }
}
  • onDraw()函数中不允许创建任意变量,因为当需要重绘时就会调用onDraw()函数,所以在onDraw()函数中创建的变量就会一直被重复创建,这样会引起频繁的程序GC(回收内存),进而引起程序卡顿。
    一般在自定义控件的构造函数中创建变量,既在初始化时一次性被创建,开始绘制图画的过程中不要在绘制函数里创建任意变量;

2.9 onWindowFocusChanged()

  • 该方法也可能在绘制过程中被调用,具体是在包含当前View的Window获得或失去焦点时被调用。此时可以设置代码中定义的View的一些LayoutParameter。
  • 当包含此视图的窗口获得或失去焦点时调用。请注意,这与视图焦点是分开的:要接收键事件,视图及其窗口都必须具有焦点。如果一个窗口显示在你的窗口的顶部,接受输入焦点,那么你自己的窗口将失去焦点,但视图焦点将保持不变。
  • 如果View进入了销毁阶段,肯定是会被调用的。

2.10 onWindowVisibilityChanged()

该方法同上,具体是在包含当前View的Window可见性改变时被调用。

  • 当包含的 window 可见性被改变时调用。请注意,这将告诉您 window 是否对 window manager可见;这并不能告诉您您的窗口是否被屏幕上的其他窗口所隐藏,即使它本身是可见的。
  • 从onWindowFocusChanged被执行起,用户可以与应用进行交互了,而这之前,对用户的操作需要做一点限制。

2.11 onDetachedFromWindow()

onDetachedFromWindow
当View离开附着的窗口时触发,比如在Activity调用onDestroy方法时View就会离开窗口。

注意:该方法与onAttachedToWindow 对应,同样只会调用一次

3. View其它的一些生命周期相关方法

  • onFocusChanged()

该方法在当前View获得或失去焦点时被调用。

  • onKeyDown()

该方法在有按键按下后被调用。

  • onKeyUp()

与上面对应,该方法在有按键按下后弹起时触发。

  • onTrackballEvent()

该方法在一个轨迹球运动事件发生时被调用。

  • onTouchEvent()

该方法在触屏事件发生时被调用。

  • onSaveInstanceState()

这个方法就不用说了,在Activity被Pause的时候被调用。被Pause后回到界面时View就没方法被调用了。只有在比如Activity被销毁时进入View的销毁流程。

4. 总结:

  • View 的关键生命周期为 [改变可见性] --> 构造View --> onFinishInflate --> onAttachedToWindow --> onMeasure --> onSizeChanged --> onLayout --> onDraw --> onDetackedFromWindow

  • 在Activity的onCreate方法中加载View,View的onFinishInflate会被调用,继而Activity的生命周期执行到onResume方法之后View才被附着到窗口上,继而进行绘制工作,onMeasure、onSizeChanged 、onLayout、onDraw。这几个方法可能由于setVisible或onResume被调用多次,最后是Window失去焦点后的销毁阶段。

  • onVisibilityChanged()方法在View是可见状态时如上所示时机调用,但是View的状态如果是不可见或者GONE时,是首先被调用的。如果是Invisible状态,View的创建到layout即结束,不会绘制出来。如果是GONE状态,View也会被加载并添加到Window,但是不会再Measure、Layout和Draw了。也就时说即使是GONE状态,销毁时一样有Detach的过程,即View的销毁过程和可见性无关。

  • 创建和销毁流程设置可见性区别:
    visibility和Invisibitlity差距只有invisibility不需要绘制view(ondraw)
    visibitlity和gone差距是gone不需要测量大小(onmeasure)、不需要给子类分配尺寸(onlayout)、不需要绘制view(ondraw)。
    Invisibility和gone的差距是gone不需要测量大小(onmeasure)、不需要给子类分配尺寸(onlayout)

5. 关联面试题举例:

  1. View的生命周期(整体或单个的)
  2. 自定义 View 的工作流程
  3. View 与 Activity 生命周期间的关系
  4. View,Window,Activity 的关系
  5. 事件分发机制
  6. View 中的内存泄漏
  7. View 的性能优化

6. 相关推荐博文:

Android View生命周期 https://blog.csdn.net/u013353866/article/details/48597251

深入理解android view 生命周期 https://blog.csdn.net/sun_star1chen/article/details/44626433

View生命周期流程图 https://blog.csdn.net/yangshuaionline/article/details/91993532

Android开发——View的生命周期总结 https://blog.csdn.net/SEU_Calvin/article/details/72855537

Android自定义控件系列七:详解onMeasure()方法中如何测量一个控件尺寸(一) http://doc.okbase.net/cyp331203/archive/140383.html

Android视图绘制流程完全解析,带你一步步深入了解View(二) https://blog.csdn.net/guolin_blog/article/details/16330267

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 撸撸猫 设计师:设计师小姐姐 返回首页