Java 面向对象如何落到 Android View 体系

点击一个商品卡片、列表滚动后复用条目、主题切换后重新绘制,这些 UI 场景不是一堆互不相干的 API:它们是 Java 对象协作在 Android 界面层的具体结果。

一句话结论:在 Android UI 中,优先用组合表达界面结构、用接口表达可替换行为、仅在确有"是一个"关系时继承 View 或框架基类,才能把生命周期与职责留在正确的对象手里。

先划清版本与事实边界

本文以当前 Android Framework 与 AndroidX RecyclerView 的公开设计为参照;源码实现和私有调用顺序会随 Android 版本变化。文中的 ViewViewGroupActivityContextWrapperLayoutInflaterOnClickListenerRecyclerView.Adapter 是可追踪的源码锚点,不把内部细节当作稳定 API。标为"简化"的代码只说明对象关系,不能直接替代生产实现。

一个按钮从 XML 到业务回调

设想订单页:Activity 显示一个根布局,布局里有商品列表和"提交订单"按钮。谁创建它们,谁持有它们,谁触发回调,结果交给谁?先沿事件走一遍,再回看 Java 概念。
#mermaid-svg-jgLnGzoDD9fZWX2v{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-jgLnGzoDD9fZWX2v .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-jgLnGzoDD9fZWX2v .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-jgLnGzoDD9fZWX2v .error-icon{fill:#552222;}#mermaid-svg-jgLnGzoDD9fZWX2v .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-jgLnGzoDD9fZWX2v .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-jgLnGzoDD9fZWX2v .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-jgLnGzoDD9fZWX2v .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-jgLnGzoDD9fZWX2v .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-jgLnGzoDD9fZWX2v .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-jgLnGzoDD9fZWX2v .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-jgLnGzoDD9fZWX2v .marker{fill:#333333;stroke:#333333;}#mermaid-svg-jgLnGzoDD9fZWX2v .marker.cross{stroke:#333333;}#mermaid-svg-jgLnGzoDD9fZWX2v svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-jgLnGzoDD9fZWX2v p{margin:0;}#mermaid-svg-jgLnGzoDD9fZWX2v .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-jgLnGzoDD9fZWX2v .cluster-label text{fill:#333;}#mermaid-svg-jgLnGzoDD9fZWX2v .cluster-label span{color:#333;}#mermaid-svg-jgLnGzoDD9fZWX2v .cluster-label span p{background-color:transparent;}#mermaid-svg-jgLnGzoDD9fZWX2v .label text,#mermaid-svg-jgLnGzoDD9fZWX2v span{fill:#333;color:#333;}#mermaid-svg-jgLnGzoDD9fZWX2v .node rect,#mermaid-svg-jgLnGzoDD9fZWX2v .node circle,#mermaid-svg-jgLnGzoDD9fZWX2v .node ellipse,#mermaid-svg-jgLnGzoDD9fZWX2v .node polygon,#mermaid-svg-jgLnGzoDD9fZWX2v .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-jgLnGzoDD9fZWX2v .rough-node .label text,#mermaid-svg-jgLnGzoDD9fZWX2v .node .label text,#mermaid-svg-jgLnGzoDD9fZWX2v .image-shape .label,#mermaid-svg-jgLnGzoDD9fZWX2v .icon-shape .label{text-anchor:middle;}#mermaid-svg-jgLnGzoDD9fZWX2v .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-jgLnGzoDD9fZWX2v .rough-node .label,#mermaid-svg-jgLnGzoDD9fZWX2v .node .label,#mermaid-svg-jgLnGzoDD9fZWX2v .image-shape .label,#mermaid-svg-jgLnGzoDD9fZWX2v .icon-shape .label{text-align:center;}#mermaid-svg-jgLnGzoDD9fZWX2v .node.clickable{cursor:pointer;}#mermaid-svg-jgLnGzoDD9fZWX2v .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-jgLnGzoDD9fZWX2v .arrowheadPath{fill:#333333;}#mermaid-svg-jgLnGzoDD9fZWX2v .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-jgLnGzoDD9fZWX2v .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-jgLnGzoDD9fZWX2v .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-jgLnGzoDD9fZWX2v .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-jgLnGzoDD9fZWX2v .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-jgLnGzoDD9fZWX2v .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-jgLnGzoDD9fZWX2v .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-jgLnGzoDD9fZWX2v .cluster text{fill:#333;}#mermaid-svg-jgLnGzoDD9fZWX2v .cluster span{color:#333;}#mermaid-svg-jgLnGzoDD9fZWX2v div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-jgLnGzoDD9fZWX2v .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-jgLnGzoDD9fZWX2v rect.text{fill:none;stroke-width:0;}#mermaid-svg-jgLnGzoDD9fZWX2v .icon-shape,#mermaid-svg-jgLnGzoDD9fZWX2v .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-jgLnGzoDD9fZWX2v .icon-shape p,#mermaid-svg-jgLnGzoDD9fZWX2v .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-jgLnGzoDD9fZWX2v .icon-shape .label rect,#mermaid-svg-jgLnGzoDD9fZWX2v .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-jgLnGzoDD9fZWX2v .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-jgLnGzoDD9fZWX2v .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-jgLnGzoDD9fZWX2v :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Activity 创建界面
LayoutInflater 解析布局
ViewGroup 根容器
RecyclerView 列表
Button 提交按钮
商品项 View
商品项 View

  • **谁创建:**系统创建 Activity;应用通常通过 setContentViewLayoutInflater.inflateLayoutInflater 按 XML 创建 ViewViewGroup 实例。代码直接 new 一个自定义 View 也可以。
  • **谁拥有:**根 ViewGroup 持有子 View;Activity 持有当前窗口所展示的根 View 的引用和界面生命周期。RecyclerView 持有可见子项的 View,Adapter 负责为它提供和绑定数据。
  • **谁触发:**触摸分发、点击判定和绘制由 Framework 驱动。业务代码不应自己调用 onDraw()onMeasure() 或 Activity 生命周期方法。
  • **结果给谁:**点击最终进入注册的 OnClickListener;列表的绑定结果回到 RecyclerView,绘制结果交给窗口和屏幕,而不是由 Adapter 自己"画到屏幕"。

