【Android】項(xiàng)目維護(hù)幾年了汽绢,為啥還這么卡?

淺談

前段時(shí)間有個(gè)客戶問(wèn)我侧戴,為啥你們項(xiàng)目都搞了好幾年了宁昭,為啥線上還會(huì)經(jīng)常反饋卡頓,呃呃呃酗宋。积仗。

于是根據(jù)自己的理解以及網(wǎng)上大佬們的思路總結(jié)了一篇關(guān)于卡頓優(yōu)化這塊的文章。

卡頓問(wèn)題是一個(gè)老生常談的話題了蜕猫,一個(gè)App的好壞寂曹,卡頓也許會(huì)占一半,它直接決定了用戶的留存問(wèn)題丹锹,各大app排行版上稀颁,那些知名度較高芬失,但是排行較低的楣黍,可能就要思考思考是不是和你app本身有關(guān)系了。

卡頓一直是性能優(yōu)化中相對(duì)重要的一個(gè)點(diǎn)棱烂,因?yàn)槠渖婕傲?strong>UI繪制租漂、垃圾回收(GC)、線程調(diào)度以及Binder颊糜,CPU哩治,GPU方面等JVM以及FrameWork相關(guān)知識(shí)

如果能做好卡頓優(yōu)化,那么也就間接證明你對(duì)Android FrameWork的理解之深衬鱼。

下面兩篇是筆者之前總結(jié)的兩篇關(guān)于啟動(dòng)優(yōu)化和內(nèi)存優(yōu)化的文章

Android 性能優(yōu)化(一): 啟動(dòng)優(yōu)化理論與實(shí)踐

Android性能優(yōu)化(二):內(nèi)存優(yōu)化你一定要了解的知識(shí)點(diǎn)

下面我們就來(lái)講解下卡頓方面的知識(shí)业筏。

什么是卡頓:

對(duì)用戶來(lái)講就是界面不流暢,滯頓鸟赫。 場(chǎng)景如下

  • 1.視頻加載慢蒜胖,畫(huà)面卡頓消别,卡死,黑屏
  • 2.聲音卡頓台谢,音畫(huà)不同步寻狂。
  • 3.動(dòng)畫(huà)幀卡頓,交互響應(yīng)慢
  • 4.滑動(dòng)不跟手朋沮,列表自動(dòng)更新蛇券,滾動(dòng)不流暢
  • 5.網(wǎng)絡(luò)響應(yīng)慢,數(shù)據(jù)和畫(huà)面展示慢樊拓、
  • 6.過(guò)渡動(dòng)畫(huà)生硬纠亚。
  • 7.界面不可交互,卡死筋夏,等等現(xiàn)象菜枷。

卡頓是如何發(fā)生的

卡頓產(chǎn)生的原因一般都比較復(fù)雜,如CPU內(nèi)存大小叁丧,IO操作啤誊,鎖操作,低效的算法等都會(huì)引起卡頓拥娄。

站在開(kāi)發(fā)的角度看: 通常我們講蚊锹,屏幕刷新率是60fps,需要在16ms內(nèi)完成所有的工作才不會(huì)造成卡頓稚瘾。

為什么是16ms牡昆,不是17,18呢摊欠?

下面我們先來(lái)理清在UI繪制中的幾個(gè)概念:

SurfaceFlinger:

SurfaceFlinger作用是接受多個(gè)來(lái)源的圖形顯示數(shù)據(jù)Surface丢烘,合成后發(fā)送到顯示設(shè)備,比如我們的主界面中:可能會(huì)有statusBar,側(cè)滑菜單些椒,主界面播瞳,這些View都是獨(dú)立Surface渲染和更新,最后提交給SF后免糕,SF根據(jù)Zorder赢乓,透明度,大小石窑,位置等參數(shù)牌芋,合成為一個(gè)數(shù)據(jù)buffer,傳遞HWComposer或者OpenGL處理松逊,最終給顯示器躺屁。

在顯示過(guò)程中使用到了bufferqueue,surfaceflinger作為consumer方经宏,比如windowmanager管理的surface作為生產(chǎn)方產(chǎn)生頁(yè)面犀暑,交由surfaceflinger進(jìn)行合成熄捍。

VSYNC

Android系統(tǒng)每隔16ms發(fā)出VSYNC信號(hào),觸發(fā)對(duì)UI進(jìn)行渲染母怜,VSYNC是一種在PC上很早就有應(yīng)用余耽,可以理解為一種定時(shí)中斷技術(shù)。

tearing 問(wèn)題:

早期的 Android 是沒(méi)有 vsync 機(jī)制的苹熏,CPU 和 GPU 的配合也比較混亂碟贾,這也造成著名的 tearing 問(wèn)題,即 CPU/GPU 直接更新正在顯示的屏幕 buffer 造成畫(huà)面撕裂轨域。 后續(xù) Android 引入了雙緩沖機(jī)制袱耽,但是 buffer 的切換也需要一個(gè)比較合適的時(shí)機(jī),也就是屏幕掃描完上一幀后的時(shí)機(jī)干发,這也就是引入 vsync 的原因朱巨。

早先一般的屏幕刷新率是 60fps,所以每個(gè) vsync 信號(hào)的間隔也是 16ms枉长,不過(guò)隨著技術(shù)的更迭以及廠商對(duì)于流暢性的追求冀续,越來(lái)越多 90fps 和 120fps 的手機(jī)面世,相對(duì)應(yīng)的間隔也就變成了 11ms 和 8ms必峰。

VSYNC信號(hào)種類(lèi):

  • 1.屏幕產(chǎn)生的硬件VSYNC:硬件VSYNC是一種脈沖信號(hào)洪唐,起到開(kāi)關(guān)和觸發(fā)某種操作的作用。
  • 2.由SurfaceFlinger將其轉(zhuǎn)成的軟件VSYNC信號(hào)吼蚁,經(jīng)由Binder傳遞給Choreographer

Choreographer:

編舞者凭需,用于注冊(cè)VSYNC信號(hào)并接收VSYNC信號(hào)回調(diào),當(dāng)內(nèi)部接收到這個(gè)信號(hào)時(shí)最終會(huì)調(diào)用到doFrame進(jìn)行幀的繪制操作肝匆。

Choreographer在系統(tǒng)中流程

如何通過(guò)Choreographer計(jì)算掉幀情況:原理就是:

通過(guò)給Choreographer設(shè)置FrameCallback粒蜈,在每次繪制前后看時(shí)間差是16.6ms的多少倍,即為前后掉幀率旗国。

使用方式如下:

//Application.java
public void onCreate() {
     super.onCreate();
     //在Application中使用postFrameCallback
     Choreographer.getInstance().postFrameCallback(new FPSFrameCallback(System.nanoTime()));
}
public class FPSFrameCallback implements Choreographer.FrameCallback {

  private static final String TAG = "FPS_TEST";
  private long mLastFrameTimeNanos = 0;
  private long mFrameIntervalNanos;

  public FPSFrameCallback(long lastFrameTimeNanos) {
      mLastFrameTimeNanos = lastFrameTimeNanos;
      mFrameIntervalNanos = (long)(1000000000 / 60.0);
  }

  @Override
  public void doFrame(long frameTimeNanos) {

      //初始化時(shí)間
      if (mLastFrameTimeNanos == 0) {
          mLastFrameTimeNanos = frameTimeNanos;
      }
      final long jitterNanos = frameTimeNanos - mLastFrameTimeNanos;
      if (jitterNanos >= mFrameIntervalNanos) {
          final long skippedFrames = jitterNanos / mFrameIntervalNanos;
          if(skippedFrames>30){
            //丟幀30以上打印日志
              Log.i(TAG, "Skipped " + skippedFrames + " frames!  "
                      + "The application may be doing too much work on its main thread.");
          }
      }
      mLastFrameTimeNanos=frameTimeNanos;
      //注冊(cè)下一幀回調(diào)
      Choreographer.getInstance().postFrameCallback(this);
  }
}

UI繪制全路徑分析:

有了前面幾個(gè)概念枯怖,這里我們讓SurfaceFlinger結(jié)合View的繪制流程用一張圖來(lái)表達(dá)整個(gè)繪制流程:

  • 生產(chǎn)者:APP方構(gòu)建Surface的過(guò)程。
  • 消費(fèi)者:SurfaceFlinger

UI繪制全路徑分析卡頓原因:

接下來(lái)粗仓,我們逐個(gè)分析嫁怀,看看都會(huì)有哪些原因可能造成卡頓:

