Kết xuất chậm

Kết xuất giao diện người dùng là hành động tạo một khung hình từ ứng dụng và hiện khung hình đó trên màn hình. Nhằm giúp đảm bảo quá trình tương tác của người dùng với ứng dụng diễn ra suôn sẻ, ứng dụng phải kết xuất các khung hình trong khoảng thời gian dưới 16 mili giây để đạt được 60 khung hình/giây (fps). Để tìm hiểu lý do bạn nên chọn tốc độ 60 khung hình/giây, hãy xem video Android Performance Patterns: Why 60fps? (Mẫu hiệu suất Android: Tại sao nên dùng tốc độ 60 khung hình/giây?) Nếu bạn đang cố đạt được tốc độ 90 khung hình/giây thì khoảng thời gian này sẽ giảm xuống 11 mili giây, còn với tốc độ 120 khung hình/giây là 8 mili giây.

Nếu bạn vượt quá khoảng thời gian này 1 mili giây, thì điều đó không có nghĩa là khung hình sẽ hiện trễ 1 mili giây, mà Choreographer sẽ làm rớt toàn bộ khung hình. Nếu ứng dụng của bạn kết xuất hình ảnh trên giao diện người dùng với tốc độ chậm, thì hệ thống buộc phải bỏ qua một số khung hình và người dùng nhận thấy tình trạng giật hình trong ứng dụng của bạn. Đây được gọi là hiện tượng giật. Trang này cho biết cách chẩn đoán và khắc phục hiện tượng giật.

Nếu đang phát triển các trò chơi không sử dụng hệ thống View, thì bạn sẽ bỏ qua Choreographer. Trong trường hợp này, Thư viện Frame Pacing (Tốc độ khung hình) giúp các trò chơi OpenGLVulkan đạt được khả năng kết xuất mượt mà và tốc độ khung hình chính xác trên Android.

Để giúp bạn cải thiện chất lượng ứng dụng, Android sẽ tự động theo dõi các hiện tượng giật trong ứng dụng và hiện thông tin trên trang tổng quan Android vitals. Để biết thông tin về cách thu thập dữ liệu, hãy xem phần Theo dõi chất lượng kỹ thuật của ứng dụng bằng Android vitals.

Xác định hiện tượng giật

Bạn có thể khó xác định được mã trong ứng dụng của mình đang gây ra hiện tượng giật. Phần này mô tả 3 phương thức để xác định hiện tượng giật:

Công cụ Kiểm tra bằng hình ảnh cho phép bạn xem nhanh tất cả các trường hợp sử dụng trong ứng dụng của mình sau vài phút, nhưng không cung cấp nhiều chi tiết như Systrace. Systrace cung cấp thêm thông tin chi tiết nhưng nếu đã chạy Systrace cho mọi trường hợp sử dụng trong ứng dụng của mình, bạn có thể sẽ có quá nhiều dữ liệu, gây khó khăn cho việc phân tích. Cả tính năng kiểm tra bằng hình ảnh và Systrace đều phát hiện được hiện tượng giật trên thiết bị cục bộ. Nếu không thể mô phỏng hiện tượng giật trên các thiết bị cục bộ, bạn có thể tạo công cụ theo dõi hiệu suất tuỳ chỉnh để đo lường các phần cụ thể của ứng dụng trên các thiết bị đang chạy trong trường.

Kiểm tra bằng hình ảnh

Công cụ kiểm tra bằng hình ảnh giúp bạn xác định những trường hợp sử dụng gây hiện tượng giật. Để kiểm tra bằng hình ảnh, hãy mở ứng dụng và xem nhiều phần của ứng dụng theo cách thủ công, đồng thời kiểm tra xem giao diện người dùng nào bị giật.

Dưới đây là một số mẹo khi kiểm tra bằng hình ảnh:

  • Chạy bản phát hành (hoặc ít nhất là phiên bản không thể gỡ lỗi) của ứng dụng. Môi trường thời gian chạy ART sẽ vô hiệu hoá một số phương thức tối ưu hoá quan trọng để hỗ trợ các tính năng gỡ lỗi. Vì vậy, hãy đảm bảo những gì bạn nhìn thấy giống với những gì người dùng thấy.
  • Bật tính năng Kết xuất GPU cấu hình. Tính năng Kết xuất GPU cấu hình hiển thị các thanh trên màn hình, cung cấp cho bạn hình ảnh trực quan về thời gian kết xuất khung hình của cửa sổ giao diện người dùng với điểm chuẩn 16 mili giây trên mỗi khung hình. Mỗi thanh có các thành phần màu liên kết với một giai đoạn trong quy trình kết xuất, vì vậy, bạn có thể xem phần nào mất nhiều thời gian nhất. Ví dụ: nếu khung hình dành nhiều thời gian để xử lý đầu vào, hãy xem mã ứng dụng nào của bạn xử lý hoạt động đầu vào của người dùng.
  • Chạy qua các thành phần là các nguồn thường gây hiện tượng giật, chẳng hạn như RecyclerView.
  • Mở ứng dụng bằng quy trình khởi động nguội.
  • Chạy ứng dụng trên một thiết bị chậm hơn để làm vấn đề thêm trầm trọng.

Sau khi tìm thấy các trường hợp sử dụng gây hiện tượng giật, bạn có thể biết rõ nguyên nhân gây ra tình trạng giật trong ứng dụng. Nếu cần thêm thông tin, bạn có thể dùng Systrace để xem xét thêm nguyên nhân.