类、对象与组合:一棵真实的 UI 树

View 是所有可见控件的基类,负责自身状态、测量、布局位置和绘制入口。TextViewButton 是它的具体子类。这里的"类"是蓝图,"对象"是运行时真正挂在窗口上的那一个按钮实例。

ViewGroup 也是 View,但额外管理子 View。它并不只是"父类复用代码",而是组合的拥有者:一个 LinearLayout 对象包含多个子 View 对象。界面多半应先问"这个对象是否拥有另一个对象",而不是急着设计继承层级。

源码锚点 回答的问题 创建与持有 何时工作,结果给谁
View 单个元素如何测量、布局、绘制、接收事件? LayoutInflater 或业务代码创建;父 ViewGroup 持有 Framework 在遍历中调用;Canvas 输出交给窗口渲染链路
ViewGroup 子元素如何组成树? 布局膨胀或代码创建;其父容器持有 测量和布局时调度子项;子项的位置和绘制顺序由它组织
LayoutInflater XML 如何变成对象树? LayoutInflater.from(context) 获取并执行;调用方接收根 View inflate 时创建 View;把根对象返回给 Activity、Fragment 或 Adapter
Activity 一个界面在哪个生命周期和窗口内? 系统创建;系统任务与窗口体系持有 系统调用生命周期;Activity 设置内容 View 并把事件结果路由到业务协作对象
ContextWrapper 为什么 Context 能附加环境能力? Activity 经由 ContextThemeWrapper 继承它 包装基础 Context;资源、主题和系统服务通过 Context 解析

这也解释了组合为何常优于继承:一个"订单卡片"不必继承 RecyclerView 才能含有列表;它可以是一个 ViewGroup,内部组合标题、列表和按钮。继承适合稳定的类型身份,例如 PriceBadgeView is a View;组合适合"由什么构成"。

接口与回调:行为可以换,拥有者不用换

