Hiển kết xuất chậm

Kết xuất giao diện người dùng là hành động tạo khung từ ứng dụng và hiển thị khung trên màn hình. Để đảm bảo quá trình tương tác giữa người dùng với ứng dụng diễn ra suôn sẻ, ứng dụng cần kết xuất các khung hình trong khoảng thời gian dưới 16 mili giây để đạt được tiêu chuẩn 60 khung hình/giây (vì sao lại là 60 khung hình/giây?). Nếu ứng dụng của bạn gặp lỗi trong khi hiển thị giao diện người dùng chậm, thì hệ thống buộc phải bỏ qua các khung hình và người dùng sẽ cảm nhận hiện tượng giật khung hình trong ứng dụng của bạn. Chúng tôi gọi đây là hiện tượng giật.

Để 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ị thông tin trong trang tổng quan Android vitals. Để biết thông tin về cách thu thập dữ liệu, vui lòng xem Tài liệu trên Play Console.

Nếu ứng dụng của bạn gặp phải tình trạng giật, trang này sẽ cung cấp hướng dẫn về cách chẩn đoán và khắc phục vấn đề.

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

Việc xác định mã trong ứng dụng của bạn đang gây ra hiện tượng giật có thể gặp khó khăn. 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 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 hơn nhưng nếu bạn đã chạy Systrace cho tất cả các trường hợp sử dụng trong ứng dụng của mình thì bạn 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à hệ thống đều phát hiện hiện tượng giật trên thiết bị cục bộ. Nếu không thể mô phỏng ngôn ngữ của bạn 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 gây hiện tượng giật. Để kiểm tra 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 và 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à 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ố 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, biểu thị nhanh bằng hình ảnh thời gian kết xuất khung hình của cửa sổ giao diện người dùng với tiêu 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ụ như nếu khung hình dành nhiều thời gian để xử lý đầu vào, bạn nên xem mã ứng dụng nào của mình sẽ xử lý hoạt động đầu vào của người dùng.
  • Có một số thành phần nhất định, chẳng hạn như RecyclerView, là một nguồn thông thường của hiện tượng giật. Nếu ứng dụng của bạn sử dụng các thành phần này, bạn nên chạy các phần đó của ứng dụng.
  • Đôi khi, hiện tượng giật chỉ có thể được sao chép khi ứng dụng được khởi chạy từ một khởi động nguội.
  • Hãy thử chạy ứng dụng của bạn trên một thiết bị có tốc độ chậm hơn để khắc phục sự cố.

Sau khi tìm thấy các trường hợp sử dụng tạo ra 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. Tuy nhiên, nếu cần thêm thông tin, bạn có thể sử dụng Systrace để biết thêm thông tin chi tiết.

Với Systrace

Mặc dù Systrace là công cụ cho thấy toàn bộ hoạt động của thiết bị, nhưng nó cũng có thể hữu ích cho việc xác định trạng thái giật trong ứng dụng của bạn. Systrace có hệ thống định mức tối thiểu, do đó, bạn sẽ 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 bị giật xảy ra trên thiết bị của bạn. Xem Hướng dẫn Systrace để biết hướng dẫn về cách sử dụng. Hệ điều hành được chia nhỏ theo quá trình và luồng. Tìm quy trình của ứng dụng trong Systrace. Quy trình này sẽ giống như hình 1.

Hình 1: systrace

Systrace trong hình 1 chứa các thông tin sau để xác định độ trễ:

  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. Điều này giúp bạn tìm thấy các khung hình riêng lẻ 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 vấn đề trong ứng dụng của bạn và hiển thị cảnh báo trong cả khung riêng lẻ và bảng cảnh báo. Tốt nhất là bạn nên làm theo hướng dẫn trong thông 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à 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ờ đã gây ra hiện tượng giật. Ví dụ như nếu tiến trình cho thấy khung hình chậm do RecyclerView kết xuất mất quá nhiều thời gian, bạn có thể thêm Điểm đánh dấu Truy cập vào mã có liên quan và chạy lại systrace để biết thêm thông tin. Trong systrace mới, dòng thời gian sẽ hiển thị 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 hiện 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, bạn cần phải sử dụng Trình phân tích CPU của Android để ghi lại theo dõi 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 độ giật vì chúng tạo ra độ 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 đó, hãy thêm các điểm đánh dấu Theo dõi và chạy lại systrace để xem các phương thức đó có gây ra tình trạng giật không.

