View Lifecycle — nó là gì, các quá trình onMeasure onLayout onDraw

Tác giả: IT Sectr Đã đăng: 2026-03-05 Thời gian đọc: 10 phút

View Lifecycle — trình tự các phương thức mà Android gọi để vẽ và vẽ lại một phần tử giao diện người dùng (View) trên màn hình. Không giống như Activity hay Fragment, View là một thành phần nhẹ không có vòng đời mở rộng, nhưng trải qua một quá trình ba giai đoạn nghiêm ngặt: onMeasure (đo lường), onLayout (định vị), onDraw (vẽ). Hiểu về View Lifecycle là cần thiết để tạo View tùy chỉnh, tối ưu hiệu suất và giải quyết các vấn đề vẽ. Theo Google, View tùy chỉnh làm tăng tốc UI lên 15–40% so với kết hợp ViewGroups lồng nhau tiêu chuẩn khi được triển khai đúng cách. Tài liệu Android về View tùy chỉnh mô tả onMeasure, onLayout và onDraw là ba trụ cột của View Lifecycle.

Những điểm chính

  • View Lifecycle bao gồm ba giai đoạn: onMeasure (kích thước), onLayout (vị trí), onDraw (vẽ) — và được kích hoạt bởi invalidate() hoặc requestLayout().
  • onMeasure tính toán chiều rộng và chiều cao của View dựa trên MeasureSpec (AT_MOST, EXACTLY, UNSPECIFIED).
  • onLayout sắp xếp View con bên trong ViewGroup, xác định tọa độ left, top, right, bottom của chúng.
  • onDraw hiển thị nội dung View lên Canvas: nền, văn bản, hình dạng, ảnh.
  • View Lifecycle không chính xác là nguyên nhân chính gây ra các vấn đề hiệu suất UI (jank, frame bị rơi) và vấn đề phân cấp.

View Lifecycle — nó là gì trong Android

View Lifecycle là quá trình mà một View Android (và ViewGroup) trải qua để hiển thị chính nó trên màn hình. Không giống Activity hay Fragment, View không có onStart/onStop/onDestroy — “đời” của nó bao gồm một quá trình theo chu kỳ của đo lường, định vị và vẽ. Chu kỳ này được kích hoạt mỗi khi một View cần được hiển thị hoặc vẽ lại.

Ba giai đoạn của View Lifecycle:

  • onMeasure(int widthMeasureSpec, int heightMeasureSpec) — xác định kích thước mong muốn của View. Hệ thống truyền MeasureSpec — một chỉ dẫn về kích thước được phép (giá trị chính xác, tối đa hoặc không giới hạn).
  • onLayout(boolean changed, int left, int top, int right, int bottom) — định vị View và các con của nó trên màn hình. Đối với View, nó xác định ranh giới riêng; đối với ViewGroup, nó định vị các phần tử con.
  • onDraw(Canvas canvas) — vẽ nội dung View lên Canvas được cung cấp. Hệ thống cung cấp Canvas dịch các lệnh thành bitmap hoặc GPU.

Chu kỳ View Lifecycle đầy đủ cũng bao gồm các phương thức liên quan đến việc gắn View vào cửa sổ: onAttachedToWindow (View được gắn vào cửa sổ, có tăng tốc HW) và onDetachedFromWindow (View bị tách rời, tài nguyên được giải phóng). Các phương thức này được gọi một lần trong vòng đời View và quan trọng để đăng ký/hủy hoạt ảnh và cảm biến.

Theo Android Performance Blog, 65% các vấn đề hiệu suất UI (jank, frame bị rơi) liên quan đến việc triển khai không đúng onMeasure và onDraw: ghi đè quá mức, gọi requestLayout() không cần thiết, tạo đối tượng trong onDraw.

onMeasure: đo kích thước View

onMeasure — giai đoạn quan trọng nhất và phức tạp nhất của View Lifecycle. Ở giai đoạn này, Android xác định View sẽ chiếm bao nhiêu không gian trên màn hình. Hệ thống truyền MeasureSpec — các chỉ dẫn được đóng gói trong int bao gồm một chế độ và kích thước.

Ba chế độ MeasureSpec:

Chế độHằng sốÝ nghĩaVí dụ
EXACTLYMeasureSpec.EXACTLYKích thước chính xác do cha xác định (match_parent hoặc chiều rộng cố định)width=400dp → MeasureSpec(400, EXACTLY)
AT_MOSTMeasureSpec.AT_MOSTView có thể có kích thước tối đa được chỉ định (wrap_content)width ≤ 400dp → MeasureSpec(400, AT_MOST)
UNSPECIFIEDMeasureSpec.UNSPECIFIEDKhông có giới hạn — View có thể có kích thước bất kỳ (ScrollView, RecyclerView)chiều rộng không giới hạn → MeasureSpec(0, UNSPECIFIED)

