博主从牛客等地方收集的与Android View系统相关的面试题,答案由自己整理,非标准答案,仅供参考;

View 基础

View的渲染流程(UI绘制)

View 的渲染大致分为三部分:测量、布局和绘制;

通过 ViewRootImpl#requestLayout 方法,向主线程的 Handler 发送一个异步Message并且开启同步屏障,让渲染过程能尽快执行;

异步Messahe中的Runnable会调用到 performTraversals 方法开始整颗 View 的渲染流程;

  1. 先进行测量 performMeasure ,调用 DecorView#measure 并传递屏幕的宽高,由 DecorView 开始自上而下的测量整颗 View 树的尺寸

    1. 每个View 经过测量后都存有 measureHeightmeasureWidth 测量结果,但这个并不是最终的宽高,最终的宽高在布局结束后确定;

    2. 自定义View需要实现 onMeasure 来测量自身尺寸,如果是 ViewGroup 还需要先测量子View的尺寸来确定自身的尺寸;

  2. 进行布局 performLayout ,调用 DecorView#layout,由 DecorView 开始自上而下的对 View 树进行布局

    1. 布局是通过 left、top、right、bottom 这四个相较于父容器的相对坐标确定 View 的位置,确定了这四个值也就确定了最终的宽高;

    2. View的onLayout是空实现,但是ViewGroup的onLayout是抽象方法,因此其子类需要实现该方法来对其子View布局;

  3. 进行绘制 performDraw ,调用 DecorView#draw,传递一个 Canvas 绘制整颗 View 树的内容;

    1. draw 方法实现了绘制的通用逻辑:先绘制background、用 onDraw 绘制自身内容、用 dispatchDraw 绘制子View内容、绘制 foreground等;

    2. 自定义 View 需要实现 onDraw 方法来绘制内容,对于 ViewGroup 还需要调用子View的 draw 方法绘制其内容;

如何获取view的视图的宽高

  1. 通过 View#post 能够获取 view 的真实宽高

  2. 在非初次进入 onResume 生命周期直接获取其宽高;

  3. 通过 viewTreeObserver.OnGlobalLayoutListener回调中获取到视图的宽高

  4. 重写 view#onSizeChange 方法获取

  5. 重写 view#onLayout 方法获取

  6. 如果在 xml 文件中具体声明了精确值,那么可以通过 LayoutParams 来获取到宽高;

View的宽高只有在布局阶段结束后才能得到:

view.post()的原理

ViewRootImpl 中有一个持有主线程 Looper 的 ViewRootHandler,view#post 一般是给这个 Handler 发送消息;

这个 ViewRootHandler 的执行时机是在 ViewRootImpl#performTraversals 执行后,此时已经完成了 View 树的测量布局和绘制过程,然后才执行 view#post 发送的 Runnable,因此我们能通过该方法获取到 View 的真实宽高;

触发invalidate()和requestLayout()会发生什么?

invalidate

invalidate 会将重绘操作不断向其父容器传递,最终会调用 ViewRootImpl#performTraversals 方法,因为没有设置 LayoutRequested 标志位,因此不会触发测量和布局操作,而是直接进行绘制过程:

  • 如果开启了硬件加速,则遍历整颗子树,对设置了 PFLAG_INVALIDATED 标志的View进行重绘操作,即调用其 draw 方法;

    • 如果是 ViewGroup 则会重绘当前 ViewGroup 为根节点的 View树;

  • 如果没有开启硬件加速,则会调用 drawSoft 方法,对整颗 View 树进行重绘操作;

如果只是绘制的内容有变化,则可以调用该方法进行重绘操作;

requestLayout

requestLayout 会将该操作不断向其父容器传递,最终会调用到 ViewRootImpl#performTraversals 方法,依次执行 performMeasureperformLayoutperformDraw

并不是整颗 View 树都会重新测量、布局和绘制,在 reuqestLayout 的调用过程中,会给调用者及其所有父容器设置标志位,在执行 measurelayout 方法中都会对标志位进行判断,只有存在该标志位才会执行;

绘制操作也不一定会执行,只有在布局过程中有View发生尺寸发生变化才会重新绘制;

一般在 requestLayout 之后调用 invalidate 保证能重新绘制;


View事件分发

View的事件分发机制