1.渲染流程

  • 1.Vsync 調(diào)度:這個(gè)是起始點(diǎn),但是調(diào)度的過(guò)程會(huì)經(jīng)過(guò)線程切換以及一些委派的邏輯借浊,有可能造成卡頓,但是一般可能性比較小萝招,我們也基本無(wú)法介入蚂斤;

  • 2.消息調(diào)度:主要是 doframe Message 的調(diào)度,這就是一個(gè)普通的 Handler 調(diào)度槐沼,如果這個(gè)調(diào)度被其他的 Message 阻塞產(chǎn)生了時(shí)延曙蒸,會(huì)直接導(dǎo)致后續(xù)的所有流程不會(huì)被觸發(fā)

  • 3.input 處理:input 是一次 Vsync 調(diào)度最先執(zhí)行的邏輯捌治,主要處理 input 事件。如果有大量的事件堆積或者在事件分發(fā)邏輯中加入大量耗時(shí)業(yè)務(wù)邏輯纽窟,會(huì)造成當(dāng)前幀的時(shí)長(zhǎng)被拉大肖油,造成卡頓,可以嘗試通過(guò)事件采樣的方案臂港,減少 event 的處理

  • 4.動(dòng)畫(huà)處理:主要是 animator 動(dòng)畫(huà)的更新森枪,同理,動(dòng)畫(huà)數(shù)量過(guò)多审孽,或者動(dòng)畫(huà)的更新中有比較耗時(shí)的邏輯县袱,也會(huì)造成當(dāng)前幀的渲染卡頓。對(duì)動(dòng)畫(huà)的降幀和降復(fù)雜度其實(shí)解決的就是這個(gè)問(wèn)題佑力;

  • 5.view 處理:主要是接下來(lái)的三大流程式散,過(guò)度繪制、頻繁刷新打颤、復(fù)雜的視圖效果都是此處造成卡頓的主要原因暴拄。比如我們平時(shí)所說(shuō)的降低頁(yè)面層級(jí),主要解決的就是這個(gè)問(wèn)題编饺;

  • 6.measure/layout/draw:view 渲染的三大流程揍移,因?yàn)樯婕暗奖闅v和高頻執(zhí)行,所以這里涉及到的耗時(shí)問(wèn)題均會(huì)被放大反肋,比如我們會(huì)降不能在 draw 里面調(diào)用耗時(shí)函數(shù)那伐,不能 new 對(duì)象等等;

  • 7.DisplayList 的更新:這里主要是 canvas 和 displaylist 的映射石蔗,一般不會(huì)存在卡頓問(wèn)題罕邀,反而可能存在映射失敗導(dǎo)致的顯示問(wèn)題;

  • 8.OpenGL 指令轉(zhuǎn)換:這里主要是將 canvas 的命令轉(zhuǎn)換為 OpenGL 的指令养距,一般不存在問(wèn)題

  • 9.buffer 交換:這里主要指 OpenGL 指令集交換給 GPU诉探,這個(gè)一般和指令的復(fù)雜度有關(guān)

  • 10.GPU 處理:顧名思義,這里是 GPU 對(duì)數(shù)據(jù)的處理棍厌,耗時(shí)主要和任務(wù)量和紋理復(fù)雜度有關(guān)肾胯。這也就是我們降低 GPU 負(fù)載有助于降低卡頓的原因;

  • 11.layer 合成:Android P 修改了 Layer 的計(jì)算方法 , 把這部分放到了 SurfaceFlinger 主線程去執(zhí)行, 如果后臺(tái) Layer 過(guò)多, 就會(huì)導(dǎo)致 SurfaceFlinger 在執(zhí)行 rebuildLayerStacks 的時(shí)候耗時(shí) , 導(dǎo)致 SurfaceFlinger 主線程執(zhí)行時(shí)間過(guò)長(zhǎng)耘纱。 可以選擇降低Surface層級(jí)來(lái)優(yōu)化卡頓敬肚。

  • 12.光柵化/Display:這里暫時(shí)忽略,底層系統(tǒng)行為束析; Buffer 切換:主要是屏幕的顯示艳馒,這里 buffer 的數(shù)量也會(huì)影響幀的整體延遲,不過(guò)是系統(tǒng)行為,不能干預(yù)弄慰。

2.系統(tǒng)負(fù)載

  • 內(nèi)存:內(nèi)存的吃緊會(huì)直接導(dǎo)致 GC 的增加甚至 ANR第美,是造成卡頓的一個(gè)不可忽視的因素;
  • CPU:CPU 對(duì)卡頓的影響主要在于線程調(diào)度慢陆爽、任務(wù)執(zhí)行的慢和資源競(jìng)爭(zhēng)什往,比如
    • 1.降頻會(huì)直接導(dǎo)致應(yīng)用卡頓

    • 2.后臺(tái)活動(dòng)進(jìn)程太多導(dǎo)致系統(tǒng)繁忙慌闭,cpu \ io \ memory 等資源都會(huì)被占用, 這時(shí)候很容易出現(xiàn)卡頓問(wèn)題 别威,這種情況比較常見(jiàn),可以使用dumpsys cpuinfo查看當(dāng)前設(shè)備的cpu使用情況:

    • 3.主線程調(diào)度不到 , 處于 Runnable 狀態(tài),這種情況比較少見(jiàn)

    • 4.System 鎖:system_server 的 AMS 鎖和 WMS 鎖 , 在系統(tǒng)異常的情況下 , 會(huì)變得非常嚴(yán)重 , 如下圖所示 , 許多系統(tǒng)的關(guān)鍵任務(wù)都被阻塞 , 等待鎖的釋放 , 這時(shí)候如果有 App 發(fā)來(lái)的 Binder 請(qǐng)求帶鎖 , 那么也會(huì)進(jìn)入等待狀態(tài) , 這時(shí)候 App 就會(huì)產(chǎn)生性能問(wèn)題 ; 如果此時(shí)做 Window 動(dòng)畫(huà) , 那么 system_server 的這些鎖也會(huì)導(dǎo)致窗口動(dòng)畫(huà)卡頓

  • GPU:GPU 的影響見(jiàn)渲染流程贡必,但是其實(shí)還會(huì)間接影響到功耗和發(fā)熱兔港;
  • 功耗/發(fā)熱:功耗和發(fā)熱一般是不分家的,高功耗會(huì)引起高發(fā)熱仔拟,進(jìn)而會(huì)引起系統(tǒng)保護(hù)衫樊,比如降頻、熱緩解等利花,間接的導(dǎo)致卡頓科侈。

