Công cụ và hiệu suất

Vượt qua chính mình trong Compose

Loa

Giới thiệu

Giới thiệu về bài thuyết trình

Jetpack Compose giúp bạn dễ dàng thiết kế giao diện người dùng chất lượng cao, nhưng có thể khó theo dõi các vấn đề ảnh hưởng đến hiệu suất giao diện người dùng. Những vấn đề này có thể dẫn đến các vấn đề mà người dùng nhận thấy, chẳng hạn như độ trễ khi khởi động và khung hình bị rớt xuất hiện dưới dạng ảnh động bị giật hoặc cuộn bị giật.

Trong buổi trò chuyện này, chúng tôi sẽ giới thiệu các công cụ giúp bạn khám phá những yếu tố ảnh hưởng đến hiệu suất giao diện người dùng, tập trung vào Perfetto và tính năng theo dõi quá trình (tái) kết hợp. Sau đó, chúng ta sẽ tìm hiểu các mẫu phản thiết kế cụ thể mà bạn có thể đang sử dụng và đang âm thầm gây tốn kém cho bạn, cách tìm các mẫu đó trong mã và những mẫu tích cực mà bạn có thể dùng để thay thế. Khi kết thúc bài nói chuyện này, bạn sẽ hiểu cách tư duy trong Compose với tư duy về hiệu suất, cũng như cách lập hồ sơ, phân tích và xác minh các hoạt động tối ưu hoá hiệu suất trong ứng dụng của bạn.

Tóm tắt

  • Dấu vết Perfetto ghi lại hoạt động trên thiết bị và cho thấy các lát cắt Compose như thành phần và bố cục dọc theo luồng chính. Hãy sử dụng các công cụ này để kiểm tra dòng thời gian của khung hình và theo dõi thời lượng của các giai đoạn.
  • Mọi thao tác bên trong thành phần kết hợp đều có chi phí, bao gồm cả việc khởi chạy và huỷ coroutine. Kiểm tra các API có hiệu ứng phụ và cân nhắc việc hợp nhất các hiệu ứng có khoá giống hệt nhau để giảm mức hao tổn của thành phần kết hợp.
  • Tính năng theo dõi thành phần chú thích các hàm composable, còn tính năng theo dõi recomposition (phiên bản 1.13 trở lên) theo dõi các hoạt động đọc và ghi state gây ra recomposition. Các điểm đánh dấu này giúp chẩn đoán công việc ngoài dự kiến ngay trong dấu vết Perfetto.
  • ComputedState (Compose 1.13 trở lên) tránh được các trường hợp vô hiệu hoá không mong muốn và thường nhanh hơn DerivedState trong các trường hợp vô hiệu hoá đọc sau khi ghi. DerivedState vẫn hữu ích khi các phép tính hoặc quá trình phân bổ tốn kém được hưởng lợi từ việc lưu vào bộ nhớ đệm.
  • Việc bỏ qua các thành phần kết hợp sẽ làm phát sinh chi phí bảo trì và chi phí so sánh tham số. Dựa vào tính năng bỏ qua mạnh mẽ và tính bình đẳng về danh tính đối tượng, hoặc tránh hoàn toàn việc vô hiệu hoá, thay vì buộc phải ổn định.

Kỹ năng

Tài nguyên

Gemini Notebook

Chủ đề

  • jetpack-compose
  • android-performance
  • perfetto
  • đo điểm chuẩn
  • coroutine
  • composition-tracing
  • android-cli

Bản chép lời

Giới thiệu

  • Xin chào,

  • Chào các bạn!

  • Mong là bạn đã có một kỳ nghỉ vui vẻ.

  • Tôi hy vọng bạn đã uống cà phê, dưỡng ẩm và làm những việc tương tự.

  • Hôm nay, chúng ta sẽ tìm hiểu sâu hơn một chút.

  • Vâng, đúng vậy, chúng ta bắt đầu nhé.

  • Hôm nay, chúng ta sẽ tìm hiểu sâu hơn một chút.

  • Tôi là Andrei Shikov, và hôm nay tôi sẽ cùng Andrew Bailey trình bày nội dung này.

  • Chúng tôi đều làm việc trong nhóm thời gian chạy của trình biên dịch Compose, nhưng cũng làm nhiều việc khác, bao gồm cả hiệu suất.

  • Và chủ đề của buổi trò chuyện hôm nay là giải đáp thắc mắc của bạn, những câu hỏi về hiệu suất, vì Jetpack Compose được thiết lập làm bộ công cụ giao diện người dùng mặc định cho hoạt động phát triển Android.

  • Và giờ đây, nhiều người đang hỏi làm cách nào để đảm bảo giao diện người dùng Compose hoạt động hiệu quả nhất có thể.

  • Vì vậy, hôm nay, chúng ta sẽ tìm hiểu sâu hơn, như tôi đã nói.

  • Ngoài ra, chúng tôi cũng sẽ giới thiệu một số công cụ để bạn có thể tự mình vượt trội trong Compose.

  • Trước tiên, tôi muốn nói về lý do hiệu suất lại quan trọng đối với chúng tôi.

  • Khi nghĩ đến chất lượng và việc xây dựng ứng dụng chất lượng, bạn thường nghĩ đến ảnh động, chúng tôi nghĩ đến tập hợp tính năng phong phú và đôi khi hiệu suất bị bỏ qua, nhưng nhận thức của người dùng thực sự phụ thuộc vào hiệu suất của ứng dụng.

  • Vì vậy, nếu ảnh động bị giật, thì nhận thức sẽ giảm ngay lập tức.

  • Vì vậy, tất cả các chỉ số, con số mà chúng tôi sẽ đề cập hôm nay, thực ra là các chỉ số đại diện mà chúng tôi đã chọn để thể hiện cảm nhận của người dùng về ứng dụng, cảm nhận của họ về tất cả các ảnh động đó, tất cả những gì đang diễn ra trên màn hình.

  • Vì vậy, về cơ bản, việc tối ưu hoá ứng dụng là tối ưu hoá để mang lại trải nghiệm tốt hơn.

  • Vì vậy, đây là tư duy mà tôi muốn các bạn có được khi chúng ta tìm hiểu sâu về nhiều chủ đề hôm nay.

  • Bây giờ, khi bạn bắt đầu xem xét hiệu suất của ứng dụng, tôi muốn bạn làm ba việc nhanh chóng, ba việc nhanh chóng.

  • Điều đầu tiên, nếu đo lường hiệu suất, bạn sẽ đo lường hiệu suất ở chế độ phát hành, vì vậy, chúng tôi không có nhiều thông tin có thể gỡ lỗi và tất cả những thông tin này làm chậm ứng dụng của chúng tôi.

  • Thứ hai, bạn bật R8 hoặc các trình giảm thiểu khác.

  • Chỉ trong năm nay, chúng tôi đã giới thiệu một tính năng tối ưu hoá giúp cải thiện hiệu suất coroutine gấp đôi và chúng tôi cũng làm nhiều việc khác.

  • Chúng tôi dựa rất nhiều vào R8 khi tối ưu hoá Compose.

  • Và điều cuối cùng, đặc biệt là đối với các chỉ số khởi động, bạn cần đảm bảo rằng mình sử dụng hồ sơ cơ sở.

  • Vì vậy, các lớp mà bạn tải trong quá trình khởi động sẽ được biên dịch trước và tải trước khi ứng dụng đó khởi động.

  • Như bạn có thể thấy, con số này gấp 6 lần so với bản dựng có thể gỡ lỗi thông thường của ứng dụng.

  • Và khi bạn đo lường tất cả những điều đó, chúng sẽ kết hợp với những thành công khác mà bạn đang đạt được trong mã của mình.

  • Bây giờ, bạn đã làm tất cả những việc đó, ứng dụng vẫn hơi bị trễ hoặc vẫn còn các vấn đề khác về hiệu suất, và đây là lúc bạn muốn chuyển sang vòng lặp hiệu suất này.

  • Bạn muốn bắt đầu xem xét một số chỉ số về một số trường hợp cụ thể.

  • Ví dụ: giả sử ứng dụng của bạn mất quá nhiều thời gian để tải, nên có thể bạn muốn đặt lại ứng dụng, khởi động ứng dụng từ trình chạy thông thường và kiểm tra xem ứng dụng mất bao lâu để chuyển đến màn hình dữ liệu đầu tiên.

  • Hoặc có thể ứng dụng bị giật một chút, vì vậy, bạn cuộn ứng dụng và đảm bảo rằng thao tác cuộn này có thể lặp lại và ổn định.

  • Vì vậy, môi trường ổn định trong trang trình bày này thực sự quan trọng, vì khi tối ưu hoá sau này, bạn muốn đảm bảo rằng bạn thấy các chỉ số của mình được cải thiện và bạn có thể xác nhận rằng những hoạt động tối ưu hoá đó thực sự hoạt động như dự kiến.

  • Điểm chuẩn rất phù hợp cho việc này, vì điểm chuẩn đã thu thập nhiều dữ liệu, thực hiện nhiều lần lặp lại để đảm bảo không có quá nhiều độ nhiễu trong thiết bị và cũng thực hiện nhiều bước thiết lập thiết bị để giúp bạn.

  • Ngoài ra, theo mặc định, điểm chuẩn cũng là các tình huống có thể lặp lại và đây là điều chúng tôi đang nỗ lực thực hiện.

  • Nhưng bạn cũng có thể làm việc này bằng một loạt tập lệnh nếu muốn.

  • Và điều cuối cùng, bạn lập hồ sơ, cải thiện một số điểm trong ứng dụng, xem xét dữ liệu rồi lặp lại, và có thể lặp lại với các chỉ số kịch bản khác nhau mà ứng dụng của bạn gặp khó khăn.

