轉譯速度緩慢

UI 轉譯是指從應用程式產生並在螢幕中顯示影格的動作。為了確保使用者與應用程式的互動順暢,應用程式的影格轉譯速度應在 16 毫秒以內,才能達到每秒 60 個影格 (為什麼是 60fps?)。如果應用程式使用緩慢的 UI 轉譯,系統會強制略過影格,使用者就會覺得應用程式有延遲的情況。這就是所謂的「資源浪費」

為協助改善應用程式品質,Android 會自動監控應用程式的資源浪費,並在 Android Vitals 資訊主頁中顯示資訊。若要瞭解收集資料的方式,請參閱 Play 管理中心文件

如果應用程式發生資源浪費的問題,本頁面會提供診斷及修正問題的說明。

辨識資源浪費

找出應用程式中造成資源浪費的程式碼並不容易。本節說明辨識資源浪費的三種方法:

「目視檢測」可讓您在幾分鐘內快速檢查應用程式的所有用途,但提供的細節不如 Systrace。「Systrace」提供更多詳細資訊,但如果是針對應用程式的所有用途執行 Systrace,就會發現有大量的資料難以分析。目視檢測和 Systrace 都可偵測本機裝置中的資源浪費。如果無法在本機裝置上重現資源浪費,您可以建構「自訂效能監控」,以在實際執行的裝置中評估應用程式的特定部分。

目視檢測

目視檢測可協助您找出產生資源浪費的用途。如要執行目視檢測,請開啟應用程式並手動瀏覽應用程式的不同部分,注意使 UI 是否有資源浪費的情形。以下是執行目視檢測的幾個秘訣:

  • 執行應用程式的發布版本 (或至少是不需偵錯的版本)。ART 執行階段會停用多項重要的最佳化功能以支援偵錯功能,因此請確保顯示的內容與使用者看到的內容類似。
  • 啟用剖析 GPU 轉譯。「剖析 GPU 轉譯」會在畫面中顯示長條,方便您快速查看轉譯 UI 視窗影格所需的時間 (以每影格 16 豪秒的基準做比較)。每個長條都有不同顏色的元件,可對應至轉譯管道中的某個階段,方便查看哪些部分的所需時間最長。舉例來說,如果影格需要很長時間才能處理輸入內容,您就應該查看處理使用者輸入內容的應用程式程式碼。
  • 有些元件 (例如 RecyclerView) 是資源浪費的常見來源。如果應用程式使用這些元件,建議您檢查應用程式的相應部分。
  • 有時候,只有使用冷啟動方式啟動應用程式時,才能重現資源浪費的問題。
  • 建議使用速度較慢的裝置執行應用程式,藉此找出問題所在。

發現有產生資源浪費的用途後,也許就可以知道在應用程式中產生資源浪費的原因。但如果需要更多資訊,可以使用 Systrace 進一步查看

Systrace

雖然 Systrace 是顯示完整裝置運作情形使用的工具,但這個工具也可以在應用程式中識別資源浪費的情形。Systrace 的系統成本最低,因此在檢測過程中可以協助您以最接近真實情況的方式找出資源浪費問題。

使用 Systrace 記錄追蹤記錄,同時在裝置中執行 jkyj 用途。如需 Systrace 的使用說明,請參閱 Systrace 逐步操作說明。Systrace 會依照程序和執行緒細分。在 Systrace 中尋找應用程式的程序,如圖 1 所示。

圖 1:Systrace

圖 1 的 Systrace 中包含以下資訊,可識別資源浪費:

  1. Systrace 會顯示每個影格繪製的時間,並為每個影格加上顏色,醒目顯示轉譯速度緩慢。如此一來,您就能更精準地找出個別資源浪費的影格,比目視檢查更準確。詳情請參閱「檢查 UI 頁框和快訊」。
  2. Systrace 可偵測應用程式中的問題,並在個別頁框和快訊面板中顯示「快訊」。建議您依照快訓中提供的指示進行操作。
  3. Android 架構和程式庫的某些部分 (例如 RecyclerView) 都包含追蹤標記。因此,「Systrace 時間軸」會顯示這些方法在 UI 執行緒上的執行,以及執行所需時間。