Systrace

Mặc dù Systrace là một công cụ cho thấy toàn bộ hoạt động của thiết bị, nhưng nó cũng hữu ích khi xác định hiện tượng giật trong ứng dụng. Systrace có hệ thống định mức tối thiểu, do đó, bạn có thể trải nghiệm hiện tượng giật thực tế trong quá trình đo lường.

Ghi lại dấu vết bằng Systrace trong khi trường hợp sử dụng bị giật xảy ra trên thiết bị của bạn. Để biết hướng dẫn về cách sử dụng Systrace, hãy xem phần Ghi lại dấu vết hệ thống qua dòng lệnh. Systrace được chia tách theo quy trình và luồng. Hãy tìm quy trình của ứng dụng trong Systrace. Quy trình này có dạng như hình 1.

Ví dụ về Systrace
Hình 1. Ví dụ về Systrace.

Ví dụ về Systrace trong hình 1 chứa các thông tin sau để xác định hiện tượng giật:

  1. Systrace cho thấy thời điểm mỗi khung hình được vẽ và mã hoá từng khung hình để làm nổi bật thời gian kết xuất chậm. Việc này giúp bạn phát hiện các khung hình riêng lẻ bị giật chính xác hơn là kiểm tra bằng hình ảnh. Để biết thêm thông tin, vui lòng xem phần Kiểm tra cảnh báo và khung giao diện người dùng.
  2. Systrace phát hiện các vấn đề trong ứng dụng của bạn và hiện cảnh báo trong cả khung hình riêng lẻ lẫn bảng cảnh báo. Tốt nhất là bạn nên làm theo chỉ dẫn trong cảnh báo.
  3. Các phần của khung và thư viện Android, chẳng hạn như RecyclerView, chứa dấu đánh dấu. Vì vậy, tiến trình systrace cho biết thời điểm các phương thức đó được thực thi trên luồng giao diện người dùng và khoảng thời gian thực thi các phương thức đó.

Sau khi xem dữ liệu đầu ra Systrace, sẽ có những phương thức trong ứng dụng mà bạn nghi ngờ là gây ra hiện tượng giật. Ví dụ: nếu tiến trình cho thấy khung hình chậm do RecyclerView mất quá nhiều thời gian, bạn có thể thêm sự kiện theo dõi tuỳ chỉnh vào mã có liên quan và chạy lại Systrace để biết thêm thông tin. Trong Systrace mới, tiến trình cho biết thời điểm các phương thức của ứng dụng được gọi và khoảng thời gian thực thi các phương thức đó.

Nếu Systrace không cho bạn thấy thông tin chi tiết về lý do khiến tác vụ luồng giao diện người dùng mất nhiều thời gian, hãy sử dụng Trình phân tích CPU của Android để ghi lại dấu vết phương thức được lấy mẫu hoặc đo lường. Nhìn chung, dấu vết phương thức không phù hợp để xác định tình trạng giật vì chúng tạo ra tình trạng giật giả do mức hao tổn lớn, cũng như không phân biệt được các luồng đang chạy hay bị chặn. Tuy nhiên, dấu vết phương thức có thể giúp bạn xác định phương thức nào trong ứng dụng của bạn mất nhiều thời gian nhất. Sau khi xác định các phương thức này, hãy thêm điểm đánh dấu Theo dõi và chạy lại Systrace để xem các phương thức này có gây ra tình trạng giật hay không.

Để biết thêm thông tin, hãy xem bài viết Tìm hiểu về Systrace.

Giám sát hiệu suất tuỳ chỉnh

Nếu không thể mô phỏng hiện tượng giật trên thiết bị cục bộ, bạn có thể tạo chế độ giám sát hiệu suất tuỳ chỉnh vào ứng dụng của mình để giúp xác định nguồn gây ra hiện tượng giật trên các thiết bị trong trường.

Để làm điều này, hãy thu thập thời gian kết xuất khung hình từ các phần cụ thể trong ứng dụng bằng FrameMetricsAggregator, đồng thời ghi lại và phân tích dữ liệu bằng cách sử dụng tính năng Giám sát hiệu suất Firebase.

Để tìm hiểu thêm, hãy xem bài viết Bắt đầu sử dụng tính năng Giám sát hiệu suất cho Android.

Khung hình bị treo

Khung hình bị treo là khung giao diện người dùng mất hơn 700 mili giây để hiển thị. Đây là vấn đề do ứng dụng của bạn dường như bị nghẽn và không phản hồi hoạt động đầu vào của người dùng gần một giây trong khi khung hình đang hiển thị. Bạn nên tối ưu hoá các ứng dụng để hiển thị khung hình trong vòng 16 mili giây nhằm đảm bảo giao diện người dùng mượt mà. Tuy nhiên, trong khi ứng dụng đang khởi động hoặc chuyển sang một màn hình khác, thì việc khung ban đầu mất hơn 16 mili giây để vẽ là điều bình thường vì ứng dụng của bạn phải tăng cường chế độ xem, bố trí màn hình và thực hiện bản vẽ ban đầu từ đầu. Đó là lý do Android theo dõi khung hình bị treo tách biệt với kết xuất chậm. Không có khung hình nào trong ứng dụng của bạn phải mất nhiều hơn 700 mili giây để hiển thị.

