文章

11.View面试题

11.View面试题

View 相关问题

measure

自定义 view 时,super.onMeasure 一定需要调用吗?为什么?

不一定

最顶层的 View,谁 measure 了?顶层的 MeasureSpec 怎么来的?

ViewRootImple 的 performMeasure();顶层的 MeasureSpec 通过 ViewRootImpl#getRootMeasureSpec 生成的

draw

同步刷新

丢帧 (掉帧),是说这一帧延迟显示还是丢弃不再显示?

延迟显示,因为缓存交换的时机只能等下一个 VSync 了。

布局层级较多/主线程耗时是如何造成丢帧的呢?

布局层级较多/主线程耗时 会影响 CPU/GPU 的执行时间,大于 16.6ms 时只能等下一个 VSync 了

避免丢帧的方法之一是保证每次绘制界面的操作要在 16.6ms 内完成,如果某次用户点击屏幕导致的界面刷新操作是在某一个 16.6ms 帧快结束的时候,那么即使这次绘制操作小于 16.6 ms,按道理不也会造成丢帧么?

代码里调用了某个 View 发起的刷新请求 invalidate,这个重绘工作并不会马上就开始,而是需要等到下一个 VSync 来的时候 CPU/GPU 才开始计算数据存到 Buffer,下下一帧数据屏幕才从 Buffer 拿到数据展示

也就是说一个绘制操作后,至少需要等 2 个 vsync,在第 3 个 vsync 信号到来才会真正展示

Android 每隔 16.6 ms 刷新一次屏幕到底指的是什么意思?是指每隔 16.6ms 调用 onDraw() 绘制一次么?

Android 每隔 16.6 ms 刷新一次屏幕其实是指底层会以这个固定频率来切换每一帧的画面,而这个每一帧的画面数据就是我们 App 在接收到屏幕刷新信号之后去执行遍历绘制 View 树工作所计算出来的屏幕数据。而 app 并不是每隔 16.6ms 的屏幕刷新信号都可以接收到,只有当 app 向底层注册监听下一个屏幕刷新信号之后,才能接收到下一个屏幕刷新信号到来的通知。而只有当某个 View 发起了刷新请求时,App 才会去向底层注册监听下一个屏幕刷新信号。

如果界面一直保持没变的话,那么还会每隔 16.6ms 刷新一次屏幕么?

只有当界面有刷新的需要时,我们 app 才会在下一个屏幕刷新信号来时,遍历绘制 View 树来重新计算屏幕数据。如果界面没有刷新的需要,一直保持不变时,我们 app 就不会去接收每隔 16.6ms 的屏幕刷新信号事件了,但底层仍然会以这个固定频率来切换每一帧的画面,只是后面这些帧的画面都是相同的而已。

measure/layout/draw 走完,界面就立刻刷新了吗?

不是。measure/layout/draw 走完后,只是 CPU 计算数据完成,会在下一个 VSync 到来时进行缓存交换,屏幕才能显示出来。

VSYNC 这个具体指啥?在屏幕刷新中如何工作的?

屏幕刷新使用 双缓存、三缓存,这又是啥意思呢?

双缓存是 Back buffer、Frame buffer,用于解决 screen tearing 画面撕裂。三缓存增加一个 Back buffer,用于减少 Jank。

有了同步屏障消息的控制就能保证每次一接收到屏幕刷新信号就第一时间处理遍历绘制 View 树的工作么?

只能说,同步屏障是尽可能去做到,但并不能保证一定可以第一时间处理。因为,同步屏障是在 scheduleTraversals() 被调用时才发送到消息队列里的,也就是说,只有当某个 View 发起了刷新请求时,在这个时刻后面的同步消息才会被拦截掉。如果在 scheduleTraversals() 之前就发送到消息队列里的工作仍然会按顺序依次被取出来执行。

View 绘制基础

View 的 measure、layout 和 draw

Activity 中的 view 是如何被添加上的?

