Android自定义View学习总结
面试官:能说说自定义 View 的绘制流程,比如从 onMeasure()
到 onDraw()
是怎么串联起来的吗?
你:
嗯,这个问题其实有点像问“盖房子需要哪些步骤”。自定义 View 的绘制流程,简单说就是先量尺寸、再摆位置、最后画内容。不过背后其实是 Android 系统通过 ViewRootImpl
这个“总指挥”来调度的。
我之前在做一个自定义进度条的时候,遇到过布局不生效的问题,后来通过看源码才搞明白。比如调用 requestLayout()
的时候,系统会从当前 View 往上找父容器,直到 ViewRootImpl
,然后触发 performTraversals()
方法。这个方法就像项目经理,统筹安排测量(measure)、布局(layout)、绘制(draw)三个阶段。
测量阶段就像量房,父容器会给子 View 一个“预算”(比如最大宽度),子 View 在 onMeasure()
里确定自己需要多大空间。比如我的进度条需要根据文字长度动态调整宽度,就得在这里计算好,然后调用 setMeasuredDimension()
提交结果。如果忘了调用这个方法,系统会直接抛异常,就像施工队没交图纸就跑了。
布局阶段就是摆家具,确定每个 View 的位置。父容器在 onLayout()
里告诉子 View 该放哪儿(left, top, right, bottom)。比如我做过一个流式布局标签组件,这里要遍历所有子 View,计算每行能放几个标签,然后累加位置。
绘制阶段就是刷墙装修了,onDraw()
里用 Canvas
画内容。比如进度条要画背景、进度条和文字,这里得注意别画到边界外面。但这里有个坑:如果直接在主线程做复杂计算(比如实时绘制图表),会导致掉帧,后来我改成在子线程计算数据,主线程只负责绘制,才解决了卡顿问题。
对了,说到源码,ViewRootImpl
的 performTraversals()
方法会根据需要决定是否触发这三个阶段。比如只有尺寸变化了才会重新测量,否则会复用之前的测量结果,这点对性能优化很重要。
(举个实际例子)
之前做项目时,发现一个自定义按钮在动态更新文字时偶尔会错位。后来发现是因为在子线程调了 requestLayout()
,结果 ViewRootImpl
的线程检查没通过,导致布局没生效。最后改成用 post()
方法切回主线程,问题才解决。这也让我理解了为什么系统要求 UI 操作必须在主线程。
面试官:那 invalidate()
和 requestLayout()
有什么区别?什么时候用哪个?
你:
这两个方法有点像“刷新”和“重建”的区别。invalidate()
是局部刷新,比如按钮颜色变了,但大小没变,这时候只需要重绘(onDraw()
);而 requestLayout()
是整体重建,比如按钮大小变了,得重新测量和布局。
举个例子,我做过一个支持动态调整大小的图表 View。当用户拖拽边缘调整大小时,需要调用 requestLayout()
,因为宽高变了;而只是数据更新(比如折线图的点变了),只需要 invalidate()
触发重绘。
但这里有个细节:invalidate()
会标记脏区域,系统在下一帧绘制时只刷新这部分。源码里能看到,如果脏区域积累过多(比如频繁调用 invalidate()
),反而会浪费性能。所以后来我做动画时,会用 ValueAnimator
控制刷新频率,避免无意义的重复绘制。
(踩坑经历)
之前有次在 onDraw()
里调了 requestLayout()
,结果陷入死循环——重绘触发重新布局,布局又触发重绘……最后用 Handler
延迟调用才绕过去。这也让我明白,绘制流程的方法不能随便嵌套调用。
面试官:如果自定义 View 需要处理触摸事件,怎么避免手势冲突?
你:
手势冲突就像两个人同时说话,得有个优先级。比如我做过一个支持双击放大的图片 View,同时还要支持长按显示菜单。
解决方案是结合 GestureDetector
和自定义判断逻辑:
- 在
onTouchEvent()
里,ACTION_DOWN
时启动一个延迟任务,500ms 后触发长按回调。 - 如果在这期间手指移动了(
ACTION_MOVE
超过阈值),就取消长按任务,转为拖拽模式。 - 记录两次
ACTION_DOWN
的时间差,小于 300ms 且坐标接近时触发双击。
但这里有个问题:长按和双击会冲突。后来参考了系统 ImageView
的源码,发现长按触发后要标记状态,屏蔽后续的单击事件。
(实际案例)
在做画图 App 的橡皮擦功能时,长按激活橡皮擦,双击切换工具。一开始两个事件总打架,后来在长按回调里加了 isLongPressHandled
标记,如果长按已处理,就拦截后续的双击事件,这才让两者和谐共存。
扩展追问:
自定义 View 里的手势冲突啊,这个确实是个挺常见也挺头疼的问题,尤其是在那种嵌套滑动的场景里,比如说外面是一个上下滑动的列表 RecyclerView
,里面又嵌了一个可以左右滑动的自定义 View,手指一搓,到底是上下滑还是左右滑,就很容易打架。
要避免这种冲突,关键就是要搞明白安卓那个触摸事件是怎么一层层往下传(dispatchTouchEvent
),又是怎么决定谁来处理(onInterceptTouchEvent
和 onTouchEvent
)的。这里面最重要的“裁判”和“沟通”机制,我理解有这么几个点:
-
onInterceptTouchEvent(MotionEvent ev)
方法(主要在父 View/ViewGroup 里用):- 这个方法是父 View 的一个“特权”。当触摸事件从上往下传递的时候,会先经过父 View 的
dispatchTouchEvent
,然后就会调用到这个onInterceptTouchEvent
。 - 父 View 就在这里“瞅一眼”这个事件(比如是
ACTION_DOWN
、ACTION_MOVE
),然后决定要不要“截胡”。- 如果它觉得这个手势应该是它来处理(比如用户开始明显地上下滑动了,而它本身就是个上下滑动的列表),它就可以从
onInterceptTouchEvent
返回true
。一旦返回true
,就表示“这个事件以及后续的同一系列事件,我收了!”,这些事件就不会再传给它的子 View 了(子 View 会收到一个ACTION_CANCEL
事件,告诉它别等了)。 - 如果它返回
false
,就表示“我先不管,让孩子们试试看”。事件就会继续往下传给子 View。
- 如果它觉得这个手势应该是它来处理(比如用户开始明显地上下滑动了,而它本身就是个上下滑动的列表),它就可以从
- 所以,父 View 可以通过这个方法来判断什么时候该自己处理,什么时候该放给子 View。
- 这个方法是父 View 的一个“特权”。当触摸事件从上往下传递的时候,会先经过父 View 的
-
onTouchEvent(MotionEvent event)
方法(父 View 和子 View 都有):- 如果事件没有被父 View 的
onInterceptTouchEvent
拦截掉,那么它就会传到子 View 的dispatchTouchEvent
,如果子 View 也是个 ViewGroup,它也会有onInterceptTouchEvent
的机会。如果最终事件到了一个“真正想处理”的 View 那里,就会调用到这个 View 的onTouchEvent
方法。 - 在
onTouchEvent
里,如果这个 View 觉得“嗯,这个事件是我的菜,我能处理,并且我还需要后续的事件(比如ACTION_MOVE
、ACTION_UP
)”,那它就应该返回true
。返回true
就等于告诉系统:“我消费了这个事件,接下来的事件请继续发给我。” - 如果返回
false
,就表示“我不处理这个事件”,那这个事件就会往回冒泡,传给它的父 View 的onTouchEvent
去处理。
- 如果事件没有被父 View 的
-
requestDisallowInterceptTouchEvent(boolean disallow)
方法(主要由子 View 调用):- 这个是子 View 的一个“反制”手段。有时候,父 View 可能比较“霸道”,或者判断条件比较模糊,老是想抢事件。但子 View 一旦开始处理某个手势(比如它是一个左右滑动的 View,并且用户确实开始左右滑了),它就特别不希望父 View 再来捣乱把它后续的事件给截走。
- 这时候,子 View 就可以调用
getParent().requestDisallowInterceptTouchEvent(true)
。这个方法一调用,就是在跟它的父 View 说:“爹,你接下来别再用onInterceptTouchEvent
拦截我的事件了哈,这个手势我接管了!” - 通常情况下,有良好教养的父 View (比如安卓 SDK 里的那些 ViewGroup) 在收到这个请求后,在其后续的
onInterceptTouchEvent
调用中,只要disallow
标志位还是true
,并且不是ACTION_DOWN
事件,它就不会再拦截事件了,会把事件继续透传给这个子 View,直到整个手势结束(比如手指抬起,或者子 View 又调用了requestDisallowInterceptTouchEvent(false)
把权限还给父 View)。
所以,怎么避免冲突呢?常见的思路有两种:
- 外部拦截法:主要靠父 View 在
onInterceptTouchEvent()
里做精密的判断。比如,在ACTION_DOWN
的时候先返回false
(不拦截),让子 View 有机会处理。然后在ACTION_MOVE
的时候,根据滑动的距离、角度、速度等综合判断,这个手势到底是想触发父 View 的行为(比如上下滑)还是子 View 的行为(比如左右滑)。如果判断是自己的,就返回true
把事件拦截下来。 - 内部拦截法:这种方式下,父 View 在
onInterceptTouchEvent()
里通常比较“佛系”,可能除了ACTION_DOWN
之外都返回false
(或者只在某些特定情况下才拦截)。主要靠子 View 在自己的onTouchEvent()
里判断。如果子 View 觉得这个手势是自己的,并且不想被父 View 打断,它就主动调用getParent().requestDisallowInterceptTouchEvent(true)
来“请求”父 View 不要拦截。
具体用哪种,要看具体场景和需求。但核心思想就是,父子 View 之间要有一个明确的“协商”和“责任划分”机制,不能各干各的,不然就很容易打架。一般我们会在 ACTION_MOVE
事件里,根据用户滑动的方向和幅度来做这个“仲裁”的判断。
面试官:自定义 View 的性能优化有什么经验?
你:
性能优化的核心是“少干活,干巧活”。比如:
- 减少过度绘制:用
canvas.clipRect()
限制绘制区域。之前做股票 K 线图,只画可视区域的数据,帧率直接翻倍。 - 避免主线程耗时操作:在
onDraw()
里解析数据是大忌。后来我把数据预处理放到子线程,主线程只拿结果绘制。 - 硬件加速:对静态部分用
setLayerType(LAYER_TYPE_HARDWARE, null)
缓存为纹理,但动态内容要切回软件层,否则反而更卡。
有个实战技巧:用 Android Studio 的 Layout Inspector 和 GPU 渲染分析 工具,能直接看到哪部分绘制耗时最长。比如之前发现一个圆角背景的过度绘制,改成 canvas.drawRoundRect()
替代多层叠加,性能提升明显。
(源码启发)
看 RecyclerView
的源码发现,它对不可见 Item 会回收复用。受此启发,我做图表组件时也加了缓存池,复用 Path
和 Paint
对象,减少内存抖动。
面试官:如果让你设计一个支持动画的自定义 View,需要注意什么?
你:
动画 View 的关键是“流畅”和“可控”。比如做过一个下拉刷新控件,下拉时箭头旋转,松手后加载动画:
- 使用
ValueAnimator
而不是死循环:通过插值器(如LinearInterpolator
)控制动画进度,在onAnimationUpdate()
里调用invalidate()
。 - 及时释放资源:在
onDetachedFromWindow()
里取消动画,避免内存泄漏。 - 硬件加速:对复杂动画启用
LAYER_TYPE_HARDWARE
,但要注意 Android 4.0 以下兼容性问题。
(踩坑案例)
第一次做加载动画时,没考虑屏幕旋转,横竖屏切换后动画错乱。后来在 onSaveInstanceState()
里保存动画进度,恢复时从进度继续执行,才解决这个问题。
设计模式在自定义VIew的应用
1.模板方法模式:View 生命周期的骨架控制
面试官问:能举个模板方法模式在自定义 View 中的例子吗?
回答:
模板方法模式的核心是定义算法骨架,子类实现具体步骤。在 Android 的自定义 View 中,View
类通过 onMeasure()
、onLayout()
、onDraw()
这三个方法,构建了绘制流程的“模板”。
举个例子,比如我要实现一个圆形 View:
- 继承
View
类,重写onMeasure()
方法确定 View 的大小。 - 在
onMeasure()
中根据MeasureSpec
处理父容器的约束条件,调用setMeasuredDimension()
设置最终尺寸。 - 重写
onDraw()
,用Canvas
绘制一个圆形,圆心在 View 中心,半径为宽高较小值的一半。
源码体现:
View
的draw()
方法内部调用onDraw()
,但View
的默认onDraw()
是空实现,这就是模板方法模式的典型应用——父类定义流程(draw()
),子类实现细节(onDraw()
)。- 如果子类不重写
onMeasure()
且未调用setMeasuredDimension()
,直接抛IllegalStateException
,这正是模板方法对流程的强约束。
2. 策略模式:动态切换绘制效果
面试官问:如何让一个 View 支持多种绘制效果,比如圆形和矩形?
回答:
这时候可以用策略模式,把绘制逻辑抽象成接口,不同效果实现不同策略。比如:
- 定义
DrawingStrategy
接口,包含draw(Canvas)
方法。 - 实现两种策略:
CircleStrategy
(画圆)和RectangleStrategy
(画矩形)。 - 在自定义 View 中持有
DrawingStrategy
的引用,通过setStrategy()
方法动态切换策略,调用invalidate()
触发重绘。
代码示例:
// 自定义 View 的 onDraw() 中调用策略对象
@Override
protected void onDraw(Canvas canvas) {if (strategy != null) {strategy.draw(canvas, getWidth(), getHeight());}
}
优势:
- 解耦:绘制逻辑与 View 主体分离,新增效果只需新增策略类。
- 灵活:运行时动态切换策略,比如根据用户选择切换形状。
3. 观察者模式:状态变化的监听与通知
面试官问:如果 View 的状态变化需要通知外部,如何设计?
回答:
可以用观察者模式,比如自定义进度条需要监听进度变化:
- 定义接口
OnProgressChangeListener
,包含onProgressChanged(int)
方法。 - 在 View 中维护一个监听器列表(
List<OnProgressChangeListener>
)。 - 当进度变化时(如
setProgress()
被调用),遍历列表通知所有监听器。
代码示例:
// 在 View 中定义添加/移除监听器的方法
public void addProgressListener(OnProgressChangeListener listener) {listeners.add(listener);
}// 进度更新时触发通知
private void notifyProgressChanged() {for (OnProgressChangeListener listener : listeners) {listener.onProgressChanged(progress);}
}
实际应用:
- 类似
RecyclerView.addOnScrollListener()
,Android 系统广泛使用观察者模式处理事件回调。
4. 组合模式:ViewGroup 的树形结构
面试官问:ViewGroup 如何管理子 View?这用到了什么模式?
回答:
组合模式是 ViewGroup 的核心设计思想。ViewGroup 本身是一个 View,同时可以包含多个子 View 或子 ViewGroup,形成树形结构。
关键行为:
- 测量子 View:在
onMeasure()
中遍历所有子 View,调用child.measure()
。 - 布局子 View:在
onLayout()
中确定每个子 View 的位置(如垂直排列时累加高度)。 - 绘制子 View:
ViewGroup
默认不重绘自身,但会调用dispatchDraw()
绘制子 View。
源码体现:
ViewGroup
继承自View
,但重写了dispatchDraw()
方法,遍历子 View 调用child.draw(canvas)
。- 用户操作
ViewGroup
时(如设置背景),无需关心内部子 View 的细节,这正是组合模式的“整体与部分一致对待”思想。
5. requestLayout()的坑与优化
面试官:“为什么在onCreate()
中调用requestLayout()
无效?”
回答:
原因:onCreate()
时View
还未附加到窗口(mAttachInfo == null
),布局请求会被忽略。
解决方案:用post()
延迟到View
附加后执行:
view.post(() -> view.requestLayout()); // 等View挂载到窗口再触发布局
性能优化技巧:
- 避免冗余调用:在
onLayout()
或onMeasure()
中通过isLayoutRequested()
判断是否需要重新布局。 - 局部刷新:优先用
invalidate(Rect)
代替全量重绘,减少性能开销。
Android自定义View全流程深度剖析
面试官:
“你在项目中做过复杂自定义View对吧?说说整个绘制流程的源码级理解,比如onMeasure()
到onDraw()
是怎么串联起来的。”
候选人:
“绘制流程就像造房子,得先打地基再盖屋顶。系统通过ViewRootImpl
这个‘总工程师’来调度整个过程:
-
测量阶段(量房):
- 当调用
requestLayout()
时,ViewRootImpl
会启动performTraversals()
,先调用performMeasure()
。 - 父容器(比如
LinearLayout
)通过measureChildWithMargins()
给子View传递MeasureSpec
(包含宽高约束),就像给装修队图纸。 - 子View在
onMeasure()
里确定自己的尺寸,必须调用setMeasuredDimension()
交卷,否则系统会抛异常——这就像施工队必须签字确认图纸。
- 当调用
-
布局阶段(放样):
performLayout()
触发后,父容器在onLayout()
里调用每个子View的layout(l, t, r, b)
方法,确定它们的位置坐标。- 这里有个优化技巧:
View.layout()
内部会先检查位置是否变化,避免无意义的重绘。就像装修师傅发现墙面没动,直接跳过刷漆。
-
绘制阶段(施工):
performDraw()
最终调用View.draw()
,这里藏着四个关键步骤:- 画背景(
drawBackground()
)→ 相当于刷墙底漆 - 画内容(
onDraw()
)→ 手绘壁画,开发者在这里发挥创意 - 画子View(
dispatchDraw()
)→ 给每个房间装家具 - 画前景(
onDrawForeground()
)→ 最后挂装饰画
- 画背景(
- 系统用
DisplayList
记录绘制指令,通过RenderThread
异步渲染,避免阻塞主线程。”
面试官:
“提到MeasureSpec
,能具体说说它的三种模式吗?实际项目中怎么处理?”
候选人:
“MeasureSpec
就像装修预算书,分三种情况:
- EXACTLY(精确尺寸):父容器给了确定数值,比如
layout_width="100dp"
,这时候直接按数值施工。 - AT MOST(最大限制):父容器给上限,比如
layout_width="wrap_content"
但父容器剩余空间有限。这时候要量力而行,比如文本不能超过父容器宽度。 - UNSPECIFIED(自由发挥):少见但重要,比如
ScrollView
的子View高度可以无限延伸。
实战案例:我们做过一个流式布局标签组件,处理AT MOST
时需要动态计算每行能放几个标签。算法类似摆俄罗斯方块——遍历子View,累加宽度直到超出剩余空间就换行。”
面试官:
“如果自定义View需要同时处理双击和长按,怎么避免手势冲突?”
候选人:
“这就像同时要听清两个人的指令,得设置优先级和状态机。我们的解决方案:
-
长按检测:
- 在
onTouchEvent()
的ACTION_DOWN
里启动postDelayed()
,延迟500ms触发长按回调。 - 如果在这期间发生移动(
ACTION_MOVE
超过触摸容差)或抬起(ACTION_UP
),立即移除延迟任务。
- 在
-
双击检测:
- 记录两次
ACTION_DOWN
的时间差,若小于300ms且坐标在阈值内视为双击。 - 检测到第一次点击时启动定时器,超时未收到第二次点击则触发单击事件。
- 记录两次
-
冲突仲裁:
- 当长按先触发时,标记
isLongPressHandled=true
,后续抬起事件不再触发单击。 - 使用
GestureDetector
简化代码,但需要重写onDoubleTap()
和onLongPress()
方法。
- 当长按先触发时,标记
去年做画图APP时,这个逻辑让橡皮擦工具(长按激活)和撤销功能(双击)完美共存。”
面试官:
“invalidate()
和postInvalidate()
在源码层面有什么区别?为什么子线程要用后者?”
候选人:
“这两个方法就像快递派件:
- **
invalidate()
** 是主线程专送——直接在当前线程标记脏区域,要求立即重绘。如果子线程调用,会触发CalledFromWrongThreadException
,就像让外卖小哥进办公楼没工牌被拦下。 - **
postInvalidate()
** 是跨线程中转——内部通过Handler
把重绘请求抛到主线程的MessageQueue
。源码里能看到它调用ViewRootImpl.dispatchInvalidateOnAnimation()
,确保线程安全。
源码级区别:
// View.java
public void postInvalidate() {final AttachInfo attachInfo = mAttachInfo;if (attachInfo != null) {attachInfo.mViewRootImpl.dispatchInvalidateOnAnimation(this); // 跨线程转发}
}
我们直播礼物动画模块就大量使用postInvalidate()
,在渲染线程计算粒子位置,主线程只负责绘制。”
面试官:
“自定义View导致过度绘制严重,有哪些优化手段?”
候选人:
“过度绘制就像反复刷墙浪费油漆,我们有四把手术刀:
-
裁剪画布:
- 在
onDraw()
里先用canvas.clipRect()
划定绘制区域,避免绘制不可见部分。 - 比如列表项只绘制可见区域,滚动时动态调整
clipRect
的边界。
- 在
-
分层绘制:
- 对静态背景使用
setLayerType(LAYER_TYPE_HARDWARE, null)
,生成纹理缓存。 - 动态内容用软件绘制,减少GPU指令提交次数。
- 对静态背景使用
-
合并绘制:
- 将多个
drawText()
调用合并为一次,利用StaticLayout
预生成文本布局。 - 使用
drawTextRun()
替代多个drawText()
,减少JNI调用开销。
- 将多个
-
工具检测:
- 开启开发者选项中的‘显示过度渲染’,红色区域需要优先优化。
- 使用
Tracer for OpenGL
跟踪GPU绘制命令,定位重复绘制区域。
最近优化股票K线图组件,通过canvas.clipRect()
把绘制区域限制在可视范围,帧率从45FPS提升到58FPS。”
面试官:能详细说说 Android 中 View 的绘制源码调用顺序吗?比如从 ViewRootImpl
到 onDraw()
的完整流程。
你:
这个问题其实可以比喻成“一场精心策划的演出”,ViewRootImpl
是总导演,View
是演员,而 Choreographer
是舞台调度。整个流程大致分三步:量舞台(Measure)→ 摆道具(Layout)→ 演出(Draw),但背后源码的协作非常精密。
1. 流程起点:ViewRootImpl 的 performTraversals()
当你在代码里调用 requestLayout()
或者界面需要更新时,ViewRootImpl
的 performTraversals()
方法会被触发。这个方法是整个绘制流程的“总开关”,它根据当前状态(比如是否需要重新测量、布局或绘制)决定执行哪些步骤。
源码关键逻辑:
// ViewRootImpl.java
private void performTraversals() {// 1. 检查是否需要重新测量if (mLayoutRequested) {performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);}// 2. 检查是否需要重新布局if (layoutRequested) {performLayout(lp, desiredWindowWidth, desiredWindowHeight);}// 3. 检查是否需要重新绘制if (!mDirty.isEmpty() || mIsAnimating) {performDraw();}
}
2. Measure 阶段:确定 View 的尺寸
调用链:
ViewRootImpl.performMeasure()
→ DecorView.measure()
→ View.measure()
→ View.onMeasure()
源码细节:
- 父容器传递约束:父容器(如
LinearLayout
)通过MeasureSpec
告诉子 View 可用的空间和模式(EXACTLY
/AT_MOST
/UNSPECIFIED
)。 - 子 View 计算尺寸:子 View 在
onMeasure()
中根据父容器的约束,调用setMeasuredDimension()
确定自己的宽高。
示例:
假设一个 TextView
的 onMeasure()
需要计算文本占用的空间:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {// 计算文本宽度(伪代码)int textWidth = calculateTextWidth(mText);int textHeight = calculateTextHeight(mText);// 处理父容器的约束int width = resolveSize(textWidth, widthMeasureSpec);int height = resolveSize(textHeight, heightMeasureSpec);setMeasuredDimension(width, height); // 必须调用!
}
3. Layout 阶段:确定 View 的位置
调用链:
ViewRootImpl.performLayout()
→ DecorView.layout()
→ View.layout()
→ View.onLayout()
源码细节:
- 递归布局子 View:父容器(如
RelativeLayout
)在onLayout()
中遍历所有子 View,调用每个子 View 的layout(l, t, r, b)
方法,传递左上右下坐标。 - 位置是否变化:
View.layout()
内部会检查新旧坐标是否一致,避免无意义的重复布局。
示例:
LinearLayout
垂直排列子 View 的核心逻辑:
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {int childTop = mPaddingTop;for (int i = 0; i < getChildCount(); i++) {View child = getChildAt(i);// 计算子 View 的位置child.layout(l, childTop, l + child.getMeasuredWidth(), childTop + child.getMeasuredHeight());childTop += child.getMeasuredHeight() + mDividerHeight; // 累加高度}
}
4. Draw 阶段:绘制内容到屏幕
调用链:
ViewRootImpl.performDraw()
→ DecorView.draw()
→ View.draw()
→ View.onDraw()
源码细节:
- 绘制顺序:
View.draw()
方法内部按顺序执行四个步骤:- 画背景:
drawBackground(canvas)
- 画内容:
onDraw(canvas)
(开发者自定义) - 画子 View:
dispatchDraw(canvas)
(ViewGroup
实现) - 画前景:
onDrawForeground(canvas)
(滚动条、边缘效果等)
- 画背景:
示例:
自定义圆形按钮的 onDraw()
:
@Override
protected void onDraw(Canvas canvas) {super.onDraw(canvas);// 画圆形背景canvas.drawCircle(getWidth()/2, getHeight()/2, mRadius, mPaint);// 画文本canvas.drawText(mText, getWidth()/2, getHeight()/2, mTextPaint);
}
5. 关键角色:Choreographer 与 VSYNC
- Choreographer:负责协调绘制时序,监听 VSYNC 信号(屏幕刷新信号,16ms 一次)。
- VSYNC 的作用:确保
performTraversals()
的执行与屏幕刷新同步,避免撕裂(tearing)现象。
源码中的协作:
// ViewRootImpl 中安排绘制任务
void scheduleTraversals() {if (!mTraversalScheduled) {mTraversalScheduled = true;// 通过 Choreographer 在下一个 VSYNC 信号到来时执行mChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);}
}
6. 性能优化关键点
- 减少 Measure 次数:避免在
onMeasure()
中频繁修改布局参数。 - 局部重绘:用
invalidate(left, top, right, bottom)
替代全局刷新。 - 硬件加速:对复杂图形使用
Canvas
的硬件加速(setLayerType(LAYER_TYPE_HARDWARE, null)
)。
案例:
在实现一个滚动时间选择器时,发现频繁调用 requestLayout()
导致卡顿。通过缓存子 View 的测量结果,只在滚动停止时触发重新布局,帧率从 30 提升到 60。
7. 高频面试题:为什么 requestLayout() 后不立即绘制?
答案:
因为 requestLayout()
只是标记需要重新测量和布局,真正的绘制需要等待下一个 VSYNC 信号。Choreographer
会确保在屏幕刷新周期内统一处理所有 UI 更新请求,避免频繁无意义的绘制。
基础知识讲解 :
Android 中 View 的绘制源码调用顺序详解
1. 整体流程概述
Android 中 View 的绘制流程由 **ViewRootImpl
驱动,通过 performTraversals()
方法协调 测量(Measure)**、布局(Layout) 和 绘制(Draw) 三个阶段。以下是完整的源码调用顺序:
2. 流程起点:ViewRootImpl.performTraversals()
当 View
首次附加到窗口或需要更新时,ViewRootImpl
的 performTraversals()
方法被触发。
核心逻辑:
- 根据标志位
mLayoutRequested
和mDirty
决定是否执行测量、布局或绘制。 - 通过
Choreographer
同步 VSync 信号,确保绘制在屏幕刷新周期内完成。
// ViewRootImpl.java
private void performTraversals() {// 1. 测量(Measure)if (mLayoutRequested) {performMeasure(...);}// 2. 布局(Layout)if (layoutRequested) {performLayout(...);}// 3. 绘制(Draw)if (!mDirty.isEmpty()) {performDraw(...);}
}
3. 测量阶段(Measure)
测量阶段确定每个 View
的尺寸,从根 DecorView
开始递归调用子 View
。
关键方法调用顺序:
-
ViewRootImpl.performMeasure()
调用根View
(即DecorView
)的measure()
方法。 View.measure(int, int)
内部调用onMeasure()
,并强制要求子类必须调用setMeasuredDimension()
。View.onMeasure(int, int)
开发者重写此方法计算View
的宽高。View.setMeasuredDimension(int, int)
保存测量结果,否则抛出IllegalStateException
。
// View.java
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {onMeasure(widthMeasureSpec, heightMeasureSpec);if (!setMeasuredDimension) {throw new IllegalStateException("必须调用 setMeasuredDimension()");}
}// 自定义 View 示例
@Override
protected void onMeasure(int widthSpec, int heightSpec) {int width = resolveSize(widthSpec, desiredWidth);int height = resolveSize(heightSpec, desiredHeight);setMeasuredDimension(width, height);
}
4. 布局阶段(Layout)
布局阶段确定每个 View
的位置,从根 View
开始递归调用子 View
。
关键方法调用顺序:
- **
ViewRootImpl.performLayout()
**
调用根View
的layout()
方法。 - **
View.layout(int, int, int, int)
**
检查布局是否变化,调用onLayout()
。 - **
View.onLayout(boolean, int, int, int, int)
**- 普通
View
默认无操作。 ViewGroup
必须重写此方法布局子View
。
- 普通
// ViewGroup.java
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {for (int i = 0; i < getChildCount(); i++) {View child = getChildAt(i);child.layout(childLeft, childTop, childRight, childBottom);}
}
5. 绘制阶段(Draw)
绘制阶段将 View
内容渲染到屏幕,从根 View
开始递归调用子 View
。
关键方法调用顺序:
-
ViewRootImpl.performDraw()
调用根View
的draw()
方法。 - **
View.draw(Canvas)
**
按顺序执行以下绘制步骤:- 绘制背景:
drawBackground(Canvas)
- 绘制内容:
onDraw(Canvas)
(开发者重写) - **绘制子
View
**:dispatchDraw(Canvas)
(ViewGroup
实现) - 绘制前景:
onDrawForeground(Canvas)
- 绘制背景:
-
View.onDraw(Canvas)
开发者在此实现自定义绘制逻辑(如绘制图形、文字等)。
// View.java
public void draw(Canvas canvas) {drawBackground(canvas); // 步骤1:背景onDraw(canvas); // 步骤2:内容dispatchDraw(canvas); // 步骤3:子 ViewonDrawForeground(canvas); // 步骤4:前景
}// 自定义 View 示例
@Override
protected void onDraw(Canvas canvas) {super.onDraw(canvas);canvas.drawCircle(centerX, centerY, radius, paint);
}
6. 递归流程(View 树的遍历)
- 根
View
(DecorView)
performTraversals()
→measure()
→layout()
→draw()
. - 子
ViewGroup
measure()
→onMeasure()
→layout()
→onLayout()
→draw()
→dispatchDraw()
. - 叶子
View
measure()
→onMeasure()
→layout()
→onLayout()
→draw()
→onDraw()
.
7. 性能优化关键点
- 减少测量和布局次数
- 避免在
onMeasure()
或onLayout()
中频繁修改布局参数。 - 使用
View.isLayoutRequested()
检查是否需要重新布局。
- 避免在
- 局部重绘
- 调用
invalidate(left, top, right, bottom)
而非全局刷新。
- 调用
- 避免绘制阻塞
- 不在
onDraw()
中创建对象或执行耗时操作。 - 使用硬件加速(
setLayerType(LAYER_TYPE_HARDWARE, null)
)。
- 不在