Phân tích bằng dấu vết Perfetto

  • Khi xem xét dữ liệu, công cụ đầu tiên mà chúng tôi giới thiệu hôm nay và bạn muốn thực sự làm quen là dấu vết Perfetto.

  • Dấu vết Perfetto ghi lại mọi hoạt động diễn ra trên thiết bị trong khi bạn ghi lại dấu vết đó, chẳng hạn như trong quá trình chạy macrobenchmark hoặc có thể là một số tập lệnh mà bạn đang ghi.

  • Đây là một công cụ hỗ trợ đắc lực cho mọi kỹ sư đang nỗ lực cải thiện hiệu suất của Android.

  • Đối với tôi, tôi dành phần lớn thời gian làm việc trong ngày để xem dấu vết Perfetto.

  • Chúng có dạng như sau.

  • Đó là một lượng dữ liệu lớn.

  • Như tôi đã nói, các tệp này ghi lại mọi hoạt động diễn ra trên thiết bị.

  • Vì vậy, ban đầu bạn có thể gặp khó khăn khi tìm chính xác vị trí của ứng dụng, nên tôi khuyên bạn nên sử dụng thanh tìm kiếm tiện dụng này.

  • Tìm kiếm Compose, tìm kiếm gói ứng dụng của bạn và những nội dung tương tự.

  • Hãy tập trung vào những điều quen thuộc.

  • Khi bạn đã quen và tìm được quy trình ứng dụng, bạn sẽ thấy các dòng thời gian kết xuất khung hình và thời gian thực hiện nhiều việc trên ứng dụng.

  • Nếu tách riêng một trong số đó, bạn sẽ thấy một hình ảnh như thế này.

  • Đầu tiên là công việc của luồng chính.

  • Đây là nơi Compose thực hiện hầu hết các công việc của mình.

  • Sau đó là RenderThread.

  • Đây là nơi chúng ta xử lý lệnh canvas, mã hoá lệnh đó cho GPU.

  • Sau đó, GPU sẽ thực hiện công việc của mình và hệ thống sẽ kết hợp các thành phần đó.

  • Hôm nay, chúng ta sẽ chỉ xem xét luồng chính.

  • Vì đây là nơi Compose thực hiện công việc của mình.

  • Chúng ta đang nói về Compose, vì vậy, chỉ nên tập trung vào Compose.

  • Và khi tách riêng khung hình đó trên luồng chính, chúng ta thực sự bắt đầu thấy các lát cắt Compose.

  • Tôi đã chọn một vài trong số đó cho bạn.

  • Nó giống như cấu trúc, đo lường, bố cục, vẽ.

  • Nếu đã nghe nói về các giai đoạn của Compose, bạn có thể sẽ thấy các giai đoạn này khi xem xét hiệu suất trước đây.

  • Vì vậy, các giai đoạn này có các lát cắt riêng lẻ trong dấu vết Perfetto và bạn có thể dễ dàng tìm ra thời lượng của mỗi giai đoạn cũng như giai đoạn nào chạy trong mỗi khung hình cụ thể.

  • Chúng ta đã thảo luận rất nhiều và chi tiết về việc tối ưu hoá một số công việc này, và bạn muốn đảm bảo rằng mình làm đúng việc ở đúng giai đoạn.

  • Ví dụ: nếu có ảnh động cập nhật màu sắc của hộp hoặc nội dung tương tự, bạn chỉ muốn vô hiệu hoá giai đoạn vẽ chứ không phải thành phần và bố cục, những nội dung tương tự.

  • Đây là kiến thức cơ bản về hiệu suất và hôm nay chúng ta sẽ tìm hiểu sâu hơn một chút về một số vấn đề, đặc biệt là tập trung vào thành phần.

  • Bây giờ, xin mời Andrew chia sẻ về vấn đề này.

  • Cảm ơn Andrei.

  • Như Andrei đã đề cập, trước đây chúng tôi đã dành nhiều thời gian để nói về hiệu suất, chẳng hạn như tránh đọc ngược, đảm bảo rằng bạn trì hoãn việc đọc trạng thái đến đúng giai đoạn trong vòng đời của thành phần kết hợp, tối ưu hoá và đảm bảo rằng các thành phần kết hợp của bạn đang vô hiệu hoá như bạn mong đợi, để bạn không phải làm quá nhiều việc trên mỗi khung hình.

  • Đây là một điểm khởi đầu rất tốt, nhưng chúng tôi đã có nhiều tài liệu về vấn đề này và chúng tôi muốn tiến xa hơn một chút trong cuộc trò chuyện này.

  • Vì vậy, hôm nay chúng ta sẽ giải quyết những gì còn lại trong thành phần sau khi bạn thực hiện các hoạt động tối ưu hoá đó, đồng thời giải quyết những việc mà chính các thành phần kết hợp của bạn đang làm.

  • Thực tế đáng tiếc về mọi thứ bên trong mã Compose và mọi thứ bạn cung cấp trong ứng dụng về mặt hiệu suất là tất cả đều có chi phí liên quan.

  • Không có mã nào bạn cung cấp trong ứng dụng là miễn phí, và ngay cả những việc nhỏ nhặt như đọc trạng thái và soạn những thứ như hiệu ứng phụ bên trong thành phần cũng có một số chi phí hữu hình liên quan đến chúng, và điều quan trọng là bạn phải lưu ý đến chúng, đặc biệt là khi bạn bắt đầu phân tích những gì các thành phần kết hợp của bạn thực sự đang làm và bắt đầu tăng hiệu suất bên trong chính ứng dụng của bạn.

  • Để cho thấy điều đó thực sự trông như thế nào ở quy mô vi mô nhất, sau đây là một số thao tác nhỏ nhất mà bạn có thể thực hiện ở bất kỳ đâu trong mã của mình, đặc biệt là bên trong các thành phần kết hợp và ở độ phân giải này, chúng ta đang nói về quy mô nano giây về mức độ chi phí của những thứ này như một tư duy.

  • Nếu muốn cải thiện hiệu suất, bạn có hai lựa chọn.

  • Bạn có thể xoá một số lượng lớn các phép toán không tốn kém hoặc một số lượng nhỏ các phép toán tốn kém hơn.

  • Cụ thể trên biểu đồ này, coroutine là m-- ở phía tốn kém hơn khi các thao tác này diễn ra và đây là một ví dụ điển hình về nơi bắt đầu tìm kiếm, vì nếu xoá một vài thực thể này, chúng ta có thể bắt đầu thấy nhiều lợi ích hữu hình hơn trong ứng dụng của mình nhanh hơn.

  • Một điều khác cần lưu ý về coroutine là như tôi đã trình bày trên trang trình bày, không chỉ việc khởi chạy coroutine mới tốn chi phí mà việc huỷ coroutine cũng tốn chi phí.

  • Chi phí huỷ là điều đặc biệt đáng chú ý vì việc này chủ yếu diễn ra khi bạn loại bỏ nội dung và có thể chuyển sang các khu vực khác bên trong ứng dụng.

  • Thao tác rời khỏi là một trường hợp cực kỳ tốn kém trong nhiều ứng dụng mà bạn cần tận dụng tối đa thời gian kết xuất khung hình và đảm bảo rằng bạn đang sử dụng tối đa những gì mình có.

  • Vì vậy, bạn cần lưu ý cả hai loại chi phí này.

  • Tất cả những điều này, bất kể chúng ta đang xem xét điều gì, sẽ cộng lại ở quy mô khoảng 16 triệu nano giây để xử lý.

  • Vì vậy, chúng ta có một số ngân sách ở đây cần phải giảm bớt và coroutine sẽ là một ví dụ hay để bắt đầu.

