25 tháng 2, 2013

Tối ưu hóa layout trong Android (1)

Ngày nay việc phát triển ứng dụng trên di động không còn là gì quá mới mẻ, tuy nhiên, việc làm một ứng dụng thực sự tốt vẫn không phải là việc đơn giản, với những ứng dụng có giao diện phức tạp thì việc thiết kế layout sẽ ảnh hưởng khá lớn tới độ mượt của ứng dụng đó. Do vậy, tui sẽ làm một số bài viết về tối ưu hóa layout.
Trước tiên, các bạn cần nắm được sơ bộ quá trình tính toán và vẽ layout lên màn hình với Android diễn ra như thế nào (đọc topic How Android Draws Views) và những topic về tăng hiệu quả layout cũng là cực kỳ cần thiết phải đọc (xem link này). Đây là những kiến thức căn bản nhưng rất quan trọng để bạn có thể hiểu rõ về UI framework của Android, tới đây, tui mặc định là các bạn đã nắm được sơ bộ những kiến thức trên rồi nhé.
Bài viết này sẽ đề cập tới sự ảnh hưởng của việc qui định kích thước các child view tới tốc độ cập nhật layout khi có thay đổi trong nội dung hiển thị. Ta có một activity với layout định nghĩ như sau:


<tvtbinh.bino.demo.drawsviewsflow.LoggerLinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res/tvtbinh.bino.demo.drawsviewsflow"
    android:id="@+id/root"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    app:log_tag_linear="Layout_Root" >

    <tvtbinh.bino.demo.drawsviewsflow.LoggerLinearLayout
        android:id="@+id/layout_1"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        app:log_tag_linear="Layout_1" >

        <tvtbinh.bino.demo.drawsviewsflow.LoggerTextView
            android:id="@+id/textview_1_2"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:paddingLeft="5dip"
            android:text="TextView 1_2"
            app:log_tag_textview="TextView_1_2" />

        <tvtbinh.bino.demo.drawsviewsflow.LoggerTextView
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:gravity="right"
            android:text="TextView 1_3"
            app:log_tag_textview="TextView_1_3" />

    </tvtbinh.bino.demo.drawsviewsflow.LoggerLinearLayout>

    <tvtbinh.bino.demo.drawsviewsflow.LoggerButton
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:onClick="onButtonClicked"
        android:text="Button 1_1"
        app:log_tag_button="Button_1_1" />

</tvtbinh.bino.demo.drawsviewsflow.LoggerLinearLayout>

(Mấy cái class được kế thừa với cái tên Logger thêm vào đầu chỉ có một chức năng duy nhất là mỗi lần phương thức onLayout, onMeasure, onDraw được gọi thì nó xuất log ra các thông số liên quan thôi, ngoài ra nó ko khác gì class gốc hết)

Activity này làm một việc khá đơn giản là mỗi khi bấm vào cai nút trên màn hình thì nó thay đổi nội dung của cái TextView đầu tiên, lý do vì sao bạn sẽ hiểu vào cuối bài viết. Để tính chính xác thì tui bấm vào, giữ nút đó một tí sau đó thả tay ra (lúc này sự kiện onLick mới được bắt đầu tính) đợi tới khi tất cả các view trong layout được vẽ lại hết (hàm onDraw được gọi xong).
Với layout này, thống kê trên máy nexus one chạy Android 2.3.6 thì thời gian để vẽ lai layout sau khi bấm nút vào khoảng 20 millisecond.

Sau đó, tui chỉnh lại layout một chút ở 2 cái TextView như sau:

<tvtbinh.bino.demo.drawsviewsflow.LoggerTextView
            android:id="@+id/textview_1_2"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="2"
            android:paddingLeft="5dip"
            android:text="TextView 1_2"
            app:log_tag_textview="TextView_1_2" />

<tvtbinh.bino.demo.drawsviewsflow.LoggerTextView
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:gravity="right"
            android:text="TextView 1_3"
            app:log_tag_textview="TextView_1_3" />

Lặp lại các bước tương tự như trên thì thời gian để vẽ lại layout sau khi bấm nút vào khoảng 10 millisecond  Wow, xem ra hiệu quả hơn khá nhiều, vậy sự khác biết này ở đâu mà có?
Khi kiểm tra log kỹ càng thì thấy chênh lệch 10 millisecond này chính là khoảng thời gian dành cho các thao tác tính lại kích thước của các child view và sắp đặt lại chúng trên màn hình, với cách thứ 2, các kích thước và vị trí không có sự thay đổi và layout cha dựa vào các thuộc tính chúng ta khai báo để hiểu được điều đó, khi cần cập nhật lại chỉ có mỗi việc gọi hàm onDraw của các child view. Các bạn xem log:

