本系列为小说《逆袭西二旗》的技术讲解,用于详细说明剧情里涉及的开发细节。

Android UI 是开发的核心,提供了设计屏幕、布局、控件等组件的工具,构成应用的结构与交互方式。尽管开发的其他方面也很重要,但 UI 系统定义了用户的第一印象,是打造美观、响应迅速应用的关键。
如今 Jetpack Compose 生态发展迅速,已广泛用于生产环境的 Android UI 开发,堪称 Android UI 的未来。新手甚至可以直接从 Jetpack Compose 入手,再慢慢了解传统视图系统。
然而,部分大型企业仍重度依赖传统 View 系统(迁移到 Compose 存在挑战)。若你要面试这类企业,扎实掌握传统 View 系统依然必要。
在 Android View 中,理解 View 的生命周期和常用 UI 组件是构建高性能应用的关键------因为所有 UI 元素默认运行在主线程。此外,理解 Android UI 系统的核心原则(如窗口、尺寸单位),能帮助开发者做出合理的应用构建决策。
很多场景下,为了满足设计团队的复杂需求,必须自定义 View。因此,深入理解 Android UI 系统是高效开发的关键技能,也是成为优秀 Android 开发者的必经之路。
View 的生命周期
在 Android 中,View 生命周期指 View (如TextView、Button)从创建、绑定到Activity/Fragment、显示在屏幕,再到最终销毁/解绑的整个过程。理解 View 生命周期有助于开发者管理 View 的初始化、渲染、销毁,以及在合适时机释放资源。
面试问题
若要创建一个需执行高开销初始化(如加载图片、设置动画)的自定义 View ,应在 View 生命周期的哪个阶段初始化这些资源?如何确保资源被正确释放以避免内存泄漏?
应用存在复杂 UI,动态创建的 View 出现性能问题时,如何优化 onMeasure() 和 onLayout() 方法,在保证响应性的同时提升渲染效率?
图解生命周期