Để biết thêm thông tin, vui lòng xem bài viết Tìm hiểu về tính năng Systrace.

Theo dõi hiệu suất tuỳ chỉnh

Nếu không thể mô phỏng hoạ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 của 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 Theo dõi hiệu suất của Firebase.

Để tìm hiểu thêm, vui lòng xem phần Sử dụng tính năng giám sát hiệu suất của Firebase với Android Vitals.

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

Để khắc phục sự cố giật khùng hình, hãy kiểm tra xem những khung hình nào chưa hoàn thành trong 16,7 mili giây và tìm xem liệu nó có thể là lỗi gì. Liệu Record View#draw có mất nhiều thời gian bất thường trong một số khung hình, hoặc có thể là Bố cục không? Hãy xem Các nguồn giật phố biến khác bên dưới để tìm hiểu về vấn đề này cũng như các vấn đề khác.

Để tránh hiện tượng giật, tác vụ chạy lâu phải được chạy không đồng bộ bên ngoài luồng giao diện người dùng. Luôn lưu ý đến chuỗi mã mà bạn đang chạy và thận trọng sử dụng khi đăng các thao tác 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 (có thể là danh sách cuộn trung tâm), hãy xem xét việc viết thử nghiệm đo lường có thể tự động phát hiện thời gian kết xuất chậm và chạy thường xuyên thử nghiệm để ngăn chặn sự cố tái diễn. Để biết thêm thông tin chi tiết, vui lòng xem Lớp học lập trình kiểm tra hiệu suất tự động.

Các nguồn giật thông thường

Các phần sau giải thích các nguồn giật khác nhau trong ứng dụng và các phương pháp hay nhất để giải quyết vấn đề.

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ể sử 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 nhớ truyền đối số dòng lệnh -a <your-package-name> để hiển thị các phần dấu vết trong RecyclerView (cũng như mọi điểm đánh dấu dấu vết 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 đó báo hiệu toàn bộ nội dung danh sách đã thay đổi và sẽ hiển thị ở Systrace dưới dạng RV FullInvalidate. Thay vào đó, hãy sử 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 đó lên Trình chuyển đổi, bạn có thể gọi notifyDataSetChanged() như dưới đây:

Kotlin

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

Java

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

Tuy nhiên, điều này đi kèm với một nhược điểm lớn: nếu đó là một thay đổi không quan trọng (có thể chỉ có một mục được thêm vào đầu), thì RecyclerView sẽ không nhận biết về điều này – nó được yêu cầu bỏ tất cả trạng thái của mục đã lưu vào bộ nhớ đệm, và do đó cần phải kết hợp lại mọi thứ.

Bạn nên sử dụng DiffUtil để có thể tính toán và gửi đ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);
}

Bạn chỉ cần xác định MyCallback dưới dạng DiffUtil.Callback để thông báo cho DiffUtil cách kiểm tra danh sách của bạn.

RecyclerView: Nested RecyclerViews