Java 接口描述能力,而不是存储实现。View.OnClickListener 就是典型例子:按钮不知道提交订单的业务细节,只知道在点击时调用一个实现了接口的对象。Activity、Fragment、ViewModel 协调器或独立类都可以接收这个结果,只要它实现同一接口或提供等价回调。
#mermaid-svg-BXpJjOAdLYiQvOnA{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-BXpJjOAdLYiQvOnA .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-BXpJjOAdLYiQvOnA .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-BXpJjOAdLYiQvOnA .error-icon{fill:#552222;}#mermaid-svg-BXpJjOAdLYiQvOnA .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-BXpJjOAdLYiQvOnA .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-BXpJjOAdLYiQvOnA .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-BXpJjOAdLYiQvOnA .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-BXpJjOAdLYiQvOnA .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-BXpJjOAdLYiQvOnA .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-BXpJjOAdLYiQvOnA .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-BXpJjOAdLYiQvOnA .marker{fill:#333333;stroke:#333333;}#mermaid-svg-BXpJjOAdLYiQvOnA .marker.cross{stroke:#333333;}#mermaid-svg-BXpJjOAdLYiQvOnA svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-BXpJjOAdLYiQvOnA p{margin:0;}#mermaid-svg-BXpJjOAdLYiQvOnA .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-BXpJjOAdLYiQvOnA .cluster-label text{fill:#333;}#mermaid-svg-BXpJjOAdLYiQvOnA .cluster-label span{color:#333;}#mermaid-svg-BXpJjOAdLYiQvOnA .cluster-label span p{background-color:transparent;}#mermaid-svg-BXpJjOAdLYiQvOnA .label text,#mermaid-svg-BXpJjOAdLYiQvOnA span{fill:#333;color:#333;}#mermaid-svg-BXpJjOAdLYiQvOnA .node rect,#mermaid-svg-BXpJjOAdLYiQvOnA .node circle,#mermaid-svg-BXpJjOAdLYiQvOnA .node ellipse,#mermaid-svg-BXpJjOAdLYiQvOnA .node polygon,#mermaid-svg-BXpJjOAdLYiQvOnA .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-BXpJjOAdLYiQvOnA .rough-node .label text,#mermaid-svg-BXpJjOAdLYiQvOnA .node .label text,#mermaid-svg-BXpJjOAdLYiQvOnA .image-shape .label,#mermaid-svg-BXpJjOAdLYiQvOnA .icon-shape .label{text-anchor:middle;}#mermaid-svg-BXpJjOAdLYiQvOnA .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-BXpJjOAdLYiQvOnA .rough-node .label,#mermaid-svg-BXpJjOAdLYiQvOnA .node .label,#mermaid-svg-BXpJjOAdLYiQvOnA .image-shape .label,#mermaid-svg-BXpJjOAdLYiQvOnA .icon-shape .label{text-align:center;}#mermaid-svg-BXpJjOAdLYiQvOnA .node.clickable{cursor:pointer;}#mermaid-svg-BXpJjOAdLYiQvOnA .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-BXpJjOAdLYiQvOnA .arrowheadPath{fill:#333333;}#mermaid-svg-BXpJjOAdLYiQvOnA .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-BXpJjOAdLYiQvOnA .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-BXpJjOAdLYiQvOnA .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-BXpJjOAdLYiQvOnA .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-BXpJjOAdLYiQvOnA .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-BXpJjOAdLYiQvOnA .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-BXpJjOAdLYiQvOnA .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-BXpJjOAdLYiQvOnA .cluster text{fill:#333;}#mermaid-svg-BXpJjOAdLYiQvOnA .cluster span{color:#333;}#mermaid-svg-BXpJjOAdLYiQvOnA div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-BXpJjOAdLYiQvOnA .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-BXpJjOAdLYiQvOnA rect.text{fill:none;stroke-width:0;}#mermaid-svg-BXpJjOAdLYiQvOnA .icon-shape,#mermaid-svg-BXpJjOAdLYiQvOnA .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-BXpJjOAdLYiQvOnA .icon-shape p,#mermaid-svg-BXpJjOAdLYiQvOnA .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-BXpJjOAdLYiQvOnA .icon-shape .label rect,#mermaid-svg-BXpJjOAdLYiQvOnA .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-BXpJjOAdLYiQvOnA .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-BXpJjOAdLYiQvOnA .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-BXpJjOAdLYiQvOnA :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 用户触摸
View 事件分发
View 点击判定
OnClickListener 回调
业务处理对象
状态更新
界面请求刷新

button.setOnClickListener(listener) 的调用方创建并注册行为对象,View 保存这个接口引用,Framework 在合适的触摸时机触发,业务处理对象接收点击结果。这里实现了多态 :调用点面对的是 OnClickListener,运行时实际执行的可以是不同的实现。

不要把"接口"误解为只能写匿名类。一个命名策略对象通常更容易测试,也能避免让 Activity 承担所有业务。Kotlin 的 lambda 只是把同样的回调接口写得更紧凑。

继承、抽象类与多态分发