Quản lý tác dụng phụ

  • Trong Compose, coroutine thường xuất hiện nhất với các API hiệu ứng.

  • Có một loạt API mà bạn có thể sử dụng với SideEffects, từ chức năng cao đến độ trễ thấp.

  • Ở phía chức năng nhất của phổ này, chúng ta có LaunchedEffect, bao gồm mọi thứ cho công việc không đồng bộ, đặc biệt là ảnh động, bất cứ thứ gì bạn cần phạm vi coroutine.

  • Tuy nhiên, nếu không cần đến và có thể hoàn thành nhiệm vụ bằng một API ít tốn kém hơn, thì bạn nên cân nhắc sử dụng API đó nếu phù hợp với nhu cầu của trường hợp sử dụng.

  • Để minh hoạ điều đó trong thực tế, tôi có một thành phần kết hợp trình phát nội dung nghe nhìn để minh hoạ tất cả các SideEffect khác nhau và thời điểm chúng có thể xuất hiện.

    @Composable
    fun MediaPlayer(contentSource: Uri, playbackRate: Float) {
        val mediaPlayer = rememberMediaPlayer()
    
        // Asynchronous work and animations
        LaunchedEffect(contentSource) { mediaPlayer.load(contentSource) }
    }
  • Chúng ta có thể bắt đầu bằng LaunchedEffect.

  • Trong ví dụ này, chúng ta sẽ dùng để tải dữ liệu không đồng bộ bên trong trình phát nội dung nghe nhìn.

  • Nếu bạn cần một phạm vi coroutine hoặc nếu bạn cần coroutine nói chung hoặc để thực hiện lệnh gọi tạm ngưng, hãy sử dụng LaunchedEffect.

  • Mặc dù vẫn tốn kém hơn và có chi phí liên quan đến coroutine, nhưng miễn là bạn đang tận dụng và chứng minh được điều đó, hãy sử dụng API phù hợp cho công việc.

  • Tuy nhiên, nếu bạn đang sử dụng LaunchedEffect nhưng không tận dụng coroutine, thì bạn nên cân nhắc các API khác có thể làm điều tương tự nhưng với chi phí thấp hơn.

  • Để làm được điều đó, khi xem xét kỹ hơn, chúng ta có thể thấy các ví dụ khác về tác dụng phụ như DisposableEffect.

    @Composable
    fun MediaPlayer(contentSource: Uri, playbackRate: Float) {
        val activity = LocalActivity.current
    
        // Setup and teardown
        DisposableEffect(Unit) {
            activity.requestedOrientation = SCREEN_ORIENTATION_LANDSCAPE
            onDispose {
                activity.requestedOrientation = SCREEN_ORIENTATION_UNSPECIFIED
            }
        }
    }
  • DisposableEffect rất phù hợp khi bạn có quy trình thiết lập và chia nhỏ liên quan đến hiệu ứng của mình.

  • Trong cùng ví dụ về trình phát nội dung nghe nhìn, tôi có một ví dụ khoá hướng của hoạt động thành hướng ngang khi thành phần kết hợp xuất hiện, sau đó nhả khoá hướng đó khi thành phần kết hợp biến mất sau đó.

  • Nếu muốn có hiệu ứng một lần của LaunchedEffect và DisposableEffect, nhưng không cần coroutine và không có hiệu ứng loại bỏ, bạn cũng có thể sử dụng API SideEffect mới có khoá mà chúng tôi đã giới thiệu trong Compose 1.12.

    @Composable
    fun MediaPlayer(contentSource: Uri, playbackRate: Float) {
        val mediaPlayer = rememberMediaPlayer()
    
        // One-shot events
        SideEffect(Unit) { mediaPlayer.play() }
    }
  • Đây là một giải pháp thay thế tuyệt vời cho hai API còn lại nếu bạn đã sử dụng chúng, LaunchedEffect vì sự tiện lợi hoặc DisposableEffect vì cần hiệu ứng một lần, việc điều hướng hoặc di chuyển sang API SideEffect có khoá có thể đạt được kết quả tương tự với mức hao tổn thấp hơn và hiệu suất được cải thiện, đặc biệt là nếu di chuyển nhiều lần trong toàn bộ ứng dụng.

  • Và cuối cùng, API ít tốn kém nhất là API SideEffect cơ bản.

    @Composable
    fun MediaPlayer(contentSource: Uri, playbackRate: Float) {
        val mediaPlayer = rememberMediaPlayer()
    
        // Synchronize the outside world to the state of composition
        SideEffect { mediaPlayer.playbackRate = playbackRate }
    }
  • Điều này phù hợp nhất khi bạn muốn đồng bộ hoá trạng thái bên ngoài của thế giới với những gì đang diễn ra trong thành phần.

  • Vì vậy, trong ví dụ về trình phát nội dung nghe nhìn, tôi có một ví dụ khác cho thấy việc đồng bộ hoá một số trạng thái phát với một số tham số mà thành phần nhận được.

  • Một điều cần lưu ý là nếu bạn quyết định kiểm tra việc sử dụng tác dụng phụ và chuyển sang một tác dụng phụ ít tốn kém hơn, hãy nhớ rằng ngoài các hành vi và trường hợp sử dụng khác mà chúng ta đã nói, còn có sự khác biệt về thứ tự đảm bảo với tác dụng phụ.

  • Tất cả các hiệu ứng có thể loại bỏ đều thực thi trước, sau đó là các hiệu ứng phụ thông thường, có khoá hoặc không, và sau khi khung hoàn tất là lúc tất cả các hiệu ứng được khởi chạy chạy vì có liên quan đến việc điều phối.

  • Nếu bạn phụ thuộc vào thời gian này cho các hiệu ứng đã khởi chạy, hãy lưu ý điều đó nếu bạn đang cân nhắc việc di chuyển.

  • Và đối với mọi hoạt động di chuyển, hãy nhớ kiểm thử mã của bạn và đảm bảo rằng bạn không phụ thuộc vào bất kỳ đảm bảo nào về thời gian và mã của bạn vẫn hoạt động sau đó.

  • Nếu không thể giảm độ phức tạp của hiệu ứng phụ, bạn vẫn có thể giảm số lượng hiệu ứng phụ bằng cách hợp nhất chúng lại với nhau.

    @Composable
    fun ShoppingCartScreen(
        viewModel: CartViewModel
    ) {
        LaunchedEffect(Unit) { viewModel.loadCart() }
        LaunchedEffect(Unit) { viewModel.loadPaymentMethods() }
        LaunchedEffect(Unit) { viewModel.loadDeliveryAddresses() }
    
        CartContent(...)
    }
  • Điều này đặc biệt đáng cân nhắc nếu bạn có nhiều hiệu ứng sử dụng cùng một khoá.

  • Đây là một đề xuất phù hợp để xem xét hợp nhất vì chúng có thể thực thi cùng một lúc.

  • Trong ví dụ này, tôi có một số hiệu ứng khởi chạy, mỗi hiệu ứng thực hiện một lệnh gọi không đồng bộ vào một số mô hình khung hiển thị.

  • Nếu tôi thay thế chúng bằng một LaunchedEffect duy nhất và sau đó là các lệnh gọi khởi chạy khác bên trong đó, thì mặc dù tôi vẫn có cùng số lượng coroutine đang chạy ở đây, nhưng cách này vẫn ít tốn tài nguyên hơn vì nó đưa công việc ra khỏi thành phần và chuyển vào coroutine.

    @Composable
    fun ShoppingCartScreen(
        viewModel: CartViewModel
    ) {
        LaunchedEffect(Unit) {
            launch { viewModel.loadCart() }
            launch { viewModel.loadPaymentMethods() }
            launch { viewModel.loadDeliveryAddresses() }
        }
    
        CartContent(...)
    }
  • Vì vậy, bạn nên cân nhắc việc di chuyển vào trong những trường hợp như thế này để giảm số lượng hiệu ứng khi có thể.

  • Hiệu ứng và coroutine chỉ là một ví dụ về một nơi bạn có thể kiểm tra bên trong ứng dụng, nhưng mỗi composable và mỗi ứng dụng sẽ khác nhau và có các cơ hội khác nhau bên trong chúng.

  • Để hiểu được điều gì phù hợp với ứng dụng của bạn, chúng ta cần nói về công cụ tiếp theo mà Andrei sẽ giới thiệu, đó là tính năng theo dõi thành phần.