Thông thường, bạn nên lồng RecyclerView, đặc biệt là đối với một danh sách dọc của các danh sách cuộn theo chiều ngang (như 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 chế độ xem 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, bạn nên kiểm tra xem mình có đang chia sẻ RecyclerView.RecycledViewPool giữa các RecyclerView (ngang) bên trong hay không. Theo mặc định, mỗi RecyclerView sẽ có 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 tất cả các hàng đang hiển thị các loại 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 như bạn sẽ luôn hiển thị 3,5 mục trong một hàng, hãy gọi innerLLM.setInitialItemPrefetchCount(4);. Điều 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/Tạo quá lâu

Tính năng tìm nạp trước trong RecyclerView sẽ giúp giải quyết chi phí lạm phát trong hầu hết các trường hợp bằng cách thực hiện công việc trước thời hạn, 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 (chứ không phải trong phần được gắn 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ị gần đây (Tính năng Tìm nạp trước hiện chỉ được hỗ trợ trên Android 5.0 API cấp 21 trở lên) và sử dụng phiên bản mới nhất của Thư viện hỗ trợ.

Nếu bạn thường xuyên thấy lạm phát 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 bạn không có nhiều loại chế độ xem hơn mức bạn cần. Càng có ít loại chế độ xem trong nội dung của RecyclerView, thì càng cần ít lạm phát 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 loại khung hiển thị nếu phù hợp – trong trường hợp chỉ thay đổi loại 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 (đồng thời giảm mức sử dụng bộ nhớ của ứng dụng).

Nếu các loại chế độ xem của bạn có giao diện phù hợp, hãy xem xét giảm chi phí lạm phát của bạn. Việc giảm vùng chứa và Khung hiển thị có cấu trúc 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úp dễ dàng giảm các Khung hiển thị có cấu trúc. Nếu bạn muốn thực sự tối ưu hoá hiệu suất, đơn giản hoá hệ thống phân cấp các mục và bạn không cần các tính năng định kiểu và giao diện phức tạp, hãy cân nhắc việc gọi các hàm khởi tạo. Tuy nhiên, hãy lưu ý 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 (nghĩa là onBindViewHolder(VH, int)) sẽ rất đơn giản và chỉ mất chưa đến một mili giây cho tất cả các mục trừ những mục phức tạp nhất. Bạn chỉ cần lấy các mục POJO từ dữ liệu mục nội bộ của bộ chuyển đổi, sau đó gọi phương thức setter trên các chế độ xem trong ViewHolder. Nếu RVOnBindView mất nhiều thời gian, hãy xác minh bạn chỉ thực hiện ít thao tác nhất trong mã liên kết.

Nếu đang sử dụng các đối tượng POJO đơn giản để giữ dữ liệu trong trình chuyển đổi, bạn có thể hoàn toàn tránh 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/vẽ mất quá nhiều thời gian

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

ListView: Lạm phát

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 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 cao, ứng dụng của bạn sẽ không nhận được lợi ích về 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 bên dưới:

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 khúc 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 Chế độ xem 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 việc đo/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) về độ sâu của 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. Bạn có thể thực hiện việc này theo một số cách sau:

  • Bạn có thể sắp xếp lại chế độ xem cấu trúc.
  • Bạn có thể 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, một trong những công cụ 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 chế độ xem 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ể khiến khung hình bị sụt. Nhìn chung, ảnh động phải chạy trên các thuộc tính vẽ của View (ví dụ như setTranslationX/Y/Z(), setRotation(), setAlpha(), v.v.). Quyết định này có thể được thay đổi với ít chi phí 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ề). Thay đổi các thuộc tính vẽ của chế độ xem cũng có chi phí thấp hơn nhiều, thường 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 tiếp theo. Thao tác này sẽ ghi lại thao tác vẽ cho chế độ xem 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 hai giai đoạn – Record View#draw, trên luồng giao diện người dùng và DrawFrame trên RenderingThread. Giai đoạn đầu tiên chạy draw(Canvas) trên mọi View không hợp lệ, và có thể gọi các lệnh gọi vào chế độ xem tuỳ chỉnh hoặc vào mã. Giai đoạn thứ hai chạy trên RenderThread gốc, nhưng sẽ 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 sơ đồ bit đượ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 CPU để kết xuất, vì vậy, bạn nên tránh điều này nếu có thể. Bạn có thể sử 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 ứng dụng muốn trang trí bitmap trước khi hiển thị. Đôi khi một 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 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 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à loại 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ư thế này, thậm chí bạn có thể thực hiện tác vụ 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);
}

