Phân tích chuyên sâu cơ chế hiển thị View trong Android Framework

Tổng quan kiến trúc hiển thị

Hệ thống UI của Android hoạt động dựa trên cơ chế kết hợp giữa ViewRootImpl, ChoreographerSurface. Quá trình render một màn hình bắt đầu khi ViewRootImpl nhận tín hiệu VSYNC từ Choreographer, khởi động chuỗi xử lý qua ba giai đoạn chính: đo đạc (Measure), bố trí (Layout) và vẽ (Draw). Dữ liệu sau khi xử lý sẽ được gửi tới SurfaceFlinger để tổng hợp và hiển thị lên màn hình.

Chi tiết giai đoạn đo đạc (Measure)

Giai đoạn này xác định kích thước của View và View con. Hệ thống sử dụng lớp MeasureSpec để đóng gói thông tin về kích thước và mode đo lường thành một giá trị nguyên 32-bit.

Cơ chế MeasureSpec

Mỗi MeasureSpec bao gồm 2 bit cao nhất lưu trữ mode (chế độ đo) và 30 bit thấp nhất lưu trữ size (kích thước). Có 3 chế độ chính:

  • UNSPECIFIED: Cha không giới hạn kích thước, con có thể tùy ý (thường dùng trong ListView, ScrollView).
  • EXACTLY: Cha quy định kích thước chính xác (ví dụ: match_parent hoặc giá trị cụ thể 100dp).
  • AT_MOST: Cha quy định kích thước tối đa, con không được vượt quá (ví dụ: wrap_content).

Để minh họa cách tính toán thông số này, ta có thể tham khảo logic mô phỏng sau:

public class DimensionHelper {
    private static final int BIT_SHIFT = 30;
    private static final int MODE_MASK = 0x3 << BIT_SHIFT;

    public static final int MODE_UNSPECIFIED = 0 << BIT_SHIFT;
    public static final int MODE_EXACTLY = 1 << BIT_SHIFT;
    public static final int MODE_AT_MOST = 2 << BIT_SHIFT;

    public static int createSpec(int rawSize, int modeType) {
        return (rawSize & ~MODE_MASK) | (modeType & MODE_MASK);
    }

    public static int getSpecSize(int packedValue) {
        return packedValue & ~MODE_MASK;
    }

    public static int getSpecMode(int packedValue) {
        return packedValue & MODE_MASK;
    }
}

Trong quá trình đo đạc, ViewGroup sẽ duyệt qua các View con và tính toán MeasureSpec phù hợp dựa trên LayoutParams của chúng.

// Logic đơn giản hóa việc đo đạc View con
void measureChild(View child, int parentWidthSpec, int parentHeightSpec) {
    LayoutParams params = child.getLayoutParams();

    int childWidthSpec = calculateChildSpec(parentWidthSpec, params.width);
    int childHeightSpec = calculateChildSpec(parentHeightSpec, params.height);

    child.measure(childWidthSpec, childHeightSpec);
}

Chi tiết giai đoạn bố trí (Layout)

Sau khi đã có kích thước, giai đoạn Layout xác định vị trí tọa độ (trái, trên, phải, dưới) của View trên màn hình. Phương thức layout(l, t, r, b) được gọi để gán tọa độ, sau đó gọi onLayout() để ViewGroup sắp xếp các con.

public void assignPosition(int left, int top, int right, int bottom) {
    boolean positionChanged = setBounds(left, top, right, bottom);

    if (positionChanged || isLayoutRequired()) {
        onLayout(positionChanged, left, top, right, bottom);
        clearLayoutFlag();
    }
}

Chi tiết giai đoạn vẽ (Draw)

Đây là bước nội dung thực sự được hiển thị. Quy trình vẽ diễn ra theo thứ tự sau:

  1. Vẽ nền (drawBackground).
  2. Lưu trạng thái Canvas (nếu cần).
  3. Vẽ nội dung chính (onDraw).
  4. Vẽ các View con (dispatchDraw).
  5. Vẽ các trang trí như thanh cuộn (onDrawForeground).
public void renderView(Canvas canvas) {
    drawBackground(canvas);

    // Vẽ nội dung do tự implement
    if (!isOpaque()) {
        onDraw(canvas);
    }

    // ủy quyền vẽ cho các View con
    dispatchDraw(canvas);

    // Vẽ các lớp phủ phía trên
    drawDecorations(canvas);
}

Tăng tốc phần cứng (Hardware Acceleration)

Android hỗ trợ chuyển đổi tác vụ vẽ từ CPU sang GPU để tăng hiệu năng. Khi bật chế độ này, các thao tác vẽ Canvas được chuyển đổi thành các lệnh OpenGL ES.

  • Software Rendering: Sử dụng CPU để tính toán từng pixel, chậm hơn nhưng tương thích tốt hơn với các hiệu ứng phức tạp không hỗ trợ GPU.
  • Hardware Acceleration: Sử dụng GPU, xử lý song song các thao tác hình ảnh, mượt mà hơn cho animation.

