Hiển thị chậm

Hiển thị giao diện người dùng là hành động tạo khung từ ứng dụng của bạn và hiển thị khung trên màn hình. Để đảm bảo quá trình tương tác của người dùng với ứng dụng của bạn diễn ra suôn sẻ, ứng dụng của bạn phải hiển thị các khung hình có thời lượng dưới 16 mili giây để đạt được 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 chi tiết, 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 khiến việc phân tích trở nên khó khăn. 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 tùy 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 bằng hình ảnh, hãy mở ứng dụng của bạn và xem các phần khác nhau của ứng dụng theo cách thủ công và kiểm tra giao diện người dùng 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. Thời gian chạy ART tắt một số tối ưu hóa quan trọng để hỗ trợ các tính năng gỡ lỗi, vì vậy, hãy đảm bảo bạn đang xem nội dung tương tự như những gì người dùng sẽ 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 để bạn hiển thị nhanh thông tin về thời gian cần để hiển thị khung hình của cửa sổ giao diện người dùng so 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ụ 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 đó, bạn nên chạy qua 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 thực hiện trường hợp bị giật 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ã hóa 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 dòng thời gian cho thấy khung hình chậm do RecyclerView kết xuất trong một thời gian dài, 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 hiển thị cho bạn 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, thì 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 được ổn định cho việc xác định độ trễ vì chúng tạo ra độ trễ dương tính giả do chi phí vận hành cao, và chúng không thể biết khi nào các luồng đang chạy so với các luồng 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 tùy 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 tùy 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ớ chuyển đối số dòng lệnh -a <your-package-name> để nhận 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) hiển thị. 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 Bộ 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 là 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 cuộn dọc 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 giả 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. Trong trường hợp với hàng tá itemViews 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 chế độ xem 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 lạm phát trong một khung (chứ không phải trong phần được gắn nhãn Tìm nạp trước RV), hãy đảm bảo bạn đang thử nghiệm trên một thiết bị gần đây (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 gần đây 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 chế độ xem nếu hợp lý – nếu chỉ có một biểu tượng, màu sắc hoặc đoạn văn bản thay đổi giữa các loại, bạn có thể thực hiện thay đổi đó vào thời điểm liên kết và tránh lạm phát (đồng thời giảm dung lượ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à Chế độ xem cấu trúc không cần thiết có thể khá hữu ích – hãy cân nhắc việc tạo itemViews bằng ConstraintLayout, giúp dễ dàng giảm các Chế độ xem cấu trúc. Nếu bạn muốn thực sự tối ưu hóa hiệu suất, thì hệ thống phân cấp các mục của bạn rất đơn giản, 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 dựng – 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 bộ 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 hệ phân cấp Chế độ xem có tham số bố cục hoặc đầu vào thay đổi.

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 kém nhất choRelativeLayouts 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 tùy chỉnh. Xem hướng dẫn về tối ưu hóa bố cục của bạn để tham khảo các 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 đang xuất hiện trên mỗi 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 tùy 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 có thể sử dụng tính năng hiển thị CPU, vì vậy, bạn nên tránh điều này khi 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);
}

Vui lòng 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 được chuyển trực tiếp đến Chế độ xem hoặc Có thể vẽ, và nếu cần, hãy xem xét việc gọisetLayerType() với LAYER_TYPE_HARDWARE để lưu trữ kết quả kết xuất phức tạp trong bộ nhớ đệm, đồng thời vẫn tận dụng lợi thế của việc kết xuất 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 Canvas.saveLayer() – việc này có thể kích hoạt quá trình hiển thị tốn kém, không được lưu vào bộ nhớ đệm và hiển thị từng khung hình ngoài 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 hóa để tránh chuyển đổi mục tiêu hiển thị trên GPU), bạn vẫn nên tránh API này nếu có thể, hoặc tối thiểu, hãy đả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 để có thể lưu vào bộ nhớ đệm và vẽ một cách hiệu quả. drawPoints(), drawLines()drawRect/Circle/Oval/RoundRect() hiệu quả hơn – tốt hơn là bạn nên dùng ngay cả khi bạn sử dụng nhiều lệnh 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 kết cấu 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 khu vực mà màn hình đang 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 đang tự quản lý, hoặc muốn đảm bảo bạn không nhấn tải lên trên các thiết bị mới hơn, bạn có thể gọi prepareToDraw() bằng mã của riêng 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 kết nối 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 – nhưng cũng dễ dàng tìm và khắc phục chúng 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ư xóa 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 khóa 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 thập rác (GC) đã cải thiện ít vấn đề hơn đáng kể 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.