继承让子类获得父类契约和部分实现。ViewGroup extends View 是合理的框架层次:容器首先也是一个可绘制、可测量的 View。抽象类则用于"流程相同、留一个步骤给子类填"的场景。许多 View 绘制扩展点可理解为这个模式:Framework 控制遍历时机,具体 View 在自己的绘制入口补充内容。

多态不是"到处 instanceof"。它是让调用者通过共同父类或接口发消息,由运行时对象选择实现。布局遍历与绘制分发必须分开理解:前者确定每个子 View 的尺寸和位置,后者才把内容交给 Canvas。绘制时,容器只需把子项当作 View 调度;不同的 TextView、图片 View、自定义 View 各自完成对应绘制。
#mermaid-svg-gL0ps5vSJb3Z80rn{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-gL0ps5vSJb3Z80rn .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-gL0ps5vSJb3Z80rn .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-gL0ps5vSJb3Z80rn .error-icon{fill:#552222;}#mermaid-svg-gL0ps5vSJb3Z80rn .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-gL0ps5vSJb3Z80rn .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-gL0ps5vSJb3Z80rn .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-gL0ps5vSJb3Z80rn .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-gL0ps5vSJb3Z80rn .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-gL0ps5vSJb3Z80rn .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-gL0ps5vSJb3Z80rn .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-gL0ps5vSJb3Z80rn .marker{fill:#333333;stroke:#333333;}#mermaid-svg-gL0ps5vSJb3Z80rn .marker.cross{stroke:#333333;}#mermaid-svg-gL0ps5vSJb3Z80rn svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-gL0ps5vSJb3Z80rn p{margin:0;}#mermaid-svg-gL0ps5vSJb3Z80rn .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-gL0ps5vSJb3Z80rn .cluster-label text{fill:#333;}#mermaid-svg-gL0ps5vSJb3Z80rn .cluster-label span{color:#333;}#mermaid-svg-gL0ps5vSJb3Z80rn .cluster-label span p{background-color:transparent;}#mermaid-svg-gL0ps5vSJb3Z80rn .label text,#mermaid-svg-gL0ps5vSJb3Z80rn span{fill:#333;color:#333;}#mermaid-svg-gL0ps5vSJb3Z80rn .node rect,#mermaid-svg-gL0ps5vSJb3Z80rn .node circle,#mermaid-svg-gL0ps5vSJb3Z80rn .node ellipse,#mermaid-svg-gL0ps5vSJb3Z80rn .node polygon,#mermaid-svg-gL0ps5vSJb3Z80rn .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-gL0ps5vSJb3Z80rn .rough-node .label text,#mermaid-svg-gL0ps5vSJb3Z80rn .node .label text,#mermaid-svg-gL0ps5vSJb3Z80rn .image-shape .label,#mermaid-svg-gL0ps5vSJb3Z80rn .icon-shape .label{text-anchor:middle;}#mermaid-svg-gL0ps5vSJb3Z80rn .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-gL0ps5vSJb3Z80rn .rough-node .label,#mermaid-svg-gL0ps5vSJb3Z80rn .node .label,#mermaid-svg-gL0ps5vSJb3Z80rn .image-shape .label,#mermaid-svg-gL0ps5vSJb3Z80rn .icon-shape .label{text-align:center;}#mermaid-svg-gL0ps5vSJb3Z80rn .node.clickable{cursor:pointer;}#mermaid-svg-gL0ps5vSJb3Z80rn .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-gL0ps5vSJb3Z80rn .arrowheadPath{fill:#333333;}#mermaid-svg-gL0ps5vSJb3Z80rn .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-gL0ps5vSJb3Z80rn .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-gL0ps5vSJb3Z80rn .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-gL0ps5vSJb3Z80rn .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-gL0ps5vSJb3Z80rn .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-gL0ps5vSJb3Z80rn .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-gL0ps5vSJb3Z80rn .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-gL0ps5vSJb3Z80rn .cluster text{fill:#333;}#mermaid-svg-gL0ps5vSJb3Z80rn .cluster span{color:#333;}#mermaid-svg-gL0ps5vSJb3Z80rn div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-gL0ps5vSJb3Z80rn .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-gL0ps5vSJb3Z80rn rect.text{fill:none;stroke-width:0;}#mermaid-svg-gL0ps5vSJb3Z80rn .icon-shape,#mermaid-svg-gL0ps5vSJb3Z80rn .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-gL0ps5vSJb3Z80rn .icon-shape p,#mermaid-svg-gL0ps5vSJb3Z80rn .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-gL0ps5vSJb3Z80rn .icon-shape .label rect,#mermaid-svg-gL0ps5vSJb3Z80rn .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-gL0ps5vSJb3Z80rn .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-gL0ps5vSJb3Z80rn .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-gL0ps5vSJb3Z80rn :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} ViewRootImpl 遍历入口
测量与布局阶段
绘制阶段
ViewGroup 绘制分发
View 子项引用
TextView 绘制实现
ImageView 绘制实现
PriceBadgeView 绘制实现
Canvas 输出