wysbo

  • startActivity 时,最终会调用到 ActivityThread 的 handleLaunchActivity 方法来创建 Activity,执行 Activity 的 onCreate 方法,执行 setContentView,从而完成 DecorView 的创建
  • 然后执行 handleResumeActivity,首先执行 Activity 的 onResume 方法,然后通过 WindowManager addView 将 View 添加到 DecorView 上
  • WindowManager 的实现类是 WindowManagerImpl,最终会调用到 WindowManagerGlobal 的 addView,创建 ViewRootImpl,将 View 添加到 Window 上。
  • ViewRootImpl.setView() 会调用 requestLayout() 方法;requestLayout() 方法首先会检查当前执行的线程是不是 UI 线程,随后调用 scheduleTraversals()。scheduleTraversals 会把本次请求封装成一个 TraversalRunnable 对象,这个对象最后会交给 Handler 去处理。最后 ViewRootImpl.performTraversals() 被调用;performTraversals() 主要是处理 View 树的 measure、layout、draw 等流程
  • 真正 View 的绘制流程,从 ViewRoot 的 performTraversals() 开始的,它经过 measure、layout 和 draw 三个过程最终将 View 展示出来;performTraversals 会依次调用 performMeasure,performLayout,performDraw 三个方法,他们会依次调用 measure,layout,draw 方法,然后又调用了 onMeasure,onLayout,dispatchDraw。

measure 测量

measure 基本流程

  • 在 ViewGroup 中的 measureChildren() ⽅法中会遍历测量 ViewGroup 中所有的 View,当 View 的可见性处于 GONE 状态时,不对其进⾏测量
  • 然后,测量某个指定的 View 时,根据⽗容器的 MeasureSpec 和⼦ View 的 LayoutParams 等信息计算⼦ View 的 MeasureSpec
  • 最后,将计算出的 MeasureSpec 传⼊ View 的 measure ⽅法,对于 ViewGroup 的测量,⼀般要重写 onMeasure ⽅法,在 onMeasure ⽅法中,⽗容器会对所有的⼦ View 进⾏ Measure,⼦元素⼜会作为⽗容器,重复对它⾃⼰的⼦元素进⾏ Measure,这样 Measure 过程就从 DecorView ⼀级⼀级传递下去了,也就是要遍历所有⼦ View 的的尺⼨,最终得出总的 ViewGroup 的尺寸。
见 [[05.measure测量measure 流程]]

MeasureSpec?

MeasureSpec 表示的是⼀个 32 位的整形值,它的⾼ 2 位表示测量模式 SpecMode,低 30 位表示某种测量模式下的规格大小 SpecSize。MeasureSpec 是 View 类的⼀个静态内部类,⽤来说明应该如何测量这个 View,它有三种测量模式:

  • EXACTLY:精确测量模式,视图宽⾼指定为 match_parent 或具体数值时⽣效,表示父视图已经决定了子视图的精确大小,这种模式下 View 的测量值就是 SpecSize 的值。
  • AT_MOST:最大值测量模式,当视图的宽⾼指定为 wrap_content 时生效,此时⼦视图的尺寸可以是不超过父视图允许的最大尺寸的任何尺寸
  • UNSPECIFIED:不指定测量模式,父视图没有限制子视图的大小,子视图可以是想要的任何尺寸,通常用于系统内部,应用开发中很少用到

对于 DecorView 而言,它的 MeasureSpec 由窗口尺寸和其⾃身的 LayoutParams 共同决定;对于普通的 View,它的 MeasureSpec 由父视图的 MeasureSpec 和其⾃身的 LayoutParams 共同决定。
普通 View 的 MeasureSpec 的创建规则:当 parentSpecMode 为 EXACTLY 且 childLayoutParams 取具体值或 match_parent 时,childSpecMode 取 EXACTLY,当 parentSpecMode 为 AT_MOST 且仅 childLayoutParams 取具体值时,childSpecMode 才取 EXACTLY,剩下的三种情况均取 AT_MOST

ViewGroup 的如何计算子 View 的 MeasureSpce,getChildMeasureSpec