Theo dõi quá trình kết hợp và kết hợp lại

  • Được rồi.

  • Vì vậy, khi theo dõi thành phần, nếu xem bản tái tạo trung thực này của khung Perfetto mà chúng ta đã xem trước đó trong một trang trình bày khác, bạn sẽ thấy rằng bản tái tạo này cung cấp cho chúng ta thông tin về thời gian với các giai đoạn khác nhau, nhưng không thực sự cung cấp cho chúng ta nhiều thông tin chi tiết về những gì đang xảy ra.

  • Loại hình minh hoạ nào?

  • Đúng vậy, những gì đang diễn ra bên trong thành phần của các hàm có khả năng kết hợp đang chạy.

  • Và tính năng theo dõi thành phần sẽ giúp chúng ta làm việc đó.

  • Vì vậy, chúng tôi giới thiệu một số điểm đánh dấu trong trình biên dịch Compose, về cơ bản là chú thích mọi hàm và hiển thị hàm đó trên dấu vết Perfetto, nhờ đó bạn có thể kiểm tra thời gian thực hiện của từng hàm.

  • Bạn có thể sử dụng thông tin đó để tìm ra chính xác hàm nào đang kết hợp lại.

  • Tuy nhiên, việc này có một chút hao tổn tài nguyên, vì vậy, nếu muốn thu thập dữ liệu hiệu suất chính xác nhất, bạn nên sử dụng Perfetto thông thường hoặc chạy mà không cần Perfetto thu thập và sử dụng các chỉ số khác.

  • Tuy nhiên, tính năng theo dõi composition thực sự hữu ích nếu bạn muốn tìm hiểu xem mỗi composable riêng lẻ mất bao lâu.

  • Đôi khi, bạn cũng muốn tìm hiểu lý do khiến các thành phần kết hợp lại, và đó là điều chúng tôi đang cố gắng giải quyết trong bản phát hành tiếp theo của tính năng theo dõi thời gian chạy của Compose Tracing, cụ thể là phiên bản 1.13.

  • Chúng tôi hiện đang giới thiệu tính năng theo dõi quá trình kết hợp lại, tính năng này chú thích mọi trạng thái đọc và trạng thái ghi dẫn đến quá trình kết hợp lại.

  • Vì vậy, bạn sẽ nhận được kết quả như thế này trong dấu vết Perfetto, nơi bạn sẽ thấy từng lượt ghi trạng thái và ngăn xếp lệnh gọi nơi xảy ra lượt ghi và lượt đọc, nhờ đó, bạn có thể liên kết chúng với các dòng thực tế trong mã nơi các trạng thái đó được truy cập hoặc sửa đổi.

  • Nếu bạn muốn tìm hiểu thêm, hãy xem đường liên kết tại đây.

  • Tài liệu này có tất cả thông tin về cả tính năng theo dõi thành phần và theo dõi quá trình kết hợp lại.

  • Bây giờ, hãy cùng Andrew tìm hiểu cách sử dụng hiệu quả hơn để tránh ghi trạng thái.

