Khắc phục vấn đề về hiệu suất thực tế trong Jetpack Compose

1. Trước khi bắt đầu

Trong lớp học lập trình này, bạn sẽ tìm hiểu cách cải thiện hiệu suất thời gian chạy của một ứng dụng Compose. Bạn sẽ thực hiện một phương pháp khoa học để đo lường, gỡ lỗi và tối ưu hoá hiệu suất. Bạn kiểm tra nhiều vấn đề về hiệu suất bằng tính năng theo dõi hiệu suất, đồng thời thay đổi mã thời gian chạy không hiệu quả trong một ứng dụng mẫu. Ứng dụng này có một số màn hình hiển thị nhiều tác vụ. Những màn hình này được tạo khác nhau và bao gồm:

  • Màn hình đầu tiên là một danh sách 2 cột có các mục hình ảnh và một số thẻ nằm trên những mục đó. Tại đây, bạn có thể tối ưu hoá các thành phần kết hợp có kích thước lớn.

8afabbbbbfc1d506.gif

  • Màn hình thứ hai và thứ ba chứa một trạng thái kết hợp lại thường xuyên. Tại đây, bạn có thể xoá các thành phần kết hợp lại không cần thiết để tối ưu hoá hiệu suất.

f0ccf14d1c240032.gif 51dc23231ebd5f1a.gif

  • Màn hình cuối cùng chứa các mục không ổn định. Tại đây, bạn có thể ổn định các mục này bằng nhiều kỹ thuật.

127f2e4a2fc1a381.gif

Điều kiện tiên quyết

Kiến thức bạn sẽ học được

Những gì bạn cần

2. Bắt đầu thiết lập

Để bắt đầu, hãy làm theo các bước sau:

  1. Sao chép kho lưu trữ GitHub:
$ git clone https://github.com/android/codelab-android-compose.git

Ngoài ra, bạn có thể tải kho lưu trữ xuống dưới dạng tệp ZIP:

  1. Mở dự án PerformanceCodelab chứa các nhánh sau đây:
  • main: Chứa mã khởi đầu cho dự án này, nơi bạn thực hiện các thay đổi để hoàn tất lớp học lập trình.
  • end: Chứa mã giải pháp cho lớp học lập trình này.

Bạn nên bắt đầu bằng nhánh main và làm theo hướng dẫn từng bước trong lớp học lập trình theo tiến độ của riêng mình.

  1. Nếu bạn muốn thấy mã giải pháp, hãy chạy lệnh sau:
$ git clone -b end https://github.com/android/codelab-android-compose.git

Ngoài ra, bạn có thể tải mã giải pháp xuống:

Không bắt buộc: Dấu vết hệ thống được dùng trong lớp học lập trình này

Bạn sẽ chạy một số phép đo điểm chuẩn để ghi lại dấu vết hệ thống trong lớp học lập trình này.

Nếu bạn không thể chạy các phép đo điểm chuẩn này, dưới đây là danh sách dấu vết hệ thống mà bạn có thể tải xuống:

3. Phương pháp khắc phục các vấn đề về hiệu suất

Bạn có thể phát hiện giao diện người dùng chậm, hoạt động không hiệu quả chỉ bằng cách nhìn đơn thuần và tìm hiểu ứng dụng. Tuy nhiên, trước khi tiến hành và bắt đầu khắc phục mã dựa trên các giả định của mình, bạn cần đo lường hiệu suất của mã để biết các thay đổi có đem lại sự khác biệt hay không.

Trong quá trình phát triển, nhờ bản dựng debuggable của ứng dụng, bạn có thể nhận thấy tính năng nào chưa hoạt động hiệu quả như cần thiết và có thể bắt đầu xử lý vấn đề này. Tuy nhiên, hiệu suất của ứng dụng debuggable không đại diện cho những gì người dùng sẽ nhìn thấy. Vì vậy, bạn cần phải xác minh bằng ứng dụng non-debuggable để biết đó thực sự là một vấn đề. Trong ứng dụng debuggable, toàn bộ mã đều được diễn giải bằng thời gian chạy.

Khi cân nhắc về hiệu suất trong Compose, bạn không cần phải tuân theo quy tắc cứng nhắc nào để triển khai một chức năng cụ thể. Bạn không cần làm trước những việc sau đây:

  • Không theo đuổi và khắc phục mọi tham số không ổn định xuất hiện trong mã của bạn.
  • Không xoá các ảnh động gây ra việc kết hợp lại của thành phần kết hợp đó.
  • Không thực hiện các quy trình tối ưu hoá khó đọc một cách cảm tính.

Bạn nên thực hiện mọi sửa đổi này một cách sáng suốt bằng các công cụ có sẵn để đảm bảo chúng giúp giải quyết vấn đề về hiệu suất.

Khi xử lý các vấn đề về hiệu suất, bạn nên làm theo phương pháp khoa học sau:

  1. Thiết lập hiệu suất ban đầu bằng cách đo lường.
  2. Quan sát xem điều gì gây ra vấn đề này.
  3. Sửa đổi mã dựa trên các quan sát đó.
  4. Đo lường và so sánh với hiệu suất ban đầu.
  5. Lặp lại.

Nếu bạn không làm theo bất kỳ phương thức có cấu trúc nào, một số thay đổi có thể cải thiện hiệu suất nhưng những thay đổi khác có thể làm giảm hiệu suất. Rốt cuộc lại là bạn có thể đạt được cùng một kết quả.

Bạn nên xem video sau về việc nâng cao hiệu suất ứng dụng bằng Compose. Video này hướng dẫn quá trình khắc phục vấn đề về hiệu suất và còn đưa ra một số mẹo về cách cải thiện hiệu suất.

Tạo Hồ sơ cơ sở

Trước khi bạn bắt đầu kiểm tra các vấn đề về hiệu suất, hãy tạo một Hồ sơ cơ sở cho ứng dụng. Trên Android 6 (API cấp 23) trở lên, các ứng dụng chạy mã được diễn giải trong thời gian chạy và được biên dịch trong khi thực thi (JIT) và biên dịch trước khi thực thi (AOT) trong quá trình cài đặt. Mã được diễn giải và biên dịch JIT chạy chậm hơn so với mã được biên dịch AOT, nhưng chiếm ít dung lượng hơn trên ổ đĩa và trong bộ nhớ. Đây là lý do vì sao chỉ nên biên dịch AOT cho một số mã.

Bằng cách triển khai Hồ sơ cơ sở, bạn có thể cải thiện 30% quá trình khởi động ứng dụng và giảm 8 lần thời gian chạy mã ở chế độ JIT như minh hoạ trong hình ảnh sau, dựa trên ứng dụng mẫu Now in Android:

b51455a2ca65ea8.png

Để biết thêm thông tin về Hồ sơ cơ sở, hãy xem các tài nguyên sau:

Đo lường hiệu suất

Để đo lường hiệu suất, bạn nên thiết lập và viết các phép đo điểm chuẩn bằng Jetpack Macrobenchmark. Macrobenchmark là các kiểm thử đo lường tương tác với ứng dụng của bạn giống như người dùng, đồng thời theo dõi hiệu suất của ứng dụng đó. Điều này có nghĩa là chúng không làm hỏng mã ứng dụng bằng mã kiểm thử nên cung cấp thông tin đáng tin cậy về hiệu suất.

Trong lớp học lập trình này, chúng tôi đã thiết lập cơ sở mã và viết các phép đo điểm chuẩn để tập trung ngay vào việc khắc phục các vấn đề về hiệu suất. Nếu bạn chưa hiểu rõ cách thiết lập và sử dụng Macrobenchmark trong dự án, hãy xem các tài nguyên sau:

Nhờ Macrobenchmark, bạn có thể chọn một trong những chế độ biên dịch sau:

  • None: Đặt lại trạng thái biên dịch và chạy mọi thứ ở chế độ JIT.
  • Partial: Biên dịch trước ứng dụng bằng Hồ sơ cơ sở và/hoặc các lần lặp lại khởi động và chạy ở chế độ JIT.
  • Full: Biên dịch trước toàn bộ mã của ứng dụng để không có mã nào chạy ở chế độ JIT.