Để giúp bạn cải thiện chất lượng ứng dụng, Android sẽ tự động theo dõi ứng dụng của bạn để biết các khung hình bị treo và hiển thị thông tin trong trang tổng quan Android vitals. Để biết thông tin về cách thu thập dữ liệu, hãy xem phần Theo dõi chất lượng kỹ thuật của ứng dụng bằng Android vitals.

Khung hình bị treo là một dạng kết xuất chậm quá mức, vì vậy, quy trình chẩn đoán và khắc phục sự cố cũng tương tự.

Theo dõi hiện tượng giật

FrameTimeline trong Perfetto có thể giúp theo dõi các khung hình chậm hoặc bị treo.

Mối quan hệ giữa khung hình chậm, khung hình bị treo và lỗi ANR

Khung hình chậm, khung hình bị treo và lỗi ANR đều là các dạng giật khác nhau mà ứng dụng của bạn có thể gặp phải. Hãy xem bảng bên dưới để hiểu sự khác biệt.

Khung hình chậm Khung hình bị treo ANR
Thời gian kết xuất Từ 16 mili giây đến 700 mili giây Từ 700 mili giây đến 5 giây Hơn 5 giây
Vùng tác động rõ ràng của người dùng
  • RecyclerView bất ngờ cuộn
  • Trên các màn hình có ảnh động phức tạp hoạt động không đúng cách
  • Trong khi khởi động ứng dụng
  • Di chuyển từ màn hình này sang màn hình khác (ví dụ: chuyển đổi màn hình)
  • Khi hoạt động ở nền trước, ứng dụng của bạn không phản hồi một sự kiện đầu vào hoặc BroadcastReceiver (chẳng hạn như sự kiện nhấn phím hoặc nhấn vào màn hình) trong vòng 5 giây.
  • Mặc dù bạn không có hoạt động nào ở nền trước, nhưng BroadcastReceiver của bạn chưa hoàn thành việc thực thi trong một khoảng thời gian đáng kể.

Theo dõi riêng khung hình chậm và khung hình bị treo

Trong khi ứng dụng đang khởi động hoặc chuyển sang một màn hình khác, thì việc khung ban đầu mất hơn 16 mili giây để vẽ là điều bình thường vì ứng dụng phải tăng cường chế độ xem, bố trí màn hình và thực hiện bản vẽ ban đầu từ đầu.

Các phương pháp hay nhất để ưu tiên và giải quyết hiện tượng giật

Hãy lưu ý các phương pháp hay nhất sau đây khi tìm cách giải quyết hiện tượng giật trong ứng dụng:

  • Xác định và giải quyết các trường hợp giật dễ mô phỏng.
  • Ưu tiên lỗi ANR. Mặc dù khung hình chậm hoặc bị treo có thể khiến ứng dụng chạy chậm, nhưng lỗi ANR sẽ khiến ứng dụng ngừng phản hồi.
  • Quá trình kết xuất chậm rất khó tái hiện, nhưng bạn có thể bắt đầu bằng cách tắt các khung hình bị treo 700 mili giây. Đây là trường hợp hay xảy ra nhất khi ứng dụng khởi động hoặc thay đổi màn hình.

Khắc phục hiện tượng giật

Để khắc phục hiện tượng giật, hãy kiểm tra xem khung hình nào chưa hoàn thành trong 16 mili giây và tìm xem có vấn đề gì không. Kiểm tra xem Record View#draw hoặc Layout có đang mất nhiều thời gian một cách bất thường trong một số khung hình hay không. Hãy xem phần Các nguồn thường gây hiện tượng giật để biết những vấn đề này và các vấn đề khác.

Để tránh hiện tượng giật, hãy chạy các tác vụ chạy trong thời gian dài theo cách không đồng bộ bên ngoài luồng giao diện người dùng. Luôn lưu ý đến luồng mà mã của bạn đang chạy và thận trọng khi đăng các tác vụ không quan trọng lên luồng chính.

Nếu bạn có một giao diện người dùng chính phức tạp và quan trọng cho ứng dụng của mình (như danh sách cuộn trung tâm), hãy cân nhắc viết các kiểm thử đo lường có thể tự động phát hiện thời gian kết xuất chậm và thường xuyên chạy thử nghiệm để ngăn chặn sự hồi quy.

Các nguồn thường gây hiện tượng giật

Những phần sau đây giải thích về các nguồn thường gây hiện tượng giật trong các ứng dụng dùng hệ thống View cũng như các phương pháp hay nhất để giải quyết hiện tượng đó. Để biết thông tin về cách khắc phục các vấn đề về hiệu suất bằng Jetpack Compose, hãy xem phần Hiệu suất của Jetpack Compose.

Danh sách có thể cuộn

ListView và đặc biệt là RecyclerView thường được dùng cho các danh sách cuộn phức tạp, dễ bị giật nhất. Cả hai đều có điểm đánh dấu Systrace, vì vậy, bạn có thể dùng Systrace để tìm hiểu xem các điểm đánh dấu đó có góp phần gây ra hiện tượng giật trong ứng dụng của bạn hay không. Hãy truyền đối số dòng lệnh -a <your-package-name> để hiện các phần dấu vết trong RecyclerView (cũng như mọi điểm đánh dấu theo dõi bạn đã thêm). Nếu có, hãy làm theo hướng dẫn của cảnh báo được tạo trong kết quả Systrace. Bên trong Systrace, bạn có thể nhấp vào các phần được theo dõi bằng RecyclerView để xem nội dung giải thích về việc RecyclerView đang làm.