如何監(jiān)控卡頓

線下監(jiān)控:

我們知道卡頓問(wèn)題的原因錯(cuò)綜復(fù)雜,但最終都可以反饋到CPU使用率上來(lái)

1.使用dumpsys cpuinfo命令

這個(gè)命令可以獲取當(dāng)時(shí)設(shè)備cpu使用情況炒事,我們可以在線下通過(guò)重度使用應(yīng)用來(lái)檢測(cè)可能存在的卡頓點(diǎn)

A8S:/ $ dumpsys cpuinfo
Load: 1.12 / 1.12 / 1.09
CPU usage from 484321ms to 184247ms ago (2022-11-02 14:48:30.793 to 2022-11-02 1
4:53:30.866):
  2% 1053/scanserver: 0.2% user + 1.7% kernel
  0.6% 934/system_server: 0.4% user + 0.1% kernel / faults: 563 minor
  0.4% 564/signserver: 0% user + 0.4% kernel
  0.2% 256/ueventd: 0.1% user + 0% kernel / faults: 320 minor
  0.2% 474/surfaceflinger: 0.1% user + 0.1% kernel
  0.1% 576/vendor.sprd.hardware.gnss@2.0-service: 0.1% user + 0% kernel / faults
: 54 minor
  0.1% 286/logd: 0% user + 0% kernel / faults: 10 minor
  0.1% 2821/com.allinpay.appstore: 0.1% user + 0% kernel / faults: 1312 minor
  0.1% 447/android.hardware.health@2.0-service: 0% user + 0% kernel / faults: 11
75 minor
  0% 1855/com.smartpos.dataacqservice: 0% user + 0% kernel / faults: 755 minor
  0% 2875/com.allinpay.appstore:pushcore: 0% user + 0% kernel / faults: 744 mino
r
  0% 1191/com.android.systemui: 0% user + 0% kernel / faults: 70 minor
  0% 1774/com.android.nfc: 0% user + 0% kernel
  0% 172/kworker/1:2: 0% user + 0% kernel
  0% 145/irq/24-70900000: 0% user + 0% kernel
  0% 575/thermald: 0% user + 0% kernel / faults: 300 minor
...

2.CPU Profiler

這個(gè)工具是AS自帶的CPU性能檢測(cè)工具臀栈,可以在PC上實(shí)時(shí)查看我們CPU使用情況。 AS提供了四種Profiling Model配置:

  • 1.Sample Java Methods:在應(yīng)用程序基于Java的代碼執(zhí)行過(guò)程中挠乳,頻繁捕獲應(yīng)用程序的調(diào)用堆棧 獲取有關(guān)應(yīng)用程序基于Java的代碼執(zhí)行的時(shí)間和資源使用情況信息权薯。
  • 2.Trace java methods:在運(yùn)行時(shí)對(duì)應(yīng)用程序進(jìn)行檢測(cè),以在每個(gè)方法調(diào)用的開(kāi)始和結(jié)束時(shí)記錄時(shí)間戳睡扬。收集時(shí)間戳并進(jìn)行比較以生成方法跟蹤數(shù)據(jù)盟蚣,包括時(shí)序信息和CPU使用率。