Tối ưu hoá và vô hiệu hoá trạng thái

  • Cảm ơn Andrei.

  • Dựa trên kết quả tìm được từ cuộc điều tra này, bạn có thể phát hiện thấy có những trường hợp vô hiệu hoá trong thành phần không hữu ích cho ứng dụng và có thể tránh được ngay từ đầu.

  • Ví dụ: trường hợp phổ biến nhất mà tôi gặp là một số nút cuộn lên đầu trang phụ thuộc vào chế độ hiển thị hoặc tiến trình cuộn để hiển thị.

    @Composable
    fun ScrollToTopButton(
        scrollState: ScrollState
    ) {
        val showScrollToTopButton = scrollState.value > 0
        val coroutineScope = rememberCoroutineScope()
    
        if (showScrollToTopButton) {
            FloatingActionButton(
                onClick = {
                    coroutineScope.launch { scrollState.animateScrollTo(0) }
                }
            ) {
                Icon(Icons.Default.ArrowUpward, "Scroll to Top")
            }
        }
    }
  • Ở đây, vì tôi đang đọc giá trị của trạng thái cuộn bên trong thành phần kết hợp, nên đây là trạng thái thay đổi rất thường xuyên, có thể thay đổi một lần mỗi khung hình và điều này sẽ gây ra nhiều lần kết hợp lại có thể tránh được vì điều kiện này, mặc dù bản thân trạng thái cuộn đang thay đổi, nhưng điều kiện thực tế mà chúng ta quan tâm ở đây không thay đổi thường xuyên.

  • Đây là ví dụ về trường hợp bạn nên sử dụng một trạng thái được tính toán, giúp tránh việc vô hiệu hoá các thay đổi về trạng thái không quan trọng đối với ứng dụng của bạn.

  • Vì vậy, trong ví dụ này, tôi đã gói phép tính bên trong một trạng thái được tính toán và khi chúng ta đọc trạng thái này, thay vì vô hiệu hoá dựa trên chính vị trí cuộn, thì trạng thái này sẽ vô hiệu hoá thành phần composable này theo điều kiện dựa trên toàn bộ điều kiện và lấy trạng thái này từ chính trạng thái khác.

    @Composable
    fun ScrollToTopButton(
        scrollState: ScrollState
    ) {
        val showScrollToTopButton by remember { computedStateOf { scrollState.value > 0 } }
        val coroutineScope = rememberCoroutineScope()
    
        if (showScrollToTopButton) {
            FloatingActionButton(
                onClick = {
                    coroutineScope.launch { scrollState.animateScrollTo(0) }
                }
            ) {
                Icon(Icons.Default.ArrowUpward, "Scroll to Top")
            }
        }
    }
  • Nếu đã từng thấy mẫu này, thì có thể bạn đã thấy mẫu này sử dụng với trạng thái phái sinh.

  • ComputedState là trạng thái mới trong Compose 1.13. Mặc dù cả ComputedState và DerivedState đều có mục tiêu tương tự là vô hiệu hoá có điều kiện khi phép tính thay đổi, nhưng cách thực hiện lại có sự khác biệt rất nhỏ.

  • DerivedState (đã được cung cấp từ những ngày đầu của Compose) lưu giá trị mà nó tính toán vào bộ nhớ đệm và sử dụng lại giá trị đó cho đến khi các phần phụ thuộc của nó thay đổi.

  • Với ComputedState, giá trị sẽ được tính toán mỗi khi bạn đọc.

  • Việc lưu vào bộ nhớ đệm nghe có vẻ hữu ích, nhưng thực tế trong nhiều trường hợp, việc lưu vào bộ nhớ đệm tốn kém hơn so với việc chỉ đọc kết quả.

  • Để hiểu cách thức hoạt động, chúng ta có thể xem ví dụ về cách sử dụng các trạng thái này trong ứng dụng và tìm hiểu xem mỗi trạng thái thực sự làm gì.

  • Ở đây, tôi có cùng một trạng thái phản ánh gần đúng những gì chúng ta đang làm trong nút cuộn lên đầu, trong đó có một số điều kiện dựa trên một số trạng thái khác và tôi có cả DerivedState và ComputedState.

    var a by mutableStateOf(0)
    
    val derivedState = derivedStateOf { a > 0 }
    val computedState = computedStateOf { a > 0 }
    
    // First read
    derivedState.value
    computedState.value
    
    // Second read
    derivedState.value
    computedState.value
  • Sau đó, tôi cũng đọc trạng thái hai lần và chúng ta sẽ nhận được cùng một giá trị cho cả hai lần, nhưng những gì diễn ra trong nội bộ giữa hai lần này là khác nhau.

  • Để đơn giản hoá dấu vết của những gì thực sự xuất hiện ở đây trong một công cụ như Perfetto, chúng ta có thể kiểm tra những gì thực sự xảy ra khi bạn đọc cả hai trạng thái này.

  • Tại đây, chúng ta bắt đầu bằng cách gọi vào computedState.value.

  • Hàm này sẽ thực thi hàm lambda tính toán, sau đó đọc giá trị của trạng thái và cũng có một chút hoạt động bảo trì để thông báo rằng quá trình đọc đã diễn ra.

  • Điều này sẽ đúng với tất cả trạng thái ảnh chụp nhanh trong Compose.

  • Lần đọc thứ hai hoạt động giống hệt như lần đọc đầu tiên và có cùng đặc điểm hiệu suất trong suốt quá trình.

  • Để so sánh, lần đọc đầu tiên của DerivedState cũng thực hiện một việc tương tự, trong đó nó vẫn phải gọi hàm lambda tính toán và vẫn thực hiện thông báo khác, nhưng nó có một chút hao tổn quản lý bộ nhớ đệm, điều này hơi bị đoản mạch trong lần đầu tiên vì không có bộ nhớ đệm để kiểm tra.

  • Vì vậy, nó sẽ thực hiện một chút công việc bảo trì cho chính bộ nhớ đệm và sau đó hoàn thành cùng một việc chỉ trong thời gian lâu hơn một chút.

  • Tuy nhiên, lần đọc thứ hai cho DerivedState thực sự nhanh hơn lần đọc thứ hai cho ComputedState, mặc dù cả hai đều là trạng thái khá đơn giản.

  • Như tôi đã nói ở các trang trình bày trước, chi phí của một lần đọc ảnh chụp nhanh là không đáng kể trong một số trường hợp và việc đọc từ bộ nhớ đệm thực sự nhanh hơn so với việc tự đọc, đó là lý do chúng ta thấy mức tăng rất nhỏ này cho lần đọc thứ hai đối với DerivedState.

  • Nhưng trường hợp thú vị thực sự là điều gì sẽ xảy ra khi trạng thái cần bị vô hiệu hoá.

  • Đây là trường hợp mà chúng ta gọi là đọc sau khi ghi, trong đó bạn cập nhật một trong các trạng thái đang được dùng trong phép tính và phép tính phải thực thi lại.

  • Trong mã, chúng ta sẽ làm gì đó như sửa đổi a, có thể là tăng giá trị của a, rồi đọc lại giá trị hoặc có thể là kết hợp lại.

    var a by mutableStateOf(0)
    
    val derivedState = derivedStateOf { a > 0 }
    val computedState = computedStateOf { a > 0 }
    
    ...
    
    // Read-after-write
    a++
    derivedState.value
    computedState.value
  • Trong biểu đồ thực tế, ComputedState cũng sẽ có dạng giống hệt như vậy.

  • Nó không có bộ nhớ đệm để quản lý, điều này trông hoàn toàn giống nhau, lần đọc thứ nhất, thứ hai, thứ ba, bất kể lần đọc này thực sự là gì.

  • Tuy nhiên, DerivedState có nhiệm vụ riêng.

  • Nó phải trải qua một quy trình rất tốn kém để nhận thấy rằng bộ nhớ đệm không hợp lệ trước khi cập nhật và gọi chính phép tính đó, sau đó nó phải theo dõi phần phụ thuộc và quản lý nhiều phần phụ thuộc, nhiều hoạt động quan sát để ghi lại giá trị bộ nhớ đệm và hiểu rằng tất cả các giá trị này thực sự đã thay đổi và tự quản lý việc duy trì.

  • Tất cả những điều này cộng lại và khiến chi phí vô hiệu hoá DerivedState rất tốn kém so với ComputedState.

  • Để đưa ra con số cho tất cả các tuyên bố này, ComputedState thường nhanh hơn trong lần đọc đầu tiên khoảng 200 ns.

  • Cụ thể, ComputedState mất khoảng 350 ns trên Pixel 6 trong các điều kiện đo điểm chuẩn cụ thể.

  • Vì vậy, trong lần đọc thứ hai, ComputedState sẽ mất đi một chút so với DerivedState.

  • Nhưng trong trường hợp chúng ta thực sự quan tâm, trường hợp chúng ta đang vô hiệu hoá và có thể là trường hợp bạn thực sự phải làm việc trên khung hình của mình, ComputedState nhanh hơn gần như và trên thực tế là nhanh hơn gấp đôi so với DerivedState.

  • Vì vậy, tuỳ thuộc vào mục đích sử dụng trạng thái, ComputedState có thể là lựa chọn đầu tiên rất phù hợp.

  • Nhìn chung, bạn vẫn cần đưa ra quyết định khi sử dụng DerivedState và ComputedState.

  • Chúng tôi không ngừng sử dụng ComputedState, cả hai vẫn được sử dụng.

  • Nhìn chung, nếu bạn không chắc chắn nên chọn trạng thái nào, thì trạng thái được tính toán là một lựa chọn mặc định phù hợp.

  • Trạng thái bắt nguồn hữu ích nhất khi bạn thực sự tận dụng được bộ nhớ đệm, tức là bạn làm điều gì đó không hề đơn giản như một thuật toán phức tạp, một số lượt phân bổ lớn, nhiều lượt đọc trạng thái bên trong thực sự hưởng lợi từ bộ nhớ đệm ở đây, hoặc bất cứ điều gì không hề đơn giản hoặc phụ thuộc vào những thứ như sự bằng nhau của đối tượng và sự bằng nhau về danh tính của đối tượng, nơi bạn tạo ra sự khác biệt và nhận được lợi ích của bộ nhớ đệm mà trạng thái bắt nguồn cung cấp.