实际 Framework 调用涉及 measurelayoutdraw 等多个阶段。ViewRootImpl 的遍历中,测量和布局阶段先由容器协商子项尺寸、写入位置;随后绘制阶段从根 View 开始,ViewGroup 再通过自己的绘制分发把子项作为 View 交给具体实现。上图只把后半段展开为多态关系,不能读成"布局遍历直接调用子项绘制"。不要在业务中直接调用生命周期式回调来强行刷新。数据变更后,按 View API 请求重新布局或重绘,例如视情况调用 requestLayout()invalidate(),让 Framework 选择正确时机。

一个可替换的绘制策略:接口 + 组合 + 自定义 View

下面是可直接改造成项目代码的 Kotlin 练习。MeterViewView 的子类,因此继承只承担"它是一个 View"的身份;颜色计算是可替换的 RenderPolicy,因此用接口和组合承载变化。

kotlin 复制代码
// 简化示例:可放入 Android 项目后继续完善
interface RenderPolicy {
    fun colorFor(value: Int): Int
}

class ThresholdPolicy(private val warningAt: Int) : RenderPolicy {
    override fun colorFor(value: Int): Int =
        if (value >= warningAt) Color.RED else Color.GREEN
}

class MeterView(context: Context) : View(context) {
    var value: Int = 0
        set(newValue) {
            field = newValue.coerceIn(0, 100)
            invalidate()
        }

    var renderPolicy: RenderPolicy = ThresholdPolicy(warningAt = 80)
        set(newPolicy) {
            field = newPolicy
            invalidate()
        }

    private val paint = Paint(Paint.ANTI_ALIAS_FLAG)

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        paint.color = renderPolicy.colorFor(value)
        canvas.drawRect(0f, 0f, width * value / 100f, height.toFloat(), paint)
    }
}

Activity 或 Fragment 创建 MeterView 后拥有它的配置引用;View 自己持有 RenderPolicy;数据层把新数值交给 valueinvalidate() 请求 Framework 在下一次合适绘制中回调 onDraw。策略返回颜色,View 接收结果并负责画到自己的 Canvas。策略没有持有 View 或 Activity,因此更容易复用和单测。

**刻意省略:**示例没有实现 onMeasure 的最小尺寸与 MeasureSpec 协商,没有 contentDescription、无障碍状态和键盘操作,没有 XML 属性解析,也没有观察者订阅与生命周期清理。生产代码还要处理配置变更、状态保存、动画、主线程更新及画笔对象复用。

动手练习:让进度条行为可配置

  1. 在一个空 Activity 中把 MeterView 加入 FrameLayout,设置固定高度和 MATCH_PARENT 宽度。
  2. 用滑块、按钮或测试数据连续设置 meter.value = 0..100,确认 setter 触发重绘而不是手动调用 onDraw
  3. 点击"切换规则"时,将 renderPolicyThresholdPolicy(80) 换成自己实现的渐变或分段策略。不要修改 MeterView.onDraw
  4. 给 View 提供内容描述,并为极小或零尺寸编写 onMeasure 策略;验证旋转屏幕后规则和数值的恢复方案。

这项练习的验收点是:新增一种颜色规则只新增一个 RenderPolicy 实现,View 的绘制流程与 Activity 的生命周期代码都不需要复制。它把接口、多态、组合与 View 继承边界放在同一个可观察结果里。

Inner class 的位置:靠近宿主,不是躲开职责

内部类适合表达与宿主紧密相关、不会单独复用的协作。例如自定义 View 的私有触摸状态或一个短生命周期的点击监听器。它可以直接访问外部类成员,但这份便利也意味着引用关系更紧。