查看 Systrace 輸出內容後,就可以找出應用程式中是否有某些方法導致資源浪費。舉例來說,如果時間軸顯示緩慢影格式由於 RecyclerView 花費太多時間而導致,您就可以在相關的程式碼中加入追蹤記錄標記,然後重新執行 Systrace 以取得更多資訊。在新的 Systrace 中,時間軸會顯示應用程式呼叫方法的時間,以及執行所需時間。

如果 Systrace 沒有顯示 UI 執行緒花費過長時間的原因相關詳細資料,您就必須使用 Android CPU 分析器記錄取樣或檢測方法追蹤記錄。一般來說,方法追蹤記錄並不適合用於識別資源浪費,因為這會因產生由於高負載而導致的誤判資源浪費,也不會提供執行緒執行與封鎖時的比較。但方法追蹤記錄可協助您找出應用程式中花費最多時間的方法。找出這些方法後,加入 Trace 標記,然後重新執行 Systrace,看看這些方法是否導致資源浪費。

詳情請參閱「瞭解 Systrace」。

使用自訂效能監控

如果無法在本機裝置中重現資源浪費,您可以在應用程式中建構自訂效能監控,在實際使用的裝置中協助找出資源浪費的來源。

如要執行此操作,請使用 FrameMetricsAggregator 從應用程式的特定部分收集影格轉譯時間,然後使用 Firebase 效能監控記錄和分析資料。

詳情請參閱「搭配 Android Vitals 使用 Firebase 效能監控」。

修正資源浪費

如要修正資源浪費,請檢查哪些影格未能在 16.7 毫秒內完成,然後找出問題所在。「記錄 View#draw」是否在某些影格中,或者是「版面配置」中花費時間過長?請參閱下文的資源浪費的常見來源,以進一步瞭解此類問題。

為了避免資源浪費,長時間執行的工作必須在 UI 執行緒外以非同步方式執行。請隨時留意程式碼執行的執行緒位置,並注意在主要執行序張貼非一般工作。

如果應用程式有複雜且重要的主要 UI (也許是中央捲動清單),請考慮編寫檢測設備測試,這可自動偵測轉譯速度緩慢,並經常執行測試以避免迴歸。詳情請參閱自動化效能測試程式碼研究室

資源浪費的常見來源

以下各節說明應用程式中常見的資源浪費來源,以及解決這些錯誤的最佳做法。

可捲動的清單

ListView 以及尤其是 RecyclerView,通常用於容易發生資源浪費情況的複雜捲動清單。這兩者皆包含 Systrace 標記,因此您可以使用 Systrace 確認其是否造成應用程式中的資源浪費。請務必傳送指令列引數 -a <your-package-name>,以便在 RecyclerView 中顯示追蹤區段 (以及新增的任何追蹤標記)。如果適用,請按照 Systrace 輸出中產生的快訊指引進行操作。按一下 Systrace 中的 RecyclerView 追蹤區段,即可查看 RecyclerView 正在執行的工作說明。

RecyclerView:notifyDataSetChanged

如果可以在一個影格中查看 RecyclerView 中的每一個項目,請確保您沒有呼叫小規模更新的 notifyDataSetChanged()setAdapter(Adapter)swapAdapter(Adapter, boolean)。這些方法表示完整清單內容已經變更,並且會在 Systrace 中顯示為「RV FullInvalidate」。如果內容變更或新增,請改用 SortedListDiffUtil 以產生最少更新。

舉例來說,請設想某應用程式會接收伺服器中的最新消息內容清單。在 Adapter 中張貼該資訊時,可以呼叫 notifyDataSetChanged(),如下所示:

Kotlin

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

Java

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

但這有很大的缺點;如果是一般變更 (也許是在頂端加入單一項目),RecyclerView 卻不會發現,而是會收到指令,要求放棄所有快取項目狀態,因此必須重新繫結所有內容。

建議使用 DiffUtil,這可協助您計算並傳送少量的更新。

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);
}

只要將 MyCallback 定義為 DiffUtil.Callback 實作,即可通知 DiffUtil 檢查清單的方式。

RecyclerView:巢狀 Nestcycle