Bỏ qua và độ ổn định trong Compose

  • Một vấn đề khác mà tôi muốn nói đến là cả trạng thái phái sinh và trạng thái được tính toán đều cố gắng tránh làm mất hiệu lực của chính thành phần kết hợp.

  • Điều này khác với việc bỏ qua một composable, tức là điều chúng ta thường nói đến khi nghĩ về việc vô hiệu hoá có điều kiện hoặc không thực hiện công việc bên trong một composable.

  • Sự thật mà tôi muốn tiết lộ ở đây là mặc dù có rất nhiều cuộc thảo luận về tính ổn định và việc bỏ qua trong Compose, nhưng đây không phải là tất cả và không phải lúc nào cũng là điều phù hợp để tối ưu hoá.

  • Như đã nói trước đó, mọi thứ trong thành phần đều có một số chi phí liên quan, bao gồm cả việc bỏ qua.

    // What you write
    @Composable fun ContactListItem(
        contact: Contact
    ) { ... }
    
    // What you get
    @Composable fun ContactListItem(
        contact: Contact,
        $composer: Composer?
        $changed: Int
    ) {
        val $dirty = $changed or
            $composer.changed(contact)
    
        if ($dirty) { /* Your function body */ }
        else $composer.skipToGroupEnd()
    }
  • Việc bỏ qua không nhất thiết là không quan trọng.

  • Chúng tôi phải bảo trì rất nhiều để có thể bật tính năng này.

  • Chúng ta phải thực hiện những việc như lưu trữ tất cả tham số và tìm không gian cho các tham số đó trong cấu trúc ban đầu, sau đó cập nhật bản ghi đó khi bạn kết hợp lại.

  • Ngoài ra, chúng ta cũng phải so sánh các giá trị của bạn với các giá trị trước đó để biết liệu composable của bạn có nên bỏ qua hay không.

  • Sau đó, tất cả những điều này chỉ là sức mạnh của một lệnh kiểm tra if để bỏ qua mã hay không.

  • Điều này có thể tốn kém tuỳ thuộc vào những gì đang diễn ra ở đây.

  • Có một tính năng tối ưu hoá nâng cao cho trình biên dịch, trong đó nếu bạn truyền cùng một tham số, chúng ta có thể sử dụng lại các quy trình kiểm tra tính bằng nhau, nhưng đây không phải là tất cả và khá dễ dàng để vượt qua quy trình này.

  • Để xem một ví dụ về việc bỏ qua không hoạt động như mong đợi, chúng ta có thể xem một ví dụ thực sự kinh điển về một danh sách cuộn có một đối tượng lớn chứa tất cả dữ liệu cần được kết hợp trên màn hình.

    data class ContactsModel(
        val header: String,
        val contacts: List<Contact>
    )
    
    data class Contact(...)
    
    @Composable
    fun ContactsScreen(model: ContactsModel) {
        Column {
            TopAppBar(title = { Text(model.header) })
            ContactsList(model.contacts)
        }
    }
    
    @Composable
    fun ContactsList(contacts: List<Contact>) {
        LazyColumn {
            items(contacts) { ContactListItem(it) }
        }
    }
    
    @Composable
    fun ContactListItem(contact: Contact) { ... }
  • Vì vậy, ở đây, mô hình màn hình chứa một thành phần như màn hình tiêu đề và sau đó là tất cả danh sách người liên hệ mà mô hình này truyền qua một vài thành phần kết hợp đã được gọi là danh sách tải từng phần, rồi đưa các thành phần đó lên màn hình bằng một số ô mục.

  • Hệ phân cấp thành phần cho mã này có dạng như sau, trong đó chúng ta lại bắt đầu với màn hình liên hệ, chúng ta phân nhánh một chút, sau đó có một số thành phần kết hợp khác gọi và trình bày các mục của chúng ta.

  • Nếu xem xét cách dữ liệu được truyền, chúng ta sẽ bắt đầu với mô hình ở trên cùng.

  • Chúng ta phân rã nó thành một chuỗi rồi thành một danh sách, sau đó danh sách đó được chuyển đổi thành một hàm lambda của các mục cho danh sách tải từng phần, rồi danh sách tải từng phần sẽ kết hợp tất cả các mục xuất hiện trên màn hình.

  • Hãy xem điều gì sẽ xảy ra khi chúng ta sửa đổi người liên hệ cuối cùng trong danh sách này.

  • Điều chúng ta muốn xảy ra là chỉ vô hiệu hoá mục cuối cùng trong danh sách, nhưng điều đó sẽ không xảy ra ở đây.

  • Nếu xem dấu vết thành phần cho quá trình kết hợp lại này, chúng ta sẽ thấy một kết quả tương tự như sau, trong đó chúng ta bắt đầu tại màn hình liên hệ và cuối cùng là chuyển xuống mục danh sách này.

  • Và đây là một cách khá tốn kém để xuống đây.

  • Chúng ta cũng có thể thấy tất cả các phần này, nơi có vẻ như không có gì xảy ra bên trong dấu vết.

  • Và mặc dù không rõ ràng ở đây, nhưng đây thực sự là nơi các thành phần kết hợp quyết định xem chúng có sắp bỏ qua hay không.

  • Trong ví dụ này, chỉ có hai thành phần kết hợp bị bỏ qua do cách danh sách tải từng phần kết hợp lại.

  • Khi bạn cung cấp cho nó một lambda nội dung mới, nó sẽ vô hiệu hoá tất cả các mục mà nó có.

  • Vì vậy, nó sẽ vô hiệu hoá hai người liên hệ không thay đổi ngoài người liên hệ có thay đổi, và hai người liên hệ đó là những mục duy nhất ở đây được bỏ qua.

  • Mọi thứ khác đều thực hiện việc bỏ qua so sánh bằng này chỉ để nhận ra rằng nó thực sự phải thực thi.

  • Nếu các đối tượng của bạn ổn định, thì việc so sánh này thực sự có thể khá tốn kém.

  • Nếu bạn buộc chúng tôi phải xem qua tất cả cấu trúc dữ liệu của bạn, thì việc này có thể làm tăng chi phí và trở thành một chi phí không hề nhỏ chỉ để biết hai đối tượng có bằng nhau hay không.

  • Tệ hơn nữa, vì đã phân rã nên chúng ta thực sự phải thực hiện cùng một phép so sánh trên danh sách hai lần.

  • Nếu bạn để trình biên dịch hoạt động theo chế độ mặc định và sử dụng tính năng bỏ qua mạnh, thì bạn vẫn có thể bỏ qua và vẫn có thể kiểm tra để bỏ qua các thành phần kết hợp theo điều kiện, nhưng các thành phần kết hợp này sẽ sử dụng phương thức nhanh hơn nhiều là sử dụng tính bằng nhau về danh tính đối tượng thay vì tính bằng nhau về cấu trúc.

  • Cách này giúp chúng ta tính toán nhanh hơn nhiều và mặc dù chúng ta kết hợp lại các mục giống nhau trên khung hình này, nhưng điều quan trọng là chúng ta có thể nhanh chóng thực hiện việc này mà không mất quá nhiều thời gian để nhận ra rằng chúng ta không thể bỏ qua các thành phần kết hợp này.

  • Đây cũng là lý do chúng tôi nhấn mạnh việc sử dụng các công cụ như điểm chuẩn và dấu vết để hiểu những gì đang diễn ra bên trong ứng dụng của bạn thay vì trực tiếp đếm số lần kết hợp lại.

  • Các chỉ số này có thể là một chỉ báo hữu ích, nhưng không nhất thiết phải tập trung quá mức hoặc chú trọng quá nhiều trong suốt quá trình tối ưu hoá ứng dụng.

  • Hãy nhớ rằng, thay vì cố gắng bỏ qua composable, hãy tránh làm mất hiệu lực của composable ngay từ đầu.