RecyclerView: notifyDataSetChanged()

Nếu bạn thấy mọi mục trong RecyclerView được liên kết lại (sau đó được sắp xếp và vẽ lại trong một khung hình), hãy đảm bảo là bạn không gọi notifyDataSetChanged(), setAdapter(Adapter) hoặc swapAdapter(Adapter, boolean) đối với các bản cập nhật nhỏ. Các phương thức này cho biết có thay đổi đối với toàn bộ nội dung danh sách và hiện trong Systrace dưới dạng RV FullInvalidate. Thay vào đó, hãy dùng SortedList hoặc DiffUtil để tạo cập nhật tối thiểu khi nội dung thay đổi hoặc được thêm vào.

Chẳng hạn, hãy xem xét một ứng dụng nhận được phiên bản mới của danh sách nội dung tin tức từ máy chủ. Khi đăng thông tin này lên Trình chuyển đổi, bạn có thể gọi notifyDataSetChanged() như trong ví dụ sau:

Kotlin

fun onNewDataArrived(news: List<News>) {
    myAdapter.news = news
    myAdapter.notifyDataSetChanged()
}

Java

void onNewDataArrived(List<News> news) {
    myAdapter.setNews(news);
    myAdapter.notifyDataSetChanged();
}

Nhược điểm của cách này là nếu có một thay đổi không quan trọng, chẳng hạn như một mục duy nhất được thêm vào phần trên cùng, thì RecyclerView không nhận biết được. Do đó, toàn bộ trạng thái của mục được lưu vào bộ nhớ đệm thường sẽ bị bỏ nên cần phải liên kết lại mọi thứ.

Bạn nên dùng DiffUtil để tính toán và gửi các thông tin cập nhật tối thiểu cho bạn:

Kotlin

fun onNewDataArrived(news: List<News>) {
    val oldNews = myAdapter.items
    val result = DiffUtil.calculateDiff(MyCallback(oldNews, news))
    myAdapter.news = news
    result.dispatchUpdatesTo(myAdapter)
}

Java

void onNewDataArrived(List<News> news) {
    List<News> oldNews = myAdapter.getItems();
    DiffResult result = DiffUtil.calculateDiff(new MyCallback(oldNews, news));
    myAdapter.setNews(news);
    result.dispatchUpdatesTo(myAdapter);
}

Để thông báo cho DiffUtil cách kiểm tra danh sách, hãy xác định MyCallback dưới dạng quy trình triển khai Callback.

RecyclerView: RecyclerView lồng nhau

Thông thường, bạn nên lồng nhiều thực thể của RecyclerView, đặc biệt là với một danh sách dọc của các danh sách cuộn theo chiều ngang. Ví dụ: các lưới ứng dụng trên trang chính của Cửa hàng Play. Thao tác này có thể hiệu quả, nhưng cũng có rất nhiều khung hiển thị di chuyển xung quanh.

Nếu thấy nhiều mục bên trong tăng cường khi lần đầu di chuyển xuống dưới trang, thì bạn nên kiểm tra để đảm bảo rằng bạn đang chia sẻ RecyclerView.RecycledViewPool giữa các thực thể (ngang) bên trong của RecyclerView. Theo mặc định, mỗi RecyclerView sẽ có một nhóm mục riêng. Tuy nhiên, trong trường hợp có nhiều itemViews xuất hiện trên màn hình cùng lúc, bạn không thể chia sẻ itemViews qua các danh sách ngang khác nếu mọi hàng đang hiện các kiểu khung hiển thị tương tự nhau.

Kotlin

class OuterAdapter : RecyclerView.Adapter<OuterAdapter.ViewHolder>() {

    ...

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        // Inflate inner item, find innerRecyclerView by ID.
        val innerLLM = LinearLayoutManager(parent.context, LinearLayoutManager.HORIZONTAL, false)
        innerRv.apply {
            layoutManager = innerLLM
            recycledViewPool = sharedPool
        }
        return OuterAdapter.ViewHolder(innerRv)
    }
    ...

Java

class OuterAdapter extends RecyclerView.Adapter<OuterAdapter.ViewHolder> {
    RecyclerView.RecycledViewPool sharedPool = new RecyclerView.RecycledViewPool();

    ...