請(qǐng)注意與檢測(cè)每種方法相關(guān)的開(kāi)銷(xiāo)會(huì)影響運(yùn)行時(shí)性能卖怜,并可能影響性能分析數(shù)據(jù)屎开。對(duì)于生命周期相對(duì)較短的方法,這一點(diǎn)甚至更為明顯马靠。此外奄抽,如果您的應(yīng)用在短時(shí)間內(nèi)執(zhí)行大量方法,則探查器可能會(huì)很快超過(guò)其文件大小限制甩鳄,并且可能無(wú)法記錄任何進(jìn)一步的跟蹤數(shù)據(jù)逞度。

  • 3.Sample C/C++ Functions:捕獲應(yīng)用程序本機(jī)線程的示例跟蹤。要使用此配置娩贷,您必須將應(yīng)用程序部署到運(yùn)行Android 8.0(API級(jí)別26)或更高版本的設(shè)備第晰。
  • 4.Trace System Calls:捕獲細(xì)粒度的詳細(xì)信息,使您可以檢查應(yīng)用程序與系統(tǒng)資源的交互方式 您可以檢查線程狀態(tài)的確切時(shí)間和持續(xù)時(shí)間彬祖,可視化CPU瓶頸在所有內(nèi)核中的位置茁瘦,并添加自定義跟蹤事件進(jìn)行分析。在對(duì)性能問(wèn)題進(jìn)行故障排除時(shí)卖鲤,此類(lèi)信息可能至關(guān)重要较锡。要使用此配置只搁,您必須將應(yīng)用程序部署到運(yùn)行Android 7.0(API級(jí)別24)或更高版本的設(shè)備。

使用方式

Debug.startMethodTracing("");
// 需要檢測(cè)的代碼片段
...
Debug.stopMethodTracing();

優(yōu)點(diǎn):**有比較全面的調(diào)用棧以及圖像化方法時(shí)間顯示腔稀,包含所有線程的情況

缺點(diǎn):本身也會(huì)帶來(lái)一點(diǎn)的性能開(kāi)銷(xiāo),可能會(huì)帶偏優(yōu)化方向**

火焰圖:可以顯示當(dāng)前應(yīng)用的方法堆棧:

3.Systrace

Systrace在前面一篇分析啟動(dòng)優(yōu)化的文章講解過(guò)

這里我們簡(jiǎn)單來(lái)復(fù)習(xí)下:

Systrace用來(lái)記錄當(dāng)前應(yīng)用的系統(tǒng)以及應(yīng)用(使用Trace類(lèi)打點(diǎn))的各階段耗時(shí)信息包括繪制信息以及CPU信息等羽历。

使用方式

Trace.beginSection("MyApp.onCreate_1");
alt(200);
Trace.endSection();

在命令行中:

python systrace.py -t 5 sched gfx view wm am app webview -a "com.chinaebipay.thirdcall" -o D:\trac1.html

記錄的方法以及CPU中的耗時(shí)情況:

優(yōu)點(diǎn)

  • 1.輕量級(jí)焊虏,開(kāi)銷(xiāo)小,CPU使用率可以直觀反映
  • 2.右側(cè)的Alerts能夠根據(jù)我們應(yīng)用的問(wèn)題給出具體的建議秕磷,比如說(shuō)诵闭,它會(huì)告訴我們App界面的繪制比較慢或者GC比較頻繁。

4.StrictModel

StrictModel是Android提供的一種運(yùn)行時(shí)檢測(cè)機(jī)制澎嚣,用來(lái)幫助開(kāi)發(fā)者自動(dòng)檢測(cè)代碼中不規(guī)范的地方疏尿。 主要和兩部分相關(guān): 1.線程相關(guān) 2.虛擬機(jī)相關(guān)

基礎(chǔ)代碼:

private void initStrictMode() {
    // 1、設(shè)置Debug標(biāo)志位易桃,僅僅在線下環(huán)境才使用StrictMode
    if (DEV_MODE) {
        // 2褥琐、設(shè)置線程策略
        StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
                .detectCustomSlowCalls() //API等級(jí)11,使用StrictMode.noteSlowCode
                .detectDiskReads()
                .detectDiskWrites()
                .detectNetwork() // or .detectAll() for all detectable problems
                .penaltyLog() //在Logcat 中打印違規(guī)異常信息
//              .penaltyDialog() //也可以直接跳出警報(bào)dialog
//              .penaltyDeath() //或者直接崩潰
                .build());
        // 3晤郑、設(shè)置虛擬機(jī)策略
        StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
                .detectLeakedSqlLiteObjects()
                // 給NewsItem對(duì)象的實(shí)例數(shù)量限制為1
                .setClassInstanceLimit(NewsItem.class, 1)
                .detectLeakedClosableObjects() //API等級(jí)11
                .penaltyLog()
                .build());
    }
}

線上監(jiān)控:

線上需要自動(dòng)化的卡頓檢測(cè)方案來(lái)定位卡頓敌呈,它能記錄卡頓發(fā)生時(shí)的場(chǎng)景。

自動(dòng)化監(jiān)控原理

采用攔截消息調(diào)度流程造寝,在消息執(zhí)行前埋點(diǎn)計(jì)時(shí)磕洪,當(dāng)耗時(shí)超過(guò)閾值時(shí),則認(rèn)為是一次卡頓匹舞,會(huì)進(jìn)行堆棧抓取和上報(bào)工作

首先褐鸥,我們看下Looper用于執(zhí)行消息循環(huán)的loop()方法,關(guān)鍵代碼如下所示:

/**
 * Run the message queue in this thread. Be sure to call
 * {@link #quit()} to end the loop.
 */
public static void loop() {

    ...

    for (;;) {
        Message msg = queue.next(); // might block
        if (msg == null) {
            // No message indicates that the message queue is quitting.
            return;

        // This must be in a local variable, in case a UI event sets the logger
        final Printer logging = me.mLogging;
        if (logging != null) {
            // 1
            logging.println(">>>>> Dispatching to " + msg.target + " " +
                    msg.callback + ": " + msg.what);
        }

        ...

        try {
             // 2 
             msg.target.dispatchMessage(msg);
            dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
        } finally {
            if (traceTag != 0) {
                Trace.traceEnd(traceTag);
            }
        }

        ...

        if (logging != null) {
            // 3
            logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
        }

在Looper的loop()方法中赐稽,在其執(zhí)行每一個(gè)消息(注釋2處)的前后都由logging進(jìn)行了一次打印輸出叫榕。可以看到姊舵,在執(zhí)行消息前是輸出的">>>>> Dispatching to "晰绎,在執(zhí)行消息后是輸出的"<<<<< Finished to ",它們打印的日志是不一樣的,我們就可以由此來(lái)判斷消息執(zhí)行的前后時(shí)間點(diǎn)括丁。

具體的實(shí)現(xiàn)可以歸納為如下步驟

  • 1荞下、首先,我們需要使用Looper.getMainLooper().setMessageLogging()去設(shè)置我們自己的Printer實(shí)現(xiàn)類(lèi)去打印輸出logging。這樣尖昏,在每個(gè)message執(zhí)行的之前和之后都會(huì)調(diào)用我們?cè)O(shè)置的這個(gè)Printer實(shí)現(xiàn)類(lèi)仰税。
  • 2、如果我們匹配到">>>>> Dispatching to "之后抽诉,我們就可以執(zhí)行一行代碼:也就是在指定的時(shí)間閾值之后陨簇,我們?cè)谧泳€程去執(zhí)行一個(gè)任務(wù),這個(gè)任務(wù)就是去獲取當(dāng)前主線程的堆棧信息以及當(dāng)前的一些場(chǎng)景信息迹淌,比如:內(nèi)存大小河绽、電腦、網(wǎng)絡(luò)狀態(tài)等唉窃。
  • 3耙饰、如果在指定的閾值之內(nèi)匹配到了"<<<<< Finished to ",那么說(shuō)明message就被執(zhí)行完成了纹份,則表明此時(shí)沒(méi)有產(chǎn)生我們認(rèn)為的卡頓效果苟跪,那我們就可以將這個(gè)子線程任務(wù)取消掉。

這里我們使用blockcanary來(lái)做測(cè)試:

BlockCanary

APM是一個(gè)非侵入式的性能監(jiān)控組件矮嫉,可以通過(guò)通知的形式彈出卡頓信息削咆。它的原理就是我們剛剛講述到的卡頓監(jiān)控的實(shí)現(xiàn)原理。 使用方式

  • 1.導(dǎo)入依賴(lài)
implementation 'com.github.markzhai:blockcanary-android:1.5.0'
  • Application的onCreate方法中開(kāi)啟卡頓監(jiān)控
// 注意在主進(jìn)程初始化調(diào)用
BlockCanary.install(this, new AppBlockCanaryContext()).start();
  • 3.繼承BlockCanaryContext類(lèi)去實(shí)現(xiàn)自己的監(jiān)控配置上下文類(lèi)
public class AppBlockCanaryContext extends BlockCanaryContext {
    ...
    ...
     /**
    * 指定判定為卡頓的閾值threshold (in millis),  
    * 你可以根據(jù)不同設(shè)備的性能去指定不同的閾值
    *
    * @return threshold in mills
    */
    public int provideBlockThreshold() {
        return 1000;
    }
    ....
}

  • 4.在Activity的onCreate方法中執(zhí)行一個(gè)耗時(shí)操作
try {
    Thread.sleep(4000);
} catch (InterruptedException e) {
    e.printStackTrace();
}
  • 5.結(jié)果:

可以看到一個(gè)和LeakCanary一樣效果的阻塞可視化堆棧圖

那有了BlockCanary的方法耗時(shí)監(jiān)控方式是不是就可以解百愁了呢蠢笋,呵呵拨齐。有那么容易就好了

根據(jù)原理:我們拿到的是msg執(zhí)行前后的時(shí)間和堆棧信息,如果msg中有幾百上千個(gè)方法昨寞,就無(wú)法確認(rèn)到底是哪個(gè)方法導(dǎo)致的耗時(shí)瞻惋,也有可能是多個(gè)方法堆積導(dǎo)致

這就導(dǎo)致我們無(wú)法準(zhǔn)確定位哪個(gè)方法是最耗時(shí)的援岩。如圖中:堆棧信息是T2的歼狼,而發(fā)生耗時(shí)的方法可能是T1到T2中任何一個(gè)方法甚至是堆積導(dǎo)致。

那如何優(yōu)化這塊享怀?

這里我們采用字節(jié)跳動(dòng)給我們提供的一個(gè)方案:基于 Sliver trace 的卡頓監(jiān)控體系

Sliver trace

整體流程圖

主要包含兩個(gè)方面:

  • 檢測(cè)方案: 在監(jiān)控卡頓時(shí)羽峰,首先需要打開(kāi) Sliver 的 trace 記錄能力,Sliver 采樣記錄 trace 執(zhí)行信息添瓷,對(duì)抓取到的堆棧進(jìn)行 diff 聚合和緩存梅屉。

同時(shí)基于我們的需要設(shè)置相應(yīng)的卡頓閾值,以 Message 的執(zhí)行耗時(shí)為衡量鳞贷。對(duì)主線程消息調(diào)度流程進(jìn)行攔截坯汤,在消息開(kāi)始分發(fā)執(zhí)行時(shí)埋點(diǎn),在消息執(zhí)行結(jié)束時(shí)計(jì)算消息執(zhí)行耗時(shí)搀愧,當(dāng)消息執(zhí)行耗時(shí)超過(guò)閾值惰聂,則認(rèn)為產(chǎn)生了一次卡頓疆偿。

  • 堆棧聚合策略: 當(dāng)卡頓發(fā)生時(shí),我們需要為此次卡頓準(zhǔn)備數(shù)據(jù)搓幌,這部分工作是在端上子線程中完成的杆故,主要是 dump trace 到文件以及過(guò)濾聚合要上報(bào)的堆棧。分為以下幾步:
    • 1.拿到緩存的主線程 trace 信息并 dump 到文件中鼻种。
    • 2.然后從文件中讀取 trace 信息反番,按照數(shù)據(jù)格式沙热,從最近的方法棧向上追溯叉钥,找到當(dāng)前 Message 包含的全部 trace 信息,并將當(dāng)前 Message 的完整 trace 寫(xiě)入到待上傳的 trace 文件中篙贸,刪除其余 trace 信息投队。
    • 3.遍歷當(dāng)前 Message trace,按照(Method 執(zhí)行耗時(shí) > Method 耗時(shí)閾值 & Method 耗時(shí)為該層堆棧中最耗時(shí))為條件過(guò)濾出每一層函數(shù)調(diào)用堆棧的最長(zhǎng)耗時(shí)函數(shù)爵川,構(gòu)成最后要上報(bào)的堆棧鏈路敷鸦,這樣特征堆棧中的每一步都是最耗時(shí)的,且最底層 Method 為最后的耗時(shí)大于閾值的 Method寝贡。

之后扒披,將 trace 文件和堆棧一同上報(bào),這樣的特征堆棧提取策略保證了堆棧聚合的可靠性和準(zhǔn)確性圃泡,保證了上報(bào)到平臺(tái)后堆棧的正確合理聚合碟案,同時(shí)提供了進(jìn)一步分析問(wèn)題的 trace 文件。

可以看到字節(jié)給的是一整套監(jiān)控方案颇蜡,和前面BlockCanary不同之處就在于价说,其是定時(shí)存儲(chǔ)堆棧,緩存风秤,然后使用diff去重的方式鳖目,并上傳到服務(wù)器,可以最大限度的監(jiān)控到可能發(fā)生比較耗時(shí)的方法缤弦。

開(kāi)發(fā)中哪些習(xí)慣會(huì)影響卡頓的發(fā)生

1.布局太亂领迈,層級(jí)太深。

  • 1.1:通過(guò)減少冗余或者嵌套布局來(lái)降低視圖層次結(jié)構(gòu)碍沐。比如使用約束布局代替線性布局和相對(duì)布局狸捅。
  • 1.2:用 ViewStub 替代在啟動(dòng)過(guò)程中不需要顯示的 UI 控件。
  • 1.3:使用自定義 View 替代復(fù)雜的 View 疊加抢韭。

2.主線程耗時(shí)操作

  • 2.1:主線程中不要直接操作數(shù)據(jù)庫(kù)薪贫,數(shù)據(jù)庫(kù)的操作應(yīng)該放在數(shù)據(jù)庫(kù)線程中完成。
  • 2.2:sharepreference盡量使用apply刻恭,少使用commit瞧省,可以使用MMKV框架來(lái)代替sharepreference扯夭。
  • 2.3:網(wǎng)絡(luò)請(qǐng)求回來(lái)的數(shù)據(jù)解析盡量放在子線程中,不要在主線程中進(jìn)行復(fù)制的數(shù)據(jù)解析操作鞍匾。
  • 2.4:不要在activity的onResume和onCreate中進(jìn)行耗時(shí)操作交洗,比如大量的計(jì)算等。
  • 2.5:不要在 draw 里面調(diào)用耗時(shí)函數(shù)橡淑,不能 new 對(duì)象

3.過(guò)度繪制

過(guò)度繪制是同一個(gè)像素點(diǎn)上被多次繪制构拳,減少過(guò)度繪制一般減少布局背景疊加等方式,如下圖所示右邊是過(guò)度繪制的圖片梁棠。

4.列表

RecyclerView使用優(yōu)化置森,使用DiffUtil和notifyItemDataSetChanged進(jìn)行局部更新等。

5.對(duì)象分配和回收優(yōu)化

自從Android引入 ART 并且在Android 5.0上成為默認(rèn)的運(yùn)行時(shí)之后符糊,對(duì)象分配和垃圾回收(GC)造成的卡頓已經(jīng)顯著降低了凫海,但是由于對(duì)象分配和GC有額外的開(kāi)銷(xiāo),它依然又可能使線程負(fù)載過(guò)重男娄。 在一個(gè)調(diào)用不頻繁的地方(比如按鈕點(diǎn)擊)分配對(duì)象是沒(méi)有問(wèn)題的行贪,但如果在在一個(gè)被頻繁調(diào)用的緊密的循環(huán)里,就需要避免對(duì)象分配來(lái)降低GC的壓力模闲。

減少小對(duì)象的頻繁分配和回收操作建瘫。

好了,關(guān)于卡頓優(yōu)化的問(wèn)題就講到這里尸折,下篇文章會(huì)對(duì)卡頓中的ANR情況的處理啰脚,這里做個(gè)鋪墊。

參考

Android卡頓檢測(cè)及優(yōu)化

一文讀懂直播卡頓優(yōu)化那些事兒

“終于懂了” 系列:Android屏幕刷新機(jī)制—VSync翁授、Choreographer 全面理解拣播!

深入探索Android卡頓優(yōu)化(上)

西瓜卡頓 & ANR 優(yōu)化治理及監(jiān)控體系建設(shè)

作者:小余的自習(xí)室
鏈接:https://juejin.cn/post/7161757546875715615

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市收擦,隨后出現(xiàn)的幾起案子贮配,更是在濱河造成了極大的恐慌,老刑警劉巖塞赂,帶你破解...
    沈念sama閱讀 217,277評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件泪勒,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡宴猾,警方通過(guò)查閱死者的電腦和手機(jī)圆存,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)仇哆,“玉大人沦辙,你說(shuō)我怎么就攤上這事《锾蓿” “怎么了油讯?”我有些...
    開(kāi)封第一講書(shū)人閱讀 163,624評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵详民,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我陌兑,道長(zhǎng)沈跨,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,356評(píng)論 1 293
  • 正文 為了忘掉前任兔综,我火速辦了婚禮饿凛,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘软驰。我一直安慰自己涧窒,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,402評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布碌宴。 她就那樣靜靜地躺著杀狡,像睡著了一般。 火紅的嫁衣襯著肌膚如雪贰镣。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,292評(píng)論 1 301
  • 那天膳凝,我揣著相機(jī)與錄音碑隆,去河邊找鬼。 笑死蹬音,一個(gè)胖子當(dāng)著我的面吹牛上煤,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播著淆,決...
    沈念sama閱讀 40,135評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼劫狠,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了永部?” 一聲冷哼從身側(cè)響起独泞,我...
    開(kāi)封第一講書(shū)人閱讀 38,992評(píng)論 0 275
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎苔埋,沒(méi)想到半個(gè)月后懦砂,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,429評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡组橄,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,636評(píng)論 3 334
  • 正文 我和宋清朗相戀三年荞膘,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片玉工。...
    茶點(diǎn)故事閱讀 39,785評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡羽资,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出遵班,到底是詐尸還是另有隱情屠升,我是刑警寧澤瞄勾,帶...
    沈念sama閱讀 35,492評(píng)論 5 345
  • 正文 年R本政府宣布,位于F島的核電站弥激,受9級(jí)特大地震影響进陡,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜微服,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,092評(píng)論 3 328
  • 文/蒙蒙 一趾疚、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧以蕴,春花似錦糙麦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,723評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至宝与,卻和暖如春焚廊,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背习劫。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,858評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工咆瘟, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人诽里。 一個(gè)月前我還...
    沈念sama閱讀 47,891評(píng)論 2 370
  • 正文 我出身青樓袒餐,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親谤狡。 傳聞我的和親對(duì)象是個(gè)殘疾皇子灸眼,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,713評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容