Việc triển khai onMeasure nên:

  • Gọi setMeasuredDimension(int width, int height) để lưu kích thước đã đo.
  • Tính đến padding — trừ getPaddingLeft() + getPaddingRight() khỏi chiều rộng khả dụng.
  • Đối với ViewGroup — đo tất cả các con qua measureChild() hoặc measureChildWithMargins().
  • Đối với wrap_content — tính kích thước dựa trên nội dung (văn bản, hình ảnh).
  • Không gọi requestLayout() trong onMeasure — điều này sẽ gây ra vòng lặp vô hạn.

Lỗi điển hình: không tính đến MeasureSpec khi sử dụng wrap_content. Nếu View được đặt là wrap_content, nhưng onMeasure không xử lý AT_MOST và trả về kích thước cố định, View sẽ bị cắt hoặc chiếm nhiều không gian hơn cần thiết.

onLayout: đặt View trên màn hình

onLayout — giai đoạn mà View hoặc ViewGroup sắp xếp các con trong ranh giới của nó. Đối với View thông thường (không phải ViewGroup), onLayout không bắt buộc — hệ thống gọi layout() với các tham số được truyền từ cha. Đối với ViewGroup, onLayout là bắt buộc — nếu không có nó, View con sẽ không được đặt.

Chữ ký của onLayout:

java
@Override
protected void onLayout(boolean changed,
        int left, int top,
        int right, int bottom) {
    // sắp xếp View con
}

Tham số changed cho biết vị trí hoặc kích thước của View đã thay đổi so với layout trước đó hay chưa. Nếu là false, View có thể bỏ qua việc tính lại vị trí con để tối ưu.

Đối với ViewGroup, onLayout nên:

  • Duyệt qua tất cả các con qua getChildCount()getChildAt(i).
  • Cho mỗi con, xác định left, top, right, bottom — tọa độ trong ViewGroup (tính đến padding).
  • Gọi child.layout(l, t, r, b) cho mỗi con.
  • Tính đến gravity, margins, alignment.

onLayout được gọi sau onMeasure — kích thước đã đo có sẵn qua getMeasuredWidth()/getMeasuredHeight(). Nếu View con có kích thước thực tế khác sau layout(), requestLayout() sẽ được gọi để đo lại. Điều này được gọi là “layout pass” và có thể kích hoạt phản ứng dây chuyền tính toán lại.

onDraw: vẽ nội dung Canvas

onDraw — giai đoạn mà View tự vẽ trên Canvas. Đây là giai đoạn duy nhất có thể được gọi nhiều lần mà không cần onMeasure và onLayout — nếu View được đánh dấu là invalidate(). Canvas cung cấp API vẽ: drawLine, drawRect, drawCircle, drawText, drawBitmap và drawPath.

Các quy tắc onDraw:

  • Không tạo đối tượng trong onDraw — mỗi lần gọi onDraw nên sử dụng các đối tượng được tạo trước (Path, Paint, Rect). Tạo đối tượng trong onDraw gây ra tạm dừng GC và frame bị rơi.
  • Không gọi requestLayout() hoặc invalidate() trong onDraw — điều này sẽ kích hoạt vòng lặp vẽ lại vô hạn.
  • Không thực hiện tính toán dài — onDraw chạy trên luồng UI. Các tính toán phức tạp nên được chuyển sang luồng nền hoặc tính toán trước.
  • Sử dụng tăng tốc phần cứng — từ API 14+, Canvas có thể hoạt động qua GPU. Đối với đồ họa phức tạp (gradient, bóng, xoay), tăng tốc HW cải thiện hiệu suất lên đến 300%.
  • Chỉ vẽ vùng nhìn thấy — sử dụng canvas.clipRect() để cắt các phần không nhìn thấy.

Thứ tự vẽ trong ViewGroup: nền (setBackgroundDrawable) → onDraw (nội dung) → dispatchDraw (View con) → onDrawForeground (tiền cảnh). dispatchDraw gọi onDraw của mỗi con. Ghi đè dispatchDraw được sử dụng để áp dụng hiệu ứng lên trên các phần tử con.

Theo thống kê Android Vitals, các nguyên nhân phổ biến nhất của frame bị rơi trong onDraw là tạo đối tượng trong phương thức (48%), gọi decodeResource (22%) và thao tác Path phức tạp không có bộ nhớ đệm (15%).

Vô hiệu hóa: khi nào View được vẽ lại

