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

本文全面解析View的生命周期,包括构造、加载、布局、绘制及销毁等关键阶段,深入探讨自定义View的工作流程,关联Activity生命周期,揭示View与Window、Activity的关系,以及事件分发机制,为读者提供详尽的知识点,帮助理解和掌握View生命周期的每一个细节。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

面试就是一次技术的博弈过程,能唬住面试官就是胜利。如果每当面试官提出一个问题,都能掰扯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

<think>嗯,用户问的是如何拆除SATA百分表的表壳。首先,我需要确认用户指的是哪种类型的百分表。SATA可能是一个品牌,比如生产工具的那个SATA,也可能是其他类型的设备,但通常百分表指的是机械式百分表,用于测量工件的尺寸或形状误差。这类工具的表壳拆除可能需要一些特定的步骤,需要小心处理以免损坏内部结构。 接下来,我需要回忆一下百分表的结构。通常,百分表的外壳可能由前盖、后盖和主体部分组成,可能用螺丝固定,或者有卡扣设计。如果用户要拆除表壳,可能是为了维修、更换电池(如果是电子式)或清洁内部零件。需要注意的是,百分表内部有精密的齿轮和弹簧机构,拆卸需非常谨慎,避免零件丢失或变形。 然后,我应该考虑可能的步骤。首先,确保工具齐全,比如小螺丝刀、镊子、防静电垫等。然后,检查表壳是否有可见的螺丝,通常在背面或侧面。如果有螺丝,用合适的螺丝刀拧下。如果没有螺丝,可能是卡扣式设计,需要用塑料撬棒轻轻撬开,避免划伤外壳。 另外,需要注意是否有隐藏的螺丝,比如在标签或橡胶垫下面。有些制造商可能会把螺丝藏在这样的地方,需要用户先移除这些覆盖物。拆下螺丝后,轻轻分离前后盖,注意可能有排线连接,比如电子百分表的显示屏,这候需要断开排线,避免拉扯导致损坏。 拆开后,用户可能会看到内部结构,这候需要记录每个零件的位置,或者拍照,以便后续组装。如果遇到阻力不要强行拆卸,可能还有未注意到的固定点。同,提醒用户在操作前断开电源(如果是电子式),并避免触碰敏感部件如游丝或齿轮。 最后,组装需要按照相反的顺序进行,确保所有零件正确归位,螺丝拧紧但不过度用力,避免损坏螺纹。如果过程中遇到困难,建议联系专业维修人员,以免造成不可逆的损坏。 可能用户遇到的问题包括:不确定螺丝位置,卡扣太紧不敢用力,或者拆开后不知道如何操作。需要给出具体的步骤,并强调谨慎操作的重要性。此外,不同型号的百分表可能有不同的拆卸方式,最好参考用户手册,但用户可能没有手册,所以需要通用步骤。 总结一下,步骤大致是:准备工具,检查螺丝或卡扣,小心分离外壳,注意内部连接,记录拆卸过程,组装反向操作。同提醒风险,建议非专业人士寻求帮助。</think>拆卸SATA百分表表壳需谨慎操作,避免损坏精密部件。以下是通用步骤(具体结构可能因型号而异): --- ### **工具准备** - 小号十字螺丝刀(PH00或PH0) - 塑料撬棒或薄片(避免金属划伤外壳) - 防静电手套/软布 - 镊子(处理小零件) --- ### **拆卸步骤** 1. **断电与检查** - 若为电子百分表,先取出电池。 - 观察表壳是否有螺丝孔、标签或橡胶垫遮挡的隐藏螺丝。 2. **移除可见螺丝** - 用螺丝刀拧下表壳背面/侧面的螺丝,分类保存避免丢失。 3. **分离卡扣式外壳** - 若无螺丝,用塑料撬棒沿表壳缝隙(通常在侧边或底部)轻压,逐步撬开卡扣。 - 避免强行掰扯,防止卡扣断裂。 4. **断开内部连接(电子表适用)** - 若表盘有排线连接主板,用镊子轻抬排线扣,缓慢拔出排线。 5. **取出机芯(如需进一步维修)** - 用镊子固定机芯支架,小心将其从外壳中托出,注意避让指针和游丝。 --- ### **注意事项** - 全程轻拿轻放,避免震动导致齿轮错位。 - 拆卸拍照记录零件位置,方便还原。 - 若表壳与玻璃表盘一体,不建议非专业人士继续拆解,需专用工具分离。 - 组装按反向顺序操作,确保螺丝拧紧力度均匀。 --- ### **风险提示** - 自行拆解可能导致精度下降或失去保修资格。 - 若无经验,建议联系SATA官方售后或专业仪器维修机构。 (提示:部分老款机械百分表需专用工具调整游丝张力,非必要勿拆卸机芯内部。)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值