对于应用层 View ,其 MeasureSpec 由父容器的 MeasureSpec 和自身的 LayoutParams 来共同决定
ubngs

  • 当 view 采用固定宽高的时候,不管父容器的 MeasureSpec 是什么,view 的 MeasureSpec 都是精确模式并且其大小遵循 Layoutparams 中的大小;
  • 当 view 的宽高是 match_parent 时,这个时候如果父容器的模式是精准模式,那么 view 也是精准模式并且其大小是父容器的剩余空间,如果父容器是最大模式,那么 view 也是最大模式并且其大小不会超过父容器的剩余空间;
  • 当 view 的宽高是 wrap_content 时,不管父容器的模式是精准还是最大化,view 的模式总是最大化并且大小不能超过父容器的剩余空间
  • Unspecified 模式,这个模式主要用于系统内部多次 measure 的情况下,一般来说,我们不需要关注此模式 (这里注意自定义 View 放到 ScrollView 的情况 需要处理)。

常用布局的 onMeasure

  1. **FrameLayout **
  • FrameLayout 的宽或高为 wrap_content(不为 Exactly)时,且子 view 大于 1 个的宽或高为 match_parent,测量 2 次
  1. LinearLayout
  • 没有 layout_weight 时,只 measure 一次(横向或者纵向)
  • 设置了 layout_weight,先 measure 没有设置的,再 measure 一次设置的了,共 2 次
  1. RelativeLayout
  • RelativeLayout 总共 measure2 次,horizontal 和 vertical 各一次

layout 布局

根据 measure 子 View 所得到布局大小和布局参数,将子 View 放在合适的位置上

见 [[06.layout布局layout 流程]]

draw 绘制

见 [[07.draw绘制draw 绘制流程]]

硬件加速和软件绘制绘制

硬件加速和软件绘制基础

jccri

  • 软件绘制是通过 Surface,最终生成 Bitmap,然后交给 SurfaceFlinger 合成
  • 硬件绘制先是 CPU 计算要绘制的 View,每个绘制操作生成 XXXDrawOp,生成 DisplayList;DisplayList 的本质是一个缓冲区,它里面记录了即将要执行的绘制命令序列,这些绘制命令最终会转化为 Open GL 命令由 GPU 执行。这意味着我们在调用 Canvas API 绘制 UI 时,实际上只是将 Canvas API 调用及其参数记录在 Display List 中,然后等到下一个 Vsync 信号到来时,记录在 Display List 里面的绘制命令才会转化为 Open GL 命令由 GPU 执行。

与直接执行绘制命令相比,先将绘制命令记录在 DisplayList 中然后再执行有两个好处。 第一个好处是在绘制窗口的下一帧时,若某一个视图的 UI 没有发生变化,那么就不必执行与它相关的 Canvas API,即不用执行它的成员函数 onDraw,而是直接复用上次构建的 Display List 即可。 第二个好处是在绘制窗口的下一帧时,若某一个视图的 UI 发生了变化,但是只是一些简单属性发生了变化,例如位置和透明度等简单属性,那么也不必重建它的 Display List,而是直接修改上次构建的 Display List 的相关属性即可,这样也可以省去执行它的成员函数 onDraw。

硬件加速的优点是啥?

  1. 硬件加速可以减轻 CPU 的负担和主线程的负担,因为硬件加速分 CPU 构建和 GPU 绘制两个步骤,CPU 只需要执行构建部分即可,另外 GPU 绘制是在渲染线程进行,该线程是子线程,解放了主线程的压力。
  2. 在对 view 属性更改时,硬件加速下只需要更改对应 View 的 RenderNode 属性值,CPU 构建的工作几乎可以忽略,特别是在执行 alpha、旋转等动画时,性能极大提高,而纯软件绘制每一帧都需要重绘脏区交叉的所有 View

非硬件加速下 view 的绘制都是依赖于 bitmap 绘制,所以不管是属性变更还是全量更新都需要重新绘制 bitmap,系统唯一能优化的就是画布的大小,尽可能减少绘制区域来提升性能。但不管怎么优化,当 view 处于交叉重叠时,仍无法避免重复绘制和过度绘制的事实

硬件绘制和软件绘制区别对比
823zp

  • 软件绘制:应用程序调用 invalidate() 更新 UI 的某一部分,失效 (invalidation) 消息将会在整个视图层中传递,计算每个需要重绘的区域(即脏区域)。然后 Android 系统将会重绘所有和脏区域有交集的 view
  • 硬件绘制:Android 系统仍然使用 invalidate() 和 draw() 来绘制 view,但在处理绘制上有所不同。Android 系统记录绘制命令到显示列表,而不是立即执行绘制命令。另一个优化就是 Android 系统只需记录和更新标记为脏(通过 invalidate())的 view