Vô hiệu hóa — cơ chế kích hoạt việc vẽ lại View. Gọi invalidate() đánh dấu View là “bẩn” và lên lịch gọi onDraw trong chu kỳ vẽ tiếp theo. Gọi requestLayout() là một thao tác “nặng hơn”, kích hoạt chu kỳ đầy đủ: onMeasure → onLayout → onDraw.

Phương thứcChức năngKhi nào sử dụng
invalidate()Kích hoạt onDraw không cần onMeasure/onLayoutChỉ thay đổi ngoại quan (màu, văn bản, tiến trình)
invalidate(Rect)Chỉ vẽ lại vùng được chỉ địnhPhần View thay đổi — hoạt ảnh, lựa chọn
postInvalidate()Gọi invalidate từ luồng không phải UILuồng nền đã cập nhật dữ liệu để vẽ
requestLayout()Kích hoạt onMeasure → onLayout → onDrawKích thước nội dung thay đổi (văn bản, hình ảnh)
forceLayout()Đánh dấu View để đo lại bắt buộcTrạng thái nội bộ thay đổi, kích thước có thể thay đổi

Hoạt ảnh và View Lifecycle: ViewPropertyAnimator và ValueAnimator gọi invalidate() trên mỗi khung hoạt ảnh. ObjectAnimator gọi setter trên View, nếu setter thay đổi kích thước (rộng/cao), tự động gọi requestLayout(). Điều này có thể tốn kém cho ViewGroups phức tạp: mỗi requestLayout kích hoạt toàn bộ phân cấp đến view gốc.

Quy tắc tối ưu: invalidate() thay vì requestLayout() ở mọi nơi chỉ thay đổi ngoại quan (màu, độ trong suốt, xoay không thay đổi kích thước). Chỉ sử dụng requestLayout khi thay đổi kích thước hoặc nội dung ảnh hưởng đến kích thước.

Tối ưu View tùy chỉnh: các phương pháp tốt nhất

View tùy chỉnh là công cụ mạnh mẽ để tạo UI độc đáo, nhưng yêu cầu tuân thủ nghiêm ngặt các quy tắc hiệu suất. Dưới đây là các khuyến nghị chính của Google để tối ưu View Lifecycle.

  • Tính toán trước mọi thứ có thể tính — kích thước, tọa độ, đường dẫn, màu gradient. Trong onDraw, chỉ thực hiện vẽ.
  • Lưu kết quả đo vào bộ nhớ đệm — nếu View có kích thước cố định, lưu MeasureSpec và trả về setMeasuredDimension không cần tính toán thêm.
  • Sử dụng ViewConfiguration — getScaledTouchSlop, getScaledMinimumFlingVelocity — để xử lý chạm.
  • Giảm thiểu số View trong phân cấp — View tùy chỉnh kết hợp nhiều phần tử luôn nhanh hơn ViewGroup với 3–5 View lồng nhau. Google khuyến nghị không quá 10 View lồng nhau trên một màn hình.
  • Sử dụng ConstraintLayout cho phân cấp phẳng — nó xây dựng một ViewGroup duy nhất với hiệu suất gần với RelativeLayout, nhưng không lồng nhau.
  • Tắt lớp phần cứng sau khi vẽ lại — sử dụng setLayerType(LAYER_TYPE_HARDWARE) cho View có hoạt ảnh và setLayerType(LAYER_TYPE_NONE) sau khi hoàn thành.
  • Sử dụng invalidate() với Rect — chỉ vẽ lại vùng đã thay đổi, không phải toàn bộ View.
  • Tránh overdraw — sử dụng Profile GPU Rendering trong Android Studio để xác định các lần vẽ lại không cần thiết. Overdraw trung bình cho ứng dụng Google là 1.5x, tối đa khuyến nghị là 2.5x.

Ví dụ mã View trong Kotlin

Ví dụ 1: View tùy chỉnh — chỉ báo tiến trình

Một chỉ báo tiến trình hình tròn đơn giản với triển khai đúng onMeasure, onDraw và invalidate.