- View创建(onAttachedToWindow) :
View被实例化的阶段(通过代码创建或加载XML布局)。在此阶段需完成初始设置,如绑定监听器、处理数据;当View被添加到父容器并准备渲染时,onAttachedToWindow()方法会被触发。 - 布局阶段(onMeasure、onLayout) :核心是计算
View的尺寸和位置。onMeasure()方法会根据布局参数和父容器约束,确定View的宽高;测量完成后,onLayout()方法进一步确定View在父容器中的具体位置,最终明确其在屏幕上的显示区域。 - 绘制阶段(onDraw) :当
View的尺寸和位置确定后,onDraw()方法会将View的内容(如文本、图片)渲染到Canvas上。自定义View时,可重写此方法实现个性化绘制逻辑。 - 事件处理(onTouchEvent、onClick) :对于交互式
View(如Button),此阶段负责处理触摸、点击等用户输入事件。通过这些方法定义View的响应逻辑,实现与用户的交互。 - View解绑(onDetachedFromWindow) :当
View从屏幕和父容器(如Activity/Fragment销毁)中移除时,onDetachedFromWindow()方法会被触发。此阶段需重点清理资源,如注销监听器,避免内存泄漏。 - View销毁 :当
View不再被使用时,会被系统垃圾回收。开发者需确保释放所有关联资源(如事件监听器、后台任务),以优化性能并杜绝内存泄漏问题。
总结
View 的生命周期涵盖创建、测量、布局、绘制、事件处理、解绑等关键阶段,完整覆盖了 View 在应用中的显示与使用全过程。
进阶:findViewTreeLifecycleOwner
findViewTreeLifecycleOwner() 会遍历 View 树层级,查找并返回最近的 LifecycleOwner(通常是 Activity、Fragment 或实现了 LifecycleOwner 接口的自定义组件);若未找到匹配的 LifecycleOwner,则返回 null。
为什么使用它
该方法在自定义 View 或第三方组件中极具实用性------这类组件往往需要与 LifecycleObserver、LiveData 等生命周期感知型元素交互,但又无需显式依赖宿主 Activity/Fragment。通过它, View 能便捷访问宿主的生命周期,具体优势如下:
- 确保生命周期感知型组件与正确的生命周期绑定;
- 生命周期结束时自动清理观察者,避免内存泄漏。
下面是一个自定义 View 绑定 LifecycleObserver 的例子
kotlin
class CustomView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
init {
val observer = object : LifecycleObserver {
// 核心逻辑
}
val lifecycleOwner = findViewTreeLifecycleOwner()
lifecycleOwner?.lifecycle?.addObserver(observer) ?: run {
Log.d("CustomView", "未找到当前View对应的LifecycleOwner")
}
}
}
此示例中,自定义 CustomView 会动态绑定 View 树中最近的 LifecycleOwner,确保LifecycleObserver 与合适的生命周期关联,保障资源的合理管理。
关键使用场景
- 自定义View :让自定义
View中的生命周期感知型组件能够观察生命周期变化,精准管理资源; - 第三方库:使第三方 UI 组件无需显式依赖生命周期管理,即可与生命周期感知型资源交互;
- 解耦逻辑 :让
View自主查找View树中的LifecycleOwner,减少View与宿主组件的耦合度,提升代码灵活性。
局限性
尽管 findViewTreeLifecycleOwner() 实用性强,但它依赖 View 树中存在 LifecycleOwner。若未找到,方法会返回 null,需妥善处理该情况,避免应用崩溃或出现异常行为。
总之。
View 的 findViewTreeLifecycleOwner() 方法是获取 View 树中最近 LifecycleOwner 的实用工具,简化了自定义 View 或第三方库中生命周期感知型组件的使用,既保证了正确的生命周期管理,又降低了 View 与宿主组件的耦合度。
View 和 ViewGroup 的区别
View 和 ViewGroup 是实现 Android UI 组件的基础概念,均属于 android.view 包,但在 UI 层级中承担的职责截然不同。
面试问题
解释 requestLayout()、invalidate() 和 postInvalidate() 在 View 生命周期中的作用,以及各自的适用场景。
View 生命周期与 Activity 生命周期有什么区别?为什么理解二者对实现高效 UI 渲染至关重要?
什么是 View
View 是屏幕上单个的矩形 UI 元素,是所有 UI 组件(如Button、TextView、ImageView、EditText)的基类。每个 View 都负责在屏幕上绘制内容,并处理用户交互(如触摸、按键事件)。
kotlin
val textView = TextView(context).apply {
text = "Hello, World!"
textSize = 16f
setTextColor(Color.BLACK)
}
补充提示
View 系统是 Android 开发的核心基础,支撑着整个 UI 框架(负责渲染、更新 UI 组件,以及管理用户交互的事件系统)。查看 AOSP 中 View.java 的实现可知,其代码量超3.4万行------如此高的复杂度,凸显了创建和管理 View 实例涉及的大量处理工作。因此,不必要的 View 创建会直接影响应用性能,导致内存占用增加、渲染速度变慢。
优化性能的核心方法:尽量避免创建不必要的 View 实例;减少布局层级(保持UI层级扁平、高效),以此提升应用流畅度、响应速度,降低资源消耗。
什么是 ViewGroup
ViewGroup 是容纳多个 View 或其他 ViewGroup 的容器,是布局(如 LinearLayout、RelativeLayout、ConstraintLayout、FrameLayout)的基类。它主要负责管理子 View 的布局和位置,定义子 View 的测量与绘制规则。
kotlin
val linearLayout = LinearLayout(context).apply {
orientation = LinearLayout.VERTICAL
addView(TextView(context).apply { text = "Child 1" })
addView(TextView(context).apply { text = "Child 2" })
}
补充提示
ViewGroup 继承自 View ,同时实现了 ViewParent 和 ViewManager 接口。由于 ViewGroup 作为其他 View 的容器,其内部实现比标准 View 更复杂、更耗资源。过度嵌套布局(如多层 LinearLayout )会严重影响应用性能。
ViewParent 接口定义了父 View 作为 View 对象容器的核心职责,包括管理布局测量、监听触摸事件等;ViewManager 接口提供了动态添加/移除子 View 的方法。
正因为 ViewGroup 需要执行额外的布局计算并管理多个子 View ,其绘制和测量开销比单个 View 更大。
核心区别
| 对比维度 | View | ViewGroup |
|---|---|---|
| 用途 | 单个 UI 元素,用于显示内容或与用户交互 | 容器组件,用于组织和管理多个子 View |
| 层级 | UI 层级中的叶节点,不能包含其他 View |
UI 层级中的分支节点,可包含多个子 View 或 ViewGroup |
| 布局行为 | 自身尺寸和位置由布局参数直接定义 | 通过特定布局规则(如 LinearLayout 的垂直/水平排列)定义子 View 的尺寸和位置 |
| 事件处理 | 仅处理自身的触摸、按键等事件 | 通过 onInterceptTouchEvent 等方法拦截并管理子 View 的事件 |
| 性能考量 | 性能开销较低,无额外子 View 管理成本 |
因层级结构增加渲染复杂度;过度嵌套会导致渲染时间变长、UI 更新变慢等性能问题 |
总结
View 是所有 UI 元素的基础,ViewGroup 是组织管理多个 View 的容器,二者共同构成了 Android 复杂用户界面的核心构建块。理解它们的角色与区别,是优化布局结构、确保应用具备响应式用户体验的关键。
ViewStub