巢狀 RecyclerView 很常見,特別是在水平捲動清單中 (例如 Play 商店主要網頁中的應用程式格狀清單)。此功能的效果不錯,但移動的檢視畫面也很多。如果在第一次向下捲動頁面時發現許多內部項目加載,建議您先確認內部 (水平) RecyclerViews 之間共用 RecyclerView.RecycledViewPool。根據預設,每個 RecyclerView 都會有自己的項目集區。但是如果畫面中一次顯示十多個 itemViews,如果不同的水平清單中所有列都顯示畫面中的類似類型,但是又無法共用 itemViews,就會發生問題。

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);

    }
    ...

如果要進一步最佳化,也可以在內部 RecyclerView 的 LinearLayoutManager 上呼叫 setInitialPrefetchItemCount(int)。舉例來說,如果要總是在一列中顯示 3.5 個項目,請呼叫 innerLLM.setInitialItemPrefetchCount(4);。這會在水平列即將於畫面中顯示時向 RecyclerView 發出信號,如果 UI 執行緒中有剩餘的時間,就會嘗試預先擷取內部的項目。

RecyclerView:過多加載/建立需時過長

在大多數情況下,RecyclerView 的預先擷取功能應能在使用者介面執行緒處於閒置狀態時提前完成工作,以便處理加載。如果在影格處理期間 (且沒有在「RV 預先擷取」部分中) 看到加載,請確認您使用最新版裝置測試 (預先擷取功能目前僅支援 Android 5.0 API 級別 21 以上),並使用最新版本的支援資料庫

如果在畫面中顯示新項目時畫面中經常出現加載而導致資源浪費,請確認僅使用所需的檢視類型。RecyclerView 內容中的檢視畫面類型越少,新的項目類型在畫面中顯示所需的加載就越少。如果情況允許,請在合理的情況下合併可檢視畫面類型;如果只有一個圖示、顏色或一小段文字會在類型之間變更,您可以在繫結時進行更改,並且避免加載 (同時減少應用程式的記憶體足跡)。

如果檢視畫面類型看起來沒問題,就要尋找方式減少加載。減少不必要的容器和結構檢視也會有幫助,您可以考慮使用 ConstraintLayout 建構 itemViews,以簡化結構檢視。如果要「確實」最佳化效能,而項目階層簡單,而且不需要複雜的主題和樣式,建議您自行呼叫建構函式;但是請注意,一般來說,放棄 XML 的簡化和功能並不值得。

RecyclerView:繫結需時過長

繫結 (也就是 onBindViewHolder(VH, int)) 理應非常簡單,而且除了最複雜的項目之外,所有所需時間通常不到 1 毫秒。系統應該只會從轉接程式的內部項目資料中取用 POJO 項目,並於 ViewHolder 中的檢視呼叫 setter。如果 RV OnBindView 所需時間過長,請確認您在繫結程式碼中執行最少的工作。

如果您是使用簡易 POJO 物件保留轉接程式中的資料,只要使用資料繫結程式庫,即可完全避免在 onBindViewHolder 中寫入繫結程式碼。

RecyclerView 或 ListView:版面配置/繪圖需時過長

如有繪圖和版面配置相關問題,請參閱「版面配置」和「轉譯效能」章節。

ListView:加載

如果不小心,很容易就會在 ListView 中停用回收功能。如果每次項目出現在畫面中都顯示加載,請檢查 Adapter.getView() 的實作是否正在使用、重新繫結,並傳回 convertView 參數。如果 getView() 實作項目總是在加載,應用程式將無法使用 ListView 的回收功能。getView() 的結構應大致與下列實作類似:

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;
}

版面配置效能

如果 Systrace 顯示 Choreographicer#doFrameLayout 區隔執行太多工作,或者過於頻繁,就表示有版面配置效能問題。應用程式的版面配置效能取決於檢視區塊階層中變更版面配置參數或輸入內容的部分。

版面配置效能:成本

如果區隔超過數毫秒,就表示可能出現遇到 RelativeLayoutsweighted-LinearLayouts 最糟的巢狀效能問題。每個版面配置都可以觸發其子項的測量/版面配置傳遞,因此設定巢狀結構可能會導致巢狀結構出現 O(n^2) 行為。除了階層最低的分葉節點以外,請盡量避免使用 RLayoutativeLayout 或 LinearLayout 的權重功能。設定方式如下:

  • 重新整理結構檢視。
  • 定義自訂版面配置邏輯。如需具體範例,請參閱「最佳化版面配置」指南。
  • 您可以嘗試轉換為 ConstraintLayout,這可以提供類似的功能,不會有效能的缺點。

