常見卡頓來源及其優(yōu)化方法

1. 可滾動(dòng)列表

1.1 RecyclerView: notifyDataSetChanged

非必要不要全量更新,可以考慮使用DiffUtil實(shí)現(xiàn)差量更新。

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需要實(shí)現(xiàn)DiffUtil.Callback

1.2 RecyclerView: 嵌套R(shí)ecyclerView

例如一個(gè)縱向的RecyclerView嵌套多個(gè)橫向的RecyclerView時(shí),如果這些被嵌套的RecyclerView的itemView都是相似的,
那么我們可以在這些被嵌套的RecyclerView之間共享RecyclerView.RecycledViewPool,
使得這些被嵌套的RecyclerView的itemView跨RecyclerView復(fù)用。

更進(jìn)一步優(yōu)化,你還可以對(duì)被嵌套的RecyclerViewLinearLayoutManager調(diào)用setInitialPrefetchItemCount(int)
來預(yù)加載itemView。

1.3 RecyclerView: Inflation太多

減少非必要的 view type(同一view type的itemView才能復(fù)用)

1.4 ListView: Inflation太多

getView里的convertView復(fù)用

1.5 RecyclerView 或者 ListView: layout / draw 太慢

見后文 2.2 Layout2.3 Rendering

2. Layout

2.1 Layout: 耗時(shí)長(zhǎng)

如果layout的耗時(shí)大于幾毫秒,你有可能碰到了嵌套RelativeLayout或者帶有weight的LinearLayout的最壞情況。

例如:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:tools="http://schemas.android.com/tools"
  android:layout_width="match_parent"
  android:layout_height="wrap_content"
  android:orientation="horizontal"
  tools:context=".jankoptim.ConstraintLayoutActivity">

  <LinearLayout
      android:layout_width="0dp"
      android:layout_height="100dp"
      android:layout_weight="1"
      android:orientation="horizontal">

      <com.example.playground.view.CustomView
          android:layout_width="20dp"
          android:layout_height="20dp"
          android:background="@color/colorAccent" />

      <View
          android:layout_width="30dp"
          android:layout_height="10dp"
          android:background="@color/colorPrimary" />

  </LinearLayout>

  <View
      android:layout_width="0dp"
      android:layout_height="10dp"
      android:layout_weight="1"
      android:background="@color/colorPrimaryDark" />

</LinearLayout>
第1層 第2層 第3層
View元素 LinearLayout LinearLayout CustomView
measure次數(shù) 1 1 x 2 = 2 1 x 2 x 2 = 4

可以看到,在這種情況下,每增加1層嵌套,CustomView的measure次數(shù)就多乘1次2,
也就是說,被嵌套的View的measure次數(shù)m隨著嵌套深度n的增長(zhǎng)呈指數(shù)級(jí)增長(zhǎng)(m=2^n)。

優(yōu)化要點(diǎn):

  • 盡量只在最低層級(jí)的葉子節(jié)點(diǎn)上使用RelativeLayout或者帶有weight的LinearLayout,也就是只有1層嵌套
  • 嘗試使用ConstraintLayout實(shí)現(xiàn)類似的效果

2.2 Layout: 過于頻繁

Layout應(yīng)當(dāng)在屏幕上顯示新的內(nèi)容時(shí)發(fā)生,比如當(dāng)RecyclerView的一個(gè)新的item滾動(dòng)進(jìn)入可見區(qū)域。
如果每一幀都發(fā)生大量的layout,那么有可能是你對(duì)layout做了動(dòng)畫處理。
一般來說,動(dòng)畫應(yīng)該運(yùn)行在view的drawing properties(例如setTranslationX/Y/Z(), setRotation(), setAlpha()等)上,
而不是改變起來代價(jià)更大的layout properties(例如padding, margin)。

3. Rendering

Android UI分兩步完成rendering:

  1. Record View#draw,在UI線程執(zhí)行,調(diào)用每個(gè)invalidated Viewdraw(Canvas)方法。
  2. DrawFrame,在RenderThread根據(jù)上一步的結(jié)果執(zhí)行。

3.1 Rendering: UI線程

3.1.1 bitmap

避免在UI線程繪制bitmap。

3.1.2 避免過度繪制

過度繪制就是在同一幀情況下對(duì)同一塊像素區(qū)域進(jìn)行重復(fù)繪制。這樣會(huì)加重GPU跟CPU的渲染壓力,導(dǎo)致渲染時(shí)間過長(zhǎng)。

優(yōu)化思路:

  • 移除(不可見的)window的背景

    <item name="android:windowBackground">@null</item>
    
  • 移除控件中不需要的背景

    • 對(duì)于子控件,如果其背景顏色跟父布局一致,那么就不用再給子控件添加背景了
    • 如果子控件背景五顏六色,且能夠完全覆蓋父布局,那么父布局就可以不用添加背景了
  • 減少透明度的使用

    比如:在TextView上設(shè)置帶透明度alpha值的黑色文本可以實(shí)現(xiàn)灰色的效果。但是,直接通過設(shè)置灰色的話能夠獲得更好的性能

  • 減少布局的嵌套層級(jí)

  • 使用merge標(biāo)簽減少布局層級(jí)

  • 使用ViewStub標(biāo)簽懶加載

  • 減少自定義View的過度繪制,使用clipRect()只繪制可見區(qū)域

3.2 Rendering: RenderThread

  • 有些Canvas操作雖然在第一步record時(shí)很廉價(jià),但會(huì)在第二步觸發(fā)昂貴但計(jì)算。
  • Bitmap是作為OpenGL texture顯示的,首次顯示時(shí),它會(huì)被上傳到GPU。因此,如果Bitmap明顯的大于顯示所需,就會(huì)浪費(fèi)上傳時(shí)間和內(nèi)存。

目前用的比較少,理解不深入,詳情請(qǐng)見官方說明

4. 頻繁GC

盡管在Android 5.0引入ART后此問題已經(jīng)大大緩解,但是我們還是需要注意,
避免在View#onDrawRecyclerView.Adapter#onBindViewHolder等這類會(huì)被連續(xù)反復(fù)調(diào)用的方法中new局部對(duì)象。
因?yàn)檫@會(huì)在短時(shí)間內(nèi)生成大量生命周期短暫的對(duì)象,導(dǎo)致頻繁GC,引發(fā)UI卡頓。

5. 耗時(shí)操作

使用AsyncTask、Thread、HandlerThreadThreadPoolExecutorIntentService等手段將耗時(shí)操作移出UI線程。

注意:如果你自己開啟線程,你應(yīng)該調(diào)用Process.setThreadPriority()
并傳入THREAD_PRIORITY_BACKGROUND
設(shè)置線程的priority為"background"。如果不這樣做,你開啟的線程仍然有可能拖慢你的app,因?yàn)槟J(rèn)情況下它與UI線程的優(yōu)先級(jí)相同。

詳情請(qǐng)見Thread priority

優(yōu)化思路:

  • 使用成員變量保存引用
  • 使用對(duì)象池進(jìn)行復(fù)用

6. 線程過多

顯然,這對(duì)于app的性能是有負(fù)面影響的。線程再多,CPU的資源是有限的,CPU在同一時(shí)間能夠運(yùn)行的線程數(shù)是不多的,
其他所有的線程都只能等待,同時(shí),每個(gè)線程至少需要占用64K的內(nèi)存。過多的線程只會(huì)帶來對(duì)內(nèi)存和CPU資源的激烈競(jìng)爭(zhēng)。

優(yōu)化思路:建立線程池統(tǒng)一管理

參考文檔

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容