Hãy lưu ý hệ thống cũng có thể thực hiện việc này để bảo vệ nền (vẽ lại bằng cách thay đổi độ dốc ở trên cùng của Bitmap) và lọc hình ảnh (với ColorMatrixColorFilter), hai thao tác phổ biến khác đã thực hiện sửa đổi 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ẽ vào Canvas đã tăng tốc phần cứng được truyền trực tiếp đến Khung hiển thị hoặc Đối tượng có thể vẽ. Nếu cần, hãy xem xét việc gọi setLayerType() bằng LAYER_TYPE_HARDWARE để lưu trữ đầu ra kết xuất phức tạp vào bộ nhớ đệm, đồng thời vẫn tận dụng lợi thế của việc kết xuất bằng GPU.

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

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

Canvas.saveLayer()

Tránh dùng Canvas.saveLayer() – việc này có thể gây ra quá trình kết xuất từng khung hình tốn tài nguyên, không được lưu vào bộ nhớ đệm và không hiển thị trên màn hình. Mặc dù hiệu suất đã được cải thiện trong Android 6.0 (khi thực hiện tối ưu hoá để tránh chuyển đổi mục tiêu kết xuất trên GPU), bạn vẫn nên tránh API cần nhiều tài nguyên này nếu có thể, hoặc chí ít đảm bảo bạn truyền Canvas.CLIP_TO_LAYER_SAVE_FLAG (hoặc gọi một biến thể không bị gắn cờ).

Tạo hiệu ứ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 chuyển đến Chế độ xem, 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 từ khung này sang khung khác để chúng có thể được lưu vào bộ nhớ đệm và vẽ lại 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 bạn 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 đoạn video đắt đỏ và bạn nên 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. Nó hoạt động tốt hơn và hỗ trợ phương pháp khử răng cưa. Ví dụ như lệnh gọi clipPath sau:

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 đó, có thể biểu thị dưới dạng:

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 đượ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 kết cấu (id) chiều rộng x chiều cao. Quá trình này có thể mất vài mili giây (xem Hình 2), nhưng cần 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ố 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ẽ lãng phí thời gian và bộ nhớ. Nhìn chung, các thư viện tải Bitmap là cách đơn giản để 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 thiết. Theo đó, quá trình tải lên diễn ra sớm, trong khi tính năng Hiển thị sẽ 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 Chế độ xem, 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.

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 tập lệnh sớm khi được giải mã bằng prepareToDraw().

Tạm hoãn lên lịch trong luồng

Trình lập lịch biểu của luồng là một phần của hệ điều hành Android, chịu trách nhiệm quyết định luồng nào trong hệ thống sẽ chạy, khi nào và trong bao lâu. Đô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 sử dụng nhiều màu sắc (xem hình 3) để cho biết thời điểm một luồng Ngủ (xám), Có thể chạy (màu xanh dương: có thể chạy, nhưng bộ lập lịch chưa chọn chạy nội dung đó), Đang chạy (Xanh lục) hoặc trong chế độ Ngủ liên tục (Đỏ hoặc Cam). Điều này cực kỳ hữu ích để gỡ lỗi các sự cố giật do sự chậm trễ trong việc lập lịch chuỗi.

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 khoảng thời gian tạm dừng dài trong quá trình thực thi của ứng dụng là do các lệnh gọi liên kết, cơ chế giao tiếp giữa các quá trình (IPC) trên Android. Trên các phiên bản Android gần đây, đâ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 dùng các hàm gọi 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ể dễ dàng 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 bạn không cẩn thận – vấn đề này cũng dễ dàng nhận 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 các lệnh đó 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 cuộc gọi có vẻ vô hại như getRefreshRate() có thể kích hoạt giao dịch liên kết và gây ra các 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 tìm thấy và khắc phục các vấn đề này khi chúng xuất hiện.

Hình 4: 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. Duy trì logic liên kết đơn giản và sử 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 bạn không phải chờ một số 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 nên đăng thông tin vào đó.

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ấp 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 bạn tránh phân bổ khi có thể, đặc biệt là trong vòng lặp chặt chẽ, bạn sẽ không gặp vấn đề gì.

Hình 5: hiển thị 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 HeapTaskDaemon. Lưu ý là số lượng phân bổ đáng kể có thể đồng nghĩa với việc các tài nguyên CPU khác được chi cho GC như minh hoạ trong hình 5.