Trong lớp học lập trình này, bạn chỉ dùng chế độ CompilationMode.Full() cho các phép đo điểm chuẩn vì bạn chỉ quan tâm đến những thay đổi mà mình đã thực hiện đối với mã này, chứ không phải trạng thái biên dịch của ứng dụng. Phương pháp này giúp bạn giảm phương sai do mã chạy ở chế độ JIT tạo ra. Phương sai đó sẽ giảm khi bạn triển khai Hồ sơ cơ sở tuỳ chỉnh. Lưu ý rằng chế độ Full có thể ảnh hưởng tiêu cực đến quá trình khởi động ứng dụng. Vì vậy, bạn không dùng chế độ này cho phép đo điểm chuẩn đo lường quá trình khởi động ứng dụng, mà chỉ dùng cho phép đo điểm chuẩn đo lường các điểm cải thiện hiệu suất trong thời gian chạy.

Khi bạn đã hoàn tất việc cải thiện hiệu suất và muốn kiểm tra hiệu suất để xem hiệu quả của phương pháp này khi người dùng cài đặt ứng dụng, hãy dùng chế độ CompilationMode.Partial() sử dụng hồ sơ cơ sở.

Trong phần tiếp theo, bạn tìm hiểu cách đọc các dấu vết để tìm ra vấn đề về hiệu suất.

4. Phân tích hiệu suất bằng tính năng theo dõi hệ thống

Nhờ bản dựng debuggable của ứng dụng, bạn có thể dùng Layout Inspector có số lượng thành phần kết hợp để nhanh chóng nắm được thời điểm thành phần nào thường xuyên kết hợp lại.

b7edfea340674732.gif

Tuy nhiên, đây chỉ là một phần trong quá trình kiểm tra hiệu suất tổng thể vì bạn chỉ nhận được kết quả đo lường proxy chứ không phải thời gian thực các thành phần kết hợp đó cần để hiển thị. Việc một thành phần kết hợp lại N lần không quá quan trọng nếu tổng thời lượng dưới 1 mili giây. Mặt khác, việc một thành phần chỉ kết hợp 1 hoặc 2 lần là rất quan trọng và mất 100 mili giây. Thông thường, một thành phần kết hợp có thể chỉ kết hợp 1 lần và không mất quá nhiều thời gian để làm việc đó cũng như giảm tốc độ màn hình của bạn.

Để kiểm tra vấn đề về hiệu suất một cách đáng tin cậy và nắm rõ hoạt động của ứng dụng cũng như việc quá trình đó có mất nhiều thời gian hơn bình thường hay không, bạn có thể dùng tính năng theo dõi hệ thống cùng tính năng theo dõi thành phần.

Tính năng theo dõi hệ thống cung cấp cho bạn thông tin về thời gian mà bất cứ hoạt động nào diễn ra trong ứng dụng của bạn. Tính năng này không làm tăng mức hao tổn cho ứng dụng nên bạn có thể giữ tính năng trong ứng dụng cải thiện hiệu suất mà không cần lo lắng về những tác động tiêu cực đến hiệu suất.

Thiết lập tính năng theo dõi thành phần

Compose tự động điền một số thông tin trong các giai đoạn của thời gian chạy, chẳng hạn như khi nào một thành phần kết hợp lại hoặc khi nào bố cục lazy tìm nạp trước các mục. Tuy nhiên, không đủ thông tin để thực sự tìm ra phần nào có thể có vấn đề. Bạn có thể tăng lượng thông tin bằng cách thiết lập tính năng theo dõi thành phần. Tính năng này cung cấp cho bạn tên của từng thành phần kết hợp riêng lẻ được kết hợp trong quá trình theo dõi. Việc này giúp bạn bắt đầu kiểm tra các vấn đề về hiệu suất mà không cần thêm nhiều phần trace("label") tuỳ chỉnh.

Để bật tính năng Theo dõi thành phần, hãy làm theo các bước sau:

  1. Thêm phần phụ thuộc runtime-tracing vào mô-đun :app:
implementation("androidx.compose.runtime:runtime-tracing:1.0.0-beta01")

Tại thời điểm này, bạn có thể ghi một dấu vết hệ thống bằng trình phân tích tài nguyên của Android Studio và trình này có thể có mọi thông tin. Tuy nhiên, chúng ta sẽ sử dụng Macrobenchmark để ghi hoạt động đo lường hiệu suất và dấu vết hệ thống.

  1. Thêm các phần phụ thuộc khác vào mô-đun :measure để bật tính năng theo dõi thành phần bằng Macrobenchmark:
implementation("androidx.tracing:tracing-perfetto:1.0.0")
implementation("androidx.tracing:tracing-perfetto-binary:1.0.0")
  1. Thêm đối số đo lường androidx.benchmark.fullTracing.enable=true vào tệp build.gradle của mô-đun :measure:
defaultConfig {
    // ...
    testInstrumentationRunnerArguments["androidx.benchmark.fullTracing.enable"] = "true"
}

Để biết thêm thông tin về cách thiết lập tính năng theo dõi thành phần, chẳng hạn như cách dùng tính năng này trên thiết bị đầu cuối, hãy xem tài liệu này.

Ghi lại hiệu suất ban đầu bằng Macrobenchmark

Có một số cách để bạn có thể truy xuất một tệp dấu vết hệ thống. Ví dụ: bạn có thể ghi bằng trình phân tích tài nguyên của Android Studio, ghi lại trên thiết bị hoặc truy xuất một dấu vết hệ thống đã ghi bằng Macrobenchmark. Trong lớp học lập trình này, bạn dùng các dấu vết do thư viện Macrobenchmark ghi lại.

Dự án này bao gồm các phép đo điểm chuẩn trong mô-đun :measure mà bạn có thể chạy để thu được kết quả đo lường hiệu suất. Phép đo điểm chuẩn trong dự án này được thiết lập để chỉ chạy một lần lặp lại nhằm tiết kiệm thời gian của lớp học lập trình này. Trong ứng dụng thực tế, bạn nên thiết lập ít nhất 10 lần lặp lại nếu phương sai kết quả cao.

Để ghi lại hiệu suất ban đầu, hãy dùng quy trình kiểm thử AccelerateHeavyScreenBenchmark có thể cuộn màn hình của màn hình tác vụ đầu tiên và làm theo các bước sau:

  1. Mở tệp AccelerateHeavyScreenBenchmark.kt.
  2. Chạy phép đo điểm chuẩn bằng thao tác trong rãnh kế bên lớp phép đo điểm chuẩn:

e93fb1dc8a9edf4b.png

Phép đo điểm chuẩn này cuộn màn hình Task 1 (Tác vụ 1), đồng thời ghi lại các phần dấu vết tuỳ chỉnh và

thời gian kết xuất khung hình.

8afabbbbbfc1d506.gif

Sau khi phép đo điểm chuẩn hoàn tất, bạn sẽ thấy kết quả trong ngăn kết quả của Android Studio:

AccelerateHeavyScreenBenchmark_accelerateHeavyScreenCompilationFull
ImagePlaceholderCount               min  20.0,   median  20.0,   max  20.0
ImagePlaceholderMs                  min  22.9,   median  22.9,   max  22.9
ItemTagCount                        min  80.0,   median  80.0,   max  80.0
ItemTagMs                           min   3.2,   median   3.2,   max   3.2
PublishDate.registerReceiverCount   min   1.0,   median   1.0,   max   1.0
PublishDate.registerReceiverMs      min   1.9,   median   1.9,   max   1.9
frameDurationCpuMs                  P50    5.4,   P90    9.0,   P95   10.5,   P99   57.5
frameOverrunMs                      P50   -4.2,   P90   -3.5,   P95   -3.2,   P99   74.9
Traces: Iteration 0

Sau đây là các chỉ số quan trọng trong kết quả:

  • frameDurationCpuMs: Cho bạn biết lượng thời gian cần để kết xuất khung hình. Lượng thời gian càng ngắn càng tốt.
  • frameOverrunMs: Cho bạn biết thời gian vượt quá giới hạn khung hình, bao gồm cả hoạt động trên GPU. Số âm là hợp lý vì nó có nghĩa là bạn vẫn còn thời gian.

