文章目录
- 一、基础原理与流程
-
- [1. 简述Android中View的完整绘制流程(从顶层View到子View),并说明performTraversals()在其中的作用。**](#1. 简述Android中View的完整绘制流程(从顶层View到子View),并说明performTraversals()在其中的作用。**)
- [2. View的绘制流程(measure、layout、draw)是自上而下还是自下而上的?请分别说明三个流程的遍历顺序。](#2. View的绘制流程(measure、layout、draw)是自上而下还是自下而上的?请分别说明三个流程的遍历顺序。)
- [3. invalidate()、postInvalidate()、requestLayout()三者的区别是什么?分别会触发View绘制流程的哪些阶段?**](#3. invalidate()、postInvalidate()、requestLayout()三者的区别是什么?分别会触发View绘制流程的哪些阶段?**)
- [4. 什么是MeasureSpec?它由哪两部分组成?EXACTLY、AT_MOST、UNSPECIFIED三种模式分别对应什么场景?**](#4. 什么是MeasureSpec?它由哪两部分组成?EXACTLY、AT_MOST、UNSPECIFIED三种模式分别对应什么场景?**)
- [5. ViewGroup和普通View在measure过程中有什么本质区别?](#5. ViewGroup和普通View在measure过程中有什么本质区别?)
- [6. onLayout(boolean changed, int l, int t, int r, int b)方法中的changed参数代表什么含义?](#6. onLayout(boolean changed, int l, int t, int r, int b)方法中的changed参数代表什么含义?)
- 二、源码深度与细节
-
- [1. ViewRootImpl、DecorView、Window三者之间的关系是什么?View的绘制是由谁发起的?](#1. ViewRootImpl、DecorView、Window三者之间的关系是什么?View的绘制是由谁发起的?)
- [2. 为什么View的measure过程可能会执行多次?什么情况下会导致多次measure?](#2. 为什么View的measure过程可能会执行多次?什么情况下会导致多次measure?)
- [3. 简述draw()方法的执行流程(包含绘制背景、绘制自己、绘制子View、绘制前景等步骤)。](#3. 简述draw()方法的执行流程(包含绘制背景、绘制自己、绘制子View、绘制前景等步骤)。)
- [4. setWillNotDraw(boolean willNotDraw)这个方法的作用是什么?设置为true或false对绘制流程有什么影响?](#4. setWillNotDraw(boolean willNotDraw)这个方法的作用是什么?设置为true或false对绘制流程有什么影响?)
- [5. View的getMeasuredWidth()和getWidth()有什么区别?分别在什么时机可以获取到正确的值?**](#5. View的getMeasuredWidth()和getWidth()有什么区别?分别在什么时机可以获取到正确的值?**)
- [6. 当View的宽高设置为wrap_content时,若不重写onMeasure()会出现什么问题?底层源码是如何处理的?**](#6. 当View的宽高设置为wrap_content时,若不重写onMeasure()会出现什么问题?底层源码是如何处理的?**)
- [7. 简述Choreographer的作用,它是如何协调VSYNC信号与View绘制的?**](#7. 简述Choreographer的作用,它是如何协调VSYNC信号与View绘制的?**)
- 三、性能优化与卡顿
-
- [1. 过度绘制(Overdraw)产生的原因是什么?如何通过工具检测并优化?**](#1. 过度绘制(Overdraw)产生的原因是什么?如何通过工具检测并优化?**)
- [2 . 什么是View的刷新机制?频繁调用invalidate()会导致什么问题?如何避免?](#2 . 什么是View的刷新机制?频繁调用invalidate()会导致什么问题?如何避免?)
- [3. 为什么不建议在onDraw()方法中执行耗时操作或创建对象?**](#3. 为什么不建议在onDraw()方法中执行耗时操作或创建对象?**)
- [4. HardwareAcceleration(硬件加速)对View的绘制流程有什么影响?开启硬件加速后,onDraw()有哪些方法不能使用?](#4. HardwareAcceleration(硬件加速)对View的绘制流程有什么影响?开启硬件加速后,onDraw()有哪些方法不能使用?)
- [5. 简述UI渲染的16ms原理,丢帧(Frame Drop)与View绘制流程的关系。**](#5. 简述UI渲染的16ms原理,丢帧(Frame Drop)与View绘制流程的关系。**)
- [6. 什么是布局嵌套过深?它如何影响measure和layout流程?如何优化?](#6. 什么是布局嵌套过深?它如何影响measure和layout流程?如何优化?)
- 四、异常场景与边界问题(百度/阿里爱考)
-
- [1. 在onCreate()方法中为什么获取不到View的宽高?有哪些解决方案?**](#1. 在onCreate()方法中为什么获取不到View的宽高?有哪些解决方案?**)
- [2. 当一个View设置了GONE或INVISIBLE时,它是否会参与measure和layout流程?](#2. 当一个View设置了GONE或INVISIBLE时,它是否会参与measure和layout流程?)
- [3. ScrollView嵌套ListView/RecyclerView为什么会出现测量问题?根本原因是什么?](#3. ScrollView嵌套ListView/RecyclerView为什么会出现测量问题?根本原因是什么?)
- [4. 自定义View时,requestDisallowInterceptTouchEvent()与View的绘制流程是否有关联?](#4. 自定义View时,requestDisallowInterceptTouchEvent()与View的绘制流程是否有关联?)
- [5. 简述View的forceLayout()标记的作用,它是如何影响measure流程的?](#5. 简述View的forceLayout()标记的作用,它是如何影响measure流程的?)
- 五、自定义View实战(综合考察)
-
- [1. 自定义ViewGroup时,必须重写哪两个方法?为什么?**](#1. 自定义ViewGroup时,必须重写哪两个方法?为什么?**)
- [2. 自定义View实现圆形图片,在onDraw()中需要注意哪些绘制流程上的细节?**](#2. 自定义View实现圆形图片,在onDraw()中需要注意哪些绘制流程上的细节?**)
- [3. 如何实现一个支持宽高自适应(wrap_content)的自定义View?](#3. 如何实现一个支持宽高自适应(wrap_content)的自定义View?)
- [4. 在自定义View中,如何正确处理padding和margin属性?**](#4. 在自定义View中,如何正确处理padding和margin属性?**)
- [5. 当View需要根据内容动态改变大小时,如何正确调用requestLayout()和invalidate()?**](#5. 当View需要根据内容动态改变大小时,如何正确调用requestLayout()和invalidate()?**)
- [todo:surfaceview canvas画布](#todo:surfaceview canvas画布)
一、基础原理与流程
1. 简述Android中View的完整绘制流程(从顶层View到子View),并说明performTraversals()在其中的作用。**
- Android View绘制流程始于ViewRootImpl,核心分为三大阶段:measure(测量)、layout(布局)、draw(绘制),整体遵循自上而下的遍历顺序。
- 完整流程:系统接收到VSYNC信号后,由ViewRootImpl的performTraversals()方法发起绘制,该方法是整个绘制流程的入口和总调度方法。依次调用performMeasure()、performLayout()、performDraw(),分别触发顶层DecorView的测量、布局、绘制,再逐级传递到所有子View,完成整棵View树的绘制。
- performTraversals()作用:负责初始化绘制参数、判断是否需要重新测量、布局、绘制,协调三大流程的执行时机,控制View树的刷新逻辑,同时处理窗口尺寸变化、界面重绘等触发时机。
2. View的绘制流程(measure、layout、draw)是自上而下还是自下而上的?请分别说明三个流程的遍历顺序。
三大流程整体都是自上而下遍历,父View先完成自身相关逻辑,再调度子View执行。
- measure流程:父View调用自身measure(),在onMeasure()中根据自身规则,计算并生成子View的MeasureSpec,再调用子View的measure(),逐级传递直到叶子节点;叶子节点测量完成后,回溯确定父View最终宽高。
- layout流程:父View调用layout()确定自身在父容器中的位置,在onLayout()中遍历子View,调用子View的layout()方法,传入子View的左上右下坐标,确定子View位置。
- draw流程:父View调用draw()方法,按步骤绘制背景、自身内容、子View,遍历调用子View的draw()方法,完成逐级绘制。
3. invalidate()、postInvalidate()、requestLayout()三者的区别是什么?分别会触发View绘制流程的哪些阶段?**
- invalidate():在UI线程调用,标记当前View为需要重绘,只会触发draw流程,不会触发measure和layout,适合View尺寸不变、仅内容改变的刷新场景。
- postInvalidate():作用同invalidate(),可在子线程调用,内部通过Handler切换到UI线程执行invalidate(),适配子线程刷新View的场景。
- requestLayout():标记View需要重新测量、布局,会逐级向上传递,触发measure流程和layout流程,执行完成后会间接触发draw流程,适合View尺寸、位置发生变化的刷新场景。
4. 什么是MeasureSpec?它由哪两部分组成?EXACTLY、AT_MOST、UNSPECIFIED三种模式分别对应什么场景?**
- MeasureSpec是View的测量规格,是一个32位的int值,用于父View向子View传递测量规则,约束子View的宽高计算。
- 组成:高2位代表测量模式SpecMode,低30位代表测量大小SpecSize。
- EXACTLY(精确模式):父View指定子View确切大小,对应match_parent或固定dp值,子View必须遵循该尺寸。
- AT_MOST(最大模式):子View尺寸最大不能超过父View剩余空间,对应wrap_content,子View可根据自身内容调整大小,但不能超出上限。
- UNSPECIFIED(无限制模式):父View不对子View做任何尺寸限制,子View想多大就多大,多用于系统内部控件(如ListView、ScrollView)测量子View,日常开发极少用到。
5. ViewGroup和普通View在measure过程中有什么本质区别?
- 普通View是单一控件,measure过程只需完成自身尺寸计算,在onMeasure()中调用setMeasuredDimension()设置自身宽高即可。
- ViewGroup是容器控件,除了计算自身尺寸,更核心的工作是遍历所有子View,计算每个子View的MeasureSpec,调度子View执行measure,测量完所有子View后,再根据子View的宽高和自身规则,确定自身最终宽高。ViewGroup没有重写onMeasure(),需要子类(如LinearLayout、RelativeLayout)自行实现测量逻辑。
6. onLayout(boolean changed, int l, int t, int r, int b)方法中的changed参数代表什么含义?
changed参数表示View的尺寸或位置是否发生变化。
当值为true时,代表当前View相比上次布局,位置或大小发生了改变;当值为false时,代表尺寸和位置无变化,可跳过重复布局逻辑,提升绘制效率。
二、源码深度与细节
1. ViewRootImpl、DecorView、Window三者之间的关系是什么?View的绘制是由谁发起的?
- Window是抽象类,Android中唯一实现类是PhoneWindow,是Activity的顶级窗口,负责管理界面样式、标题栏、加载布局。
- DecorView是整个界面的顶层View,是PhoneWindow的内部View,继承自FrameLayout,包含标题栏和内容栏(ContentView),我们setContentView设置的布局就是添加到DecorView的内容栏中。
- ViewRootImpl是连接WindowManagerService和DecorView的桥梁,负责View的绘制、事件分发、窗口管理,不是View的子类。
- 三者关系:Activity持有PhoneWindow,PhoneWindow持有DecorView,ViewRootImpl关联DecorView,负责调度DecorView的绘制。
View绘制由ViewRootImpl的performTraversals()方法发起。
2. 为什么View的measure过程可能会执行多次?什么情况下会导致多次measure?
measure过程执行多次,是因为父View无法一次性确定自身尺寸,需要多次测量子View,回溯调整自身大小。
常见场景:
- 嵌套使用ScrollView、LinearLayout权重属性、RelativeLayout时,父View需要先测量子View,再根据子View尺寸调整自身,会触发二次measure。
- View尺寸设置为wrap_content,且依赖子View尺寸时。
- 动态改变View布局参数,调用requestLayout()后,会重新触发measure流程。
源码中,多次measure是正常优化逻辑,但过度嵌套会导致多次测量,降低绘制效率。
3. 简述draw()方法的执行流程(包含绘制背景、绘制自己、绘制子View、绘制前景等步骤)。
View的draw()方法是一个标准模板方法,执行步骤固定,不可轻易改写,源码步骤分为6步,核心步骤如下:
- 绘制背景(drawBackground()):绘制View的背景色、背景图,不可跳过。
- 保存画布图层(可选):为硬件加速做准备。
- 绘制自身内容(onDraw()):调用View的onDraw(),绘制控件主体内容,空实现,需子类重写。
- 绘制子View(dispatchDraw()):ViewGroup重写该方法,遍历子View,调用子View的draw()方法,普通View无此逻辑。
- 绘制装饰(如滚动条、前景):绘制View的前景、滚动条等装饰元素。
- 恢复画布图层,完成绘制。
4. setWillNotDraw(boolean willNotDraw)这个方法的作用是什么?设置为true或false对绘制流程有什么影响?
该方法用于标记当前View是否需要执行onDraw()绘制自身内容,是系统针对ViewGroup做的绘制优化。
- 设置为true:代表该View没有自身内容需要绘制,只会作为容器展示子View,系统会跳过onDraw()方法,减少绘制开销,默认所有ViewGroup都会开启该优化。
- 设置为false:代表该View需要绘制自身内容,系统会正常执行onDraw()方法。如果自定义ViewGroup需要重写onDraw()绘制内容,必须手动调用setWillNotDraw(false),否则onDraw()不会执行。
5. View的getMeasuredWidth()和getWidth()有什么区别?分别在什么时机可以获取到正确的值?**
- getMeasuredWidth():获取View测量完成后的宽度,值来自setMeasuredDimension()设置的测量宽度,在measure流程结束后即可获取,代表View期望的宽度。
- getWidth():获取View最终显示的宽度,值为layout流程中确定的right - left,代表View实际在屏幕上展示的宽度。
获取时机:两者在onCreate()、onStart()、onResume()中都无法获取正确值,因为此时View还未完成测量和布局。
正确时机:measure完成后可获取getMeasuredWidth(),layout完成后可获取getWidth(),正常情况下两者数值相等,除非手动改写layout()逻辑。
6. 当View的宽高设置为wrap_content时,若不重写onMeasure()会出现什么问题?底层源码是如何处理的?**
自定义View不重写onMeasure(),宽高设置为wrap_content时,效果会等同于match_parent ,无法自适应内容大小。
源码逻辑:View默认的onMeasure()中,当测量模式为AT_MOST(wrap_content)时,会直接将SpecSize作为自身宽高,而此时SpecSize等于父View可用空间,导致View铺满父容器。
解决方法:重写onMeasure(),针对AT_MOST模式,设置自定义的默认宽高,再调用setMeasuredDimension()完成测量。
7. 简述Choreographer的作用,它是如何协调VSYNC信号与View绘制的?**
答案:Choreographer是Android系统的帧率协调者 ,负责接收系统VSYNC同步信号,调度UI绘制、动画、输入事件等操作,保证界面流畅渲染。
工作流程:
- 系统每隔16.6ms发出一次VSYNC信号(60fps),Choreographer接收到信号后。
- 按顺序执行三大任务:输入事件处理、动画执行、View绘制。
- 触发ViewRootImpl的performTraversals()方法,启动View绘制流程。
作用:统一调度UI刷新时机,避免频繁刷新导致丢帧,保证界面渲染和系统帧率同步。
三、性能优化与卡顿
1. 过度绘制(Overdraw)产生的原因是什么?如何通过工具检测并优化?**
- 过度绘制指屏幕上同一个像素点,在一帧内被多次绘制,浪费CPU、GPU资源,严重时导致卡顿丢帧。
- 产生原因:布局嵌套过深、多个控件重叠、背景重复设置(父View和子View设置相同背景)、自定义View重复绘制。
- 检测工具:开发者选项中的调试GPU过度绘制工具,通过颜色区分绘制次数:蓝色(1次)、绿色(2次)、浅红(3次)、深红(4次及以上,严重)。
- 优化方案:
移除重复背景,只保留顶层背景。
减少布局嵌套,使用ConstraintLayout扁平化布局。
自定义View中使用clipRect()屏蔽不可见区域的绘制。
避免使用多层重叠透明控件。
2 . 什么是View的刷新机制?频繁调用invalidate()会导致什么问题?如何避免?
- View刷新机制是通过invalidate()/requestLayout()标记View需要重绘或重新布局,等待下一个VSYNC信号到来后,执行对应绘制流程的机制。
- 频繁调用invalidate()的问题:短时间内多次触发draw流程,占用大量CPU、GPU资源,导致UI线程阻塞,引发界面卡顿、丢帧,甚至内存飙升。
- 避免方案:
合并刷新逻辑,减少刷新次数。
使用postInvalidateOnAnimation(),同步VSYNC信号刷新。
加入防抖逻辑,避免无意义的重复刷新。
仅刷新需要改变的局部区域,而非整个View。
3. 为什么不建议在onDraw()方法中执行耗时操作或创建对象?**
onDraw()方法会被频繁调用,只要调用invalidate()就会执行,界面滑动、动画播放时也会触发。
- 执行耗时操作:会拉长draw流程执行时间,超出16ms阈值,直接导致丢帧、界面卡顿。
- 创建对象:会频繁分配内存,触发GC回收,GC会暂停UI线程,造成界面卡顿、抖动。
规范写法:对象初始化、耗时计算放在onDraw()外,如构造方法、onMeasure()中执行。
4. HardwareAcceleration(硬件加速)对View的绘制流程有什么影响?开启硬件加速后,onDraw()有哪些方法不能使用?
硬件加速是通过GPU分担绘制工作,提升绘制效率,Android 4.0后默认开启。
对绘制流程的影响:
- 绘制效率大幅提升,界面更流畅。
- 绘制逻辑改为GPU渲染,画布(Canvas)操作方式改变。
- 部分Canvas API不兼容,会导致绘制异常或闪退。
不支持的API:clipPath()、clipRegion()、drawPicture()、drawTextOnPath()、setShadowLayer()、drawVertices()等,以及部分滤镜、混合模式操作。遇到不兼容场景,可关闭当前View的硬件加速。
5. 简述UI渲染的16ms原理,丢帧(Frame Drop)与View绘制流程的关系。**
- Android系统屏幕刷新率为60fps,每隔16.6ms发出一次VSYNC信号,系统需要在16ms内完成一帧画面的测量、布局、绘制,否则会出现丢帧。
- 丢帧原因:View绘制流程耗时过长,超过16ms阈值,系统无法按时渲染下一帧,导致画面卡顿、掉帧。
- 关联点:measure/layout嵌套过深、onDraw()耗时、过度绘制、频繁刷新,都会拉长绘制流程,引发丢帧。优化绘制流程,缩减各阶段耗时,保证在16ms内完成,就能避免丢帧。
6. 什么是布局嵌套过深?它如何影响measure和layout流程?如何优化?
布局嵌套过深指多层ViewGroup层层嵌套,导致View树层级过多,属于常见的UI性能问题。
对流程的影响:
- measure流程:层级越多,遍历次数越多,多次测量会成倍增加耗时。
- layout流程:逐级确定位置,层级越深,耗时越长。
- 极易引发丢帧、界面卡顿。
优化方案:
- 使用ConstraintLayout替代多层嵌套LinearLayout、RelativeLayout。
- 移除无用的父布局,精简View树层级。
- 巧用merge标签减少根布局嵌套。
- 用ViewStub懒加载不常用布局。
四、异常场景与边界问题(百度/阿里爱考)
1. 在onCreate()方法中为什么获取不到View的宽高?有哪些解决方案?**
onCreate()阶段,Activity只是完成了布局加载,ViewRootImpl还未关联DecorView,View的measure、layout流程还没有执行,宽高还未计算,所以获取到的值为0。
解决方案:
- View.post():post的Runnable会在View绘制完成后执行,可获取宽高。
- ViewTreeObserver.OnGlobalLayoutListener:监听View全局布局完成事件,回调中获取宽高,记得及时移除监听防止内存泄漏。
- onWindowFocusChanged():Activity获取焦点时,View已完成绘制,可获取宽高。
2. 当一个View设置了GONE或INVISIBLE时,它是否会参与measure和layout流程?
- INVISIBLE:View不可见,但依旧占用空间,会正常参与measure和layout流程,只是不执行draw流程。
- GONE:View不可见且不占用空间,不会参与measure和layout流程,系统会跳过该View的测量和布局,完全不占用绘制资源。
3. ScrollView嵌套ListView/RecyclerView为什么会出现测量问题?根本原因是什么?
- 嵌套后会出现列表只显示一行、高度计算错误、滑动冲突等问题。
- 根本原因:ScrollView的measure模式为UNSPECIFIED,在测量子View时,会给ListView/RecyclerView传递无限高度的MeasureSpec。ListView/RecyclerView默认测量模式下,会计算所有item的高度,导致高度计算异常,同时失去复用机制,性能急剧下降。
- 解决方案:使用 NestedScrollView 或自定义测量逻辑
4. 自定义View时,requestDisallowInterceptTouchEvent()与View的绘制流程是否有关联?
两者没有直接关联,但存在间接配合场景。
requestDisallowInterceptTouchEvent()是用于事件分发的方法,作用是禁止父View拦截触摸事件,保证子View能正常接收触摸事件。
间接关联:触摸滑动事件会触发View的invalidate()或requestLayout(),进而触发绘制流程。比如滑动控件时,通过该方法处理事件冲突,保证滑动流畅,同时触发界面重绘。
5. 简述View的forceLayout()标记的作用,它是如何影响measure流程的?
- forceLayout()标记是View的一个内部标记位,用于标记View需要强制重新测量。
- 作用机制:调用requestLayout()时,会给View添加forceLayout标记;在measure流程中,系统检测到该标记,会强制执行onMeasure()重新测量,即使View尺寸看似没有变化,也会重新计算宽高,保证测量结果准确。该标记在测量完成后会自动清除。
五、自定义View实战(综合考察)
1. 自定义ViewGroup时,必须重写哪两个方法?为什么?**
必须重写onMeasure()和onLayout()两个方法。
- onMeasure():ViewGroup默认没有实现测量逻辑,需要重写该方法,遍历子View,生成MeasureSpec,完成子View测量,再确定自身宽高。
- onLayout():ViewGroup默认是空实现,需要重写该方法,遍历子View,调用子View的layout()方法,确定每个子View在父容器中的位置。
缺少任意一个方法,ViewGroup无法正常测量和摆放子View,界面会显示异常。
2. 自定义View实现圆形图片,在onDraw()中需要注意哪些绘制流程上的细节?**
- 避免在onDraw()中创建Bitmap、Paint等对象,防止频繁GC。
- 处理padding属性,绘制时减去padding值,保证内容不紧贴边框。
- 使用clipPath()或Xfermode实现圆形裁剪,注意硬件加速兼容性。
- 关闭硬件加速不兼容的API,或针对该View关闭硬件加速。
- 优化过度绘制,避免多层重叠绘制。
- 图片复用,防止内存泄漏,及时回收无用Bitmap。
3. 如何实现一个支持宽高自适应(wrap_content)的自定义View?
核心是重写onMeasure(),针对AT_MOST模式做特殊处理。
步骤:
- 重写onMeasure()方法,获取父View传递的宽高MeasureSpec。
- 解析MeasureSpec的模式和尺寸,分别处理EXACTLY、AT_MOST、UNSPECIFIED。
- 当模式为AT_MOST(wrap_content)时,设置自定义的默认宽高(根据控件内容计算)。
- 调用setMeasuredDimension(),传入计算后的宽高。
- 处理padding属性,保证自适应效果准确。
4. 在自定义View中,如何正确处理padding和margin属性?**
- padding处理:padding是View内部边距,在onMeasure()计算宽高、onDraw()绘制内容时,调用getPaddingLeft()、getPaddingTop()、getPaddingRight()、getPaddingBottom()获取边距值,在绘制和测量时减去对应边距,保证内容不紧贴边框。
- margin处理:margin是View外部边距,属于ViewGroup的布局参数,自定义ViewGroup时,在onMeasure()和onLayout()中,通过LayoutParams获取margin值,测量子View时纳入计算,布局时预留margin空间,普通View无法处理margin。
5. 当View需要根据内容动态改变大小时,如何正确调用requestLayout()和invalidate()?**
- View内容改变导致尺寸、位置变化时,先调用requestLayout(),触发measure和layout流程,重新计算宽高和位置;再调用invalidate(),触发draw流程,重绘界面。
- 如果仅内容改变,尺寸位置不变,只需调用invalidate()即可,无需调用requestLayout(),减少不必要的测量布局,提升性能。