Phân tích quá trình thực thi bằng dấu vết phương thức

  • Trong ví dụ này, chúng ta thấy rằng dấu vết thành phần có thể cung cấp nhiều thông tin về những gì đang xảy ra, nhưng không nhất thiết phải cho thấy toàn bộ bức tranh.

  • Để làm được điều đó, Andrei sẽ cho chúng ta biết về dấu vết phương thức.

  • Được rồi.

  • Như Andrew đã nói, tính năng theo dõi thành phần không phải lúc nào cũng cho bạn biết mọi thứ đang diễn ra, chỉ vì tính năng này chỉ chú thích các hàm composable và phải có một số trường hợp bạn chỉ thấy khoảng trống trong dấu vết thành phần đã tăng cường dấu vết Perfetto, nhưng vẫn không hiển thị mọi thứ đang diễn ra trong đó.

  • Đôi khi, đó có thể là việc mà Compose thực hiện, đôi khi có thể là việc trong mã của bạn, rất khó để tìm ra chỉ bằng cách xem dấu vết của thành phần tại đây.

  • Bây giờ, bạn cần lấy dấu vết phương thức.

  • Có một mục trong trình phân tích tài nguyên của Android Studio, hoặc bạn cũng có thể dùng lệnh ADB để lấy dấu vết, và kết quả bạn nhận được là mọi lệnh gọi hàm xảy ra trong Java.

  • Nó chứa mọi thứ mà Trình biên dịch Compose mã hoá trong đó, mọi thứ mà Compose Runtime thực hiện và mọi thứ mà mã của bạn thực hiện. Nó thực sự hữu ích cho việc theo dõi, điều tra chính xác những gì đang thực thi, những hàm nào được gọi trong không gian đó.

  • Có một điều, tôi muốn-- xem mục này ở giữa.

  • Tôi muốn lưu ý rằng việc theo dõi dấu vết phương thức có mức hao tổn rất cao.

  • Bạn có thêm từ 10 đến 20 lần hao tổn tài nguyên, tuỳ thuộc vào mức độ phức tạp của cây hàm, vì mỗi lệnh gọi hàm đều làm tăng thêm một chút hao tổn.

  • Để minh hoạ điều đó, chúng ta có thể xem ảnh chụp màn hình.

  • Đây là dấu vết phương thức cho thời điểm bắt đầu của ứng dụng, và bạn có thể hỏi: "Làm cách nào để hiểu được điều này?"

  • Thông thường, bạn bắt đầu theo dõi quá trình kết hợp bằng cách xem các hàm có trong đó.

  • Vì vậy, đây giống như dấu vết thành phần được đơn giản hoá, trong đó có ScreenContent, một số Nút và một số hàm khác.

  • Giả sử chúng ta muốn tìm hiểu điều gì đang xảy ra trong ScreenContent.

  • Perfetto có một thanh tìm kiếm.

  • Bạn tải dấu vết phương thức trong trình xem Perfetto, tìm kiếm cùng một hàm như ở đây, ScreenContent, nó sẽ nhận được một hàm tương tự, đó là dấu vết phương thức cho màn hình.

  • Như bạn có thể thấy ở đây, có một số cuộc gọi Composer.startGroup và cuộc gọi nhóm.

  • Đây là những gì thời gian chạy và trình biên dịch thực hiện cho bạn trong nền để đảm bảo thành phần kết hợp của bạn thực sự hoạt động hiệu quả, nhưng lệnh gọi thú vị ở đây là Composer.changed.

  • Đây là cách chúng ta kiểm tra xem các tham số có thay đổi giữa các lệnh gọi trước đó và lệnh gọi hiện tại của thành phần kết hợp hay không.

  • Và phần lớn Composer.changed này bị chi phối bởi ScreenModel.equals này.

  • Vì vậy, có thể ScreenModel là một loại lớp dữ liệu lớn và có thể chúng ta đã làm cho lớp này ổn định để giúp tính số lần kết hợp lại, nên có thể đây là một lớp xuất hiện trong báo cáo trình biên dịch.

  • Và có thể việc xoá chú thích ổn định này sẽ giúp cải thiện hiệu suất trong trường hợp cụ thể này.

  • Vì vậy, bạn nên tìm kiếm điều đó và dấu vết phương thức thành phần là công cụ tốt nhất để tìm thấy điều này.

  • Được rồi.

  • Hôm nay, chúng ta đã tìm hiểu nhiều nội dung. Giờ thì Andrew sẽ tóm tắt lại cho chúng ta.

