Khắc phục sự cố về độ ổn định

Khi gặp phải một lớp không ổn định gây ra vấn đề về hiệu suất, bạn nên tối ưu hoá cách Compose so sánh hoặc tạo thực thể cho lớp đó. Tài liệu này trình bày một số kỹ thuật mà bạn có thể sử dụng để làm việc đó.

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

Chế độ bỏ qua hữu hiệu được bật theo mặc định kể từ Kotlin 2.0.20. Chế độ bỏ qua mạnh cho phép bỏ qua các thành phần kết hợp có tham số không ổn định bằng cách so sánh các tham số không ổn định bằng cách sử dụng tính bằng nhau của thực thể (===), trong khi các tham số ổn định tiếp tục được so sánh bằng cách sử dụng tính bằng nhau về cấu trúc (Object.equals()).

Nếu bạn đang sử dụng phiên bản Kotlin thấp hơn 2.0.20, hãy bật chế độ bỏ qua mạnh mẽ trước. Nếu chế độ bỏ qua mạnh đã được bật và một composable vẫn recomposition không cần thiết, hãy kiểm tra xem các thực thể mới của tham số không ổn định có được phân bổ trên mỗi lượt recomposition hay không.

Hãy xem phần Bỏ qua mạnh để biết thêm thông tin.

Đặt lớp thành không thể thay đổi

Bạn cũng có thể đặt một lớp không ổn định thành hoàn toàn không thể thay đổi để trình biên dịch có thể tự động suy luận độ ổn định của lớp đó.

  • Immutable (Không thể thay đổi): Cho biết một loại mà giá trị của bất kỳ thuộc tính nào cũng không thể thay đổi sau khi một thực thể của loại đó được tạo và tất cả các phương thức đều minh bạch về mặt tham chiếu.
    • Đảm bảo tất cả thuộc tính của lớp đều là val thay vì var, và thuộc loại không thể thay đổi.
    • Các kiểu dữ liệu nguyên thuỷ như String, Int và Float luôn không thể thay đổi.
    • Các loại data class phẳng có thuộc tính gốc val hoặc thuộc tính String trong các mô-đun được tạo bằng trình biên dịch Compose đã được suy luận là ổn định và được so sánh tự động với .equals(). Bạn không cần chú thích các loại này bằng @Immutable.
    • Nếu không thể sử dụng tính bất biến, thì bạn phải sử dụng trạng thái Compose cho mọi thuộc tính có thể thay đổi.
  • Ổn định: Cho biết một kiểu có thể thay đổi. Thời gian chạy Compose không nhận biết được liệu có bất kỳ thuộc tính công khai hoặc hành vi phương thức nào của kiểu dữ liệu sẽ mang lại kết quả khác với lệnh gọi trước đó hay không và khi nào thì điều đó xảy ra.

Bộ sưu tập và độ ổn định

Một lý do phổ biến khiến Compose coi một lớp là không ổn định là tập hợp. Như đã lưu ý trên trang Chẩn đoán vấn đề về độ ổn định, trình biên dịch Compose không thể hoàn toàn chắc chắn rằng các giao diện tập hợp như List, Map và Set là thực sự không thể thay đổi và do đó đánh dấu chúng là không ổn định.

Hãy xem xét lớp này trong hướng dẫn Chẩn đoán vấn đề về độ ổn định:

unstable class Snack {
  …
  unstable val tags: Set<String>
  …
}

Khi bật Chế độ bỏ qua hữu hiệu (mặc định trong Kotlin 2.0.20 trở lên), các tập hợp và lớp không ổn định chứa tập hợp đã bỏ qua quá trình kết hợp lại bằng cách sử dụng tính bằng nhau của thực thể (===) bất cứ khi nào cùng một tệp đối chiếu được truyền.

Hãy thận trọng trước khi thay thế kotlin.collections.List, Set hoặc Map bằng Kotlinx Immutable Collections (ImmutableList, ImmutableSet, ImmutableMap) hoặc thêm quy tắc @Immutable, @Stable hoặc stability_config.conf vào mô hình chứa tập hợp. Trong chế độ bỏ qua mạnh, List chuẩn sẽ bỏ qua bằng cách sử dụng tính bằng nhau của tham chiếu (===), trong khi ImmutableList và các mô hình tập hợp có chú thích chạy so sánh .equals() theo từng phần tử trên mỗi lượt recomposition.

