UI 轉譯是指從應用程式產生影格並在螢幕中顯示的行為。為了確保使用者與應用程式的互動順暢,應用程式的影格轉譯速度必須在 16 毫秒以內,才能達到每秒 60 個影格 (fps)。如要瞭解建議使用 60 fps 的原因,請觀看「Android 效能模式:為什麼要使用 60fps?」影片。如果要嘗試達到 90 fps,這個轉譯速度須提升至 11 毫秒,而要達到 120 fps 則須提升至 8 毫秒。
如果把這個時間降到 1 毫秒,並不表示該影格會在 1 毫秒後顯示,而是 Choreographer
會完全捨棄該影格。如果應用程式的 UI 顯示速度緩慢,系統會強制略過影格,使用者就會感覺到應用程式有延遲現象,這就是所謂的「卡頓」。本頁說明如何診斷及修正卡頓情形。
如果開發的遊戲並未使用 View
系統,就會略過 Choreographer
。在這種情況下,Frame Pacing 程式庫可協助 OpenGL 和 Vulkan 遊戲,在 Android 裝置上流暢地顯示畫面並修正影格同步情形。
為協助改善應用程式品質,Android 會自動監控應用程式中的卡頓情形,並在 Android Vitals 資訊主頁中顯示相關資訊。如要瞭解資料收集方式,請參閱「使用 Android Vitals 監控應用程式的技術品質」一文。
找出卡頓問題
要找出應用程式中造成卡頓的程式碼並不容易。本節說明三種找出卡頓的方法:
「目視檢測」方法可在幾分鐘內快速檢查應用程式的所有用途,但詳細程度比不上 Systrace。「Systrace」可提供更多詳細資訊,但如果是針對應用程式的所有用途執行 Systrace,就可能會取得難以分析的大量資料。目視檢測方法和 Systrace 都可偵測本機裝置中的卡頓情形。如果無法在本機裝置中重現卡頓,您可以建構「自訂效能監控」,在實際執行的裝置中評估應用程式的特定部分。
目視檢測
您可以使用目視檢測方法找出產生卡頓的用途。如要執行目視檢測,請開啟應用程式並手動瀏覽應用程式的不同部分,尋找是否有 UI 卡頓情形。
以下是執行目視檢測的秘訣:
- 執行應用程式的發布版本,或至少執行不可進行偵錯的版本。為了支援偵錯功能,ART 執行階段會停用多項重要的最佳化功能,因此請確保畫面上的內容與使用者看到的內容相似。
- 啟用「剖析 GPU 轉譯」功能。剖析 GPU 轉譯功能會在畫面中顯示長條,方便您查看轉譯 UI 視窗影格所需的時間,並與每影格 16 毫秒的基準進行比較。每個長條都有不同顏色的部分,對應各個轉譯管道階段,方便您查看轉譯時間最長的部分。舉例來說,如果影格需要很長時間才能處理輸入內容,請查看處理使用者輸入內容的應用程式程式碼。
- 執行所有經常造成卡頓的元件,例如
RecyclerView
。 - 使用冷啟動方式啟動應用程式。
- 在速度較慢的裝置上執行應用程式,讓卡頓情形更加明顯。
找出產生卡頓的用途後,也許就能知道應用程式產生卡頓的原因。您可以使用 Systrace 進一步探索更多資訊。
Systrace
雖然 Systrace 是用來顯示整個裝置運作情形的工具,但也可以用來找出應用程式中的卡頓情形。Systrace 耗用最少系統資源,因此在檢測過程中可以用最接近真實情況的方式找出卡頓問題。
在裝置上執行出現卡頓的用途時,可以使用 Systrace 記下追蹤記錄。如要瞭解 Systrace 的使用方式,請參閱「在指令列擷取系統追蹤記錄」一文。Systrace 會依照程序和執行緒分割資料。請在 Systrace 中尋找應用程式程序,如圖 1 所示。
圖 1 的 Systrace 範例包含以下資訊,可識別卡頓情形:
- Systrace 會顯示每個影格的繪製時間,並為每個影格加上顏色標記,醒目顯示轉譯速度緩慢的影格。如此一來,您就能找出產生卡頓的個別影格,比目視檢查更準確。詳情請參閱「檢查 UI 影格和快訊」。
- Systrace 可偵測應用程式中的問題,並在個別影格和「快訊」面板中顯示快訊。建議您按照快訊中的指示操作。
- Android 架構和程式庫中的
RecyclerView
等部分會包含追蹤記錄標記。因此,Systrace 時間軸會顯示這些方法在 UI 執行緒上執行的時間,以及執行所需時間。
查看 Systrace 輸出內容後,或許就能找出應用程式中產生卡頓的方法。舉例來說,如果時間軸顯示產生緩慢影格的原因是 RecyclerView
花費太多時間,您就可以在相關程式碼中加入自訂追蹤記錄事件,然後重新執行 Systrace 以取得更多資訊。在新的 Systrace 中,時間軸會顯示應用程式呼叫方法的時間,以及執行所需時間。
如果 Systrace 沒有顯示 UI 執行緒花費過長時間的原因相關詳細資料,請使用 Android CPU 分析器記錄取樣或檢測方法追蹤記錄。一般來說,方法追蹤記錄會在負荷量大時誤判卡頓,且無法判斷執行緒正在執行和遭到封鎖的時間,因此不適合用於識別卡頓。不過,方法追蹤記錄可協助您找出應用程式中耗時最長的方法。找出這些方法後,加入追蹤記錄標記,然後重新執行 Systrace,確認這些方法是否會產生卡頓。
詳情請參閱「瞭解 Systrace」一文。
自訂效能監控
如果無法在本機裝置中重現卡頓,您可以在應用程式中建構自訂效能監控機制,在實際使用的裝置中找出卡頓來源。
如要採用此方法,請使用 FrameMetricsAggregator
從應用程式的特定部分收集影格轉譯時間,然後使用 Firebase 效能監控機制記錄和分析資料。
詳情請參閱「Android 效能監控入門」一文。
凍結影格
凍結影格是指轉譯時間超過 700 毫秒的 UI 影格。這會造成問題,因為應用程式在顯示時似乎停滯不動,且在影格轉譯時幾乎有整整一秒不會回應使用者輸入內容。建議您對應用程式進行最佳化調整,讓單一影格的轉譯時間不超過 16 毫秒,確保 UI 的流暢度。不過,在應用程式啟動期間或轉換至其他畫面時,由於應用程式必須加載檢視畫面、安排畫面版面配置,並從頭開始執行初始繪圖作業,初始影格所需的繪圖時間通常都會超過 16 毫秒。為此,Android 會分別追蹤凍結影格和轉譯速度緩慢情形。應用程式中不應出現轉譯時間超過 700 毫秒的影格。
為協助提升應用程式品質,Android 會自動監控應用程式的凍結影格,並在 Android Vitals 資訊主頁中顯示相關資訊。如要瞭解資料收集方式,請參閱「使用 Android Vitals 監控應用程式的技術品質」一文。
凍結影格指的是轉譯速度緩慢最嚴重的情況,因此診斷與修正問題的程序相同。
追蹤卡頓
FrameTimeline Perfetto 可協助使用者追蹤速度緩慢或 凍結影格
緩慢影格、凍結影格與 ANR 之間的關係
緩慢影格、凍結影格和 ANR 都是應用程式可能會遇到的各種卡頓形式。請參閱下表瞭解差異。
緩慢影格 | 凍結影格 | ANR | |
---|---|---|---|
轉譯時間 | 介於 16 和 700 毫秒之間 | 介於 700 毫秒和 5 秒之間 | 大於 5 秒 |
可見的使用者影響區域 |
|
|
|
分別追蹤緩慢影格和凍結影格
在應用程式啟動期間或轉換至其他畫面時,由於應用程式必須加載檢視畫面、安排畫面版面配置,並從頭開始執行初始繪圖作業,初始影格所需的繪圖時間通常都會超過 16 毫秒。
確定優先順序並解決卡頓的最佳做法
在應用程式中嘗試解決卡頓時,請牢記下列最佳做法:
- 找出並解決最容易重現的卡頓。
- 優先處理 ANR。緩慢影格或凍結影格可能導致應用程式延遲,但 ANR 會導致應用程式停止回應。
- 轉譯速度緩慢很難重現,但您可以先解決 700 毫秒的凍結影格問題。應用程式啟動或變更畫面時,這種情況最常見。
修正卡頓
如要修正卡頓,請檢查哪些影格未能在 16 毫秒內完成轉譯,然後找出問題所在。檢查是否 Record View#draw
或 Layout
在某些影格上耗時過長。請參閱「卡頓的常見來源」一節,進一步瞭解這類問題和其他情況。
為避免產生卡頓,請在 UI 執行緒之外,以非同步方式執行長時間執行的工作。 請隨時注意執行程式碼時採用的執行緒,並在將重要工作發布至主執行緒時特別留意。
如果應用程式有複雜且重要的主要 UI (例如中央捲動清單),建議您編寫檢測設備測試,這可自動偵測轉譯速度緩慢的情形,並經常執行測試來避免迴歸問題。
卡頓的常見來源
以下各節說明使用 View
系統的應用程式中常見的卡頓來源,以及解決這些問題的最佳做法。如要瞭解如何修正 Jetpack Compose 的效能問題,請參閱「Jetpack Compose 效能」一文。
可捲動的清單
最容易產生卡頓的複雜捲動清單通常會使用 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」。如果已變更或新增內容,請改用 SortedList
或 DiffUtil
產生最少量的更新內容。
舉例來說,如果應用程式會從伺服器接收新版本的消息內容清單,您將這項資訊發布至轉接程式時,該程式可能會呼叫 notifyDataSetChanged()
,如下所示:
Kotlin
fun onNewDataArrived(news: List<News>) { myAdapter.news = news myAdapter.notifyDataSetChanged() }
Java
void onNewDataArrived(List<News> news) { myAdapter.setNews(news); myAdapter.notifyDataSetChanged(); }
這樣做的缺點是,如果是簡單的變動 (例如在頂端加入單一項目),RecyclerView
就不會察覺。因此,系統會指示 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); }
如要通知 DiffUtil
如何檢查清單,請將 MyCallback
定義為 Callback
實作。
RecyclerView:巢狀 RecyclerView
應用程式常會建立多個 RecyclerView
例項的巢狀結構,特別是在多個水平捲動清單中有單一垂直清單的情況。Play 商店主頁面的應用程式格線就是一例。這種做法效果不錯,但需要移動大量檢視畫面。
如果在首次向下捲動頁面時發現許多內部項目正在加載,建議先檢查是否在多個內部 (水平) RecyclerView
例項之間共用 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:加載過多或 Create 耗時過長
在大多數情況下,RecyclerView
的預先擷取功能可在 UI 執行緒處於閒置狀態時提前完成工作,解決加載耗用資源的問題。如果在影格處理期間 (而不是在「RV Prefetch」區段中) 看到加載,請確認您使用支援的裝置測試,並使用最新版本的支援資料庫。預先擷取功能僅適用於 Android 5.0 API 級別 21 以上版本。
如果畫面顯示新項目時經常因加載作業而產生卡頓,請確認僅使用所需的檢視畫面類型。RecyclerView
內容中的檢視畫面類型越少,畫面顯示新項目類型所需的加載作業就越少。在合理情況下,請盡可能合併檢視畫面類型。如果在類型之間只變更一個圖示、顏色或一小段文字,您可以在繫結時更改,並且避免加載,這可以減少應用程式的記憶體用量。
如果檢視畫面類型沒問題,就要設法降低加載作業成本。您可以減少不必要的容器和結構檢視畫面,並考慮使用 ConstraintLayout
建構 itemViews
,這有助於減少結構檢視畫面。
如果想要進一步提升效能,而且項目階層相當簡單,也不需要複雜的主題設定和樣式功能,不妨自行呼叫建構函式。但是,捨棄 XML 的簡化效果和功能通常並不值得。
RecyclerView:繫結耗時過長
繫結 (也就是 onBindViewHolder(VH,
int)
) 必須非常簡單,除了最複雜的項目之外,所需時間通常都不到一毫秒。繫結必須只從轉接程式的內部項目資料中取用一般 Java 物件 (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 顯示 Choreographer#doFrame
的 Layout 區段正在執行太多工作,或者執行工作的頻率過高,就表示出現版面配置效能問題。應用程式的版面配置效能,取決於檢視區塊階層的哪個部分出現版面配置參數或輸入內容異動。
版面配置效能:成本
如果區段所需時間超過數毫秒,就表示 RelativeLayouts
或 weighted-LinearLayouts
可能出現最糟的巢狀結構效能問題。由於這類版面配置會觸發子項的多項測量/版面配置傳遞作業,如果為這類版面配置建立巢狀結構,可能會導致巢狀深度出現 O(n^2)
行為。
除了階層最低的分葉節點以外,請盡量避免使用 RelativeLayout
或 LinearLayout
權重功能。您可以透過下列方式進行:
- 重新整理結構檢視畫面。
- 定義自訂版面配置邏輯。如需具體範例,請參閱「最佳化版面配置階層」一文。您可以嘗試轉換為
ConstraintLayout
,這可以提供類似的功能,但不會有效能上的缺點。
版面配置效能:頻率
畫面顯示新內容時,例如新項目捲動至 RecyclerView
中的檢視畫面,就會出現版面配置。如果每個影格都有大量版面配置,就可能是在建立版面配置動畫,而這很可能導致掉格。
一般來說,動畫必須在 View
的繪圖屬性上執行,例如下列屬性:
比起邊框間距、邊界等版面配置屬性,變更這些屬性的成本要低許多。一般來說,變更檢視畫面繪圖屬性的成本也更低,方法是呼叫可觸發 invalidate()
的 setter,然後在下一個影格中呼叫 draw(Canvas)
。這會重新記錄無效檢視畫面的繪圖作業,通常也會比版面配置的成本低許多。
轉譯效能
Android UI 分兩個階段運作:
- 在 UI 執行緒上記錄 View#draw,這個階段會在每個無效檢視畫面上執行
draw(Canvas)
,並可能會在自訂檢視畫面或程式碼中叫用呼叫。 - 在
RenderThread
上執行 DrawFrame,這會在原生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 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.
如果這是您要在 UI 執行緒上執行的工作類型,您可以改為在背景的解碼執行緒中執行這項作業。在某些情況下 (例如前述範例),甚至可以在繪圖的同時執行這項工作。因此,如果您的 Drawable
或 View
程式碼類似如下:
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
的硬體加速 Canvas
。如有必要,也可以考慮透過 LAYER_TYPE_HARDWARE
呼叫 setLayerType()
,快取複雜的轉譯輸出內容,同時仍能利用 GPU 轉譯。
轉譯效能:RenderThread
部分 Canvas
作業的記錄成本很低,但會在 RenderThread
上會觸發高成本的運算作業。Systrace 一般會在快訊中指出這些情況。
建立大型路徑動畫
在傳遞至 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 中會顯示為「Texture upload(id) width x height」。這項作業可能需要數毫秒 (如圖 2 所示),但系統必須使用 GPU 顯示圖片。
如果作業時間過長,請先檢查追蹤記錄中的寬度和高度數值。請確保要顯示的點陣圖沒有明顯大於畫面上的顯示區域。如果明顯較大,代表會浪費上傳時間和記憶體。一般來說,點陣圖載入程式庫可讓您要求大小合適的點陣圖。
在 Android 7.0 中,點陣圖載入程式碼作業 (一般由程式庫執行) 可呼叫 prepareToDraw()
,在需要點陣圖前提早觸發上傳程序。這樣一來,上傳作業會在 RenderThread
處於閒置狀態時提早進行。只要知道點陣圖為何,這項作業的執行時間就可以設在解碼後,或點陣圖繫結至檢視畫面時。在理想情況下,點陣圖載入程式庫會自動完成此作業,但如果您要自行管理,或確保不會在較新型裝置中進行上傳作業,可以在自己的程式碼中呼叫 prepareToDraw()
。
執行緒排程延遲
執行緒排程器是 Android 作業系統的一部分,負責決定必須執行哪些系統中的執行緒,以及執行的時機和時間長度。
有時候,如果應用程式的 UI 執行緒遭到封鎖或未執行,就會出現卡頓。Systrace 使用不同的顏色 (如圖 3 所示) 代表執行緒狀態,包括「休眠」(灰色)、「可執行」(藍色:可以執行,但排程器尚未指定執行)、「正在執行」(綠色) 或「不中斷休眠」(紅色或橘色)。對執行緒排程延遲所造成的卡頓問題偵錯時,這非常實用。
在應用程式執行作業中造成長時間停頓的通常是繫結器呼叫,這是 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()
等看似無害的呼叫可能會觸發繫結器交易,如果經常呼叫,還會造成嚴重問題。定期追蹤記錄有助於找出並修正問題。
如果沒有看到繫結器活動,但 UI 執行緒仍沒有執行,請確認您等候的目標沒有鎖定,或其實是其他執行緒上的作業。一般來說,UI 執行緒不須等候其他執行緒的結果,其他執行緒必須將資訊發布至 UI 執行緒。
物件配置與垃圾收集
自從 Android 5.0 將 Android 執行階段導入為預設執行階段後,物件配置和垃圾收集 (GC) 問題造成的影響就大幅降低,但這類額外工作仍可能拖慢執行緒的速度。您可以針對一秒內發生次數不多的少有事件進行配置,例如使用者輕觸按鈕的事件,但請注意每項配置作業都會增加成本。如果配置作業位於經常呼叫的緊密迴圈,請考慮避免進行配置,減輕 GC 的負載。
Systrace 會顯示 GC 是否經常執行,Android 記憶體分析器則會顯示配置作業的來源。如果盡可能避免進行配置 (特別是在緊密迴圈中),就不太可能會出現問題。
在較新版本的 Android 上,GC 通常會在名為 HeapTaskDaemon 的背景執行緒上執行。大量配置作業可能代表 GC 耗用更多 CPU 資源,如圖 5 所示。
為您推薦
- 注意:系統會在 JavaScript 關閉時顯示連結文字
- 為應用程式進行基準測試
- 評估應用程式效能總覽
- 讓應用程式更臻完善的最佳做法