Các chỉ số khác (chẳng hạn như chỉ số ImagePlaceholderMs) đang dùng các phần theo dõi tuỳ chỉnh và tổng thời lượng kết quả của tất cả các phần đó trong tệp theo dõi cũng như số lần diễn ra có chỉ số ImagePlaceholderCount.

Tất cả các chỉ số này có thể giúp chúng ta nắm được liệu những thay đổi mà chúng ta thực hiện đối với cơ sở mã có cải thiện hiệu suất hay không.

Đọc tệp theo dõi

Bạn có thể đọc tệp theo dõi trong Android Studio hoặc bằng công cụ Perfetto chạy trên nền tảng web.

Dù trình phân tích tài nguyên của Android Studio là một công cụ hiệu quả để nhanh chóng mở một dấu vết và hiện quy trình của ứng dụng, nhưng Perfetto cung cấp các tính năng kiểm tra chi tiết hơn cho mọi quy trình chạy trên một hệ thống bằng các truy vấn SQL mạnh mẽ và nhiều công cụ khác. Trong lớp học lập trình này, bạn dùng Perfetto để phân tích các dấu vết hệ thống.

  1. Mở trang web Perfetto để tải trang tổng quan của công cụ.
  2. Trên hệ thống tệp lưu trữ của bạn, tìm các dấu vết hệ thống do Macrobenchmark ghi lại, được lưu trong thư mục [module]/outputs/connected_android_test_additional_output/benchmarkRelease/connected/[device]/. Mỗi lần lặp lại của phép đo điểm chuẩn đều ghi một tệp theo dõi riêng biệt, mỗi tệp chứa cùng các lần lặp lại với ứng dụng của bạn.

51589f24d9da28be.png

  1. Kéo tệp AccelerateHeavyScreenBenchmark_...iter000...perfetto-trace vào giao diện người dùng của Perfetto và chờ đến khi công cụ này tải tệp theo dõi.
  2. Không bắt buộc: Nếu bạn không thể chạy phép đo điểm chuẩn và tạo tệp theo dõi, hãy tải tệp theo dõi của chúng tôi xuống rồi kéo tệp đó vào Perfetto:

547507cdf63ae73.gif

  1. Tìm quy trình có tên com.compose.performance trong ứng dụng của bạn. Thông thường, ứng dụng trên nền trước ở bên dưới làn thông tin về phần cứng và một số làn hệ thống.
  2. Mở trình đơn thả xuống có tên quy trình của ứng dụng. Bạn sẽ thấy danh sách các luồng chạy trong ứng dụng. Hãy giữ tệp theo dõi ở trạng thái mở vì bạn sẽ cần tệp này ở bước tiếp theo.

582b71388fa7e8b.gif

Để tìm ra vấn đề về hiệu suất trong ứng dụng, bạn có thể tận dụng Dòng thời gian dự kiến và Dòng thời gian thực tế ở đầu danh sách luồng của ứng dụng:

1bd6170d6642427e.png

Expected Timeline (Dòng thời gian dự kiến) cho bạn biết thời điểm hệ thống dự đoán số khung hình do ứng dụng tạo ra để hiển thị độ linh hoạt, giao diện người dùng có hiệu quả, trong trường hợp này là 16 mili giây và 600 micrô giây (1000 mili giây/60). Actual Timeline (Dòng thời gian thực tế) cho biết thời lượng thực tế của khung hình do ứng dụng tạo ra, bao gồm cả hoạt động của GPU.

Bạn có thể sẽ thấy nhiều màu sắc, cho biết những thông tin sau:

  • Khung hình màu xanh lục: Khung hình được tạo kịp thời.
  • Khung hình màu đỏ: Khung hình bị giật và mất nhiều thời gian hơn dự kiến. Bạn nên kiểm tra công việc đã hoàn tất trong những khung hình này để ngăn các vấn đề về hiệu suất.
  • Khung hình màu xanh lục nhạt: Khung hình được tạo trong giới hạn thời gian nhưng xuất hiện muộn, dẫn đến tình trạng tăng độ trễ của dữ liệu đầu vào.
  • Khung hình màu vàng: Khung hình bị giật nhưng không phải do ứng dụng.

Khi giao diện người dùng hiển thị trên màn hình, các thay đổi phải diễn ra nhanh hơn khoảng thời gian mà thiết bị của bạn mong đợi một khung hình được tạo. Trước đây, khoảng thời gian này xấp xỉ 16,6 mili giây và tốc độ làm mới màn hình là 60 Hz. Tuy nhiên, đối với thiết bị Android hiện đại, khoảng thời gian này có thể xấp xỉ 11 mili giây hoặc ngắn hơn vì tốc độ làm mới màn hình là 90 Hz trở lên. Khoảng thời gian cho từng khung hình cũng có thể khác nhau do tốc độ làm mới thay đổi.

Ví dụ: nếu giao diện người dùng của bạn bao gồm 16 mục, thì mỗi mục mất gần 1 mili giây để tạo nhằm ngăn các khung hình bị bỏ qua. Mặt khác, nếu bạn chỉ có một mục (chẳng hạn như trình phát video), thì mục này có thể mất tối đa 16 mili giây để kết hợp mà không bị giật.

Hiểu được biểu đồ lệnh gọi cho tính năng theo dõi hệ thống

Hình ảnh sau đây là ví dụ về phiên bản tinh giản của một dấu vết hệ thống cho thấy lệnh kết hợp lại.

8f16db803ca19a7d.png

Mỗi thanh từ trên xuống là tổng thời gian của các thanh bên dưới, những thanh này cũng tương ứng với phần mã của các hàm đã gọi. Compose gọi lệnh kết hợp lại trên thứ bậc của thành phần. Thành phần kết hợp đầu tiên là MaterialTheme. Bên trong MaterialTheme là thành phần cục bộ cung cấp thông tin về chủ đề. Từ đó, thành phần kết hợp HomeScreen sẽ được gọi. Thành phần kết hợp trên màn hình chính gọi các thành phần kết hợp MyImageMyButton trong quá trình kết hợp.

Khoảng trống trong các dấu vết hệ thống đến từ mã không được theo dõi đang chạy vì dấu vết hệ thống chỉ cho thấy mã được đánh dấu để theo dõi. Mã này chạy sau khi MyImage được gọi, nhưng trước khi MyButton được gọi và chiếm khoảng thời gian điều chỉnh kích thước khoảng trống.

Trong bước tiếp theo, bạn sẽ phân tích dấu vết mà mình đã lấy ở bước trước đó.

5. Tăng tốc thành phần kết hợp có kích thước lớn

Nhiệm vụ đầu tiên trong nỗ lực tối ưu hoá hiệu suất ứng dụng là bạn cần tìm bất kỳ thành phần kết hợp có kích thước lớn nào hoặc một tác vụ chạy trong thời gian dài trên luồng chính. Tác vụ chạy trong thời gian dài có thể mang nhiều ý nghĩa, tuỳ thuộc vào mức độ phức tạp của giao diện người dùng và khoảng thời gian để tạo giao diện người dùng đó.

Do đó, nếu một khung hình bị loại bỏ, bạn cần tìm ra những thành phần kết hợp mất quá nhiều thời gian và điều chỉnh để chúng chạy nhanh hơn bằng cách giảm tải luồng chính hoặc bỏ qua một số công việc của các thành phần này trên luồng chính.

Để phân tích dấu vết lấy từ quy trình kiểm thử AccelerateHeavyScreenBenchmark, hãy làm theo các bước sau:

  1. Mở dấu vết hệ thống mà bạn đã lấy ở bước trước đó.
  2. Phóng to khung hình dài đầu tiên. Khung hình này bao gồm quy trình khởi động giao diện người dùng sau khi dữ liệu được tải. Nội dung của khung hình này có dạng tương tự như trong hình ảnh sau:

838787b87b14bbaf.png