ViewStub 是 Android 中一种轻量级的不可见占位 View,用于延迟加载布局,直到明确需要显示它时才进行加载。它的核心价值在于优化性能------避免加载应用生命周期中可能无需展示的 View,从而减少初始化阶段的系统开销。
面试问题
ViewStub加载后会发生什么?
它对 View 树的布局性能和内存使用有何具体影响?
核心特性
- 轻量级 :
ViewStub在加载目标布局前,仅作为占位符存在,占用极小的内存空间,且不占用实际布局位置; - 延迟加载 :目标布局资源仅在调用
inflate()方法,或ViewStub被设置为可见时才会加载; - 单次使用 :布局加载完成后,
ViewStub会从View树中被替换为加载的布局,无法重复使用。
常见使用场景
- 条件布局:适用于需根据条件显示的布局(如错误提示、加载进度条、可选功能模块的 UI 元素);
- 减少初始加载时间 :延迟加载复杂或耗资源的
View(如大数据列表、图表),提升Activity/Fragment的初始加载速度; - 动态UI元素:仅在用户触发特定操作(如点击按钮)时,才向屏幕添加动态内容,优化内存使用效率。
如何使用 ViewStub
首先,在 XML 布局中定义 ViewStub
在 XML 布局文件中添加 ViewStub,并通过 android:layout 属性指定需要延迟加载的布局资源:
xml
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<!-- 常规显示的View -->
<TextView
android:id="@+id/tv_main"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Main Content" />
<!-- 占位ViewStub,关联目标布局optional_content -->
<ViewStub
android:id="@+id/viewStub"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout="@layout/optional_content" />
</LinearLayout>
然后,在代码中加载 ViewStub。
在 Activity 或 Fragment 中,通过 findViewById 获取 ViewStub 实例,在需要时调用 inflate() 方法加载目标布局:
kotlin
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val viewStub = findViewById<ViewStub>(R.id.viewStub)
// 仅在需要时加载目标布局
val inflatedView = viewStub.inflate()
// 访问加载布局中的子View并设置内容
val optionalTextView = inflatedView.findViewById<TextView>(R.id.optionalText)
optionalTextView.text = "Inflated Content"
}
}
优势
- 性能优化 :延迟加载非必需的
View,减少应用初始化时的内存占用,提升初始渲染速度; - 简化布局管理 :无需手动添加或移除
View,即可灵活管理可选 UI 元素,降低布局维护成本; - 易用性强 :API 简洁直观,支持
XML直接集成,是开发者优化 UI 性能的便捷工具。
局限性
- 单次使用 :布局加载后,
ViewStub会从View树中移除,无法再次触发加载操作; - 功能受限 :作为纯占位
View,加载前无法处理用户交互,也不能执行复杂的业务逻辑。
总结
ViewStub 是 Android 中优化 UI 性能的实用工具,通过延迟加载布局的核心机制,有效减少内存使用,尤其适用于条件布局或非必需 View 的场景,能显著提升应用响应速度。合理使用 ViewStub,可帮助开发者打造更高效、流畅的用户体验。
自定义 View
实现自定义 View 是创建具备特定外观和行为 UI 组件的核心方式,尤其适用于需在多屏幕中复用的场景。
自定义 View 能统一视觉呈现和交互逻辑,保证应用内 UI 风格的一致性和代码的可维护性;同时还能封装复杂 UI 逻辑、提升组件复用率、简化项目整体结构。
面试问题
如何在自定义 View 中高效使用自定义属性,同时确保 XML 布局的向后兼容性?
创建自定义 View 类
若应用需要标准 UI 组件无法实现的独特设计元素(如自定义形状、特殊动画效果),则必须通过自定义 View 实现。在 Android 开发中,可通过以下步骤结合 XML 创建自定义 View 。
首先,定义一个继承自基础 View 类(如 View、ImageView、TextView)的新类,然后根据需求重写必要的构造函数和核心方法(如 onDraw()、onMeasure()、onLayout()),以实现自定义行为:
kotlin
class CustomCircleView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val paint = Paint().apply {
color = Color.RED
style = Paint.Style.FILL
}
// 在View中心绘制红色圆形
canvas.drawCircle(width / 2f, height / 2f, width / 4f, paint)
}
}
在 XML 布局中使用自定义 View
创建自定义 View 类后,可在 XML 布局文件中直接引用它(需使用完整包名作为 XML 标签)。后续还可通过自定义属性向该 View 传递配置参数(详见下面步骤):
xml
<com.example.CustomCircleView
android:layout_width="200dp"
android:layout_height="200dp"
android:layout_gravity="center" />
添加自定义属性(可选)
若需支持从 XML 布局中配置自定义 View 的属性(如颜色、尺寸),需在res/values文件夹的attrs.xml中定义自定义属性:
xml
<resources>
<declare-styleable name="CustomCircleView">
<attr name="circleColor" format="color" /> <!-- 圆形颜色属性 -->
<attr name="circleRadius" format="dimension" /> <!-- 圆形半径属性 -->
</declare-styleable>
</resources>
在自定义 View 的构造函数中,通过 context.obtainStyledAttributes() 获取 XML 中配置的自定义属性值:
kotlin
class CustomCircleView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
var circleColor: Int = Color.RED // 默认红色
var circleRadius: Float = 50f // 默认半径50px
init {
val typedArray = context.obtainStyledAttributes(attrs, R.styleable.CustomCircleView)
try {
// 获取XML中配置的circleColor,未配置则使用默认值Color.RED
circleColor = typedArray.getColor(
R.styleable.CustomCircleView_circleColor,
Color.RED
)
// 获取XML中配置的circleRadius,未配置则使用默认值50f
circleRadius = typedArray.getDimension(
R.styleable.CustomCircleView_circleRadius,
50f
)
} finally {
// 回收TypedArray,避免资源泄漏
typedArray.recycle()
}
}
}
处理布局测量(可选)
若需自定义 View 的尺寸计算逻辑(如固定宽高比例、自适应父容器),可重写 onMeasure() 方法,自定义测量规则:
kotlin
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val desiredSize = 200 // 期望默认尺寸200px
// 根据父容器约束和期望尺寸,计算最终宽高
val width = resolveSize(desiredSize, widthMeasureSpec)
val height = resolveSize(desiredSize, heightMeasureSpec)
// 设置测量后的宽高
setMeasuredDimension(width, height)
}
补充提示
当应用需要适配特定需求的可复用、专用组件,或需实现标准 View 无法完成的动画、计算逻辑、自定义属性等功能时,自定义 View 是最优选择。
总结
通过 XML 结合代码的方式在 Android 中实现自定义 View ,能为 UI 设计带来极高的灵活性。开发者可通过自定义 View 系统和 Compose 创建各类个性化控件。
进阶:谨慎使用 @JvmOverloads
@JvmOverloads 是 Kotlin 中用于简化 Kotlin 与 Java 互操作性的注解,它会为 Kotlin 函数或类自动生成多个重载方法/构造函数(尤其适用于 Kotlin 的默认参数------Java 原生不支持默认参数特性)。
使用 @JvmOverloads 时,Kotlin 编译器会在编译后的字节码中生成多个方法/构造函数签名,以覆盖所有带默认值参数的组合。但在实现自定义 View 时,若不谨慎使用 @JvmOverloads,可能会意外覆盖默认 View 样式,导致自定义 View 丢失预期的系统样式------这一问题在扩展 Button、TextView 等预定义样式的 Android View 时尤为突出。
我们看看下面这个示例,实现自定义 TextInputEditText:
kotlin
class ElasticTextInputEditText @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : TextInputEditText(context, attrs, defStyleAttr) {
// 自定义业务逻辑
}
在此示例中,将 ElasticTextInputEditText 作为常规 TextInputEditText 使用时,可能会出现意外行为或崩溃------原因是 defStyleAttr 被重写为 0,导致自定义 View 无法继承 TextInputEditText 的默认样式。
当从 XML 加载 View 时,系统会调用双参数构造函数(包含 Context 和 AttributeSet 的那个),而该双参数构造函数会进一步调用三参数构造函数:
java
public ElasticTextInputEditText(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
三参数构造函数的 defStyleAttr 参数,在自定义实现中常被默认设为 0,但根据 Android 官方文档描述,其用途为:
从
XML加载View时,应用主题属性中该类的特定基础样式。View的此构造函数允许子类在inflate时使用自身的基础样式------例如,Button的构造函数会调用此版本的父类构造函数,并传入com.android.internal.R.attr.buttonStyle作为defStyleAttr,使Button的样式可通过主题进行修改。
若省略正确的 defStyleAttr 值(如 R.attr.editTextStyle),会导致自定义 View 无法正确继承系统或主题定义的基础样式,在 XML inflate 时出现样式不一致或异常行为。
TextInputEditText 的内部实现
查看 TextInputEditText 的内部源码可知,它会主动传入 R.attr.editTextStyle 作为 defStyleAttr:
java
public class TextInputEditText extends AppCompatEditText {
private final Rect parentRect = new Rect();
public TextInputEditText(@NonNull Context context) {
this(context, null);
}
public TextInputEditText(@NonNull Context context, @Nullable AttributeSet attrs) {
this(context, attrs, R.attr.editTextStyle);
}
public TextInputEditText(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
}
在 TextInputEditText 的实现中,双参数构造函数会将 android.R.attr.editTextStyle 作为第三个参数(defStyleAttr)传递给父类,确保样式的正确继承。
修复方法
要解决样式继承问题,需在自定义 View 的构造函数中,指定正确的 defStyleAttr 默认值(如 R.attr.editTextStyle):
kotlin
class ElasticTextInputEditText @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = android.R.attr.editTextStyle
) : TextInputEditText(context, attrs, defStyleAttr) {
private boolean mTextInputLayoutFocusedRectEnabled;
// 自定义业务逻辑
}
通过显式将 androidx.appcompat.R.attr.editTextStyle 设为 defStyleAttr 的默认值,可确保自定义 View 在 XML inflate 时,正确继承 TextInputEditText 的预期基础样式,与原生 TextInputEditText 的行为保持一致。
Canvas

Canvas 是 Android 自定义绘制的核心组件,提供了直接在屏幕或其他绘制表面(如 Bitmap)上渲染图形的接口。开发者通过 Canvas 可完全控制绘制过程,常用于创建自定义 View 、复杂动画和个性化视觉效果。
实战问题
如何创建一个自定义 View ,以渲染标准库不支持的复杂形状或 UI 元素?
你会使用哪些 Canvas 方法和相关 API?
工作原理
Canvas 类代表一个 2D 绘制表面,开发者可在其上绘制形状、文本、图片等内容。
它与 Paint 类紧密配合------Paint 负责定义绘制内容的外观(包括颜色、样式、笔触宽度、字体大小等);当重写自定义 View 的 onDraw() 方法时,系统会自动传入一个 Canvas 对象,供开发者定义具体的绘制内容。
基础绘制示例
kotlin
class CustomView(context: Context) : View(context) {
private val paint = Paint().apply {
color = Color.BLUE // 绘制颜色:蓝色
style = Paint.Style.FILL // 填充样式:实心
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
// 在自定义View的中心绘制一个半径为100px的蓝色圆形
canvas.drawCircle(width / 2f, height / 2f, 100f, paint)
}
}
在此示例中,onDraw() 方法通过系统传入的 Canvas 对象,在自定义 View 的中心位置绘制了一个蓝色实心圆形。
Canvas 的常用操作
Canvas 支持多种绘制操作,满足不同场景的需求:
- 绘制形状 :通过
drawCircle()(圆形)、drawRect()(矩形)、drawLine()(线条)、drawOval()(椭圆)等方法,绘制基础几何形状; - 绘制文本 :通过
drawText()方法,按指定坐标、字体大小和颜色渲染文本内容; - 绘制图片 :通过
drawBitmap()方法,将Bitmap对象渲染到指定位置; - 绘制自定义路径 :结合
Path对象和drawPath()方法,绘制不规则的复杂形状(如多边形、曲线)。
变换操作
Canvas 支持缩放、旋转、平移等坐标系变换操作,便于绘制复杂场景,具体包括:
- 平移 :通过
canvas.translate(dx, dy)将Canvas的原点(0,0)移动到新坐标(dx, dy); - 缩放 :通过
canvas.scale(sx, sy)按比例(sx为x轴缩放比,sy为y轴缩放比)缩放后续绘制的内容; - 旋转 :通过
canvas.rotate(degrees)将Canvas按指定角度(顺时针为正方向)旋转。
需注意:这些变换操作是累积的,会影响后续所有的绘制行为。若需恢复原始坐标系,可结合 canvas.save() 和 canvas.restore() 方法实现。
使用场景
Canvas 在需要高级自定义图形的场景中尤为实用,典型场景包括:
- 自定义View:绘制标准控件无法实现的独特 UI 组件(如自定义进度条、仪表盘);
- 游戏开发:精确控制游戏中的图形渲染(如角色、道具、场景元素);
- 图表与示意图:以自定义格式可视化数据(如折线图、饼图、流程图);
- 图像处理:通过代码修改图片内容(如添加水印、裁剪、颜色调整)或合成多张图片。
总结
Canvas 为 Android 开发者提供了在屏幕上渲染自定义图形的灵活方式。
通过其丰富的绘制方法(形状、文本、图片)和变换操作,开发者可创建多样化的视觉效果和自定义体验,广泛应用于需要高级图形能力的自定义 View 开发中。
重绘
重绘(Invalidation )是 Android View 系统中 UI 更新的基础机制,指标记 View 需要重绘的过程。当 View 被标记为需要重绘后,系统会在下次绘制周期(Vsync 信号触发)中刷新该区域的屏幕,确保用户能看到最新的 UI 状态。
面试问题
invalidate() 方法的工作原理是什么?它与 postInvalidate() 有何区别?请分别给出适合使用它们的真实场景。
若需要从后台线程更新 UI 元素,如何确保重绘操作安全地在主线程执行?
工作原理
当开发者调用 invalidate() 或 postInvalidate() 等方法时,会触发重绘流程:系统首先将目标 View 标记为"脏(dirty)"状态(表示该 View 的 UI 已发生变化,需要重绘);随后在下一帧绘制时,系统会扫描所有标记为"脏"的 View ,将其纳入绘制流程,重新渲染该 View 的视觉呈现。
例如,当 View 的位置、尺寸、颜色或内容等属性发生变化时,通过重绘机制可确保这些变化及时反映在屏幕上。
重绘的核心方法
- invalidate() :标记单个
View为重绘,告知系统在下一次布局绘制流程中重绘该View。该方法不会立即触发重绘,而是将重绘请求加入消息队列,等待下一帧执行; - invalidate(Rect dirty) :
invalidate()的重载版本,允许指定View中需要重绘的特定矩形区域(dirty参数)。通过仅重绘局部区域,可减少不必要的绘制开销,优化性能; - postInvalidate() :专门用于在非 UI 线程中标记
View重绘。该方法会自动将重绘请求投递到主线程的消息队列,确保重绘操作在 UI 线程安全执行,避免线程安全问题。
使用 invalidate
以下是自定义 View 的示例:当 View 的状态(如圆形半径)变化时,通过调用 invalidate() 标记 View 重绘,触发重绘以更新 UI:
kotlin
class CustomView(context: Context) : View(context) {
private var circleRadius = 50f // 圆形初始半径50px
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
// 绘制当前半径的红色圆形
canvas.drawCircle(
width / 2f, height / 2f, circleRadius,
Paint().apply { color = Color.RED }
)
}
// 增加圆形半径的方法
fun increaseRadius() {
circleRadius += 20f
invalidate() // 标记View重绘,触发重绘
}
}
在上述示例中,调用 increaseRadius() 方法后,圆形半径增大,invalidate() 会标记 View 为重绘,系统在下一帧会调用 onDraw() 方法,绘制更新后的圆形。
最佳实践
- 局部更新优先使用 invalidate(Rect dirty) :仅当
View的特定区域发生变化时(如文本内容更新、局部颜色变化),使用该方法指定重绘区域,避免全区域重绘造成的性能浪费; - 避免频繁/不必要的 invalidate() 调用 :在动画循环或高频数据刷新场景中,过度调用
invalidate()会导致绘制压力过大,引发性能瓶颈,需合理控制调用频率; - 非 UI 线程必须使用 postInvalidate() :Android 要求所有 UI 操作(包括重绘)必须在主线程执行,因此在后台线程(如网络请求线程、子线程)中需通过
postInvalidate()发起重绘请求,确保线程安全。
总结
重绘是 Android 渲染流水线中的关键概念,是 UI 更新能够直观呈现的核心保障。通过 invalidate()、invalidate(Rect dirty) 或 postInvalidate() 等方法,开发者可高效刷新 View,同时保持应用的流畅性能。合理运用重绘机制,能有效减少不必要的重绘操作,打造更优化、响应更迅速的应用体验。