RecyclerView.AdapterViewHolder 在 Kotlin 中应默认写成非 inner 的嵌套类,例如 class ProductHolder(itemView: View) : RecyclerView.ViewHolder(itemView);这与 Java 的静态嵌套类一样,不会隐式保留 Adapter 引用。只有 Holder 确实需要访问 Adapter 成员时才使用 inner,并优先考虑把绑定所需的数据或回调显式传入。Adapter 创建 Holder,RecyclerView 在屏幕上显示和回收它,Adapter 在 onBindViewHolder 里把某个位置的数据绑定到 Holder 持有的 itemView。要区分两种复用:

  • View 复用: RecyclerView 把离屏 itemView 放入缓存或回收池,未来交给另一个位置使用。
  • **ViewHolder 复用:**Holder 是对一个 itemView 及其子控件引用的包装,通常随该 itemView 一起被回收和再次绑定。

所以"一个位置永远对应一个 Holder"是错误模型;绑定时必须覆盖旧状态、监听器和可见性,不能把 position 当成永久身份。Adapter 不应自己持有 Activity 来控制生命周期;它应接收数据、回调和必要的短生命周期依赖。

四个常见坑

  1. **把继承当默认复用。**为了借一个方法而继承 ViewActivity 或 Adapter,会同时继承状态与生命周期契约。先抽接口、委托对象或组合组件;只有确实是该类型时再继承。
  2. **长生命周期对象持有 Activity Context。**单例、缓存、后台任务或静态策略保留 Activity 会阻止页面释放。需要应用级资源时使用 application context;需要主题、窗口或页面能力时只在短生命周期范围内使用 Activity。
  3. **手动调用生命周期方法。**直接调用 onCreateonDestroyonMeasureonDraw 会绕开系统状态机。让 Framework 管理生命周期;通过公开 API 表达需求。
  4. **混淆 View 与 ViewHolder 的复用。**滚动复用不是数据"自动正确"。每次绑定都恢复文本、图片占位、选中状态和监听器,异步结果还要确认它仍对应当前数据项。

阅读源码时的检查表

  • View 的构造函数拿到的是哪一种 Context?它是否需要主题,是否会逃逸到长生命周期对象?
  • ViewGroup 在何处添加、移除和测量子 View?父子关系谁拥有?
  • LayoutInflater.inflate 的 root 与 attachToRoot 参数决定了谁接收根 View?
  • OnClickListener 是谁注册、谁持有、何时注销或覆盖?结果最终进入哪个状态拥有者?
  • RecyclerView.AdapteronCreateViewHolderonBindViewHolder 分别处理创建和绑定了吗?
  • 自定义 View 的重绘请求是否使用 invalidaterequestLayout,而非直接调用回调?

面试表达与下一站

面试表达: "我把 Android UI 看成对象树:ViewGroup 通过组合拥有子 View,View 通过继承提供统一的测量、布局、绘制和事件契约;变化的业务行为用接口和策略实现多态。Activity 负责页面生命周期与协作,不让长生命周期对象持有它的 Context;RecyclerView 的 Adapter 负责创建与绑定,回收由 RecyclerView 管理。"

当 UI 对象边界明确后,下一步是数据建模:把网络 DTO、领域模型和 UI state 分开,让 Adapter 和 View 接收稳定、可渲染的数据,而不是把所有业务状态塞进控件或 Activity。

相关推荐
iPad协议个微协议2 小时前
# 微信自动营销系统如何基于 WechatApi 做事件驱动设计
android·微信·企业微信·个人开发·wechatapi·微信个人号开发
AirDroid_cn2 小时前
隐私沙盒:第三方应用读取剪切板内容如何拦截?
android·gitee
2501_915918414 小时前
怎么把 Python 写的 Flet 应用打包成 iOS App 并上架 App Store?
android·ios·小程序·https·uni-app·iphone·webview
邪修king6 小时前
Re:Linux系统篇(二十六):文件系统(二):Ext 文件系统底层详解:从 inode、块组到软硬链接,结合 Windows 讲透文件管理本质
android·java·linux
又见情义6 小时前
RK3568 Android 13 版本号管理实战:基于 ROCKCHIP_BUILD_NUMBER 的定制化方案
android
终端安全笔记6 小时前
iOS 27 强制 TLS 1.2:租赁设备的注册链路会在哪一环断
android·网络·安全·ios·智能手机
mmsx6 小时前
MapLibre 实战 11|用户说"我的地块丢了":一个 sealed class 图层模型,和四个让我重构三版的坑
android·前端·开源
恋猫de小郭8 小时前
CPF-Flutter 社区提出折叠场景分栏(平行视界) 方案
android·前端·flutter
事圆则缓9 小时前
Java 常用 API 如何建模 Android 订单业务
android