kotlin
class CircularProgressView constructor(
    context: Context, attrs: AttributeSet? = null
) : View(context, attrs) {

    private val progressPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
        color = Color.BLUE
        style = Paint.Style.STROKE
        strokeWidth = 8f
        strokeCap = Paint.Cap.ROUND
    }

    private val backgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
        color = Color.LTGRAY
        style = Paint.Style.STROKE
        strokeWidth = 8f
    }

    private var progress = 0f
    private var viewWidth = 0
    private var viewHeight = 0

    fun setProgress(value: Float) {
        progress = value.coerceIn(0f, 100f)
        invalidate()
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        val desiredSize = 100 * resources.displayMetrics.density.toInt()
        val width = MeasureSpec.getSize(widthMeasureSpec)
        val height = MeasureSpec.getSize(heightMeasureSpec)
        val size = minOf(width, height).coerceAtLeast(desiredSize)
        setMeasuredDimension(size, size)
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        val padding = progressPaint.strokeWidth / 2
        val radius = (minOf(viewWidth, viewHeight) - padding) / 2
        val cx = viewWidth / 2f
        val cy = viewHeight / 2f
        canvas.drawCircle(cx, cy, radius, backgroundPaint)
        val sweepAngle = (progress / 100f) * 360f
        canvas.drawArc(cx - radius, cy - radius, cx + radius, cy + radius,
            -90f, sweepAngle, false, progressPaint)
    }

    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)
        viewWidth = w
        viewHeight = h
    }
}

Thanh tiến trình hình tròn: onMeasure trả về kích thước hình vuông dựa trên MeasureSpec, onSizeChanged ghi nhớ kích thước, onDraw vẽ nền và cung tiến trình. Invalidate được gọi khi tiến trình thay đổi — onMeasure/onLayout không bị ảnh hưởng. Paint được tạo một lần trong hàm tạo, không phải trong onDraw.

Ví dụ 2: ViewGroup — FlowLayout đơn giản

Một ViewGroup tùy chỉnh sắp xếp View con thành các hàng (giống Flexbox wrap).

kotlin
class FlowLayout constructor(
    context: Context, attrs: AttributeSet? = null
) : ViewGroup(context, attrs) {

    private val horizontalSpacing = 8.dpToPx(resources)
    private val verticalSpacing = 8.dpToPx(resources)

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        val width = MeasureSpec.getSize(widthMeasureSpec)
        var totalHeight = paddingTop + paddingBottom
        var rowWidth = paddingLeft
        var rowHeight = 0
        for (i in 0 until childCount) {
            val child = getChildAt(i)
            measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, totalHeight)
            if (rowWidth + child.measuredWidth > width - paddingRight) {
                totalHeight += rowHeight + verticalSpacing
                rowWidth = paddingLeft
                rowHeight = 0
            }
            rowWidth += child.measuredWidth + horizontalSpacing
            rowHeight = maxOf(rowHeight, child.measuredHeight)
        }
        totalHeight += rowHeight
        setMeasuredDimension(
            MeasureSpec.getSize(widthMeasureSpec),
            resolveSize(totalHeight, heightMeasureSpec)
        )
    }

    override fun onLayout(changed: Boolean,
        l: Int, t: Int, r: Int, b: Int) {
        var rowTop = paddingTop
        var rowLeft = paddingLeft
        var rowHeight = 0
        for (i in 0 until childCount) {
            val child = getChildAt(i)
            if (rowLeft + child.measuredWidth > r - paddingRight) {
                rowTop += rowHeight + verticalSpacing
                rowLeft = paddingLeft
                rowHeight = 0
            }
            child.layout(rowLeft, rowTop, rowLeft + child.measuredWidth, rowTop + child.measuredHeight)
            rowLeft += child.measuredWidth + horizontalSpacing
            rowHeight = maxOf(rowHeight, child.measuredHeight)
        }
    }

    override fun generateLayoutParams(attrs: AttributeSet?): LayoutParams {
        return MarginLayoutParams(context, attrs)
    }
}

FlowLayout ghi đè onMeasure: đo mỗi con, chuyển sang hàng mới khi vượt quá chiều rộng, tính tổng chiều cao. onLayout đặt con theo tọa độ tính đến ngắt dòng. generateLayoutParams trả về MarginLayoutParams để hỗ trợ lề cho View con.

Ví dụ 3: onDraw với lưu đệm Path

Một View tùy chỉnh vẽ đường cong Bezier mượt, tính toán trước Path và lưu vào bộ nhớ đệm.

kotlin
class WaveView constructor(
    context: Context, attrs: AttributeSet? = null
) : View(context, attrs) {

    private val wavePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
        color = Color.parseColor("#4A90D9")
        style = Paint.Style.FILL
    }

    private val wavePath = Path()
    private var isPathDirty = true
    private var viewWidth = 0
    private var viewHeight = 0

    fun refreshWave() {
        isPathDirty = true
        invalidate()
    }

    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)
        viewWidth = w
        viewHeight = h
        isPathDirty = true
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        if (isPathDirty) {
            wavePath.reset()
            val amplitude = viewHeight * 0.1f
            wavePath.moveTo(0f, viewHeight * 0.5f)
            for (x in 0..viewWidth step 4) {
                val y = viewHeight * 0.5f + amplitude * Math.sin(x * 2 * Math.PI / viewWidth).toFloat()
                wavePath.lineTo(x.toFloat(), y)
            }
            wavePath.lineTo(viewWidth.toFloat(), viewHeight.toFloat())
            wavePath.lineTo(0f, viewHeight.toFloat())
            wavePath.close()
            isPathDirty = false
        }
        canvas.drawPath(wavePath, wavePaint)
    }
}