Đối với các tập hợp lớn hoặc bố cục tải từng phần (LazyColumn, LazyRow, LazyGrid), việc kiểm tra .equals() trên mọi mục có thể mất nhiều thời gian hơn so với việc kết hợp lại. Chi phí này sẽ tăng gấp đôi nếu bạn truyền thuộc tính ImmutableList từ data class đến một thành phần kết hợp con:

data class SnackCategory(
    val name: String,
    val snacks: ImmutableList<Snack>
)

@Composable
fun CategorySection(category: SnackCategory) {
    Text(category.name)
    // Compose compares category.snacks twice: once in SnackCategory.equals()
    // for CategorySection, and again for HighlightedSnacks.
    HighlightedSnacks(snacks = category.snacks)
}

Thay vì buộc phải cân bằng cấu trúc trên các tập hợp, hãy giữ lại tham chiếu thực thể của tập hợp trên các lượt phát (ví dụ: bằng cách lưu các phép biến đổi vào bộ nhớ đệm trong ViewModel hoặc bằng remember) để Compose có thể bỏ qua việc sử dụng tính cân bằng thực thể (===). Chỉ cân nhắc sử dụng chú thích ImmutableList hoặc chú thích về độ ổn định trên các mô hình chứa tập hợp khi bạn không thể giữ lại tham chiếu thực thể của tập hợp và điểm chuẩn xác nhận rằng việc kiểm tra tính cân bằng cấu trúc nhanh hơn việc kết hợp lại. Nếu bạn đang sử dụng phiên bản Kotlin thấp hơn 2.0.20, hãy bật Chế độ bỏ qua mạnh trước.

Chú thích bằng Stable hoặc Immutable

Một cách có thể giúp giải quyết các vấn đề về độ ổn định cho các lớp nhỏ có quy trình kiểm tra .equals() không tốn kém là chú thích các lớp không ổn định bằng @Stable hoặc @Immutable.

Việc chú thích một lớp sẽ ghi đè những gì trình biên dịch suy luận về lớp của bạn. Toán tử này tương tự như !! trong Kotlin. Bạn nên hết sức cẩn thận khi sử dụng các chú thích này. Việc ghi đè hành vi của trình biên dịch có thể dẫn đến các lỗi không lường trước, chẳng hạn như thành phần kết hợp không kết hợp lại như bạn mong đợi.

Khi bật Chế độ bỏ qua mạnh, hãy làm theo các nguyên tắc sau khi quyết định xem có nên thêm @Stable hay @Immutable hay không:

  • Đừng chú thích các lớp đã được suy luận là ổn định: Các lớp có thuộc tính val thuộc loại ổn định trong các mô-đun được tạo bằng trình biên dịch Compose đã được suy luận là ổn định.
  • Chỉ chú thích các lớp có quy trình kiểm tra .equals() đơn giản khi các thực thể được tạo lại: Thêm @Stable hoặc @Immutable khi trình biên dịch không thể suy luận độ ổn định (ví dụ: giao diện hoặc lớp từ các mô-đun không phải Compose) và nguồn dữ liệu phân bổ các thực thể đối tượng mới cho các mục không thay đổi trong mỗi lần phát, miễn là lớp đó nhỏ và có quy trình kiểm tra .equals() đơn giản.
  • Thận trọng với các mô hình chứa tập hợp: Tránh thêm @Stable hoặc @Immutable vào các mô hình dữ liệu chứa tập hợp lớn (List, Set, Map), đặc biệt là đối với các mô hình dùng trong bố cục tải từng phần (LazyColumn, LazyRow, LazyGrid, Pager). Việc không chú thích các mô hình đó cho phép Compose so sánh chúng bằng cách sử dụng tính bằng nhau của thực thể (===) thay vì chạy các quy trình kiểm tra .equals() về cấu trúc trên mọi phần tử.