    @Override
    public void onCreateViewHolder(ViewGroup parent, int viewType) {
        // Inflate inner item, find innerRecyclerView by ID.
        LinearLayoutManager innerLLM = new LinearLayoutManager(parent.getContext(),
                LinearLayoutManager.HORIZONTAL);
        innerRv.setLayoutManager(innerLLM);
        innerRv.setRecycledViewPool(sharedPool);
        return new OuterAdapter.ViewHolder(innerRv);

    }
    ...

Nếu muốn tối ưu hoá hơn nữa, bạn cũng có thể gọi setInitialPrefetchItemCount(int) trên LinearLayoutManager của RecyclerView bên trong. Chẳng hạn, nếu bạn luôn hiện 3,5 mục trong một hàng, hãy gọi innerLLM.setInitialItemPrefetchCount(4). Lệnh này sẽ báo hiệu cho RecyclerView khi một hàng ngang sắp xuất hiện trên màn hình, mã này sẽ cố gắng tìm nạp trước các mục bên trong, nếu có thời gian rảnh trên luồng giao diện người dùng.

RecyclerView: Tăng cường quá nhiều hoặc Tạo quá lâu

Trong hầu hết các trường hợp, tính năng tìm nạp trước trong RecyclerView có thể giúp khắc phục chi phí tăng cường bằng cách thực hiện trước công việc, trong khi luồng giao diện người dùng không hoạt động. Nếu bạn thấy hành vi tăng cường trong một khung hình chứ không phải trong phần có nhãn RV Prefetch (Tìm nạp trước RV), hãy đảm bảo bạn đang kiểm thử trên một thiết bị được hỗ trợ và sử dụng phiên bản mới nhất của Thư viện hỗ trợ. Tính năng Tìm nạp trước chỉ được hỗ trợ trên Android 5.0 API cấp 21 trở lên.

Nếu bạn thường xuyên thấy hành vi tăng cường gây ra tình trạng giật khi các mục mới xuất hiện trên màn hình, hãy xác minh rằng bạn không có nhiều kiểu khung hiển thị hơn mức cần thiết. Càng có ít kiểu khung hiển thị trong nội dung của RecyclerView, thì càng cần ít hành vi tăng cường hơn khi các loại mục mới xuất hiện trên màn hình. Nếu có thể, hãy hợp nhất các kiểu khung hiển thị khi thích hợp. Nếu chỉ thay đổi kiểu của một biểu tượng, màu sắc hoặc đoạn văn bản, bạn có thể thực hiện thay đổi đó vào thời điểm liên kết và tránh hành vi tăng cường, từ đó giảm mức sử dụng bộ nhớ của ứng dụng.

Nếu các kiểu khung hiển thị của bạn có giao diện phù hợp, hãy xem xét giảm chi phí tăng cường. Việc giảm khung hiển thị có cấu trúc và vùng chứa không cần thiết có thể khá hữu ích. Hãy cân nhắc tạo itemViews bằng ConstraintLayout để giảm khung hiển thị có cấu trúc.

Nếu bạn muốn tối ưu hoá hiệu suất hơn nữa, đồng thời hệ phân cấp các mục của bạn khá đơn giản và bạn không cần các tính năng định kiểu và sắp xếp theo chủ đề, hãy cân nhắc tự gọi hàm khởi tạo. Tuy nhiên, việc đánh đổi tính đơn giản và các tính năng của XML thường không đáng.

RecyclerView: Liên kết quá lâu

Liên kết – tức là onBindViewHolder(VH, int) – phải đơn giản và chỉ mất chưa đến một mili giây cho mọi mục trừ những mục phức tạp nhất. Phương thức này phải lấy các mục cũ của đối tượng Java (POJO) thuần tuý từ dữ liệu mục nội bộ của bộ chuyển đổi và gọi phương thức setter trên khung hiển thị trong ViewHolder. Nếu RV OnBindView mất nhiều thời gian, hãy xác minh rằng bạn chỉ thực hiện ít thao tác nhất trong mã liên kết.

Nếu đang dùng các đối tượng POJO cơ bản để giữ dữ liệu trong bộ chuyển đổi, bạn có thể hoàn toàn tránh việc viết mã liên kết trong onBindViewHolder bằng cách sử dụng Thư viện liên kết dữ liệu.

RecyclerView hoặc ListView: Bố cục/bản vẽ mất quá nhiều thời gian

Đối với các vấn đề về bản vẽ và bố cục, vui lòng xem các phần Hiệu suất bố cụcHiệu suất hiển thị.

ListView: Tăng cường

Bạn có thể vô tình tắt tính năng tái chế trong ListView nếu không cẩn thận. Nếu bạn thấy hành vi tăng cường mỗi lần một mục xuất hiện trên màn hình, hãy kiểm tra để đảm bảo việc triển khai Adapter.getView() của bạn đang sử dụng, liên kết lại và trả về tham số convertView. Nếu cách triển khai getView() luôn tăng cường, ứng dụng của bạn sẽ không nhận được lợi ích từ việc tái chế trong ListView. Cấu trúc của getView() hầu như luôn giống với cách triển khai sau:

Kotlin

fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
    return (convertView ?: layoutInflater.inflate(R.layout.my_layout, parent, false)).apply {
        // Bind content from position to convertView.
    }
}

Java

View getView(int position, View convertView, ViewGroup parent) {

    if (convertView == null) {
        // Only inflate if no convertView passed.
        convertView = layoutInflater.inflate(R.layout.my_layout, parent, false)
    }
    // Bind content from position to convertView.
    return convertView;
}

Hiệu suất bố cục

Nếu Systrace cho thấy phân đoạn Bố cục của Choreographer#doFrame đang làm quá nhiều việc hoặc làm công việc quá thường xuyên, điều đó có nghĩa là bạn đang gặp phải các vấn đề về hiệu suất bố cục. Hiệu suất bố cục của ứng dụng phụ thuộc vào phần nào của hệ phân cấp khung hiển thị thay đổi tham số bố cục hoặc đầu vào.

Hiệu suất bố cục: Chi phí

Nếu các phân đoạn dài hơn vài mili giây, thì có thể bạn đang gặp phải trường hợp hiệu suất lồng nhau tồi tệ nhất cho RelativeLayouts hoặc weighted-LinearLayouts. Mỗi bố cục này có thể kích hoạt nhiều lần đo lường/truyền bố cục con, vì vậy, việc lồng chúng có thể dẫn đến hành vi O(n^2) trên độ sâu lồng.