Trong dấu vết này, bạn có thể thấy nhiều hoạt động diễn ra bên trong một khung hình (có trong phần Choreographer#doFrame). Trong hình ảnh này, bạn có thể thấy phần công việc lớn nhất đến từ thành phần kết hợp chứa phần ImagePlaceholder. Phần này tải một hình ảnh lớn.

Không tải các hình ảnh lớn trên luồng chính

Có thể cần tải hình ảnh không đồng bộ từ mạng sử dụng một trong các thư viện tiện lợi như Coil hoặc Glide, nhưng điều gì xảy ra nếu bạn có một hình ảnh lớn ngay trong ứng dụng mà bạn cần hiển thị?

Hàm có khả năng kết hợp painterResource thường gặp (có nhiệm vụ tải một hình ảnh từ các tài nguyên) sẽ tải hình ảnh trên luồng chính trong quá trình kết hợp. Điều này nghĩa là nếu có kích thước lớn, hình ảnh của bạn có thể chặn luồng chính bằng tác vụ nào đó.

Trong trường hợp này, bạn có thể thấy vấn đề trong phần giữ chỗ của hình ảnh không đồng bộ. Thành phần kết hợp painterResource tải một hình ảnh của phần giữ chỗ. Quá trình tải này mất khoảng 23 mili giây.

c83d22c3870655a7.jpeg

Dưới đây là một số cách bạn có thể khắc phục vấn đề này:

  • Tải hình ảnh theo cách không đồng bộ.
  • Tạo hình ảnh nhỏ hơn để tải nhanh hơn.
  • Dùng một vectơ vẽ được để điều chỉnh theo tỷ lệ, dựa trên kích thước yêu cầu.

Để khắc phục vấn đề về hiệu suất này, hãy làm theo các bước sau:

  1. Chuyển đến tệp AccelerateHeavyScreen.kt.
  2. Tìm thành phần kết hợp imagePlaceholder() tải hình ảnh. Hình ảnh của phần giữ chỗ có kích thước là 1600 x 1600 px. Kích thước này rõ ràng quá lớn đối với nội dung mà hình ảnh hiển thị.

53b34f358f2ff74.jpeg

  1. Thay đổi đối tượng có thể vẽ thành R.drawable.placeholder_vector:
@Composable
fun imagePlaceholder() =
    trace("ImagePlaceholder") { painterResource(R.drawable.placeholder_vector) }
  1. Chạy lại quy trình kiểm thử AccelerateHeavyScreenBenchmark để tạo lại ứng dụng và lấy lại dấu vết hệ thống.
  2. Kéo dấu vết hệ thống vào trang tổng quan của Perfetto.

Ngoài ra, bạn có thể tải dấu vết này xuống:

  1. Tìm phần dấu vết ImagePlaceholder. Phần này sẽ cho bạn thấy trực tiếp phần đã cải thiện.

abac4ae93d599864.png

  1. Quan sát để đảm bảo hàm ImagePlaceholder không chặn luồng chính nhiều nữa.

8e76941fca0ae63c.jpeg

Là giải pháp thay thế trong ứng dụng thực tế, hình ảnh của phần giữ chỗ có thể không phải là nguyên nhân gây ra vấn đề mà là do hình minh hoạ nào đó. Trong trường hợp này, bạn có thể dùng thành phần kết hợp rememberAsyncImage của Coil. Thành phần kết hợp này giúp tải thành phần kết hợp theo cách không đồng bộ. Giải pháp này sẽ cho thấy không gian trống cho đến khi phần giữ chỗ được tải. Vì vậy, hãy lưu ý rằng bạn có thể cần có một phần giữ chỗ cho những kiểu hình ảnh này.

Ngoài ra, vẫn còn một số thành phần khác không hoạt động hiệu quả mà bạn sẽ giải quyết trong bước tiếp theo.

6. Giảm tải hoạt động phức tạp cho luồng trong nền

Nếu chúng tôi tiếp tục kiểm tra cùng một mục để phát hiện các vấn đề khác, bạn sẽ gặp các phần có tên binder transaction, mỗi phần mất khoảng 1 mili giây.

5c08376b3824f33a.png

Các phần có tên binder transaction cho biết có một hoạt động giao tiếp liên quy trình diễn ra giữa quy trình của bạn và một số quy trình hệ thống. Đây là cách thông thường để truy xuất một số thông tin từ hệ thống, chẳng hạn như truy xuất một dịch vụ hệ thống.

Những giao dịch này có trong nhiều API giao tiếp với hệ thống. Ví dụ: khi truy xuất một dịch vụ hệ thống bằng getSystemService, đăng ký bộ nhận tín hiệu truyền tin hoặc yêu cầu một ConnectivityManager.

Rất tiếc, những giao dịch này không cung cấp nhiều thông tin về những gì chúng đang yêu cầu. Vì vậy, bạn cần phải phân tích mã khi dùng API được đề cập rồi thêm một phần trace tuỳ chỉnh để đảm bảo đó là phần có vấn đề.

Để cải thiện giao dịch liên kết, hãy làm theo các bước sau:

  1. Mở tệp AccelerateHeavyScreen.kt.
  2. Tìm thành phần kết hợp PublishedText. Thành phần kết hợp này định dạng ngày giờ theo múi giờ hiện tại và đăng ký một đối tượng BroadcastReceiver theo dõi các thay đổi về múi giờ. Thành phần kết hợp này chứa một biến trạng thái currentTimeZone theo múi giờ mặc định của hệ thống dưới dạng giá trị ban đầu và sau đó là DisposableEffect (mã này đăng ký một bộ nhận tín hiệu truyền tin để biết các thay đổi về múi giờ). Cuối cùng, thành phần kết hợp này hiển thị ngày giờ đã định dạng bằng Text. DisposableEffect là lựa chọn phù hợp trong tình huống này vì bạn cần một cách để huỷ đăng ký bộ nhận tín hiệu truyền tin đã đăng ký trong lambda onDispose. Tuy nhiên, phần có vấn đề đó là mã bên trong DisposableEffect chặn luồng chính:
@Composable
fun PublishedText(published: Instant, modifier: Modifier = Modifier) {
    val context = LocalContext.current
    var currentTimeZone: TimeZone by remember { mutableStateOf(TimeZone.currentSystemDefault()) }

    DisposableEffect(Unit) {
        val receiver = object : BroadcastReceiver() {
            override fun onReceive(context: Context?, intent: Intent?) {
                currentTimeZone = TimeZone.currentSystemDefault()
            }
        }

        // TODO Codelab task: Wrap with a custom trace section
        context.registerReceiver(receiver, IntentFilter(Intent.ACTION_TIMEZONE_CHANGED))

        onDispose { context.unregisterReceiver(receiver) }
    }

    Text(
        text = published.format(currentTimeZone),
        style = MaterialTheme.typography.labelMedium,
        modifier = modifier
    )
}
  1. Gói context.registerReceiver bằng một lệnh gọi trace để đảm bảo rằng đây thực sự là nguyên nhân gây ra mọi binder transactions:
trace("PublishDate.registerReceiver") {
    context.registerReceiver(receiver, IntentFilter(Intent.ACTION_TIMEZONE_CHANGED))
}

Nhìn chung, một mã chạy trong thời gian dài trên luồng chính có thể không gây ra nhiều vấn đề. Tuy nhiên, thực tế là giao dịch này chạy cho mọi mục đơn lẻ hiển thị trên màn hình có thể gây ra vấn đề. Giả sử có 6 mục hiển thị trên màn hình, thì những mục này cần được tạo bằng khung hình đầu tiên. Chỉ những lệnh gọi này có thể mất thời gian 12 mili giây, gần bằng toàn bộ thời gian cho một khung hình.

Để khắc phục vấn đề này, bạn cần giảm tải việc đăng ký truyền tin đến một luồng khác. Bạn có thể làm việc này bằng các coroutine.

  1. Tải một phạm vi liên kết với vòng đời của thành phần kết hợp val scope = rememberCoroutineScope().
  2. Trong hiệu ứng này, hãy chạy một coroutine trên trình điều phối không phải là Dispatchers.Main. Ví dụ: trong trường hợp này là Dispatchers.IO. Bằng cách này, việc đăng ký truyền tin không chặn luồng chính, nhưng trạng thái thực tế currentTimeZone được lưu giữ trong luồng chính.
val scope = rememberCoroutineScope()

DisposableEffect(Unit) {
    val receiver = object : BroadcastReceiver() {
        override fun onReceive(context: Context?, intent: Intent?) {
            currentTimeZone = TimeZone.currentSystemDefault()
        }
    }

    // launch the coroutine on Dispatchers.IO
    scope.launch(Dispatchers.IO) {
        trace("PublishDate.registerReceiver") {
            context.registerReceiver(receiver, IntentFilter(Intent.ACTION_TIMEZONE_CHANGED))
        }
    }

    onDispose { context.unregisterReceiver(receiver) }
}

Có một bước nữa để tối ưu hoá quá trình này. Bạn chỉ cần duy nhất một bộ nhận tín hiệu truyền tin mà không cần có cho từng mục trong danh sách. Bạn nên chuyển bộ nhận này lên trên!

Bạn có thể chuyển bộ nhận này lên trên hoặc truyền tham số múi giờ xuống theo cây thành phần kết hợp. Nếu không sử dụng bộ nhận ở nhiều vị trí trong giao diện người dùng, thì bạn có thể dùng một thành phần cục bộ.

Để phục vụ cho mục đích của lớp học lập trình này, bạn sẽ lưu giữ bộ nhận tín hiệu truyền tin như một phần của cây thành phần kết hợp. Tuy nhiên, trong ứng dụng thực tế, việc tách bộ nhận thành một lớp dữ liệu có thể hữu ích để ngăn làm hỏng mã giao diện người dùng.

  1. Xác định thành phần cục bộ theo múi giờ mặc định của hệ thống:
val LocalTimeZone = compositionLocalOf { TimeZone.currentSystemDefault() }
  1. Cập nhật thành phần kết hợp ProvideCurrentTimeZone lấy lambda content để cung cấp múi giờ hiện tại:
@Composable
fun ProvideCurrentTimeZone(content: @Composable () -> Unit) {
    var currentTimeZone = TODO()

    CompositionLocalProvider(
        value = LocalTimeZone provides currentTimeZone,
        content = content,
    )
}
  1. Di chuyển DisposableEffect khỏi thành phần kết hợp PublishedText vào một thành phần kết hợp mới để chuyển lên trên tại đó, đồng thời thay thế currentTimeZone bằng trạng thái cũng như hiệu ứng phụ:
@Composable
fun ProvideCurrentTimeZone(content: @Composable () -> Unit) {
    val context = LocalContext.current
    val scope = rememberCoroutineScope()
    var currentTimeZone: TimeZone by remember { mutableStateOf(TimeZone.currentSystemDefault()) }

    DisposableEffect(Unit) {
        val receiver = object : BroadcastReceiver() {
            override fun onReceive(context: Context?, intent: Intent?) {
                currentTimeZone = TimeZone.currentSystemDefault()
            }
        }

        scope.launch(Dispatchers.IO) {
            trace("PublishDate.registerReceiver") {
                context.registerReceiver(receiver, IntentFilter(Intent.ACTION_TIMEZONE_CHANGED))
            }
        }

        onDispose { context.unregisterReceiver(receiver) }
    }

    CompositionLocalProvider(
        value = LocalTimeZone provides currentTimeZone,
        content = content,
    )
}
  1. Dùng ProvideCurrentTimeZone để gói một thành phần kết hợp mà bạn muốn thành phần cục bộ hợp lệ. Bạn có thể gói toàn bộ AccelerateHeavyScreen như minh hoạ trong đoạn mã sau:
@Composable
fun AccelerateHeavyScreen(items: List<HeavyItem>, modifier: Modifier = Modifier) {
    // TODO: Codelab task: Wrap this with timezone provider
    ProvideCurrentTimeZone {
        Box(
            modifier = modifier
                .fillMaxSize()
                .padding(24.dp)
        ) {
            ScreenContent(items = items)

            if (items.isEmpty()) {
                CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
            }
        }
    }
}
  1. Thay đổi thành phần kết hợp PublishedText để chỉ bao gồm chức năng định dạng cơ bản và đọc giá trị hiện tại của thành phần cục bộ thông qua LocalTimeZone.current:
@Composable
fun PublishedText(published: Instant, modifier: Modifier = Modifier) {
    Text(
        text = published.format(LocalTimeZone.current),
        style = MaterialTheme.typography.labelMedium,
        modifier = modifier
    )
}
  1. Chạy lại phép đo điểm chuẩn, quá trình này sẽ tạo ứng dụng.

Ngoài ra, bạn có thể tải dấu vết hệ thống xuống bằng mã đã chỉnh sửa:

  1. Kéo tệp dấu vết vào trang tổng quan của Perfetto. Mọi phần binder transactions đã bị xoá khỏi luồng chính.
  2. Tìm tên phần tương tự như bước trước đó. Bạn có thể tìm thấy tên này ở một trong các luồng khác do coroutine (DefaultDispatch) tạo:

87feee260f900a76.png

7. Xoá thành phần kết hợp phụ không cần thiết

Bạn đã di chuyển mã có kích thước lớn khỏi luồng chính nên mã đó sẽ không chặn thành phần nữa. Bạn vẫn có khả năng cải thiện. Bạn có thể xoá một số mức hao tổn không cần thiết dưới dạng một thành phần kết hợp LazyRow trong từng mục.

Ở ví dụ này, mỗi mục chứa một hàng thẻ được làm nổi bật trong hình ảnh sau:

e821c86604d3e670.png

Hàng này được triển khai bằng một thành phần kết hợp LazyRow vì rất dễ để viết theo cách này. Truyền các mục đến LazyRow và thành phần kết hợp này sẽ xử lý phần việc còn lại:

@Composable
fun ItemTags(tags: List<String>, modifier: Modifier = Modifier) {
    // TODO: remove unnecessary lazy layout
    LazyRow(
        modifier = modifier
            .padding(4.dp)
            .fillMaxWidth(),
        horizontalArrangement = Arrangement.spacedBy(2.dp)
    ) {
        items(tags) { ItemTag(it) }
    }
}

Vấn đề là mặc dù bố cục Lazy vượt trội hơn bố cục mà ở đó bạn có các mục có kích thước lớn hơn nhiều so với kích thước bị hạn chế, những bố cục này làm phát sinh một số chi phí khác không cần thiết khi thành phần lazy không được yêu cầu.

Do bản chất của các thành phần kết hợp Lazy sử dụng một thành phần kết hợp SubcomposeLayout nên chúng luôn được hiển thị dưới dạng nhiều phần công việc. Phần đầu tiên là vùng chứa và phần thứ hai là các mục đang hiển thị trên màn hình. Bạn cũng có thể tìm một dấu vết compose:lazylist:prefetch trong dấu vết hệ thống. Dấu vết này cho biết các mục khác đang di chuyển vào khung nhìn nên chúng được tìm nạp trước để sẵn sàng hoạt động.

b3dc3662b5885a2e.jpeg

Để xác định gần đúng khoảng thời gian quá trình này cần trong trường hợp của bạn, hãy mở cùng một tệp theo dõi. Bạn cũng có thể thấy rằng có các phần đã tách ra khỏi mục mẹ. Mỗi mục bao gồm mục được tạo thực tế và tiếp đến là các mục thẻ. Theo cách này, mỗi mục mất khoảng 2,5 mili giây để tạo. Nếu bạn nhân giá trị này với số mục hiển thị, kết quả sẽ là một phần lớn công việc khác.

a204721c80497e0f.jpeg

Để khắc phục vấn đề này, hãy làm theo các bước sau:

  1. Chuyển đến tệp AccelerateHeavyScreen.kt rồi tìm thành phần kết hợp ItemTags.
  2. Thay đổi phương thức triển khai LazyRow thành một thành phần kết hợp Row lặp lại trên danh sách tags như trong đoạn mã sau:
@Composable
fun ItemTags(tags: List<String>, modifier: Modifier = Modifier) {
    Row(
        modifier = modifier
            .padding(4.dp)
            .fillMaxWidth()
        horizontalArrangement = Arrangement.spacedBy(2.dp)
    ) {
        tags.forEach { ItemTag(it) }
    }
}
  1. Chạy lại phép đo điểm chuẩn, quá trình này cũng sẽ tạo ứng dụng.
  2. Không bắt buộc: Tải bản ghi theo dõi hệ thống xuống bằng mã đã chỉnh sửa:

  1. Tìm các phần ItemTag, quan sát xem phần này có mất ít thời gian và có sử dụng cùng một phần gốc Compose:recompose không.

219cd2e961defd1.jpeg

Tình huống tương tự cũng có thể xảy ra với các vùng chứa sử dụng một thành phần kết hợp SubcomposeLayout, chẳng hạn như thành phần kết hợp BoxWithConstraints. Tình huống này có thể kéo dài quá trình tạo mục trên các phần Compose:recompose. Những phần này có thể không hiển thị trực tiếp dưới dạng một khung bị giật nhưng người dùng có thể thấy. Nếu bạn có thể, hãy cố gắng tránh dùng thành phần kết hợp BoxWithConstraints trong từng mục vì thành phần này chỉ cần thiết khi bạn tạo một giao diện người dùng khác dựa trên không gian có sẵn.

Trong phần này, bạn đã tìm hiểu cách khắc phục các thành phần mất quá nhiều thời gian.

8. So sánh kết quả với điểm chuẩn ban đầu

Bây giờ, khi đã hoàn tất việc tối ưu hoá hiệu suất màn hình, bạn cần so sánh kết quả phép đo điểm chuẩn với kết quả ban đầu.

  1. Mở Test History (Nhật ký kiểm thử) trong ngăn chạy của Android Studio 667294bf641c8fc2.png
  2. Chọn lần chạy cũ nhất liên quan đến điểm chuẩn ban đầu mà không có thay đổi nào, đồng thời so sánh chỉ số frameDurationCpuMs với chỉ số frameOverrunMs. Bạn sẽ thấy kết quả tương tự như bảng sau:

Trước

AccelerateHeavyScreenBenchmark_accelerateHeavyScreenCompilationFull
ImagePlaceholderCount               min  20.0,   median  20.0,   max  20.0
ImagePlaceholderMs                  min  22.9,   median  22.9,   max  22.9
ItemTagCount                        min  80.0,   median  80.0,   max  80.0
ItemTagMs                           min   3.2,   median   3.2,   max   3.2
PublishDate.registerReceiverCount   min   1.0,   median   1.0,   max   1.0
PublishDate.registerReceiverMs      min   1.9,   median   1.9,   max   1.9
frameDurationCpuMs                  P50    5.4,   P90    9.0,   P95   10.5,   P99   57.5
frameOverrunMs                      P50   -4.2,   P90   -3.5,   P95   -3.2,   P99   74.9
Traces: Iteration 0
  1. Chọn lần chạy mới nhất liên quan đến điểm chuẩn bằng mọi quy trình tối ưu hoá. Bạn sẽ thấy kết quả tương tự như bảng sau:

Sau

AccelerateHeavyScreenBenchmark_accelerateHeavyScreenCompilationFull
ImagePlaceholderCount               min  20.0,   median  20.0,   max  20.0
ImagePlaceholderMs                  min   2.9,   median   2.9,   max   2.9
ItemTagCount                        min  80.0,   median  80.0,   max  80.0
ItemTagMs                           min   3.4,   median   3.4,   max   3.4
PublishDate.registerReceiverCount   min   1.0,   median   1.0,   max   1.0
PublishDate.registerReceiverMs      min   1.1,   median   1.1,   max   1.1
frameDurationCpuMs                  P50    4.3,   P90    7.7,   P95    8.8,   P99   33.1
frameOverrunMs                      P50  -11.4,   P90   -8.3,   P95   -7.3,   P99   41.8
Traces: Iteration 0

Nếu kiểm tra riêng hàng frameOverrunMs, bạn sẽ có thể thấy mọi phân vị được cải thiện:

P50

P90

P95

P99

trước

-4,2

-3,5

-3,2

74,9

sau

-11,4

-8,3

-7,3

41,8

cải thiện

171%

137%

128%

44%

Trong phần tiếp theo, bạn sẽ tìm hiểu cách khắc phục một thành phần hoạt động quá thường xuyên.

9. Ngăn các lần kết hợp lại không cần thiết

Compose có 3 giai đoạn:

  • Thành phần xác định nội dung cần hiển thị bằng cách tạo một cây thành phần kết hợp.
  • Bố cục lấy cây này và xác định vị trí thành phần kết hợp sẽ xuất hiện trên màn hình.
  • Vẽ sẽ vẽ các thành phần kết hợp trên màn hình.

Thứ tự của các giai đoạn này thường giống nhau, cho phép dữ liệu truyền theo một hướng từ thành phần đến bố cục rồi đến giai đoạn vẽ để tạo một khung hình của giao diện người dùng.

2147ae29192a1556.png

BoxWithConstraints, bố cục lazy (ví dụ: LazyColumn hoặc LazyVerticalGrid) và mọi bố cục dựa trên thành phần kết hợp SubcomposeLayout là các trường hợp ngoại lệ đáng chú ý. Trong đó, giai đoạn tạo của bố cục con tuỳ thuộc vào các giai đoạn của bố cục mẹ.

Nhìn chung, giai đoạn tạo là giai đoạn tốn kém nhất để chạy vì có hầu hết các công việc cần làm và bạn cũng có thể khiến các thành phần kết hợp không liên quan khác phải tạo lại.

Hầu hết mọi khung hình đều có 3 giai đoạn, nhưng Compose thực sự có thể hoàn toàn bỏ qua một giai đoạn nếu không có công việc nào cần làm. Bạn có thể tận dụng khả năng này để tăng hiệu suất của ứng dụng.

Hoãn các giai đoạn thành phần bằng đối tượng sửa đổi lambda

Các hàm có khả năng kết hợp sẽ chạy trong giai đoạn thành phần. Để cho phép mã chạy ở một thời điểm khác, bạn có thể cung cấp mã dưới dạng hàm lambda.

Cách làm như sau:

  1. Mở tệp PhasesComposeLogo.kt
  2. Chuyển đến màn hình Task 2 (Tác vụ 2) trong ứng dụng. Bạn sẽ thấy một biểu trưng phản chiếu viền của màn hình.
  3. Mở Layout Inspector rồi kiểm tra Số lần kết hợp lại. Bạn sẽ thấy số lần kết hợp lại tăng nhanh.

a9e52e8ccf0d31c1.png

  1. Không bắt buộc: Tìm tệp PhasesComposeLogoBenchmark.kt rồi chạy tệp này để truy xuất dấu vết hệ thống và xem thành phần của phần dấu vết PhasesComposeLogo diễn ra trên mọi khung hình. Các lần kết hợp lại hiển thị trong một dấu vết dưới dạng các phần lặp lại có cùng tên.

4b6e72578c89b2c1.jpeg 7036a895a31138d3.png

  1. Nếu cần, hãy đóng trình phân tích tài nguyên và Layout Inspector rồi quay lại mã. Bạn sẽ thấy thành phần kết hợp PhaseComposeLogo có dạng như sau:
@Composable
fun PhasesComposeLogo() = trace("PhasesComposeLogo") {
    val logo = painterResource(id = R.drawable.compose_logo)
    var size by remember { mutableStateOf(IntSize.Zero) }
    val logoPosition by logoPosition(size = size, logoSize = logo.intrinsicSize)

    Box(
        modifier = Modifier
            .fillMaxSize()
            .onPlaced {
                size = it.size
            }
    ) {
        with(LocalDensity.current) {
            Image(
                painter = logo,
                contentDescription = "logo",
                modifier = Modifier.offset(logoPosition.x.toDp(), logoPosition.y.toDp())
            )
        }
    }
}

Thành phần kết hợp logoPosition bao gồm logic thay đổi trạng thái bằng mọi khung và có dạng như sau:

@Composable
fun logoPosition(size: IntSize, logoSize: Size): State<IntOffset> =
    produceState(initialValue = IntOffset.Zero, size, logoSize) {
        if (size == IntSize.Zero) {
            this.value = IntOffset.Zero
            return@produceState
        }

        var xDirection = 1
        var yDirection = 1

        while (true) {
            withFrameMillis {
                value += IntOffset(x = MOVE_SPEED * xDirection, y = MOVE_SPEED * yDirection)

                if (value.x <= 0 || value.x >= size.width - logoSize.width) {
                    xDirection *= -1
                }

                if (value.y <= 0 || value.y >= size.height - logoSize.height) {
                    yDirection *= -1
                }
            }
        }
    }

Trạng thái đang được đọc trong thành phần kết hợp PhasesComposeLogo bằng đối tượng sửa đổi Modifier.offset(x.dp, y.dp). Điều này có nghĩa là trạng thái được đọc trong thành phần.

Đối tượng sửa đổi này là lý do ứng dụng tạo lại trên mọi khung hình của ảnh động này. Trong trường hợp này, cách thay thế đơn giản là đối tượng sửa đổi Offset dựa trên lambda.

  1. Cập nhật thành phần kết hợp Image để sử dụng đối tượng sửa đổi Modifier.offset. Đối tượng sửa đổi này chấp nhận một lambda trả về đối tượng IntOffset như trong đoạn mã sau:
Image(
  painter = logo,
  contentDescription = "logo",
  modifier = Modifier.offset { IntOffset(logoPosition.x,  logoPosition.y) }
)
  1. Chạy lại ứng dụng rồi kiểm tra Layout Inspector. Bạn sẽ thấy ảnh động không còn tạo lần kết hợp lại nào nữa.

Lưu ý rằng bạn không cần phải tạo lại chỉ để điều chỉnh bố cục của màn hình, đặc biệt là trong khi cuộn vì việc này có thể khiến khung hình bị giật. Quá trình kết hợp lại diễn ra trong khi hoạt động cuộn hầu như không cần thiết và nên tránh.

Các đối tượng sửa đổi lambda khác

Modifier.offset không phải là đối tượng sửa đổi duy nhất có phiên bản lambda này. Trong bảng sau, bạn có thể thấy các đối tượng sửa đổi thường gặp sẽ kết hợp lại vào mọi lúc. Những đối tượng này có thể thay bằng các đối tượng thay thế bị hoãn lại khi truyền trong giá trị trạng thái thường xuyên thay đổi:

Đối tượng sửa đổi thường gặp

Đối tượng sửa đổi thay thế bị hoãn

.background(color)

.drawBehind { drawRect(color) }

.offset(0.dp, y)

.offset { IntOffset(0, y.roundToPx()) }

.alpha(a).rotate(r).scale(s)

.graphicsLayer { alpha = a; rotationZ = r; scaleX = s; scaleY = s}

10. Hoãn các giai đoạn của Compose bằng bố cục tuỳ chỉnh

Cách dễ nhất thường dùng để tránh vô hiệu hoá thành phần là dùng đối tượng sửa đổi dựa trên lambda. Tuy nhiên, đôi khi không có đối tượng sửa đổi dựa trên lambda làm những việc bạn cần. Trong những trường hợp này, bạn có thể triển khai ngay một bố cục tuỳ chỉnh hay thậm chí thành phần kết hợp Canvas để chuyển thẳng đến giai đoạn vẽ. Trạng thái của Compose đọc xong trong một bố cục tuỳ chỉnh sẽ chỉ vô hiệu hoá bố cục và bỏ qua lần kết hợp lại. Theo nguyên tắc chung, nếu chỉ muốn điều chỉnh bố cục hoặc kích thước mà không thêm/xoá thành phần kết hợp, thì bạn có thể thường xuyên có được hiệu ứng mà không cần vô hiệu hoá thành phần.

Cách làm như sau:

  1. Mở tệp PhasesAnimatedShape.kt rồi chạy ứng dụng.
  2. Chuyển đến màn hình Task 3 (Tác vụ 3). Màn hình này gồm một hình dạng thay đổi kích thước khi bạn nhấp vào nút. Giá trị kích thước được tạo ảnh động bằng API Ảnh động animateDpAsState của Compose.

51dc23231ebd5f1a.gif

  1. Mở Layout Inspector.
  2. Nhấp vào Toggle size (Chuyển đổi kích thước).
  3. Nhận thấy rằng hình dạng tạo lại trên mọi khung hình của ảnh động.

63d597a98fca1133.png

Thành phần kết hợp MyShape lấy đối tượng size làm tham số. Đây là một trạng thái đọc. Điều này có nghĩa là khi đối tượng size thay đổi, thành phần kết hợp PhasesAnimatedShape (phạm vi kết hợp lại gần nhất) sẽ được kết hợp lại. Sau đó, thành phần kết hợp MyShape được kết hợp lại vì dữ liệu đầu vào đã thay đổi.

Để bỏ qua lần kết hợp lại, hãy làm theo các bước sau:

  1. Thay đổi tham số size thành hàm lambda để các thay đổi về kích thước không trực tiếp tạo lại thành phần kết hợp MyShape:
@Composable
fun MyShape(
    size: () -> Dp,
    modifier: Modifier = Modifier
) {
  // ...
  1. Cập nhật vị trí gọi trong thành phần kết hợp PhasesAnimatedShape để sử dụng hàm lambda:
MyShape(size = { size }, modifier = Modifier.align(Alignment.Center))

Việc thay đổi tham số size thành một lambda sẽ làm giảm tốc độ của trạng thái đọc. Tình trạng này hiện xảy ra khi lambda được gọi.

  1. Thay đổi nội dung của thành phần kết hợp MyShape thành nội dung như sau:
Box(
    modifier = modifier
        .background(color = Purple80, shape = CircleShape)
        .layout { measurable, _ ->
            val sizePx = size()
                .roundToPx()
                .coerceAtLeast(0)

            val constraints = Constraints.fixed(
                width = sizePx,
                height = sizePx,
            )

            val placeable = measurable.measure(constraints)
            layout(sizePx, sizePx) {
                placeable.place(0, 0)
            }
        }
)

Trên dòng đầu tiên của lambda đo lường đối tượng sửa đổi layout, bạn có thể thấy rằng lambda size đã được gọi. Lambda này ở trong đối tượng sửa đổi layout, nên chỉ vô hiệu hoá bố cục chứ không vô hiệu hoá thành phần.

  1. Chạy lại ứng dụng, chuyển đến màn hình Task 3 (Tác vụ 3) rồi mở Layout Inspector.
  2. Nhấp vào Toggle Size (Chuyển đổi kích thước), sau đó quan sát để đảm bảo rằng kích thước của hình dạng thay đổi giống như trước đây nhưng không phải tạo lại thành phần kết hợp MyShape.

11. Ngăn các lần kết hợp lại bằng lớp ổn định

Compose sẽ tạo mã có thể bỏ qua quá trình thực thi thành phần kết hợp nếu mọi tham số đầu vào đều ổn định và không thay đổi so với thành phần trước đây. Một loại được coi là ổn định nếu đó là loại bất biến hoặc nếu công cụ Compose có thể xác định liệu giá trị của thành phần đó có thay đổi giữa các lần kết hợp lại hay không.

Nếu không xác định được một thành phần kết hợp có ổn định hay không, công cụ Compose sẽ coi thành phần đó là không ổn định và sẽ không tạo logic mã để bỏ qua quá trình kết hợp lại. Điều này có nghĩa là thành phần kết hợp sẽ kết hợp lại mọi lúc. Điều này có thể xảy ra khi lớp không phải là loại cơ bản và một trong các tình huống sau đây xảy ra:

  • Đó là một lớp có thể biến đổi. Chẳng hạn như, lớp này chứa một thuộc tính có thể biến đổi.
  • Đó là một lớp được xác định trong mô-đun Gradle là không dùng Compose. Lớp này không có một phần phụ thuộc trên trình biên dịch của Compose.
  • Đó là một lớp chứa thuộc tính không ổn định.

Trong một số trường hợp, đây có thể là hành vi không mong muốn vì nó gây ra các vấn đề về hiệu suất và có thể thay đổi khi bạn làm những việc sau:

  • Bật chế độ bỏ qua hữu hiệu
  • Diễn giải tham số bằng chú thích @Immutable hoặc @Stable.
  • Thêm lớp vào tệp cấu hình ổn định.

Để biết thêm thông tin về độ ổn định, hãy đọc tài liệu này.

Trong tác vụ này, bạn sẽ thấy một danh sách các mục có thể thêm, xoá hoặc kiểm tra. Bạn cần đảm bảo rằng các mục này không kết hợp lại khi không cần thiết. Có 2 loại mục thay thế giữa các loại được tạo lại mọi lúc và các loại không được tạo lại mọi lúc.

Các mục được tạo lại mọi lúc có ở đây dưới dạng mô phỏng trường hợp sử dụng thực tế. Trong đó, dữ liệu đến từ cơ sở dữ liệu cục bộ (ví dụ: Room hoặc sqlDelight) hoặc nguồn dữ liệu từ xa (chẳng hạn như các yêu cầu API hay thực thể Firestore) rồi trả về một thực thể mới của đối tượng mỗi khi có sự thay đổi.

Một số thành phần kết hợp được gắn đối tượng sửa đổi Modifier.recomposeHighlighter() mà bạn có thể thấy trong kho lưu trữ GitHub của chúng tôi. Đối tượng sửa đổi này hiển thị đường viền bất cứ khi nào một thành phần kết hợp được kết hợp lại và có thể dùng làm giải pháp thay thế tạm thời cho Layout Inspector.

127f2e4a2fc1a381.gif

Bật chế độ bỏ qua hữu hiệu

Trình biên dịch Jetpack Compose 1.5.4 trở lên có một tuỳ chọn để bật chế độ bỏ qua hữu hiệu. Điều này có nghĩa là ngay cả các thành phần kết hợp có tham số không ổn định cũng có thể tạo mã bỏ qua. Chế độ này dự kiến giảm đáng kể số lượng thành phần kết hợp không thể bỏ qua trong dự án của bạn, nhờ đó cải thiện hiệu suất mà không cần thay đổi mã.

Đối với các tham số không ổn định, logic bỏ qua được so sánh để có sự cân bằng về thực thể. Điều này nghĩa là tham số sẽ được bỏ qua nếu cùng một thực thể được truyền đến thành phần kết hợp như ở trường hợp trước. Ngược lại, các tham số ổn định lại sử dụng sự cân bằng về cấu trúc (bằng cách gọi phương thức Object.equals()) để xác định logic bỏ qua.

Ngoài logic bỏ qua, chế độ bỏ qua hữu hiệu cũng tự động ghi nhớ các lambda trong hàm có khả năng kết hợp. Như vậy tức là bạn không cần lệnh gọiremember để gói hàm lambda, chẳng hạn như lệnh gọi một phương thức ViewModel.

Có thể bật chế độ bỏ qua hữu hiệu dựa trên mô-đun Gradle.

Để bật chế độ này, hãy làm theo các bước sau:

  1. Mở tệp build.gradle.kts của ứng dụng.
  2. Cập nhật khối composeCompiler bằng đoạn mã sau:
composeCompiler {
    // Not required in Kotlin 2.0 final release
    suppressKotlinVersionCompatibilityCheck = "2.0.0-RC1"

    // This settings enables strong-skipping mode for all module in this project.
    // As an effect, Compose can skip a composable even if it's unstable by comparing it's instance equality (===).
    enableExperimentalStrongSkippingMode = true
}

Thao tác này sẽ thêm đối số của trình biên dịch experimentalStrongSkipping vào mô-đun Gradle.

  1. Nhấp vào b8a9619d159a7d8e.png Sync project with Gradle files (Đồng bộ hoá dự án với tệp Gradle).
  2. Tạo lại dự án.
  3. Mở màn hình Task 5 (Tác vụ 5). Khi đó, bạn sẽ thấy rằng những mục sử dụng sự cân bằng về cấu trúc được đánh dấu bằng biểu tượng EQU và không tạo lại khi bạn tương tác với danh sách các mục này.

1de2fd2c42a1f04f.gif

Tuy nhiên, các loại mục khác vẫn có thể được tạo lại. Bạn sẽ khắc phục vấn đề này ở bước tiếp theo.

Khắc phục vấn đề về độ ổn định bằng chú giải

Như đề cập trước đó, khi bạn bật chế độ bỏ qua hữu hiệu, một thành phần kết hợp sẽ bỏ qua việc thực thi khi tham số có cùng thực thể như trong thành phần trước. Tuy nhiên, điều này không đúng trong các tình huống mà với mỗi thay đổi, một thực thể mới của lớp không ổn định sẽ được cung cấp.

Trong tình huống của bạn, lớp StabilityItem không ổn định vì lớp này chứa thuộc tính LocalDateTime không ổn định.

Để khắc phục vấn đề về độ ổn định của lớp này, hãy làm theo các bước sau:

  1. Chuyển đến tệp StabilityViewModel.kt.
  2. Tìm lớp StabilityItem và diễn giải lớp này bằng chú giải @Immutable:
// TODO Codelab task: make this class Stable
@Immutable
data class StabilityItem(
    val id: Int,
    val type: StabilityItemType,
    val name: String,
    val checked: Boolean,
    val created: LocalDateTime
)
  1. Tạo lại ứng dụng.
  2. Chuyển đến màn hình Task 5 (Tác vụ 5). Bạn sẽ thấy rằng không có mục nào trong danh sách được tạo lại.

938aad77b78f7590.gif

Lớp này hiện sử dụng sự cân bằng về cấu trúc để kiểm tra xem lớp có thay đổi so với thành phần trước hay không và do đó không kết tạo lại các mục trong danh sách.

Vẫn có thành phần kết hợp dùng để chỉ ngày thay đổi gần đây nhất. Thành phần này luôn được tạo lại, bất kể những việc bạn đã làm cho đến thời điểm này.

Khắc phục vấn đề về độ ổn định bằng tệp cấu hình

Phương pháp trước đây phát huy hiệu quả đối với các lớp trong cơ sở mã của bạn. Tuy nhiên, phương pháp này không thể chỉnh sửa các lớp nằm ngoài phạm vi, chẳng hạn như lớp từ thư viện của bên thứ ba hoặc lớp thư viện tiêu chuẩn.

Bạn có thể kích hoạt tệp cấu hình ổn định để lấy các lớp (có ký tự đại diện có thể dùng) sẽ được coi là ổn định.

Để kích hoạt tệp này, hãy làm theo các bước sau:

  1. Chuyển đến tệp build.gradle.kts của ứng dụng.
  2. Thêm lựa chọn stabilityConfigurationFile vào khối composeCompiler:
composeCompiler {
    ...

    stabilityConfigurationFile = project.rootDir.resolve("stability_config.conf")
}
  1. Đồng bộ hoá dự án với tệp Gradle.
  2. Mở tệp stability_config.conf (cạnh tệp README.md) trong thư mục gốc của dự án này.
  3. Thêm nội dung như sau:
// TODO Codelab task: Make a java.time.LocalDate class stable.
java.time.LocalDate
  1. Tạo lại ứng dụng. Nếu ngày vẫn giữ nguyên, lớp LocalDateTime sẽ không khiến thành phần kết hợp Lần thay đổi gần đây nhất DD-MM-YYYY được tạo lại.

332ab0b2c91617f2.gif

Trong ứng dụng, bạn có thể mở rộng tệp để thêm các mẫu. Vì vậy, bạn không cần phải viết mọi lớp được coi là ổn định. Do đó, trong trường hợp này, bạn có thể dùng ký tự đại diện java.time.*. Ký tự đại diện này sẽ coi mọi lớp trong gói là ổn định, chẳng hạn như Instant, LocalDateTime, ZoneId và các lớp khác từ java.time.

Khi bạn làm theo các bước này, sẽ không có mục nào trên màn hình kết hợp lại, trừ mục đã được thêm hoặc tương tác. Đây là hành vi dự kiến.

12. Xin chúc mừng

Xin chúc mừng, bạn đã tối ưu hoá hiệu suất của ứng dụng Compose! Dù chỉ tìm hiểu một phần nhỏ vấn đề về hiệu suất mà bạn có thể gặp phải trong ứng dụng, nhưng bạn đã biết cách xem xét các vấn đề tiềm ẩn khác và cách khắc phục chúng.

Tiếp theo là gì?

Hãy tạo một Hồ sơ cơ sở cho ứng dụng của mình nếu bạn chưa tạo.

Bạn có thể tham khảo lớp học lập trình Cải thiện hiệu suất của ứng dụng nhờ Hồ sơ cơ sở. Nếu bạn muốn biết thêm thông tin về việc thiết lập phép đo điểm chuẩn, hãy xem lớp học lập trình Kiểm tra hiệu suất của ứng dụng bằng Macrobenchmark.

Tìm hiểu thêm