Linux内核接收到相关硬件中断后,得到对应的原始输入数据,由系统服务 IntputManagerService 负责读取并封装为输入事件,分发给对应的窗口;

Activity 初次到达 Resume 生命周期时,涉及 ViewRootImpl 的实例化,会在 IMS 注册并获取 InputChannel 来与 IMS 通信:IMS 将输入事件发送给 ViewRootImpl,ViewRootImpl 负责将事件分发给 DecorView,并将处理结果返回给IMS;

DecorView 的 dispatchTouchEvent 分发事件会经过 Activity、Window 再次回到 DecorView 的 superDispatchTouchEvent 方法,通过调用父类 FrameLayout 的 dispatchTouchEvent 方法将事件正式分发给 View 树;

在 View 树中的事件分发过程是一个 DFS 遍历树的过程,由外到内自上而下的分发事件,找到能够消费事件的目标 View;

对于 View 来说,事件分发会调用到 dispatchTouchEvent 方法,负责滚动条滑动事件、 setOnTouchListener 以及 onTouchEvent 方法,如果其中一个消费了事件就不会继续消费;

  • 其中 onTouchEvent 方法实现了默认的点击、长按以及 tooltip 的交互逻辑;

  • 如果设置的 setOnTouchListener 消费了事件,会导致 onTouchEvent 无法被调用,造成点击、长按事件的失效;

对于 ViewGroup 来说,事件分发会调用到 dispatchTouchEvent 方法:

  • 首先会判断是否拦截事件,会调用 onInterceptTouchEvent 来判断,该方法默认返回 false;对于 ACTION_DOWN 事件只会调用一次该方法进行拦截;

  • 如果ViewGroup 没有拦截本次事件,而且事件是 ACTION_DOWN 时,就会将事件分发给其子View,会根据子View绘制的顺序从后向前找,找到能够消费事件的View,并将其保存在 mFirstTouchTarget 里面,后续的事件序列就会直接分发给 mFirstTouchTarget 单链表中的所有 View;

    • 通过 mFirstTouchTarget 可以直接将后续的事件序列分发,而不需要重新DFS 查找对应的 View,提高了事件分发的效率;

  • 如果ViewGroup拦截了事件,或者没有找到子View消费事件,那么就会自己尝试消费事件,调用父类 View 的 dispatchTouchEvent 方法

如何解决滑动冲突

滑动冲突的本质时,内部的滑动 View 无法接收到输入事件,而是被外部的滑动View所拦截并消费了;

要解决滑动冲突,首先就是要让外部的滑动 View 对 ACTION_DOWN 事件不再拦截,让该事件能够分发到内部的滑动 View,让内部 View 来判断是否进行滑动;

  • 内部的滑动 View 接收到 ACTION_DOWN 事件后请求外部滑动 View 不拦截 requestDisallowInterceptTouchEvent 后续的事件

  • 内部的滑动 View 根据实际情况来判断是否处理滑动事件,如果需要就自己消费,如果不需要了则请求外部滑动View拦截事件,比如内部滑动View已经不能滑动等等;

长按如何实现

ACTION_DOWN 事件发生时,向 Handler postDelayed 一个执行长按逻辑的 Runnable,这个 delay 的时间是长按的触发时间;

ACTION_UP 事件发生时,如果存在长按对应的 Runnable 的话,就需要移除;

如果Runnable到了执行时间没有被移除的话,就会被执行对应的长按逻辑;

事件ACTION_CANCEL是什么

事件 ACTION_CANCEL 并不是由用户直接产生的,而是 ViewGroup 在某个View消费已经分发的事件序列中途,拦截了后续的事件序列,就会导致正在消费事件的 View 无法接收到 ACTION_UP 事件,可能会造成异常的UI显示和逻辑问题,比如按钮的按压状态不消失、异常触发长按事件等;

因此 ViewGroup 拦截了事件序列后,需要在消费的 View 分发一个 ACTION_CANCEL 事件,表示事件序列已经取消,让消费的 View 能够取消对应的UI状态以及清除一些交互逻辑;

自定义View

自定义View的绘制过程

自定义过View吗?如何实现的?

自定义view 过程中要注意的点

(触摸事件,组合,onMeasure处理, padding处理, 自绘控件,canvas,paint,path)

RecyclerView

现在需要设置RecyclerView的每个item都不一样如何实现?

recyclerView相对于ListView 的优点