View 绘制面试题

measure 相关

FrameLayout 什么情况下子 view 会 measure 两次?

FrameLayout 的宽或高为 wrap_content(不为 Exactly)时,且子 view 大于 1 个的宽或高为 match_parent,测量 2 次

ViewRootImpl.scheduleTraversals 时系统准备注册下一次屏幕刷新信号之前,往主线程的消息队列中发送了一个同步屏障消息,为什么?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void scheduleTraversals() {
    if (!mTraversalScheduled) {
        mTraversalScheduled = true;
        // 往主线程的消息队列中发送一个同步屏障
        // mTraversalBarrier是ViewRootImpl中的成员变量,用于移除同步屏障时使用
        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
        // 确保mTraversalRunnable第一时间得到执行。这里的token为null,后面回调会用到
        mChoreographer.postCallback(
                Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        if (!mUnbufferedInputDispatch) {
            scheduleConsumeBatchedInput();
        }
        notifyRendererOfFramePending();
        pokeDrawLockIfNeeded();
    }
}

什么是同步屏障?
同步屏障的作用就是为了确保:同步屏障之后的所有同步消息都将被暂停,得不到执行,直到调用了 removeSyncBarrier(token) 释放掉同步屏障,所有的同步消息将继续执行。也就是说,同步屏障之后的异步消息将会优先得到执行
添加同步屏障目的?
而 Choreographer 的 postCallback 会通过 Handler,发送一个定时的,异步消息,等待下一次 vsync 信号到来,会回调提交的 Runnable,这个 Runnable 就是 mTraversalRunnable。
在前面通过 ViewRootImpl 类中的 scheduleTraversals() 方法,发送的同步屏障消息,是为了确保 mTraversalRunnable 能够第一时间得到执行。其中,mTraversalRunnable 为 ViewRootImpl 中成员变量,具体实现为 TraversalRunnable。TraversalRunnable 则为 ViewRootImpl 中成员内部类

如何在 Activity 中获取某个 View 的宽⾼?

由于 View 的 measure 过程和 Activity 的生命周期方法不是同步执行的,如果 View 还没有测量完毕,那么获得的宽/高就是 0。所以在 onCreate、onStart、onResume 中均无法正确得到某个 View 的宽高信息。

  1. Activity/View#onWindowFocusChanged
  2. View.post(Runnable) 原因见:View.post为什么可以获取到View的宽高?
  3. ViewTreeObserver.addOnGlobalLayoutListener
  4. 手动 view.measure()

View.post 为什么可以获取到 View 的宽高?

  1. View 已经 attach 到 Window:post 的消息放在消息队列尾部,等执行的时候,view 已经 measure 好了,保证了能正确的获取宽高
  2. View 没有 attach 到 Window:会暂存到 HandlerActionQueue,等 View attach 后,在 dispatchAttachedToWindow 方法中通过 Handler 分发暂存的任务

invalidate、requestLayout

invalidate、postInvalidate 区别

invaliddate 和 postInvalidate 只会调用 View 的 onDraw 方法,onLayout 和 onMeasure 不会调用
invalidate() 与 postInvalidate() 都⽤于刷新 View,主要区别是 invalidate() 在主线程中调用,若在子线程中使⽤需要配合 handler;而 postInvalidate() 可在子线程中直接调用。

invalidate 会不会导致 onMeasure 和 onLayout 被调用呢?

invalidate 中,在 performTraversals 方法中,mLayoutRequested 为 false,所有 onMeasure 和 onLayout 都不会被调用,只会调用 onDraw

requestLayout 与 invalidate 的区别?

  1. requestLayout 只会触发 measure 和 layout;invalidate 只会触发 draw

requestLayout 最终调用到 ViewRootImpl.requestLayout,最后执行 performMeasure→performLayout→performDraw;如果没有改变 l,t,r,b,那就不会触发 onDraw

  1. requestLayout 最终调用到 ViewRootImpl.requestLayout 会调用 checkThread 方法,而 invalidate 不会
本文由作者按照 CC BY 4.0 进行授权