Cấu hình trong Manifest:

<application android:hardwareAccelerated="true">
    <activity android:name=".MainActivity" />
</application>

Hoặc bật/tắt bằng code:

// Bắt buộc dùng phần mềm vẽ cho View cụ thể
myView.setLayerType(View.LAYER_TYPE_SOFTWARE, null);

Thực hành triển khai Custom View

Dưới đây là ví dụ về một CardView tùy chỉnh vẽ một hình chữ nhật bo góc thay vì hình tròn đơn giản để thay đổi logic so với ví dụ gốc.

public class CardViewWidget extends View {
    private Paint fillPaint;
    private RectF rectBounds;
    private float cornerRadius = 20f;
    private int cardColor = Color.BLUE;

    public CardViewWidget(Context ctx, AttributeSet attrs) {
        super(ctx, attrs);
        setupWidget();
    }

    private void setupWidget() {
        fillPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        fillPaint.setColor(cardColor);
        rectBounds = new RectF();
    }

    @Override
    protected void onMeasure(int widthSpec, int heightSpec) {
        int desiredW = 200 + getPaddingLeft() + getPaddingRight();
        int desiredH = 100 + getPaddingTop() + getPaddingBottom();

        int finalW = resolveSize(desiredW, widthSpec);
        int finalH = resolveSize(desiredH, heightSpec);

        setMeasuredDimension(finalW, finalH);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        
        rectBounds.set(
            getPaddingLeft(), 
            getPaddingTop(), 
            getWidth() - getPaddingRight(), 
            getHeight() - getPaddingBottom()
        );
        
        canvas.drawRoundRect(rectBounds, cornerRadius, cornerRadius, fillPaint);
    }
}

Thực hành triển khai Custom ViewGroup

Ví dụ về một WrapContainer xếp các con theo dòng ngang, tự động xuống dòng khi hết chỗ.

public class WrapContainer extends ViewGroup {
    
    @Override
    protected void onMeasure(int widthSpec, int heightSpec) {
        int maxW = MeasureSpec.getSize(widthSpec);
        int modeW = MeasureSpec.getMode(widthSpec);
        
        measureChildren(widthSpec, heightSpec);
        
        int totalW = 0;
        int totalH = 0;
        int currentRowW = 0;
        int currentRowH = 0;
        
        int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);
            if (child.getVisibility() == GONE) continue;
            
            int childW = child.getMeasuredWidth();
            int childH = child.getMeasuredHeight();
            
            if (currentRowW + childW > maxW) {
                // Xuống dòng
                totalW = Math.max(totalW, currentRowW);
                totalH += currentRowH;
                
                currentRowW = childW;
                currentRowH = childH;
            } else {
                currentRowW += childW;
                currentRowH = Math.max(currentRowH, childH);
            }
        }
        
        // Cộng thêm dòng cuối cùng
        totalW = Math.max(totalW, currentRowW);
        totalH += currentRowH;
        
        int finalW = (modeW == MeasureSpec.EXACTLY) ? maxW : totalW;
        setMeasuredDimension(finalW, totalH + getPaddingTop() + getPaddingBottom());
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int containerW = getWidth();
        int curX = getPaddingLeft();
        int curY = getPaddingTop();
        int rowH = 0;
        
        int count = getChildCount();
        for (int i = 0; i < count; i++) {
            View child = getChildAt(i);
            if (child.getVisibility() == GONE) continue;
            
            int w = child.getMeasuredWidth();
            int h = child.getMeasuredHeight();
            
            if (curX + w > containerW) {
                curX = getPaddingLeft();
                curY += rowH;
                rowH = 0;
            }
            
            child.layout(curX, curY, curX + w, curY + h);
            
            curX += w;
            rowH = Math.max(rowH, h);
        }
    }
}

Chiến lược tối ưu hiệu năng

Để đảm bảo UI mượt mà (60fps), cần chú ý các vấn đề sau:

  • Giảm Overdraw (Vẽ thừa): Tránh việc vẽ đè lớp lên nhau quá nhiều. Sử dụng canvas.clipRect() để giới hạn vùng vẽ hoặc gán android:background="@null" cho các View nền không cần thiết.
  • Phẳng hóa cấu trúc Layout: Sử dụng ConstraintLayout thay vì lồng ghép quá nhiều LinearLayout hoặc RelativeLayout. Cấu trúc sâu làm tăng thời gian đo đạc và bố trí.
  • Tải chậm (Lazy Loading): Sử dụng ViewStub cho các UI hiếm khi hiển thị.
<ViewStub
    android:id="@+id/loadingViewStub"
    android:layout="@layout/layout_loading_panel"
    android:inflatedId="@+id/loadingPanel" />
// Chỉ hiện khi cần thiết
ViewStub stub = findViewById(R.id.loadingViewStub);
if (stub != null) {
    View inflatedView = stub.inflate();
}

Thẻ: Android ViewRendering CustomView MeasureSpec HardwareAcceleration

Đăng vào ngày 5 tháng 6 lúc 21:32