Hãy thử tránh dùng RelativeLayout hoặc tính năng trọng số của LinearLayout trong tất cả trừ những nút có lá thấp nhất trong hệ phân cấp. Sau đây là những cách mà bạn có thể làm điều này:

  • Sắp xếp lại các khung hiển thị có cấu trúc.
  • Xác định logic bố cục tuỳ chỉnh. Hãy xem bài viết Tối ưu hoá hệ phân cấp bố cục để biết ví dụ cụ thể. Bạn có thể thử chuyển đổi thành ConstraintLayout, cung cấp các tính năng tương tự mà không có hạn chế về hiệu suất.

Hiệu suất bố cục: Tần suất

Bố cục dự kiến sẽ diễn ra khi nội dung mới xuất hiện trên màn hình, ví dụ như khi một mục mới cuộn vào khung hiển thị trong RecyclerView. Nếu có bố cục quan trọng xuất hiện trên từng khung hình, thì có thể bạn đang tạo ảnh động cho bố cục, việc này có thể gây rớt khung hình.

Nhìn chung, ảnh động phải chạy trên các thuộc tính vẽ của View, chẳng hạn như sau:

Bạn có thể thay đổi tất cả các thuộc tính này với chi phí rẻ hơn nhiều so với thuộc tính bố cục, chẳng hạn như khoảng đệm hoặc lề. Nhìn chung, việc thay đổi các thuộc tính vẽ của khung hiển thị cũng có chi phí thấp hơn nhiều bằng cách gọi một phương thức setter kích hoạt invalidate(), theo sau là draw(Canvas) trong khung hình tiếp theo. Thao tác này sẽ ghi lại thao tác vẽ cho khung hiển thị không hợp lệ và cũng có chi phí thấp hơn so với bố cục.

Hiệu suất hiển thị

Giao diện người dùng Android hoạt động theo 2 giai đoạn:

  • Record View#draw trên luồng giao diện người dùng, chạy draw(Canvas) trên mọi khung hiển thị không hợp lệ và có thể gọi các lệnh gọi vào khung hiển thị tuỳ chỉnh hoặc vào mã của bạn.
  • DrawFrame trên RenderThread, chạy trên RenderThread gốc nhưng hoạt động dựa trên công việc do giai đoạn Record View#draw tạo ra.

Hiệu suất hiển thị: Luồng giao diện người dùng

Nếu Record View#draw mất nhiều thời gian, thường thì bạn sẽ thấy một bitmap được vẽ trên luồng giao diện người dùng. Việc tạo điểm ảnh cho bitmap sẽ sử dụng kết xuất CPU, vì vậy, bạn nên tránh điều này khi có thể. Bạn có thể dùng phương thức theo dõi bằng Trình phân tích CPU của Android để xem đây có phải là vấn đề không.

Việc tạo điểm ảnh cho bitmap thường được thực hiện khi một ứng dụng muốn trang trí một bitmap trước khi hiển thị bitmap đó, đôi khi là một hình trang trí như thêm các góc tròn:

Kotlin

val paint = Paint().apply {
    isAntiAlias = true
}
Canvas(roundedOutputBitmap).apply {
    // Draw a round rect to define the shape:
    drawRoundRect(
            0f,
            0f,
            roundedOutputBitmap.width.toFloat(),
            roundedOutputBitmap.height.toFloat(),
            20f,
            20f,
            paint
    )
    paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.MULTIPLY)
    // Multiply content on top to make it rounded.
    drawBitmap(sourceBitmap, 0f, 0f, paint)
    setBitmap(null)
    // Now roundedOutputBitmap has sourceBitmap inside, but as a circle.
}

Java

Canvas bitmapCanvas = new Canvas(roundedOutputBitmap);
Paint paint = new Paint();
paint.setAntiAlias(true);
// Draw a round rect to define the shape:
bitmapCanvas.drawRoundRect(0, 0,
        roundedOutputBitmap.getWidth(), roundedOutputBitmap.getHeight(), 20, 20, paint);
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.MULTIPLY));
// Multiply content on top to make it rounded.
bitmapCanvas.drawBitmap(sourceBitmap, 0, 0, paint);
bitmapCanvas.setBitmap(null);
// Now roundedOutputBitmap has sourceBitmap inside, but as a circle.

Nếu đây là kiểu công việc bạn đang thực hiện trên luồng giao diện người dùng, bạn có thể làm điều này trên luồng giải mã trong nền. Trong một số trường hợp, như ví dụ trước, bạn thậm chí có thể thực hiện công việc tại thời điểm vẽ. Vì vậy, nếu mã Drawable hoặc View của bạn trông giống như sau:

Kotlin

fun setBitmap(bitmap: Bitmap) {
    mBitmap = bitmap
    invalidate()
}

override fun onDraw(canvas: Canvas) {
    canvas.drawBitmap(mBitmap, null, paint)
}

Java

void setBitmap(Bitmap bitmap) {
    mBitmap = bitmap;
    invalidate();
}

void onDraw(Canvas canvas) {
    canvas.drawBitmap(mBitmap, null, paint);
}

Bạn có thể thay thế bằng:

Kotlin

fun setBitmap(bitmap: Bitmap) {
    shaderPaint.shader = BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)
    invalidate()
}

