MST
星途 面试题库

面试题:Kotlin自定义View绘制性能优化之减少过度绘制

在Kotlin自定义View中,如何通过优化布局和绘制顺序来减少过度绘制?请举例说明具体的实现方式。
40.1万 热度难度
编程语言Kotlin

知识考点

AI 面试

面试题答案

一键面试
  1. 优化布局
    • 减少布局嵌套
      • 原理:布局嵌套过多会导致系统在测量、布局和绘制时花费更多时间和资源,增加过度绘制的可能性。例如,原本使用多层LinearLayout嵌套来实现界面,可尝试使用ConstraintLayout等更灵活且嵌套层级少的布局方式。
      • 示例:假设原本有一个界面,使用了三层LinearLayout嵌套来排列一些TextView和ImageView。
        <!-- 原始多层嵌套布局 -->
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical">
            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:orientation="horizontal">
                <TextView
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="标题"/>
                <ImageView
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:src="@drawable/icon"/>
            </LinearLayout>
            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:orientation="vertical">
                <TextView
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="内容1"/>
                <TextView
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="内容2"/>
            </LinearLayout>
        </LinearLayout>
        
        改为ConstraintLayout后:
        <!-- 使用ConstraintLayout优化后的布局 -->
        <androidx.constraintlayout.widget.ConstraintLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent">
            <TextView
                android:id="@+id/title_text"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="标题"
                android:layout_marginStart="8dp"
                android:layout_marginTop="8dp"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toTopOf="parent"/>
            <ImageView
                android:id="@+id/icon_image"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:src="@drawable/icon"
                app:layout_constraintStart_toEndOf="@id/title_text"
                app:layout_constraintTop_toTopOf="parent"
                android:layout_marginStart="8dp"
                android:layout_marginTop="8dp"/>
            <TextView
                android:id="@+id/content1_text"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="内容1"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toBottomOf="@id/title_text"
                android:layout_marginStart="8dp"
                android:layout_marginTop="8dp"/>
            <TextView
                android:id="@+id/content2_text"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="内容2"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toBottomOf="@id/content1_text"
                android:layout_marginStart="8dp"
                android:layout_marginTop="8dp"/>
        </androidx.constraintlayout.widget.ConstraintLayout>
        
    • 使用ViewStub
      • 原理:ViewStub是一个轻量级的View,在初始时不占用布局空间,只有在调用inflate()方法时才会将其指定的布局加载到界面中。这对于一些在特定条件下才需要显示的布局非常有用,可以避免一开始就绘制不必要的视图,从而减少过度绘制。
      • 示例:假设在自定义View中有一个隐藏的提示布局,只有在用户点击某个按钮时才显示。
        • 首先在布局文件中定义ViewStub:
        <ViewStub
            android:id="@+id/hint_view_stub"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:inflatedId="@+id/hint_view"
            android:layout="@layout/hint_layout"/>
        
        • 然后在代码中,当用户点击按钮时加载ViewStub:
        class CustomView : View {
            private lateinit var hintViewStub: ViewStub
            constructor(context: Context) : super(context)
            constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
            constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
        
            init {
                hintViewStub = findViewById(R.id.hint_view_stub)
                val button = Button(context)
                button.text = "显示提示"
                button.setOnClickListener {
                    if (!hintViewStub.isInflated) {
                        hintViewStub.inflate()
                    }
                }
                addView(button)
            }
        }
        
  2. 优化绘制顺序
    • 合理设置View的visibility
      • 原理:如果一个View不可见(View.GONE),系统不会对其进行绘制,这样可以避免不必要的绘制操作,减少过度绘制。
      • 示例:假设自定义View中有一个状态指示View,在某些状态下不需要显示。
        class CustomView : View {
            private lateinit var statusIndicator: View
            constructor(context: Context) : super(context)
            constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
            constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
        
            init {
                statusIndicator = View(context)
                // 设置初始不可见
                statusIndicator.visibility = View.GONE
                addView(statusIndicator)
            }
        
            fun updateStatus(isVisible: Boolean) {
                if (isVisible) {
                    statusIndicator.visibility = View.VISIBLE
                } else {
                    statusIndicator.visibility = View.GONE
                }
            }
        }
        
    • 使用Canvas的裁剪功能
      • 原理:通过CanvasclipRect()等裁剪方法,可以限制绘制区域,只在指定区域内进行绘制,避免在不需要的区域绘制,从而减少过度绘制。
      • 示例:假设要在自定义View中绘制一个圆形进度条,只在圆形区域内绘制进度。
        class CircularProgressView : View {
            private val paint = Paint()
            private var progress = 0f
            constructor(context: Context) : super(context)
            constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
            constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
        
            override fun onDraw(canvas: Canvas) {
                super.onDraw(canvas)
                val centerX = width / 2f
                val centerY = height / 2f
                val radius = min(width, height) / 2f - 10
                // 裁剪出圆形区域
                val path = Path()
                path.addCircle(centerX, centerY, radius, Path.Direction.CW)
                canvas.clipPath(path)
                // 绘制进度条
                paint.color = Color.BLUE
                canvas.drawArc(
                    RectF(centerX - radius, centerY - radius, centerX + radius, centerY + radius),
                    -90f,
                    progress * 360f,
                    true,
                    paint
                )
            }
        
            fun setProgress(progress: Float) {
                this.progress = progress
                invalidate()
            }
        }