Đoạn mã sau đây cung cấp một ví dụ tối thiểu về lớp dữ liệu phẳng (không có thuộc tính tập hợp) được chú thích là không thể thay đổi khi được xác định trong một mô-đun mà trình biên dịch không thể suy luận độ ổn định:

@Immutable
data class SnackSummary(
    val id: Long,
    val name: String,
    val price: Long
)

Cho dù bạn sử dụng chú thích @Immutable hay @Stable, trình biên dịch Compose đều đánh dấu lớp SnackSummary là ổn định và so sánh lớp đó bằng Object.equals().

Các lớp được chú thích trong tập hợp

Hãy xem xét một composable có chứa tham số thuộc loại List<Snack>:

restartable skippable scheme("[androidx.compose.ui.UiComposable]") fun HighlightedSnacks(
  stable index: Int
  unstable snacks: List<Snack>
  stable onSnackClick: Function1<Long, Unit>
  stable modifier: Modifier? = @static Companion
)

Ngay cả khi Snack ổn định hoặc được chú thích bằng @Immutable, trình biên dịch Compose vẫn đánh dấu tham số snacks trong HighlightedSnacks là unstable vì List là một giao diện.

Khi bạn bật Chế độ bỏ qua hữu hiệu, HighlightedSnacks vẫn được đánh dấu là skippable và bỏ qua quá trình kết hợp lại bằng cách sử dụng tính bằng nhau của thực thể (===) khi cùng một thực thể List được truyền. Bạn có thể đặt tham số snacks thành stable bằng cách gói List<Snack> trong một lớp trình bao bọc @Immutable, thay thế List<Snack> bằng ImmutableList<Snack> hoặc thêm kotlin.collections.* vào tệp cấu hình độ ổn định.

Tuy nhiên, việc này khiến Compose chạy phép so sánh .equals() theo từng phần tử trên mỗi lượt kết hợp lại. Chỉ sử dụng các phương pháp này khi bạn không thể giữ lại tệp đối chiếu thực thể của danh sách và điểm chuẩn cho thấy lợi ích.

Tệp cấu hình độ ổn định

Kể từ Trình biên dịch Compose 1.5.5, bạn có thể cung cấp tệp cấu hình của các lớp được coi là ổn định tại thời gian biên dịch. Điều này cho phép xem xét các lớp mà bạn không kiểm soát, chẳng hạn như các lớp thư viện chuẩn như LocalDateTime, là ổn định.

Tệp cấu hình là một tệp văn bản thuần túy, mỗi hàng chứa một lớp. Hỗ trợ nhận xét, ký tự đại diện đơn và kép.

Ví dụ về cấu hình:

// Consider LocalDateTime stable
java.time.LocalDateTime
// Consider my datalayer stable
com.datalayer.*
// Consider my datalayer and all submodules stable
com.datalayer.**
// Consider my generic type stable based off it's first type parameter only
com.example.GenericClass<*,_>

Để bật tính năng này, hãy truyền đường dẫn của tệp cấu hình đến khối tuỳ chọn composeCompiler của cấu hình Trình bổ trợ Gradle của trình biên dịch Compose.

composeCompiler {
  stabilityConfigurationFile = rootProject.layout.projectDirectory.file("stability_config.conf")
}

Vì trình biên dịch Compose chạy riêng biệt trên từng mô-đun trong dự án, nên bạn có thể cung cấp các cấu hình khác nhau cho các mô-đun khác nhau nếu cần. Ngoài ra, bạn có thể có một cấu hình ở cấp gốc của dự án và truyền đường dẫn đó đến từng mô-đun.

Nhiều mô-đun

Một vấn đề thường gặp khác liên quan đến cấu trúc nhiều mô-đun. Trình biên dịch Compose chỉ có thể dự đoán liệu một lớp có ổn định hay không nếu tất cả các kiểu không nguyên gốc mà lớp này tham chiếu đều được đánh dấu rõ ràng là ổn định hoặc nằm trong một mô-đun cũng được tạo bằng trình biên dịch Compose.