override fun onDraw(canvas: Canvas) {
    canvas.drawRoundRect(0f, 0f, width, height, 20f, 20f, shaderPaint)
}

Java

void setBitmap(Bitmap bitmap) {
    shaderPaint.setShader(
            new BitmapShader(bitmap, TileMode.CLAMP, TileMode.CLAMP));
    invalidate();
}

void onDraw(Canvas canvas) {
    canvas.drawRoundRect(0, 0, width, height, 20, 20, shaderPaint);
}

Bạn cũng có thể thực hiện việc này để bảo vệ nền, chẳng hạn như khi vẽ một hiệu ứng chuyển màu ở đầu bitmap và lọc hình ảnh bằng ColorMatrixColorFilter – 2 thao tác phổ biến khác đã sửa đổi xong bitmap.

Nếu bạn đang vẽ bitmap vì một lý do khác, có thể là đang sử dụng làm bộ nhớ đệm chẳng hạn, hãy thử vẽ vào Canvas đã tăng tốc phần cứng được truyền trực tiếp cho View hoặc Drawable của bạn. Nếu cần, hãy cân nhắc gọi cả setLayerType() thông qua LAYER_TYPE_HARDWARE để lưu đầu ra kết xuất phức tạp vào bộ nhớ đệm, đồng thời vẫn tận dụng được tính năng kết xuất GPU.

Hiệu suất hiển thị: RenderThread

Một số thao tác Canvas có chi phí ghi lại rẻ, nhưng lại kích hoạt việc tính toán tốn kém trên RenderThread. Systrace thường gọi những thao tác này kèm theo cảnh báo.

Tạo ảnh động cho đường dẫn lớn

Khi Canvas.drawPath() được gọi trên Canvas đã tăng tốc phần cứng được truyền cho View, Android sẽ vẽ các đường dẫn này trước tiên trên CPU rồi tải chúng lên GPU. Nếu bạn có đường dẫn lớn, hãy tránh chỉnh sửa các đường dẫn này giữa các khung hình để chúng có thể được lưu vào bộ nhớ đệm và vẽ một cách hiệu quả. drawPoints(), drawLines()drawRect/Circle/Oval/RoundRect() tối ưu hơn. Bạn nên dùng các phương thức này ngay cả khi sử dụng nhiều hàm gọi vẽ hơn.

Canvas.clipPath

clipPath(Path) thường kích hoạt hành vi cắt xén đắt đỏ và bạn phải tránh sử dụng. Khi có thể, hãy chọn vẽ hình dạng, thay vì cắt thành các hình không phải hình chữ nhật. Tính năng này hoạt động hiệu quả hơn và hỗ trợ phương pháp khử răng cưa. Ví dụ: lệnh gọi clipPath sau đây có thể được diễn đạt theo cách khác:

Kotlin

canvas.apply {
    save()
    clipPath(circlePath)
    drawBitmap(bitmap, 0f, 0f, paint)
    restore()
}

Java

canvas.save();
canvas.clipPath(circlePath);
canvas.drawBitmap(bitmap, 0f, 0f, paint);
canvas.restore();

Thay vào đó, hãy diễn đạt ví dụ trước như sau:

Kotlin

paint.shader = BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)
// At draw time:
canvas.drawPath(circlePath, mPaint)

Java

// One time init:
paint.setShader(new BitmapShader(bitmap, TileMode.CLAMP, TileMode.CLAMP));
// At draw time:
canvas.drawPath(circlePath, mPaint);
Tải tệp bitmap lên

Android hiển thị bitmap dưới dạng hoạ tiết OpenGL, và khi bitmap hiển thị lần đầu trong một khung hình, khung này sẽ được tải lên GPU. Bạn có thể thấy điều này trong Systrace dưới dạng Tải lên hoạ tiết (id) chiều rộng x chiều cao. Quá trình này có thể mất vài mili giây, như minh hoạ trong hình 2, nhưng cần phải hiển thị hình ảnh bằng GPU.

Nếu việc này mất nhiều thời gian, trước tiên, hãy kiểm tra số liệu chiều rộng và chiều cao trong dấu vết. Đảm bảo bitmap đang hiển thị không lớn hơn đáng kể so với diện tích trên màn hình mà nó hiển thị. Nếu có, việc này sẽ gây lãng phí thời gian và bộ nhớ. Nhìn chung, các thư viện tải bitmap là một cách để yêu cầu một bitmap có kích thước phù hợp.

Trong Android 7.0, mã tải bitmap (thường được các thư viện thực hiện) có thể gọi prepareToDraw() để kích hoạt quá trình tải lên sớm, trước khi cần đến. Theo đó, quá trình tải lên diễn ra sớm, trong khi tính năng RenderThread không hoạt động. Bạn có thể thực hiện việc này sau khi giải mã hoặc khi liên kết bitmap với một khung hiển thị, miễn là bạn biết bitmap đó. Tốt nhất là thư viện tải bitmap của bạn sẽ thực hiện việc này cho bạn, nhưng nếu bạn muốn tự kiểm soát hoặc muốn đảm bảo bạn không nhấn vào các vùng tải lên trên các thiết bị mới hơn, bạn có thể gọi prepareToDraw() trong mã của mình.

Một ứng dụng dành thời gian đáng kể vào khung tải lên một bitmap lớn
Hình 2. Một ứng dụng dành thời gian đáng kể vào khung tải lên một bitmap lớn. Hãy giảm kích thước hoặc kích hoạt sớm tập lệnh đó khi bạn giải mã bằng prepareToDraw().