版面配置效能:頻率

當畫面中出現新內容 (例如:新項目在 RecyclerView 中捲動到檢視畫面中),就會有版面配置。如果每個影格都有大量的版面配置,就可能是您在設定動畫版面配置,而這很可能會導致捨棄影格。一般來說,動畫應在 View 的繪圖屬性上執行 (例如 setTranslationX/Y/Z()setRotation()setAlpha() 等)。和版面配置屬性 (例如邊框間距或邊界) 相比,這些屬性變更的成本要低許多。變更檢視畫面繪圖屬性的成本也更低,一般是透過呼叫可觸發 invalidate() 的 setter,然後在下一個影格中呼叫 draw(Canvas)。這會重新記錄無效檢視的繪圖作業,而且通常比版面配置的成本要低許多。

轉譯效能

Android UI 可分兩階段運作:在 UI 執行緒上的「記錄 View#draw」,以及在 RenderThread 上的「DrawFrame」。第一個會在每個無效的 View 上執行 draw(Canvas),並且可能在自訂檢視或程式碼中叫用呼叫。 第二個會在原生 RenderThread 上執行,但將根據「記錄 View#draw」階段產生的工作運作。

轉譯效能:UI 執行緒

如果「記錄 View#draw」需要很長的時間,通常是因為在 UI 執行緒上繪製點陣圖所致。繪製點陣圖會使用 CPU 轉譯,因此請盡量避免使用。您可以利用 Android CPU 分析器使用方法追蹤記錄,看看是否能解決問題。

通常當應用程式要先解碼點陣圖然後再顯示時,系統就會繪製點陣圖。有時也會加入圓角等裝飾:

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

如果這是您要在 UI 執行緒上執行的工作類型,您可以改為對背景中的解碼執行緒執行這項操作。在某些情況下,您甚至可以在繪圖時進行操作,因此如果您的 DrawableView 程式碼看起來像這樣:

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);
}

您可以將其換成:

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);
}

請注意,這通常也適用於背景保護 (在點陣圖頂端繪製漸層) 以及圖片篩選 (使用 ColorMatrixColorFilter),這是修改點陣圖的另外兩種常見操作。

如果基於另一種原因繪製點陣圖 (也許是做為快取使用),請嘗試將其繪製為直接傳遞至 View 或 Drawable 的硬體加速畫布。此外,如果有必要,您也可以考慮呼叫 setLayerType()LAYER_TYPE_HARDWARE 以快取複雜的轉譯輸出內容,同時仍能利用 GPU 轉譯。

轉譯效能:RenderThread

部分畫布作業記錄的成本很低,但會在 RenderThread 上觸發高成本的運算作業。Systrace 通常會透過快訊指出這些情況。

Canvas.saveLayer()

避免使用 Canvas.saveLayer() – 這可以為每個影格觸發成本高、無快取且在畫面外的轉譯。雖然 Android 6.0 的效能提升了 (已進行最佳化,避免在 GPU 上轉譯目標切換),但建議還是避免使用這種成本高的 API,或至少盡可能傳遞 Canvas.CLIP_TO_LAYER_SAVE_FLAG (或呼叫不接受標記的變化版本)。

大型路徑動畫設定

在傳遞至 View 的硬體加速畫布 Canvas 上呼叫 Canvas.drawPath() 時,Android 會先在 CPU 中繪製這些路徑,然後再將這些路徑上傳至 GPU。如果路徑較大,請避免逐個影格進行編輯,這樣才能以有效率的方式快取和繪製。drawPoints()drawLines()drawRect/Circle/Oval/RoundRect() 較有效率,即使最後使用更多繪製呼叫,也建議使用這些項目。

Canvas.clipPath

clipPath(Path) 會觸發成本高的裁剪,通常建議避免使用。請盡量選擇繪製圖形,而非裁剪為非矩形。這種方式的效能更好,而且支援消除鋸齒。舉例來說,下列 clipPath 呼叫:

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();

可改用以下格式:

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);

點陣圖上傳