Nếu lớp dữ liệu của bạn nằm trong một mô-đun riêng biệt với lớp giao diện người dùng (đây là phương pháp nên dùng), thì bạn có thể gặp phải vấn đề này. Trong Chế độ bỏ qua mạnh, các lớp từ mô-đun không phải Compose được coi là không ổn định và vẫn bỏ qua quá trình kết hợp lại bằng cách sử dụng tính bằng nhau của thực thể (===) khi cùng một thực thể được truyền. Tuy nhiên, nếu lớp dữ liệu của bạn thường xuyên phân bổ các thực thể mới của mô hình nhỏ, phẳng có giá trị giống hệt nhau trên mỗi lượt phát, thì bạn có thể chọn sử dụng tính năng so sánh cấu trúc (Object.equals()).

Giải pháp

Để giải quyết vấn đề này, bạn có thể áp dụng một trong những cách sau:

  1. Thêm các lớp nhỏ, phẳng có các bước kiểm tra .equals() đơn giản vào Tệp cấu hình trình biên dịch.
  2. Bật trình biên dịch Compose trên các mô-đun lớp dữ liệu hoặc gắn thẻ các lớp bằng @Stable hoặc @Immutable nếu thích hợp.
    • Việc này bao gồm việc thêm phần phụ thuộc Compose vào lớp dữ liệu. Tuy nhiên, đây chỉ là phần phụ thuộc cho thời gian chạy Compose chứ không phải cho Compose-UI.
  3. Trong mô-đun giao diện người dùng, hãy gói các lớp dữ liệu trong các lớp trình bao bọc dành riêng cho giao diện người dùng.

Vấn đề tương tự cũng xảy ra khi sử dụng thư viện bên ngoài nếu các thư viện đó không sử dụng trình biên dịch Compose.

Không phải thành phần kết hợp nào cũng có thể bỏ qua

Khi khắc phục các vấn đề về độ ổn định, bạn không nên cố gắng đặt mọi composable thành có thể bỏ qua hoặc buộc mọi tham số phải có cấu trúc bằng nhau. Việc bỏ qua một thành phần kết hợp không phải là miễn phí: mọi tham số của một thành phần kết hợp có thể bỏ qua đều được lưu trữ trong thời gian chạy Compose như thể nó được ghi nhớ, và Compose phải so sánh từng tham số với giá trị trước đó mỗi khi hàm kết hợp lại để xác định xem có nên bỏ qua hay không. Việc cố gắng buộc bỏ qua ở mọi nơi có thể dẫn đến việc tối ưu hoá sớm, gây ra nhiều hao tổn hơn so với mức tiết kiệm được.

Có nhiều trường hợp việc buộc ổn định cấu trúc không mang lại lợi ích thực sự nào và có thể làm giảm hiệu suất hoặc khả năng bảo trì. Ví dụ:

  • Một composable không được recomposition thường xuyên hoặc hoàn toàn không được recomposition.
  • Một composable mà bản thân nó chỉ gọi các composable có thể bỏ qua.
  • Một composable có số lượng tham số lớn hoặc mô hình tập hợp có các phương thức triển khai .equals() tốn kém. Trong trường hợp này, chi phí kiểm tra xem có tham số nào thay đổi hay không có thể lớn hơn chi phí của một quá trình kết hợp lại không tốn kém.

Đừng chỉ tập trung vào số lần kết hợp lại. Tập trung vào việc tạo các thành phần kết hợp gọn nhẹ để thực thi, trì hoãn việc đọc trạng thái thay đổi nhanh chóng sang giai đoạn Bố cục hoặc Vẽ và xác minh các thay đổi bằng điểm chuẩn. Khi một thành phần kết hợp có thể bỏ qua, thành phần đó sẽ làm tăng một chút mức hao tổn và có thể không đáng. Bạn thậm chí có thể chú thích composable của mình là không thể khởi động lại hoặc không thể bỏ qua trong trường hợp bạn xác định rằng việc có thể khởi động lại hoặc có thể bỏ qua sẽ tốn nhiều tài nguyên hơn mức cần thiết.