Công cụ thiết kế và hiệu suất có khả năng kết hợp nhanh chóng

  • Để kết thúc và tóm tắt tổng quan về mọi nội dung chúng ta đã thảo luận hôm nay, có một số điều cần lưu ý khi bạn thiết kế các thành phần kết hợp và sau đó tối ưu hoá các ứng dụng hiện có.

  • Trước tiên, hãy nhớ rằng các thành phần kết hợp ít tốn kém hơn và các thành phần kết hợp mà bạn không vô hiệu hoá sẽ tốt hơn các thành phần kết hợp mà bạn bỏ qua.

  • Hãy nhớ rằng khi thiết kế các thành phần kết hợp, bạn nên xem xét các đối số nhỏ hơn và dễ so sánh hơn, đồng thời có thể sử dụng lại các phép so sánh với các lệnh gọi hàm khác để tận dụng tính năng tối ưu hoá.

  • Khi sử dụng API bên trong các thành phần kết hợp, hãy lưu ý rằng hiệu suất hoặc thời gian kết xuất khung hình sẽ là kết quả của chi phí của chính API và tần suất bạn sử dụng chúng.

  • Hãy lưu ý đến cách các chi phí này cộng dồn, đồng thời lưu ý rằng có thể có chi phí gỡ bỏ liên quan đến các chi phí đó mà bạn có thể phải chịu vào thời điểm ít thuận lợi nhất cho ứng dụng của mình.

  • Ngoài ra, hãy nhớ rằng bạn cần phải là một công dân tốt khi tối ưu hoá bên trong ứng dụng của mình.

  • Bắt đầu bằng cách lấy điểm chuẩn để đưa ra quyết định sáng suốt, phân tích ứng dụng để đảm bảo rằng bạn đang nhắm đến đúng vùng mã và xác thực rằng bạn đang thực hiện các thay đổi và thấy kết quả như mong đợi từ những nội dung sửa đổi mà bạn đang thực hiện dựa trên điểm chuẩn và phân tích.

  • Ngoài ra, hãy nhớ rằng có thể có sự đánh đổi giữa khả năng bảo trì và hiệu suất bên trong ứng dụng của bạn.

  • Vì vậy, hãy làm những gì tốt nhất cho bạn và nhóm của bạn để đảm bảo rằng bạn không tối ưu hoá quá mức và tự dồn mình vào thế khó về khả năng bảo trì.

  • Ngoài ra, để được trợ giúp thêm, bạn có thể sử dụng một công cụ khác, đó là tác nhân AI mà bạn đã có.

  • Chúng tôi đã ra mắt một kỹ năng hiệu suất Jetpack Compose trong Android CLI và trong các kỹ năng mà chúng tôi cung cấp ở đó. Kỹ năng này có nhiều thông tin mà chúng ta đã thảo luận hôm nay, đồng thời có thể xem xét và kiểm tra ứng dụng của bạn cũng như đưa ra đề xuất cho những kiểu tối ưu hoá này.

  • Bạn có thể cài đặt công cụ này trong tác nhân hiện có bằng Android CLI.

    android skills add jetpack-compose-performance
  • Để tìm thêm thông tin về Android CLI, bạn có thể tìm hiểu thêm trên developer.android.com.

  • Bạn cũng có thể tìm hiểu thêm về các kỹ năng của chúng tôi trên kho lưu trữ GitHub.

  • Chúc bạn đo điểm chuẩn an toàn và lập trình vui vẻ.

Mã

Đoạn mã

Đoạn mã Kotlin ở phút 09:34 trong phần "Quản lý hiệu ứng phụ"
@Composable
fun MediaPlayer(contentSource: Uri, playbackRate: Float) {
    val mediaPlayer = rememberMediaPlayer()

    // Asynchronous work and animations
    LaunchedEffect(contentSource) { mediaPlayer.load(contentSource) }
}
Đoạn mã Kotlin ở phút 10:15 trong video "Quản lý hiệu ứng phụ"
@Composable
fun MediaPlayer(contentSource: Uri, playbackRate: Float) {
    val activity = LocalActivity.current

    // Setup and teardown
    DisposableEffect(Unit) {
        activity.requestedOrientation = SCREEN_ORIENTATION_LANDSCAPE
        onDispose {
            activity.requestedOrientation = SCREEN_ORIENTATION_UNSPECIFIED
        }
    }
}
Đoạn mã Kotlin ở phút 10:36 trong video "Quản lý tác dụng phụ"
@Composable
fun MediaPlayer(contentSource: Uri, playbackRate: Float) {
    val mediaPlayer = rememberMediaPlayer()

    // One-shot events
    SideEffect(Unit) { mediaPlayer.play() }
}
Đoạn mã Kotlin ở phút 11:12 trong video "Quản lý tác dụng phụ"
@Composable
fun MediaPlayer(contentSource: Uri, playbackRate: Float) {
    val mediaPlayer = rememberMediaPlayer()

    // Synchronize the outside world to the state of composition
    SideEffect { mediaPlayer.playbackRate = playbackRate }
}
Đoạn mã Kotlin ở phút 12:12 trong video "Quản lý tác dụng phụ"
@Composable
fun ShoppingCartScreen(
    viewModel: CartViewModel
) {
    LaunchedEffect(Unit) { viewModel.loadCart() }
    LaunchedEffect(Unit) { viewModel.loadPaymentMethods() }
    LaunchedEffect(Unit) { viewModel.loadDeliveryAddresses() }

    CartContent(...)
}
Đoạn mã Kotlin ở phút 12:36 trong video "Quản lý tác dụng phụ"
@Composable
fun ShoppingCartScreen(
    viewModel: CartViewModel
) {
    LaunchedEffect(Unit) {
        launch { viewModel.loadCart() }
        launch { viewModel.loadPaymentMethods() }
        launch { viewModel.loadDeliveryAddresses() }
    }

    CartContent(...)
}
Đoạn mã Kotlin ở phút 15:12 trong phần "Tối ưu hoá trạng thái và vô hiệu hoá"
@Composable
fun ScrollToTopButton(
    scrollState: ScrollState
) {
    val showScrollToTopButton = scrollState.value > 0
    val coroutineScope = rememberCoroutineScope()

    if (showScrollToTopButton) {
        FloatingActionButton(
            onClick = {
                coroutineScope.launch { scrollState.animateScrollTo(0) }
            }
        ) {
            Icon(Icons.Default.ArrowUpward, "Scroll to Top")
        }
    }
}
Đoạn mã Kotlin ở phút 15:51 trong video "Tối ưu hoá và vô hiệu hoá trạng thái"
@Composable
fun ScrollToTopButton(
    scrollState: ScrollState
) {
    val showScrollToTopButton by remember { computedStateOf { scrollState.value > 0 } }
    val coroutineScope = rememberCoroutineScope()

    if (showScrollToTopButton) {
        FloatingActionButton(
            onClick = {
                coroutineScope.launch { scrollState.animateScrollTo(0) }
            }
        ) {
            Icon(Icons.Default.ArrowUpward, "Scroll to Top")
        }
    }
}
Đoạn mã Kotlin ở phút 16:49 trong video "Tối ưu hoá trạng thái và vô hiệu hoá"
var a by mutableStateOf(0)

val derivedState = derivedStateOf { a > 0 }
val computedState = computedStateOf { a > 0 }

// First read
derivedState.value
computedState.value

// Second read
derivedState.value
computedState.value
Đoạn mã Kotlin ở phút 18:35 trong video "State optimization and invalidations" (Tối ưu hoá và vô hiệu hoá trạng thái)
var a by mutableStateOf(0)

val derivedState = derivedStateOf { a > 0 }
val computedState = computedStateOf { a > 0 }

...

// Read-after-write
a++
derivedState.value
computedState.value
Đoạn mã Kotlin ở phút 21:07 trong video "Bỏ qua và độ ổn định trong Compose"
// What you write
@Composable fun ContactListItem(
    contact: Contact
) { ... }

// What you get
@Composable fun ContactListItem(
    contact: Contact,
    $composer: Composer?
    $changed: Int
) {
    val $dirty = $changed or
        $composer.changed(contact)

    if ($dirty) { /* Your function body */ }
    else $composer.skipToGroupEnd()
}
Đoạn mã Kotlin ở phút 21:53 trong video "Bỏ qua và độ ổn định trong Compose"
data class ContactsModel(
    val header: String,
    val contacts: List<Contact>
)

data class Contact(...)

@Composable
fun ContactsScreen(model: ContactsModel) {
    Column {
        TopAppBar(title = { Text(model.header) })
        ContactsList(model.contacts)
    }
}

@Composable
fun ContactsList(contacts: List<Contact>) {
    LazyColumn {
        items(contacts) { ContactListItem(it) }
    }
}

@Composable
fun ContactListItem(contact: Contact) { ... }
Đoạn mã Shell ở phút 29:40 trong video "Fast composable design and performance tools" (Thiết kế thành phần kết hợp nhanh và các công cụ hiệu suất)
android skills add jetpack-compose-performance