02-25 10:17:47.087: E/DrawFlow(4321): Clicked
02-25 10:17:47.087: I/Layout_Root(4321): onMeasure(MeasureSpec: EXACTLY 320, MeasureSpec: AT_MOST 533)
02-25 10:17:47.087: I/Layout_Root(4321): onMeasure( width = 320, height = 67) start {
02-25 10:17:47.087: I/Layout_1(4321): onMeasure(MeasureSpec: EXACTLY 320, MeasureSpec: AT_MOST 533)
02-25 10:17:47.087: I/Layout_1(4321): onMeasure( width = 320, height = 19) start {
02-25 10:17:47.087: I/TextView_1_2(4321): onMeasure(MeasureSpec: AT_MOST 320, MeasureSpec: AT_MOST 533)
02-25 10:17:47.087: I/TextView_1_2(4321): onMeasure( width = 77, height = 19) start {
02-25 10:17:47.087: I/TextView_1_2(4321): onMeasure( width = 128, height = 19) end }
02-25 10:17:47.087: I/TextView_1_2(4321): onMeasure --- finished
02-25 10:17:47.087: I/TextView_1_3(4321): onMeasure(MeasureSpec: EXACTLY 192, MeasureSpec: AT_MOST 533)
02-25 10:17:47.087: I/TextView_1_3(4321): onMeasure( width = 243, height = 19) start {
02-25 10:17:47.087: I/TextView_1_3(4321): onMeasure( width = 192, height = 19) end }
02-25 10:17:47.087: I/TextView_1_3(4321): onMeasure --- finished
02-25 10:17:47.087: I/Layout_1(4321): onMeasure( width = 320, height = 19) end }
02-25 10:17:47.087: I/Layout_1(4321): onMeasure --- finished
02-25 10:17:47.097: I/Layout_Root(4321): onMeasure( width = 320, height = 67) end }
02-25 10:17:47.097: I/Layout_Root(4321): onMeasure --- finished
02-25 10:17:47.097: I/Layout_Root(4321): onLayout(changed = false, l = 0, t = + 0, r = 320, b = + 67)
02-25 10:17:47.097: I/Layout_1(4321): onLayout(changed = false, l = 0, t = + 0, r = 320, b = + 19)
02-25 10:17:47.097: I/TextView_1_2(4321): onLayout(changed = true, l = 0, t = + 0, r = 128, b = + 19)
02-25 10:17:47.097: I/TextView_1_3(4321): onLayout(changed = true, l = 128, t = + 0, r = 320, b = + 19)
02-25 10:17:47.097: I/TextView_1_2(4321): onDraw()
02-25 10:17:47.107: I/TextView_1_3(4321): onDraw()
02-25 10:17:47.107: I/Button_1_1(4321): onDraw()

Như vậy, với cách thiết kế layout ban đầu, ta khai báo chiều rộng của TextView đầu tiên là wrap_content và TextView kế bên là fill_parent, đồng nghĩa với việc mỗi lần TextView đầu tiên có sự thay đổi nội dung thì kích thước của nó thay đổi theo nội dung và kích thước của TextView kế bên cũng thay đổi theo tương ứng, cũng thấy là vì chiều cao không đổi nên sau khi có kích thước của TextView thì layout cha ko yêu cầu tính lại kích thước của button vì ko có sự ảnh hưởng.
Với một layout đơn giản đã vậy thì với một layout có nhiều nội dung cần hiển thị và nhiều control để tương tác hơn thì thời gian để tính toán lại layout trước khi vẽ càng dài hơn, đôi khi dẫn đến việc layout ko được mượt nếu thiết kế ko tốt. Ở đây, cách giải quyết thứ 2 của tui chưa phải là tốt nhất, chỉ là dùng LinearLayout thì các bạn dễ đọc và hình dung thôi, các bạn có thể thử dùng RelativeLayout.

Vậy qua bài viết này giúp các bạn hiểu rõ hơn về cách Android UI framework vẽ các view và layout lên màn hình như thế nào và từ đó giúp chúng ta thiết kế layout hiệu quả để có một ứng dụng tương tác mượt mà hơn.