Sự chậm trễ trong việc lập lịch luồng

Bộ lập lịch luồng thuộc hệ điều hành Android, chịu trách nhiệm quyết định luồng nào trong hệ thống phải chạy, thời điểm chạy và khoảng thời gian chạy.

Đôi khi, hiện tượng giật là do luồng giao diện người dùng của ứng dụng bị chặn hoặc không chạy. Systrace dùng các màu khác nhau, như minh hoạ trong hình 3, để cho biết thời điểm một luồng đang ngủ (màu xám), có thể chạy (màu xanh dương: luồng có thể chạy nhưng chưa được bộ lập lịch chọn để chạy), đang tích cực chạy (màu xanh lục) hoặc ở chế độ ngủ không gián đoạn (màu đỏ hoặc cam). Việc này cực kỳ hữu ích khi gỡ lỗi các sự cố giật do sự chậm trễ trong việc lập lịch luồng.

Làm nổi bật một khoảng thời gian khi luồng giao diện người dùng đang ngủ
Hình 3. Làm nổi bật một khoảng thời gian khi luồng giao diện người dùng đang ngủ.

Thông thường, các lệnh gọi liên kết – cơ chế giao tiếp liên quy trình (IPC) trên Android – gây ra khoảng thời gian tạm dừng dài trong quá trình thực thi của ứng dụng. Trên các phiên bản Android gần đây, đó là một trong những lý do phổ biến nhất khiến luồng giao diện người dùng ngừng chạy. Nói chung, cách khắc phục là tránh gọi các hàm thực hiện lệnh gọi liên kết. Nếu không thể tránh khỏi, bạn nên lưu giá trị vào bộ nhớ đệm hoặc di chuyển tác vụ sang luồng trong nền. Khi cơ sở mã lớn hơn, bạn có thể vô tình thêm lệnh gọi liên kết bằng cách gọi một số phương thức cấp thấp nếu không cẩn thận. Tuy nhiên, bạn có thể tìm thấy và khắc phục bằng tính năng theo dõi.

Nếu có các giao dịch liên kết, bạn có thể theo dõi các ngăn xếp lệnh gọi của chúng bằng lệnh adb sau:

$ adb shell am trace-ipc start
… use the app - scroll/animate ...
$ adb shell am trace-ipc stop --dump-file /data/local/tmp/ipc-trace.txt
$ adb pull /data/local/tmp/ipc-trace.txt

Đôi khi, các lệnh gọi dường như vô hại, chẳng hạn như getRefreshRate(), có thể kích hoạt giao dịch liên kết và gây ra những sự cố lớn khi chúng được gọi thường xuyên. Việc theo dõi định kỳ có thể giúp bạn nhanh chóng phát hiện và khắc phục các vấn đề này khi chúng xuất hiện.

Cho thấy luồng giao diện người dùng đang ngủ do các giao dịch liên kết trong một cử chỉ hất RV. Hãy tập trung vào logic liên kết và dùng trace-ipc để theo dõi cũng như xoá các lệnh gọi liên kết.
Hình 4. Luồng giao diện người dùng đang ngủ do các giao dịch liên kết trong một cử chỉ hất RV. Duy trì logic liên kết đơn giản và dùng trace-ipc để theo dõi cũng như xoá các lệnh gọi liên kết.

Nếu bạn không thấy hoạt động liên kết nhưng vẫn không thấy luồng giao diện người dùng của mình được chạy, hãy đảm bảo rằng bạn hiện không chờ một thao tác khoá hoặc thao tác khác từ một luồng khác. Thông thường, luồng giao diện người dùng không cần phải đợi kết quả từ các luồng khác. Các luồng khác phải đăng thông tin lên luồng đó.

Phân bổ đối tượng và thu gom rác

Tính năng phân bổ đối tượng và thu gom rác (GC) không còn là một vấn đề đáng kể từ khi ART được đưa vào làm môi trường thời gian chạy mặc định trong Android 5.0, nhưng bạn vẫn có thể giảm bớt số lượng công việc bằng cách thực hiện thêm tác vụ này. Bạn nên phân bổ để phản hồi một sự kiện hiếm không xảy ra nhiều lần trong một giây (chẳng hạn như người dùng nhấn vào nút), nhưng hãy nhớ mỗi lượt phân bổ đều có chi phí. Nếu nó nằm trong một vòng lặp chặt chẽ được gọi thường xuyên, hãy cân nhắc tránh việc phân bổ để giảm tải trên GC.

Systrace sẽ cho bạn biết liệu GC có chạy thường xuyên hay không và Trình phân tích bộ nhớ Android có thể cho bạn biết nguồn gốc của các lượt phân bổ. Nếu tránh phân bổ khi có thể, đặc biệt là trong vòng lặp chặt chẽ, bạn sẽ ít gặp sự cố hơn.

Hiển thị GC 94 mili giây trên HeapTaskDaemon
Hình 5. Một GC 94 mili giây trên luồng HeapTaskDaemon.

Trên các phiên bản Android gần đây, GC thường chạy trên một luồng trong nền có tên là HeapTaskDaemon. Số lượng phân bổ đáng kể có thể đồng nghĩa với việc GC sẽ sử dụng nhiều tài nguyên CPU hơn như minh hoạ trong hình 5.