面试就是一次技术的博弈过程,能唬住面试官就是胜利。如果每当面试官提出一个问题,都能掰扯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. 关联面试题举例:
- View的生命周期(整体或单个的)
- 自定义 View 的工作流程
- View 与 Activity 生命周期间的关系
- View,Window,Activity 的关系
- 事件分发机制
- View 中的内存泄漏
- 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