Android 會將點陣圖顯示為 OpenGL 材質,且第一次在影格中顯示點陣圖時,就會上傳至 GPU。您可在 Systrace 中看到這是「Txture upload(id) width x height」。這可能需要數毫秒 (請見圖 2),但搭配 GPU 顯示圖片卻是必要的。

如果作業時間過長,請先檢查追蹤記錄中的寬度和高度數值。確保點陣圖的顯示範圍沒有遠遠小於顯示位置的面積。如果有,就會浪費上傳時間和記憶體。一般來說,點陣圖載入程式庫可為您提供輕鬆的方法,方便您要求取得合適大小的點陣圖。

在 Android 7.0 中,點陣圖載入程式碼 (通常由程式庫完成) 可以呼叫 prepareToDraw(),在需要前提早觸發上傳程序。這樣會提早上傳,而 RenderThread 則處於閒置狀態時。您可以在解碼後,或是在將點陣圖繫結至 View 後進行此操作。在理想情況下,點陣圖載入程式庫會自動完成此操作,但如果您是要自行管理,或是要確保不會在更新版裝置中上傳,您可以在自己的程式碼中呼叫 prepareToDraw()

圖 2:應用程式在上傳大型點陣圖的影格中花費大量時間。使用 prepareToDraw() 解碼前,請縮減尺寸或提早觸發。

執行緒排程延遲

執行緒排程器是 Android 作業系統中的一部分,負責決定系統要執行哪些執行緒、執行的時間及持續時間。有時候,如果應用程式的 UI 執行緒遭到封鎖或未執行,就會發生資源浪費。 Systrace 使用不同的顏色 (請參閱圖 3) 代表執行緒狀態,包括休眠 (灰色)、可執行 (藍色:可以執行,但排程器尚未執行)、正在執行 (綠色) 或不中斷休眠 (紅色或橘色)。這非常適合用於針對執行緒排程延遲造成的資源浪費問題進行偵錯。

圖 3:醒目顯示 UI 執行緒處於休眠狀態的時間。

一般而言,應用程式的執行長時間停止是繫結器呼叫所致,這是 Android 的處處理序間通訊 (IPC) 機制。在新版的 Android 中,這是 UI 執行緒停止執行最常見的原因之一。一般而言,解決方法是避免呼叫會做出繫結器呼叫的函式;如果無法避免,建議您快取此值,或將工作移至背景執行緒。隨著程式碼集變大很容易會不小心因叫用某些低層級方法而加入繫結器呼叫,但使用追蹤記錄,就可以輕鬆找到問題所在並加以修正。

如果有繫結器交易,您可以使用下列 adb 指令擷取其呼叫堆疊:

$ 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

有時候看起來無害的呼叫 (例如 getRefreshRate()),可能會觸發繫結器交易,而且如果經常呼叫,會導致發生嚴重問題。定期追蹤記錄有助於快速找出問題並加以修正。

圖 4:顯示 UI 執行緒在進行 RV 快速滑過時因繫結器交易而處於休眠狀態。簡化繫結邏輯,並使用 trace-ipc 追蹤及移除繫結器呼叫。

如果沒有看到繫結器活動,但 UI 執行緒仍沒有執行,請確認您等候的目標沒有鎖定,或是另一個執行續的其他作業。一般來說,UI 執行緒不須等候其他執行緒的結果;其他執行緒應在其中張貼資訊。

物件分配與垃圾收集

自從在 Android 5.0 的預設執行階段加入 ART 之後,物件分配和垃圾收集 (GC) 的問題就減少了許多,但仍然可能會因此額外的工作而拖慢執行緒的速度。您可以為回應一秒內不會發生太多次的少有事件 (例如使用者點擊按鈕) 進行分配,但是提醒您,每個分配都會有成本。如果是經常呼叫的密集迴圈,請考慮避免使用分配,藉以減輕 GC 的負載。

Systrace 會顯示 GC 是否經常執行,且 Android 記憶體分析器會顯示分配的來源。如果盡可能避免分配,特別是在密集的迴圈中,就應該不會有問題。

圖 5:顯示 HeapTaskDaemon 執行緒上的 GC 為 94 毫秒

在最新版的 Android 上,GC 通常會在名為 HeapTaskDaemon 的背景執行緒上執行。請注意,大量分配可能代表在 GC 上耗用的 CPU 資源更多,如圖 5 所示。