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 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:
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 — 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ĩa | Ví dụ |
|---|---|---|---|
| EXACTLY | MeasureSpec.EXACTLY | Kí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_MOST | MeasureSpec.AT_MOST | View có thể có kích thước tối đa được chỉ định (wrap_content) | width ≤ 400dp → MeasureSpec(400, AT_MOST) |
| UNSPECIFIED | MeasureSpec.UNSPECIFIED | Khô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:
setMeasuredDimension(int width, int height) để lưu kích thước đã đo.getPaddingLeft() + getPaddingRight() khỏi chiều rộng khả dụng.measureChild() hoặc measureChildWithMargins().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 — 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:
@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:
getChildCount() và getChildAt(i).child.layout(l, t, r, b) cho mỗi con.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 — 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:
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 — 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ức | Chức năng | Khi nào sử dụng |
|---|---|---|
| invalidate() | Kích hoạt onDraw không cần onMeasure/onLayout | Chỉ 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ỉ định | Phần View thay đổi — hoạt ảnh, lựa chọn |
| postInvalidate() | Gọi invalidate từ luồng không phải UI | Luồng nền đã cập nhật dữ liệu để vẽ |
| requestLayout() | Kích hoạt onMeasure → onLayout → onDraw | Kí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ộc | Trạ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.
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.
setLayerType(LAYER_TYPE_HARDWARE) cho View có hoạt ảnh và setLayerType(LAYER_TYPE_NONE) sau khi hoàn thà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.
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.
Một ViewGroup tùy chỉnh sắp xếp View con thành các hàng (giống Flexbox wrap).
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.
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.
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 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() 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().
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à 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ó, 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
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.
Đọc thêm