Lưu đệm Path: isPathDirty = true chỉ khi kích thước View thay đổi hoặc refreshWave() được gọi. Trong onDraw, Path chỉ được tính lại nếu nó “bẩn”. Điều này ngăn việc tính lại đường cong Bezier trên mỗi khung hoạt ảnh, tiết kiệm CPU.

Câu hỏi thường gặp

View Lifecycle khác Activity Lifecycle như thế nào?

View Lifecycle là một quá trình vẽ theo chu kỳ (onMeasure → onLayout → onDraw) độc lập với việc tạo/hủy Activity. View không có onStart/onStop — nó hoặc là hiển thị (được gắn vào cửa sổ) hoặc không. Activity Lifecycle quản lý trạng thái thành phần ứng dụng, View Lifecycle quản lý việc vẽ UI.

requestLayout() có ảnh hưởng gì đến hiệu suất?

requestLayout() kích hoạt chu kỳ đầy đủ onMeasure → onLayout → onDraw cho toàn bộ cây View từ gốc. Nếu requestLayout() được gọi thường xuyên (ví dụ, mỗi khung hoạt ảnh), nó gây ra jank và frame bị rơi. Theo Google, một requestLayout mất trung bình 2–5 ms trên ViewGroup gồm 10 phần tử. Đối với hoạt ảnh, hãy sử dụng invalidate().

Khi nào onAttachedToWindow được gọi?

onAttachedToWindow được gọi khi View được gắn vào Window — trở thành một phần của hệ thống phân cấp hiển thị. Tại thời điểm này, View nhận được tăng tốc HW và quyền truy cập tài nguyên Window (WindowManager, Display). onAttachedToWindow là nơi thích hợp để đăng ký trình lắng nghe hoạt ảnh và BroadcastReceiver tồn tại khi View hiển thị.

Overdraw là gì và làm thế nào để giảm?

Overdraw là tình huống một pixel được vẽ nhiều lần trong một khung duy nhất. Mỗi lần vẽ thừa lãng phí thời gian GPU. Các phương pháp giảm: đặt windowBackground trong theme (không vẽ nền trong layout), sử dụng canvas.clipRect(), tránh hợp nhất nền lồng nhau, sử dụng ConstraintLayout thay vì LinearLayout lồng nhau. Android Studio → Profile GPU Rendering → Overdraw hiển thị bản đồ màu của overdraw (xanh lam = 1x, đỏ = 3x+).

Có cần super.onDraw() trong View tùy chỉnh không?

Có, nếu View có nền. super.onDraw() vẽ nền của View. Nếu View tùy chỉnh của bạn không có nền hoặc bạn tự vẽ nền riêng, có thể bỏ qua super.onDraw() — điều này tiết kiệm một lần vẽ. Đối với ViewGroup, super.dispatchDraw() là bắt buộc — nó vẽ các View con.

Tổng kết

  • View Lifecycle — ba giai đoạn vẽ: onMeasure (kích thước), onLayout (vị trí), onDraw (kết xuất).
  • onMeasure xử lý MeasureSpec (EXACTLY, AT_MOST, UNSPECIFIED) và gọi setMeasuredDimension.
  • onLayout trong ViewGroup đặt View con với tọa độ left/top/right/bottom.
  • onDraw kết xuất nội dung lên Canvas — không tạo đối tượng trong phương thức này.
  • invalidate() chỉ kích hoạt onDraw, requestLayout() kích hoạt chu kỳ đầy đủ onMeasure → onLayout → onDraw.
  • View tùy chỉnh tăng tốc UI lên 15–40%, nhưng cần triển khai đúng onMeasure và lưu đệm đối tượng trong onDraw.
  • Đối với đồ họa phức tạp, sử dụng tăng tốc phần cứng và lưu đệm Path/Bitmap.

Chúng tôi sẽ phát triển ứng dụng di động chìa khóa trao tay

IT Sectr tạo các ứng dụng iOS và Android cho các công ty khởi nghiệp và doanh nghiệp từ năm 2017. Chúng tôi sẽ tư vấn và đề xuất giải pháp tốt nhất cho bạn.

Thảo luận dự án

Đọc thêm