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

前言

成爲一名優秀的Android開發,需要一份完備的知識體系,在這裏,讓我們一起成長爲自己所想的那樣~。

在上篇,筆者詳細分析了目前的App繪製與佈局優化的相關優化方案,如果對繪製優化與佈局優化還不是非常熟悉的可以仔細看看前幾篇文章:Android性能優化之繪製優化深入探索Android佈局優化(上)深入探索Android佈局優化(下)。由於卡頓優化這一主題包含的內容太多,爲了更詳細地進行講解,因此,筆者將它分爲了上、下兩篇。本篇,即爲《深入探索Android卡頓優化》的上篇。本篇包含的主要內容如下所示:

  • 1、卡頓優化分析方法與工具
  • 2、自動化卡頓檢測方案及優化

在我們使用各種各樣的App的時候,有時會看見有些App運行起來並不流暢,即出現了卡頓現象,那麼如何去定義發生了卡頓現象呢?

如果App的FPS平均值小於30,最小值小於24,即表明應用發生了卡頓。

那我麼又如何去分析應用是否出現了卡頓呢?下面,我們就先來了解一下解決卡頓問題時需要用到的分析方法與工具。

一、卡頓優化分析方法與工具

1、背景介紹

  • 很多性能問題不易被發現,但是卡頓問題很容易被直觀感受。
  • 卡頓問題難以定位。

那麼卡頓問題到底難在哪裏呢?

  • 1、卡頓產生的原因是錯綜複雜的,它涉及到代碼、內存、繪製、IO、CPU等等。
  • 2、線上的卡頓問題在線下是很難復現的,因爲它與當時的場景是強相關的,比如說線上用戶的磁盤IO空間不足了,它影響了磁盤IO的寫入性能,所以導致卡頓。針對這種問題,我們最好在發現卡頓的時候儘量地去記錄用戶當時發生卡頓時的具體的場景信息

2、卡頓分析方法之使用shell命令分析CPU耗時

儘管造成卡頓的原因有很多種,不過最終都會反映到CPU時間上。

CPU時間包含用戶時間和系統時間。

  • 用戶時間:執行用戶態應用程序代碼所消耗的時間。
  • 系統時間:執行內核態系統調用所消耗的時間,包括I/O、鎖、中斷和其它系統調用所消耗的時間。

CPU的問題大致可以分爲以下三類:

1、CPU資源冗餘使用

  • 算法效率太低:明明可以遍歷一次的卻需要去遍歷兩次,主要出現在查找、排序、刪除等環節。
  • 沒有使用cache:明明解碼過一次的圖片還去重複解碼。
  • 計算時使用的基本類型不對:明明使用int就足夠,卻要使用long,這會導致CPU的運算壓力多出4倍。

2、CPU資源爭搶

  • 搶主線程的CPU資源:這是最常見的問題,並且在Android 6.0版本之前沒有renderthread的時候,主線程的繁忙程度就決定了是否會引發用戶的卡頓問題。
  • 搶音視頻的CPU資源:音視頻編解碼本身會消耗大量的CPU資源,並且其對於解碼的速度是有硬性要求的,如果達不到就可能產生播放流暢度的問題。我們可以採取兩種方式去優化:1、儘量排除非核心業務的消耗。2、優化自身的性能消耗,把CPU負載轉化爲GPU負載,如使用renderscript來處理視頻中的影像信息。
  • 大家平等,互相搶:比如在自定義的相冊中,我開了20個線程做圖片解碼,那就是互相搶CPU了,結果就是會導致圖片的顯示速度非常慢。這簡直就是三個和尚沒水喝的典型案例。因此,在自定義線程池的時候我們需要按照系統核心數去控制線程數。

3、CPU資源利用率低

對於啓動、界面切換、音視頻編解碼這些場景,爲了保證其速度,我們需要去好好利用CPU。而導致無法充分利用CPU的因素,不僅有磁盤和網絡I/O,還有鎖操作、sleep等等。對於鎖的優化,通常是儘可能地縮減鎖的範圍。

1、瞭解CPU 性能

我們可以通過CPU的主頻、核心數、緩存等參數去評估CPU的性能,這些參數的好壞能表現出CPU計算能力和指令執行能力的強弱,也就是CPU每秒執行的浮點計算數和每秒執行的指令數的多少

此外,現在最新的主流機型都使用了多級能效的CPU架構(即多核分層架構),以確保在平常低負荷工作時能僅使用低頻核心來節省電量。

並且,我們還可以通過shell命令直接查看手機的CPU核心數與頻率等信息,如下所示:

// 先輸入adb shell進入手機的shell環境
adb shell

// 獲取 CPU 核心數,我的手機是8核
platina:/ $ cat /sys/devices/system/cpu/possible
0-7

// 獲取第一個 CPU 的最大頻率
platina:/ $ cat
/sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_max_freq                         <
1843200

// 獲取第二個CPU的最小頻率
platina:/ $ cat
/sys/devices/system/cpu/cpu1/cpufreq/cpuinfo_min_freq                         <
633600

從 CPU 到 GPU 再到 AI 芯片(如專爲神經網絡計算打造的 NPU(Neural network Processing Unit)),隨着手機 CPU 整體性能的飛躍,醫療診斷、圖像超清化等一些 AI 應用場景也可以在移動端更好地落地。我們可以充分利用移動端的計算能力來降低高昂的服務器成本。

此外,CPU的性能越好,應用就能獲得更好的支持,如線程池可以根據不同手機的CPU核心數來配備不同的線程數、僅在手機主頻比較高或者帶有NPU的設備去開啓一些高級的AI功能

2、通過讀取/proc/stat與/proc/[PID]/stat文件來計算並評估系統的CPU耗時情況

當應用出現卡頓問題之後,首先我們應該查看系統CPU的使用率。

首先,我們通過讀取 /proc/stat 文件獲取總的 CPU 時間,並讀取 /proc/[PID]/stat 獲取應用進程 的CPU 時間,然後,採樣兩個足夠短的時間間隔的 CPU 快照與進程快照來計算其 CPU 使用率

計算總的 CPU 使用率

1、採樣兩個足夠短的時間間隔的 CPU 快照,即需要前後兩次去讀取 /proc/stat 文件,獲取兩個時間點對應的數據,如下所示:

// 第一次採樣
platina:/ $ cat /proc/stat
cpu  9931551 1082101 9002534 174463041 340947 1060438 1088978 0 0 0
cpu0 2244962 280573 2667000 22414199 99651 231869 439918 0 0 0
cpu1 2672378 421880 2943791 21540302 121818 236850 438733 0 0 0
cpu2 1648512 76856 1431036 25868789 46970 107094 52025 0 0 0
cpu3 1418757 41280 1397203 25772984 40292 110168 41667 0 0 0
cpu4 573203 79498 178263 19618235 9577 307949 10875 0 0 0
cpu5 522638 67978 155454 19684358 8793 19787 4603 0 0 0
cpu6 458438 64085 132252 19749439 8143 19942 98241 0 0 0
cpu7 392663 49951 97535 19814735 5703 26779 2916 0 0 0
intr...

// 第二次採樣
platina:/ $ cat /proc/stat
cpu  9931673 1082113 9002679 174466561 340954 1060446 1088994 0 0 0
cpu0 2244999 280578 2667032 22414604 99653 231869 439918 0 0 0
cpu1 2672434 421881 2943861 21540606 121822 236855 438747 0 0 0
cpu2 1648525 76859 1431054 25869234 46971 107095 52026 0 0 0
cpu3 1418773 41283 1397228 25773412 40292 110170 41668 0 0 0
cpu4 573203 79498 178263 19618720 9577 307949 10875 0 0 0
cpu5 522638 67978 155454 19684842 8793 19787 4603 0 0 0
cpu6 458438 64085 132252 19749923 8143 19942 98241 0 0 0
cpu7 392663 49951 97535 19815220 5703 26779 2916 0 0 0
int...    

因爲我的手機是8核,所以這裏的cpu個數是8個,從cpu0到cpu7,第一行的cpu即是8個cpu的指標數據彙總,因爲是要計算系統cpu的使用率,那當然應該以cpu爲基準了。兩次採樣的CPU指標數據如下:

cpu1  9931551 1082101 9002534 174463041 340947 1060438 1088978 0 0 0
cpu2  9931673 1082113 9002679 174466561 340954 1060446 1088994 0 0 0

其對應的各項指標如下:

CPU (user, nice, system, idle, iowait, irq, softirq, stealstolen, guest);

拿cpu1(9931551 1082101 9002534 174463041 340947 1060438 1088978 0 0 0)的數據來說,下面,我就來詳細地解釋下這些指標的含義。

  • user(9931551): 表示從系統啓動開始至今處於用戶態的運行時間,注意不包含 nice 值爲負的進程
  • nice(1082101) :表示從系統啓動開始至今nice 值爲負的進程所佔用的 CPU 時間
  • system(9002534): 表示從系統啓動開始至今處於內核態的運行時間
  • idle(174463041) :表示從系統啓動開始至今除 IO 等待時間以外的其他等待時間
  • iowait(340947):表示從系統啓動開始至今的IO 等待時間。(從Linux V2.5.41開始包含)
  • irq(1060438):表示從系統啓動開始至今的硬中斷時間。(從Linux V2.6.0-test4開始包含)
  • softirq(1088978):表示從系統啓動開始至今的軟中斷時間。(從Linux V2.6.0-test4開始包含)
  • stealstolen(0) :表示當在虛擬化環境中運行時在其他操作系統中所花費的時間。在Android系統下此值爲0。(從Linux V2.6.11開始包含)
  • guest(0) :表示當在Linux內核的控制下爲其它操作系統運行虛擬CPU所花費的時間。在Android系統下此值爲0。(從 V2.6.24開始包含)

此外,這些數值的單位都是 jiffies,jiffies 是內核中的一個全局變量,用來記錄系統啓動以來產生的節拍數,在 Linux 中,一個節拍大致可以理解爲操作系統進程調度的最小時間片,不同的 Linux 系統內核中的這個值可能不同,通常在 1ms 到 10ms 之間

瞭解了/proc/stat命令下各項參數的含義之後,我們就可以由前後兩次時間點的CPU數據計算得到cpu1與cpu2的活動時間,如下所示:

totalCPUTime = user + nice + system + idle + iowait + irq + softirq + stealstolen + guest 
cpu1 = 9931551 + 1082101 + 9002534 +  174463041 + 340947 + 1060438 + 1088978 + 0  + 0 + 0 = 196969590jiffies
cpu2 = 9931673 + 1082113 + 9002679 +  174466561 + 340954 + 1060446 + 1088994 + 0 + 0 + 0 = 196973420jiffies

因此可得出總的CPU時間,如下所示:

totalCPUTime = CPU2 – CPU1 = 3830jiffies

最後,我們就可以計算出系統CPU的使用率::

// 先計算得到CPU的空閒時間
idleCPUTime = idle2 – idle1 = 3520jiffies
// 最後得到系統CPU的使用率
totalCPUUse = (totalCPUTime – idleCPUTime) / totalCPUTime = (3830 - 3520)/ 3830 = 8%

可以看到,前後兩次時間點間的CPU使用率大概爲8%,說明我們系統的CPU是處於空閒狀態的,如果CPU 使用率一直大於 60% ,則表示系統處於繁忙狀態,此時就需要進一步分析用戶時間和系統時間的比例,看看到底是系統佔用了CPU還是應用進程佔用了CPU

3、使用top命令查看應用進程的CPU消耗情況

此外,由於Android是基於Linux內核改造而成的操作系統,自然而然也能使用Linux的一些常用命令。比如我們可以使用top命令查看哪些進程是 CPU 的主要消耗者。

// 直接使用top命令會定時不斷地輸出進程的相關信息
1|platina:/ $ top
PID USER         PR  NI VIRT  RES  SHR S[%CPU] %MEM     TIME+ ARGS
12700 u0_a945    10 -10 4.3G 122M  67M S 15.6   2.1   1:06.41 json.chao.com.w+
753 system       RT   0  90M 1.1M 1.0M S 13.6   0.0 127:47.73 android.hardwar+
2064 system      18  -2 4.6G 309M 215M S 12.3   5.4 978:15.18 system_server
22142 u0_a163    20   0 2.0G  97M  41M S 10.3   1.6   2:22.99 com.tencent.mob+
2293 system      20   0 4.7G 250M  87M S  8.6   4.3 353:15.77 com.android.sys+

從以上可知我們的Awesome-WanAndroid應用進程佔用了15.6%的CPU。最後,這裏再列舉下最常用的top命令,如下所示:

// 排除0%的進程信息
adb shell top | grep -v '0% S'

// 只打印1次按CPU排序的TOP 10的進程信息
adb shell top -m 10 -s cpu -n 1
|platina:/ $ top -d 1|grep json.chao.com.w+
5689 u0_a945      10 -10 4.3G 129M  71M S 13.8   2.2   1:04.46 json.chao.com.w+
5689 u0_a945      10 -10 4.3G 129M  71M S 19.0   2.2   1:04.51 json.chao.com.w+
5689 u0_a945      10 -10 4.3G 129M  71M S 15.0   2.2   1:04.70 json.chao.com.w+
5689 u0_a945      10 -10 4.3G 129M  71M S  9.0   2.2   1:04.85 json.chao.com.w+
5689 u0_a945      10 -10 4.3G 129M  71M S 26.0   2.2   1:04.94 json.chao.com.w+
5689 u0_a945      10 -10 4.3G 129M  71M S  9.0   2.2   1:05.20 json.chao.com.w+
5689 u0_a945      10 -10 4.3G 129M  71M R 17.0   2.2   1:05.29 json.chao.com.w+
5689 u0_a945      10 -10 4.3G 129M  71M S 20.0   2.2   1:05.46 json.chao.com.w+
5689 u0_a945      10 -10 4.3G 129M  71M S  9.0   2.2   1:05.66 json.chao.com.w+
5689 u0_a945      10 -10 4.3G 129M  71M R 21.0   2.2   1:05.75 json.chao.com.w+
5689 u0_a945      10 -10 4.3G 129M  71M S 14.0   2.2   1:05.96 json.chao.com.w+

4、PS軟件

除了top命令可以比較全面地查看整體的CPU信息之外,如果我們只想查看當前指定進程已經消耗的CPU時間佔系統總時間的百分比或其它的狀態信息的話,可以使用ps命令,常用的ps命令如下所示:

// 查看指定進程的狀態信息
platina:/ $ ps -p 31333
USER           PID  PPID     VSZ    RSS WCHAN            ADDR S NAME
u0_a945      31333  1277 4521308 127460 0                   0 S json.chao.com.w+

// 查看指定進程已經消耗的CPU時間佔系統總時間的百分比
platina:/ $ ps -o PCPU -p 31333
%CPU
10.8

其中輸出參數的含義如下所示:

  • USER:用戶名
  • PID:進程ID
  • PPID:父進程ID
  • VSZ:虛擬內存大小(1k爲單位)
  • RSS:常駐內存大小(正在使用的頁)
  • WCHAN:進程在內核態中的運行時間
  • Instruction pointer:指令指針
  • NAME:進程名字

最後的輸出參數S表示的是進程當前的狀態,總共有10種可能的狀態,如下所示:

R (running) S (sleeping) D (device I/O) T (stopped)  t (traced)
Z (zombie)  X (deader)   x (dead)       K (wakekill) W (waking)

可以看到,我們當前主進程是休眠的狀態。

5、dumpsys cpuinfo

使用dumpsys cpuinfo命令獲得的信息比起top命令得到的信息要更加精煉,如下所示:

platina:/ $ dumpsys cpuinfo
Load: 1.92 / 1.59 / 0.97
CPU usage from 45482ms to 25373ms ago (2020-02-04 17:00:37.666 to 2020-02-04 17:00:57.775):
33% 2060/system_server: 22% user + 10% kernel / faults: 8152 minor 6 major
17% 2292/com.android.systemui: 12% user + 4.7% kernel / faults: 21636 minor 3 major
14% 750/[email protected]: 4.4% user + 10% kernel
6.1% 778/surfaceflinger: 3.3% user + 2.7% kernel / faults: 128 minor
3.3% 2598/com.miui.home: 2.8% user + 0.4% kernel / faults: 7655 minor 11 major
2.2% 2914/cnss_diag: 1.6% user + 0.6% kernel
1.9% 745/[email protected]: 1.4% user + 0.5% kernel / faults: 5 minor
1.7% 4525/kworker/u16:6: 0% user + 1.7% kernel
1.6% 748/[email protected]: 0.6% user + 0.9% kernel
1.4% 4551/kworker/u16:14: 0% user + 1.4% kernel
1.4% 31333/json.chao.com.wanandroid: 0.9% user + 0.4% kernel / faults: 3995 minor 22 major
1.1% 6670/kworker/u16:0: 0% user + 1.1% kernel
0.9% 448/mmc-cmdqd/0: 0% user + 0.9% kernel
0.7% 95/system: 0% user + 0.7% kernel
0.6% 4512/mdss_fb0: 0% user + 0.6% kernel
0.6% 7393/com.android.incallui: 0.6% user + 0% kernel / faults: 2272 minor
0.6% 594/logd: 0.4% user + 0.1% kernel / faults: 38 minor 3 major
0.5% 3108/com.xiaomi.xmsf: 0.2% user + 0.2% kernel / faults: 1812 minor
0.5% 4526/kworker/u16:9: 0% user + 0.5% kernel
0.5% 4621/com.gotokeep.keep: 0.3% user + 0.1% kernel / faults: 55 minor
0.5% 354/irq/267-NVT-ts: 0% user + 0.5% kernel
0.5% 2572/com.android.phone: 0.3% user + 0.1% kernel / faults: 323 minor
0.5% 4554/kworker/u16:15: 0% user + 0.5% kernel
0.4% 290/kgsl_worker_thr: 0% user + 0.4% kernel
0.3% 2933/irq/61-1008000.: 0% user + 0.3% kernel
0.3% 3932/com.tencent.mm: 0.2% user + 0% kernel / faults: 647 minor 1 major
0.3% 4550/kworker/u16:13: 0% user + 0.3% kernel
0.3% 744/[email protected]: 0% user + 0.3% kernel / faults: 48 minor
0.3% 8906/com.tencent.mm:appbrand0: 0.2% user + 0% kernel / faults: 45 minor
0.2% 79/smem_native_rpm: 0% user + 0.2% kernel
0.2% 759/[email protected]: 0% user + 0.2% kernel / faults: 46 minor
0.2% 3197/com.miui.powerkeeper: 0% user + 0.1% kernel / faults: 141 minor
0.2% 4489/kworker/1:1: 0% user + 0.2% kernel
0.2% 595/servicemanager: 0% user + 0.2% kernel
0.2% 754/[email protected]: 0.1% user + 0% kernel
0.2% 1258/jbd2/dm-2-8: 0% user + 0.2% kernel
0.2% 5800/com.eg.android.AlipayGphone: 0.1% user + 0% kernel / faults: 48 minor
0.2% 21590/iptables-restore: 0% user + 0.1% kernel / faults: 563 minor
0.2% 21592/ip6tables-restore: 0% user + 0.1% kernel / faults: 647 minor
0.1% 3/ksoftirqd/0: 0% user + 0.1% kernel
0.1% 442/cfinteractive: 0% user + 0.1% kernel
0.1% 568/ueventd: 0% user + 0% kernel
0.1% 1295/netd: 0% user + 0.1% kernel / faults: 250 minor
0.1% 3002/com.miui.securitycenter.remote: 0.1% user + 0% kernel / faults: 818 minor 1 major
0.1% 20555/com.eg.android.AlipayGphone:push: 0% user + 0% kernel / faults: 20 minor
0.1% 7/rcu_preempt: 0% user + 0.1% kernel
0.1% 15/ksoftirqd/1: 0% user + 0.1% kernel
0.1% 76/lpass_smem_glin: 0% user + 0.1% kernel
0.1% 1299/rild: 0.1% user + 0% kernel / faults: 12 minor
0.1% 1448/android.process.acore: 0.1% user + 0% kernel / faults: 1719 minor
0% 4419/com.google.android.webview:s: 0% user + 0% kernel / faults: 602 minor
0% 20465/com.miui.hybrid: 0% user + 0% kernel / faults: 1575 minor
0% 10/rcuop/0: 0% user + 0% kernel
0% 75/smem_native_lpa: 0% user + 0% kernel
0% 90/kcompactd0: 0% user + 0% kernel
0% 1508/msm_irqbalance: 0% user + 0% kernel
0% 1745/cds_mc_thread: 0% user + 0% kernel
0% 2899/charge_logger: 0% user + 0% kernel
0% 3612/com.tencent.mm:tools: 0% user + 0% kernel / faults: 29 minor
0% 4203/kworker/0:0: 0% user + 0% kernel
0% 7377/com.android.server.telecom:ui: 0% user + 0% kernel / faults: 1083 minor
0% 32113/com.tencent.mobileqq: 0% user + 0% kernel / faults: 49 minor
0% 8/rcu_sched: 0% user + 0% kernel
0% 22/ksoftirqd/2: 0% user + 0% kernel
0% 25/rcuop/2: 0% user + 0% kernel
0% 29/ksoftirqd/3: 0% user + 0% kernel
0% 39/rcuop/4: 0% user + 0% kernel
0% 53/rcuop/6: 0% user + 0% kernel
0% 487/irq/715-ima-rdy: 0% user + 0% kernel
0% 749/[email protected]: 0% user + 0% kernel
0% 764/healthd: 0% user + 0% kernel / faults: 2 minor
0% 845/wlan_logging_th: 0% user + 0% kernel
0% 860/mm-pp-dpps: 0% user + 0% kernel
0% 1297/wificond: 0% user + 0% kernel / faults: 12 minor
 0% 1309/com.miui.weather2: 0% user + 0% kernel / faults: 729 minor 23 major
0% 1542/rild: 0% user + 0% kernel / faults: 3 minor
0% 2915/tcpdump: 0% user + 0% kernel / faults: 6 minor
0% 2974/com.tencent.mobileqq:MSF: 0% user + 0% kernel / faults: 121 minor
0% 3044/com.miui.contentcatcher: 0% user + 0% kernel / faults: 315 minor
0% 3057/com.miui.dmregservice: 0% user + 0% kernel / faults: 332 minor
0% 3095/com.xiaomi.mircs: 0% user + 0% kernel
0% 3115/com.xiaomi.finddevice: 0% user + 0% kernel / faults: 270 minor 3 major
0% 3513/com.xiaomi.metoknlp: 0% user + 0% kernel / faults: 136 minor
0% 3603/com.tencent.mm:toolsmp: 0% user + 0% kernel / faults: 35 minor
0% 4527/kworker/u16:11: 0% user + 0% kernel
0% 4841/com.gotokeep.keep:xg_service_v4: 0% user + 0% kernel / faults: 275 minor
0% 5064/com.sohu.inputmethod.sogou.xiaomi: 0% user + 0% kernel / faults: 102 minor
0% 5257/kworker/0:1: 0% user + 0% kernel
0% 5839/com.tencent.mm:push: 0% user + 0% kernel / faults: 98 minor
0% 6644/kworker/3:2: 0% user + 0% kernel
0% 6657/com.miui.wmsvc: 0% user + 0% kernel / faults: 52 minor
0% 6945/com.xiaomi.account:accountservice: 0% user + 0% kernel / faults: 1 minor
0% 9387/com.tencent.mm:appbrand1: 0% user + 0% kernel / faults: 27 minor
13% TOTAL: 6.8% user + 5.3% kernel + 0.2% iowait + 0.3% irq + 0.4% softirq

從上述信息可知,第一行顯示的是cpuload (負載平均值)信息:Load: 1.92 / 1.59 / 0.97
三個數字表示逐漸變長的時間段(平均一分鐘,五分鐘和十五分鐘)的平均值,而較低的數字則更好。數字越大表示有問題或機器過載。需要注意的是,這裏的Load需要除以核心數,比如我這裏的系統核心數爲8核,所以最終每一個單核CPU的Load爲0.24 / 0.20 / 0.12,如果Load超過1,則表示出現了問題

此外,佔用系統CPU資源最高的是system_server進程,而我們的wanandroid應用進程僅佔用了
1.4%的CPU資源,其中有0.9%的是用戶態所佔用的時間,0.4%是內核態所佔用的時間。最後,我們可以看到系統總佔用的CPU時間是13%,這個值是根據前面所有值加起來 / 系統CPU數的處理的,也就是104% / 8 = 13%

除了上述方式來分析系統與應用的CPU使用情況之外,我們還應該關注卡頓率與卡頓樹這兩個指標。它們能幫助我們有效地去評估、並且更有針對性地去優化應用發生的卡頓。

卡頓率

類似於深入探索Android穩定性優化一文中講到的UV、PV崩潰率,卡頓也可以有其對應的UV、PV卡頓率,UV就是Unique visitor,指的就是一臺手機客戶端爲一個訪客,00:00-24:00內相同的客戶端只被計算一次。而PV即Page View,即頁面瀏覽量或點擊量。所以UV、PV卡頓率的定義即爲如下所示:

// UV 卡頓率可以評估卡頓的影響範圍
UV 卡頓率 = 發生過卡頓 UV / 開啓卡頓採集 UV
// PV 卡頓率評估卡頓的嚴重程度
PV 卡頓率 = 發生過卡頓 PV / 啓動採集 PV

因爲卡頓問題的採樣規則跟內存問題是相似的,一般都是採取抽樣上報的方式,並且都應該按照單個用戶來抽樣一個用戶如果命中採集,那麼在一天內都會持續的採集數據

卡頓樹

我們可以實現卡頓的火焰圖,即卡頓樹,在一張圖裏就可以看到卡頓的整體信息。由於卡頓的具體耗時跟手機性能,還有當時的使用場景、環境等密切相關,而且卡頓問題在日活大的應用上出現的場景非常多,所以對於大於我們指定的卡頓閾值如1s\2s\3s時,我們就可以拋棄具體的耗時,只按照相同堆棧出現的比例來聚合各類卡頓信息。這樣我們就能夠很直觀地從卡頓樹上看到到底哪些堆棧出現的卡頓問題最多,以便於我們能夠優先去解決 Top 的卡頓問題,達到使用最少的精力獲取最大的優化效果的目的。

3、卡頓優化工具

1、CPU Profiler回顧

CPU Profiler的使用筆者已經在深入探索Android啓動速度優化中詳細分析過了,如果對CPU Profiler還不是很熟悉的話,可以去看看這篇文章。

下面我們來簡單來回顧一下CPU Profiler。

優勢:

  • 圖形的形式展示執行時間、調用棧等。
  • 信息全面,包含所有線程。

劣勢:

運行時開銷嚴重,整體都會變慢,可能會帶偏我們的優化方向。

使用方式:

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

最終生成的生成文件在sd卡:Android/data/packagename/files。

2、Systrace回顧

systrace 利用了 Linux 的ftrace調試工具(ftrace是用於瞭解Linux內核內部運行情況的調試工具),相當於在系統各個關鍵位置都添加了一些性能探針,也就是在代碼里加了一些性能監控的埋點。Android 在 ftrace 的基礎上封裝了atrace,並增加了更多特有的探針,比如Graphics、Activity Manager、Dalvik VM、System Server 等等。對於Systrace的使用筆者在深入探索Android啓動速度優化這篇文章中已經詳細分析過了,如果對Systrace還不是很熟悉的話可以去看看這篇文章。

下面我們來簡單回顧一下Systrace。

作用:

監控和跟蹤API調用、線程運行情況,生成HTML報告。

建議:

API 18以上使用,推薦使用TraceCompat。

使用方式:

使用python命令執行腳本,後面加上一系列參數,如下所示:

python systrace.py -t 10 [other-options] [categories]
// 筆者通常使用的systrace配置
python /Users/quchao/Library/Android/sdk/platform-tools/systrace/systrace.py -t 20 sched gfx view wm am app webview -a "com.wanandroid.json.chao" -o ~/Documents/open-project/systrace_data/wanandroid_start_1.html

具體參數含義如下:

  • -t:指定統計時間爲20s。
  • shced:cpu調度信息。
  • gfx:圖形信息。
  • view:視圖。
  • wm:窗口管理。
  • am:活動管理。
  • app:應用信息。
  • webview:webview信息。
  • -a:指定目標應用程序的包名。
  • -o:生成的systrace.html文件。

優勢:

  • 1、輕量級,開銷小。
  • 2、它能夠直觀地反映CPU的利用率。
  • 3、右側的Alerts能夠根據我們應用的問題給出具體的建議,比如說,它會告訴我們App界面的繪製比較慢或者GC比較頻繁

最後,我們還可以通過編譯時給每個函數插樁的方式來實現線下自動增加應用程序的耗時分析,但是要注意需過濾大部分的短函數,以減少性能損耗(這一點可以通過黑名單配置的方式去過濾短函數或調用非常頻繁的函數)。使用這種方式我們就可以看到整個應用程序的調用流程。包括應用關鍵線程的函數調用,例如渲染耗時、線程鎖,GC 耗時等等。這裏可以使用zhengcx的MethodTraceMan,但是目前僅僅能實現對包名和類名的過濾配置,所以需要對源碼進行定製化,以支持過濾短函數或調用非常頻繁函數的配置功能。

基於性能的考慮,如果要在線上使用此方案,最好只去監控主線程的耗時。雖然插樁方案對性能的影響並不是很大,但是建議僅在線下或灰度環境中使用。

此外,如果你需要分析Native 函數的調用,請使用Android 5.0 新增的Simpleperf性能分析工具,它利用了 CPU 的性能監控單元(PMU)提供的硬件 perf 事件。使用 Simpleperf 可以看到所有的 Native 代碼的耗時,對一些 Android 系統庫的調用,在分析問題時有比較大的幫助,例如分析加載 dex、verify class 的耗時等等。此外,在 Android Studio 3.2 中的 Profiler 也直接支持了 Simpleper(SampleNative性能分析工具 (API Level 26+)),這更加方便了native代碼的調試。

3、StrictMode

StrictMode是Android 2.3引入的一個工具類,它被稱爲嚴苛模式,是Android提供的一種運行時檢測機制,可以用來幫助開發人員用來檢測代碼中一些不規範的問題。對於我們的項目當中,可能會成千上萬行代碼,如果我們用肉眼Review,這樣不僅效率非常低效,而且比較容易出問題。使用StrictMode之後,系統會自動檢測出來在主線程中的一些異常情況,並按照我們的配置給出相應的反應

StrictMode這個工具是非常強大的,但是我們可能因爲對它不熟悉而忽略掉它。StrictMode主要用來檢測兩大問題:

1、線程策略

線程策略的檢測內容,是一些自定義的耗時調用、磁盤讀取操作以及網絡請求等

2、虛擬機策略

虛擬機策略的檢測內容如下:

  • Activity泄漏
  • Sqlite對象泄漏
  • 檢測實例數量

StrictMode實戰

如果要在應用中使用StrictMode,只需要在Applicaitoin的onCreate方法中對StrictMode進行統一配置,代碼如下所示:

private void initStrictMode() {
    // 1、設置Debug標誌位,僅僅在線下環境才使用StrictMode
    if (DEV_MODE) {
        // 2、設置線程策略
        StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
                .detectCustomSlowCalls() //API等級11,使用StrictMode.noteSlowCode
                .detectDiskReads()
                .detectDiskWrites()
                .detectNetwork() // or .detectAll() for all detectable problems
                .penaltyLog() //在Logcat 中打印違規異常信息
//              .penaltyDialog() //也可以直接跳出警報dialog
//              .penaltyDeath() //或者直接崩潰
                .build());
        // 3、設置虛擬機策略
        StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
                .detectLeakedSqlLiteObjects()
                // 給NewsItem對象的實例數量限制爲1
                .setClassInstanceLimit(NewsItem.class, 1)
                .detectLeakedClosableObjects() //API等級11
                .penaltyLog()
                .build());
    }
}

最後,在日誌輸出欄中注意使用“StrictMode”關鍵字過濾出對應的log即可。

4、Profilo

Profilo是一個用於收集應用程序生產版本的性能跟蹤的Android庫。

對於Profilo來說,它集成了atrace功能,ftrace 所有的性能埋點數據都會通過 trace_marker 文件寫入到內核緩衝區,Profilo 使用了 PLT Hook 攔截了寫入操作,以選擇部分關心的事件去做特定的分析。這樣所有的 systrace 的探針我們都可以拿到,例如四大組件生命週期、鎖等待時間、類校驗、GC 時間等等。不過大部分的 atrace 事件都比較籠統,從事件“B|pid|activityStart”,我們無法明確知道該事件具體是由哪個 Activity 來創建的。

此外,使用Profilo還能夠快速獲取Java堆棧。由於獲取堆棧需要暫停主線程的運行,所以profilo通過間隔發送 SIGPROF 信號這樣一種類似 Native 崩潰捕捉的方式去快速獲取 Java 堆棧

Profilo能夠低耗時地快速獲取Java堆棧的具體實現原理爲當Signal Handler 捕獲到信號後,它就會獲取到當前正在執行的 Thread,通過 Thread 對象就可以拿到當前線程的 ManagedStack,ManagedStack 是一個單鏈表,它保存了當前的 ShadowFrame 或者 QuickFrame 棧指針,先依次遍歷 ManagedStack 鏈表,然後遍歷其內部的 ShadowFrame 或者 QuickFrame 還原一個可讀的調用棧,從而 unwind 出當前的 Java 堆棧。關於ManagedStack與ShadowFrame、QuickFrame三者的關係如下圖所示:

image

Profilo通過這種方式,就可以實現線程同步運行的同時,我們還可以去幫它做檢查,並且耗時基本可以忽略不計。但是目前 Profilo 快速獲取堆棧的功能不支持 Android 8.0 和 Android 9.0,並且它內部使用了Hook等大量的黑科技手段,鑑於穩定性問題,建議採取抽樣部分用戶的方式來開啓該功能。

Profilo項目地址

前面我們說過,Profilo最終也使用了ftrace,而Systrace主要也是根據Linux的ftrace機制來實現的,而ftrace的作用是幫助我們瞭解 Linux 內核的運行時行爲,以便進行故障調試或性能分析。ftrace的整體架構如下所示:

image

由上圖可知,Ftrace 有兩大組成部分,一個是 framework,另外就是一系列的 tracer 。每個 tracer 用於完成不同的功能,並且它們統一由 framework 管理。 ftrace 的 trace 信息保存在 ring buffer 中,由 framework 負責管理。 Framework 利用 debugfs 系統在 /debugfs 下建立 tracing 目錄,並提供了一系列的控制文件。

下面,我這裏給出使用 PLTHook 技術來獲取 Atrace 日誌的一個項目。

1、使用profilo的PLTHook來hook libc.so的 write 與 __write_chk 方法

使用 PLTHook 技術來獲取 Atrace 的日誌-項目地址

運行項目後,我們點擊按鈕開啓Atrace日誌,然後就可以在Logcat中看到如下的native層日誌信息:

2020-02-05 10:58:00.873 13052-13052/com.dodola.atrace I/HOOOOOOOOK: ===============install systrace hoook==================
2020-02-05 10:58:00.879 13052-13052/com.dodola.atrace I/HOOOOOOOOK: ========= B|13052|inflate
2020-02-05 10:58:00.880 13052-13052/com.dodola.atrace I/HOOOOOOOOK: ========= B|13052|LinearLayout
2020-02-05 10:58:00.881 13052-13052/com.dodola.atrace I/HOOOOOOOOK: ========= E
2020-02-05 10:58:00.882 13052-13052/com.dodola.atrace I/HOOOOOOOOK: ========= B|13052|TextView
2020-02-05 10:58:00.884 13052-13052/com.dodola.atrace I/HOOOOOOOOK: ========= E
2020-02-05 10:58:00.885 13052-13052/com.dodola.atrace I/HOOOOOOOOK: ========= E
2020-02-05 10:58:00.888 13052-13075/com.dodola.atrace I/HOOOOOOOOK: ========= B|13052|notifyFramePending
2020-02-05 10:58:00.888 13052-13075/com.dodola.atrace I/HOOOOOOOOK: ========= E
2020-02-05 10:58:00.889 13052-13052/com.dodola.atrace I/HOOOOOOOOK: ========= B|13052|Choreographer#doFrame
2020-02-05 10:58:00.889 13052-13052/com.dodola.atrace I/HOOOOOOOOK: ========= B|13052|input
2020-02-05 10:58:00.889 13052-13052/com.dodola.atrace I/HOOOOOOOOK: ========= E
2020-02-05 10:58:00.889 13052-13052/com.dodola.atrace I/HOOOOOOOOK: ========= B|13052|traversal
2020-02-05 10:58:00.889 13052-13052/com.dodola.atrace I/HOOOOOOOOK: ========= B|13052|draw
2020-02-05 10:58:00.890 13052-13052/com.dodola.atrace I/HOOOOOOOOK: ========= B|13052|Record View#draw()
2020-02-05 10:58:00.891 13052-13052/com.dodola.atrace I/HOOOOOOOOK: ========= E
2020-02-05 10:58:00.891 13052-13075/com.dodola.atrace I/HOOOOOOOOK: ========= B|13052|DrawFrame
2020-02-05 10:58:00.891 13052-13075/com.dodola.atrace I/HOOOOOOOOK: ========= B|13052|syncFrameState
2020-02-05 10:58:00.891 13052-13075/com.dodola.atrace I/HOOOOOOOOK: ========= B|13052|prepareTree
2020-02-05 10:58:00.891 13052-13075/com.dodola.atrace I/HOOOOOOOOK: ========= E
2020-02-05 10:58:00.891 13052-13075/com.dodola.atrace I/HOOOOOOOOK: ========= E
2020-02-05 10:58:00.891 13052-13075/com.dodola.atrace I/HOOOOOOOOK: ========= B|13052|query
2020-02-05 10:58:00.891 13052-13052/com.dodola.atrace I/HOOOOOOOOK: ========= E
2020-02-05 10:58:00.891 13052-13075/com.dodola.atrace I/HOOOOOOOOK: ========= E
2020-02-05 10:58:00.891 13052-13075/com.dodola.atrace I/HOOOOOOOOK: ========= B|13052|query
2020-02-05 10:58:00.891 13052-13075/com.dodola.atrace I/HOOOOOOOOK: ========= E
2020-02-05 10:58:00.892 13052-13052/com.dodola.atrace I/HOOOOOOOOK: ========= E
2020-02-05 10:58:00.892 13052-13075/com.dodola.atrace I/HOOOOOOOOK: ========= B|13052|query
2020-02-05 10:58:00.892 13052-13075/com.dodola.atrace I/HOOOOOOOOK: ========= E
2020-02-05 10:58:00.892 13052-13075/com.dodola.atrace I/HOOOOOOOOK: ========= B|13052|query
2020-02-05 10:58:00.892 13052-13075/com.dodola.atrace I/HOOOOOOOOK: ========= E
2020-02-05 10:58:00.892 13052-13052/com.dodola.atrace I/HOOOOOOOOK: ========= E
2020-02-05 10:58:00.892 13052-13075/com.dodola.atrace I/HOOOOOOOOK: ========= B|13052|query
2020-02-05 10:58:00.892 13052-13075/com.dodola.atrace I/HOOOOOOOOK: ========= E
2020-02-05 10:58:00.892 13052-13075/com.dodola.atrace I/HOOOOOOOOK: ========= B|13052|query
2020-02-05 10:58:00.892 13052-13075/com.dodola.atrace I/HOOOOOOOOK: ========= E
2020-02-05 10:58:00.892 13052-13075/com.dodola.atrace I/HOOOOOOOOK: ========= B|13052|setBuffersDimensions
2020-02-05 10:58:00.892 13052-13075/com.dodola.atrace I/HOOOOOOOOK: ========= E
2020-02-05 10:58:00.892 13052-13075/com.dodola.atrace I/HOOOOOOOOK: ========= B|13052|dequeueBuffer
2020-02-05 10:58:00.894 13052-13075/com.dodola.atrace I/HOOOOOOOOK: ========= B|13052|importBuffer
2020-02-05 10:58:00.894 13052-13075/com.dodola.atrace I/HOOOOOOOOK: ========= B|13052|HIDL::IMapper::importBuffer::passthrough
2020-02-05 10:58:00.894 13052-13075/com.dodola.atrace I/HOOOOOOOOK: ========= E
2020-02-05 10:58:00.894 13052-13075/com.dodola.atrace I/HOOOOOOOOK: ========= E
2020-02-05 10:58:00.894 13052-13058/com.dodola.atrace I/HOOOOOOOOK: ========= B|13052|Compiling
2020-02-05 10:58:00.894 13052-13075/com.dodola.atrace I/HOOOOOOOOK: ========= E
2020-02-05 10:58:00.894 13052-13075/com.dodola.atrace I/HOOOOOOOOK: ========= B|13052|query

需要注意的是,日誌中的B代表begin,也就是對應時間開始的時間,而E代表End,即對應事件結束的時間,並且,B|事件和E|事件是成對出現的,這樣我們就可以通過該事件的結束時間減去對應的開始時間來獲得每個事件使用的時間。例如,上述log中我們可以看出TextView的draw方法顯示使用了3ms。

此外,在下面這個項目裏展示瞭如何使用 PLTHook 技術來獲取線程創建的堆棧。

2、使用PLTHook技術來獲取線程創建的堆棧

使用 PLTHook 技術來獲取線程創建的堆棧-項目地址

運行項目後,我們點擊開啓 Thread Hook按鈕,然後點擊新建 Thread按鈕。最後可以在Logcat 中看到Thread創建的堆棧信息:

2020-02-05 13:47:59.006 20159-20159/com.dodola.thread E/HOOOOOOOOK: stack:com.dodola.thread.ThreadHook.getStack(ThreadHook.java:16)
com.dodola.thread.MainActivity$2.onClick(MainActivity.java:40)
android.view.View.performClick(View.java:6311)
android.view.View$PerformClick.run(View.java:24833)
android.os.Handler.handleCallback(Handler.java:794)
android.os.Handler.dispatchMessage(Handler.java:99)
android.os.Looper.loop(Looper.java:173)
android.app.ActivityThread.main(ActivityThread.java:6653)
java.lang.reflect.Method.invoke(Native Method)
com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:547)
com.android.internal.os.ZygoteInit.main(ZygoteInit.java:821)
2020-02-05 13:47:59.007 20159-20339/com.dodola.thread E/HOOOOOOOOK: thread name:Thread-2
2020-02-05 13:47:59.008 20159-20339/com.dodola.thread E/HOOOOOOOOK: thread id:1057
2020-02-05 13:47:59.009 20159-20339/com.dodola.thread E/HOOOOOOOOK: stack:com.dodola.thread.ThreadHook.getStack(ThreadHook.java:16)
com.dodola.thread.MainActivity$2$1.run(MainActivity.java:38)
2020-02-05 13:47:59.011 20159-20340/com.dodola.thread E/HOOOOOOOOK: inner thread name:Thread-3
2020-02-05 13:47:59.012 20159-20340/com.dodola.thread E/HOOOOOOOOK: inner thread id:1058

由於Profilo與PLT Hook涉及了大量的C/C++、NDK開發的知識,限於篇幅,所以這部分不做詳細講解,如對NDK開發感興趣的同學可以期待下我後面的Awesome-Android-NDK系列文章,等性能優化系列文章更新完畢之後,就會開始去系統地學習NDK相關的開發知識,敬請期待。

二、自動化卡頓檢測方案及優化

1、爲什麼需要自動化卡頓檢測方案?

主要有一下兩點原因:

  • 1、Cpu Profiler、Systrace等系統工具僅適合線下針對性分析。
  • 2、線上及測試環境需要自動化的卡頓檢方案來定位卡頓,同時,更重要的是,它能記錄卡頓發生時的場景。

2、卡頓檢測方案原理

它的原理源於Android的消息處理機制,一個線程不管有多少Handler,它只會有一個Looper存在,主線程執行的任何代碼都會通過Looper.loop()方法執行。而在Looper函數中,它有一個mLogging對象,這個對象在每個message處理前後都會被調用。主線程發生了卡頓,那一定是在dispatchMessage()方法中執行了耗時操作。那麼,我們就可以通過這個mLogging對象對dispatchMessage()進行監控

卡頓檢測方案的具體實現步驟

首先,我們看下Looper用於執行消息循環的loop()方法,關鍵代碼如下所示:

/**
 * 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()方法中,在其執行每一個消息(註釋2處)的前後都由logging進行了一次打印輸出。可以看到,在執行消息前是輸出的">>>>> Dispatching to “,在執行消息後是輸出的”<<<<< Finished to ",它們打印的日誌是不一樣的,我們就可以由此來判斷消息執行的前後時間點。

所以,具體的實現可以歸納爲如下步驟:

  • 1、首先,我們需要使用Looper.getMainLooper().setMessageLogging()去設置我們自己的Printer實現類去打印輸出logging。這樣,在每個message執行的之前和之後都會調用我們設置的這個Printer實現類。
  • 2、如果我們匹配到">>>>> Dispatching to "之後,我們就可以執行一行代碼:也就是在指定的時間閾值之後,我們在子線程去執行一個任務,這個任務就是去獲取當前主線程的堆棧信息以及當前的一些場景信息,比如:內存大小、電腦、網絡狀態等。
  • 3、如果在指定的閾值之內匹配到了"<<<<< Finished to ",那麼說明message就被執行完成了,則表明此時沒有產生我們認爲的卡頓效果,那我們就可以將這個子線程任務取消掉。

3、AndroidPerformanceMonitor

它是一個非侵入式的性能監控組件,可以通過通知的形式彈出卡頓信息。它的原理就是我們剛剛講述到的卡頓監控的實現原理。

接下我們通過一個簡單的示例來講解一下它的使用。

首先,我們需要在moudle的build.gradle下配置它的依賴,如下所示:

// release:項目中實現了線上監控體系的時候去使用
api 'com.github.markzhai:blockcanary-android:1.5.0'

// 僅在debug包啓用BlockCanary進行卡頓監控和提示的話,可以這麼用
debugApi 'com.github.markzhai:blockcanary-android:1.5.0'
releaseApi 'com.github.markzhai:blockcanary-no-op:1.5.0'

其次,在Application的onCreate方法中開啓卡頓監控:

 // 注意在主進程初始化調用
BlockCanary.install(this, new AppBlockCanaryContext()).start();

最後,繼承BlockCanaryContext類去實現自己的監控配置上下文類:

public class AppBlockCanaryContext extends BlockCanaryContext {
    // 實現各種上下文,包括應用標識符,用戶uid,網絡類型,卡頓判斷闕值,Log保存位置等等

    /**
    * 提供應用的標識符
    *
    * @return 標識符能夠在安裝的時候被指定,建議爲 version + flavor.
    */
    public String provideQualifier() {
        return "unknown";
    }

    /**
    * 提供用戶uid,以便在上報時能夠將對應的
    * 用戶信息上報至服務器 
    *
    * @return user id
    */
    public String provideUid() {
        return "uid";
    }

    /**
    * 提供當前的網絡類型
    *
    * @return {@link String} like 2G, 3G, 4G, wifi, etc.
    */
    public String provideNetworkType() {
        return "unknown";
    }

    /**
    * 配置監控的時間區間,超過這個時間區間    ,BlockCanary將會停止, use
    * with {@code BlockCanary}'s isMonitorDurationEnd
    *
    * @return monitor last duration (in hour)
    */
    public int provideMonitorDuration() {
        return -1;
    }

    /**
    * 指定判定爲卡頓的閾值threshold (in millis),  
    * 你可以根據不同設備的性能去指定不同的閾值
    *
    * @return threshold in mills
    */
    public int provideBlockThreshold() {
        return 1000;
    }

    /**
    * 設置線程堆棧dump的間隔, 當阻塞發生的時候使用, BlockCanary 將會根據
    * 當前的循環週期在主線程去dump堆棧信息
    * <p>
    * 由於依賴於Looper的實現機制, 真實的dump週期 
    * 將會比設定的dump間隔要長(尤其是當CPU很繁忙的時候).
    * </p>
    *
    * @return dump interval (in millis)
    */
    public int provideDumpInterval() {
        return provideBlockThreshold();
    }

    /**
    * 保存log的路徑, 比如 "/blockcanary/", 如果權限允許的話,
    * 會保存在本地sd卡中
    *
    * @return path of log files
    */
    public String providePath() {
        return "/blockcanary/";
    }

    /**
    * 是否需要通知去通知用戶發生阻塞
    *
    * @return true if need, else if not need.
    */
    public boolean displayNotification() {
        return true;
    }

    /**
    * 用於將多個文件壓縮爲一個.zip文件
    *
    * @param src  files before compress
    * @param dest files compressed
    * @return true if compression is successful
    */
    public boolean zip(File[] src, File dest) {
        return false;
    }

    /**
    * 用於將已經被壓縮好的.zip log文件上傳至
    * APM後臺
    *
    * @param zippedFile zipped file
    */
    public void upload(File zippedFile) {
        throw new UnsupportedOperationException();
    }

    /**
    * 用於設定包名, 默認使用進程名,
    *
    * @return null if simply concern only package with process name.
    */
    public List<String> concernPackages() {
        return null;
    }

    /**
    * 使用 @{code concernPackages}方法指定過濾的堆棧信息 
    *
    * @return true if filter, false it not.
    */
    public boolean filterNonConcernStack() {
        return false;
    }

    /**
    * 指定一個白名單, 在白名單的條目將不會出現在展示阻塞信息的UI中
    *
    * @return return null if you don't need white-list filter.
    */
    public List<String> provideWhiteList() {
        LinkedList<String> whiteList = new LinkedList<>();
        whiteList.add("org.chromium");
        return whiteList;
    }

    /**
    * 使用白名單的時候,是否去刪除堆棧在白名單中的文件
    *
    * @return true if delete, false it not.
    */
    public boolean deleteFilesInWhiteList() {
        return true;
    }

    /**
    * 阻塞攔截器, 我們可以指定發生阻塞時應該做的工作
    */
    public void onBlock(Context context, BlockInfo blockInfo) {

    }
}

可以看到,在上述配置中,我們指定了卡頓的閾值爲1000ms。接下來,我們可以測試一下BlockCanary監測卡頓時的效果,這裏我在Activity的onCreate方法中添加如下代碼使線程休眠3s:

try {
    Thread.sleep(3000);
} catch (InterruptedException e) {
    e.printStackTrace();
}

然後,我們運行項目,打開App,即可看到類似LeakCanary界面那樣的卡頓信息堆棧。

除了發生卡頓時BlockCanary提供的圖形界面可供開發和測試人員直接查看卡頓原因之外。其最大的作用還是在線上環境或者自動化monkey測試的環節進行大範圍的log採集與分析,對於分析的緯度,可以從以下兩個緯度來進行:

  • 卡頓時間。
  • 根據同堆棧出現的卡頓次數來進行排序和歸類。

BlockCanary的優勢如下

  • 非侵入式。
  • 方便精準,能夠定位到代碼的某一行代碼。

那麼這種自動檢測卡頓的方案有什麼問題嗎?

在卡頓的週期之內,應用確實發生了卡頓,但是獲取到的卡頓信息可能會不準確,和我們的OOM一樣,也就是最後的堆棧信息僅僅只是一個表象,並不是真正發生問題時的一個堆棧。下面,我們先看下如下的一個示意圖:

image

假設主線程在T1到T2的時間段內發生了卡頓,卡頓檢測方案獲取卡頓時的堆棧信息是T2時刻,但是實際上發生卡頓的時刻可能是在這段時間區域內另一個耗時過長的函數,那麼可能在我們捕獲卡頓的時刻時,真正的卡頓時機已經執行完成了,所以在T2時刻捕獲到的一個卡頓信息並不能夠反映卡頓的現場,也就是最後呈現出來的堆棧信息僅僅只是一個表象,並不是真正問題的藏身之處。

那麼,我們如何對這種情況進行優化呢?

我們可以獲取卡頓週期內的多個堆棧,而不僅僅是最後一個,這樣的話,如果發生了卡頓,我們就可以根據這些堆棧信息來清晰地還原整個卡頓現場。因爲我們有卡頓現場的多個堆棧信息,我們完全知道卡頓時究竟發生了什麼,到底哪些函數它的調用時間比較長。接下來,我們看看下面的卡頓檢測優化流程圖:

image

根據圖中,可以梳理出優化後的具體實現步驟爲:

  • 1、首先,我們會通過startMonitor方法對這個過程進行監控。
  • 2、接着,我們就開始高頻採集堆棧信息。如果發生了卡頓,我們就會調用endMonitor方法
  • 3、然後,將之前我們採集的多個堆棧信息記錄到文件中。
  • 4、最後,在合適的時機上報給我們的服務器。

通過上述的優化,我們就可以知道在整個卡頓週期之內,究竟是哪些方法在執行,哪些方法比較耗時。

但是這種海量卡頓堆棧的處理又存在着另一個問題,那就是高頻卡頓上報量太大,服務器壓力較大,這裏我們來分析下如何減少服務端對堆棧信息的處理量

在出現卡頓的情況下,我們採集到了多個堆棧,大概率的情況下,可能會存在多個重複的堆棧,而這個重複的堆棧信息纔是我們應該關注的地方。我們可以對一個卡頓下的堆棧進行能hash排重,找出重複的堆棧。這樣,服務器需要處理的數據量就會大大減少,同時也過濾出了我們需要重點關注的對象。對於開發人員來說,就能更快地找到卡頓的原因。

4、小結

在本節中,我們學習了自動化卡頓檢測的原理,然後,我們使用這種方案進行了實戰,最後,我還介紹了這種方案的問題和它的優化思路。

三、總結

在本篇文章中,我們主要對卡頓優化分析方法與工具
、自動化卡頓檢測方案及優化相關的知識進行了全面且深入地講解,這裏再簡單總結一下本篇文章涉及的兩大主題:

  • 1、卡頓優化分析方法與工具:背景介紹、卡頓分析方法之使用shell命令分析CPU耗時、卡頓優化工具。
  • 2、自動化卡頓檢測方案及優化:卡頓檢測方案原理、AndroidPerformanceMonitor實戰及其優化。

下篇,筆者將帶領大家更加深入地去學習卡頓優化的相關知識,敬請期待~

參考鏈接:

1、國內Top團隊大牛帶你玩轉Android性能分析與優化 第6章 卡頓優化

2、極客時間之Android開發高手課 卡頓優化

3、《Android移動性能實戰》第四章 CPU

4、《Android移動性能實戰》第七章 流暢度

5、Android dumpsys cpuinfo 信息解讀

6、如何清楚易懂的解釋“UV和PV"的定義?

7、nanoscope-An extremely accurate Android method tracing tool

8、DroidAssist-A lightweight Android Studio gradle plugin based on Javassist for editing bytecode in Android.

9、lancet-A lightweight and fast AOP framework for Android App and SDK developers

10、MethodTraceMan-用於快速找到高耗時方法,定位解決Android App卡頓問題

11、Linux環境下進程的CPU佔用率

12、使用 ftrace

13、profilo-A library for performance traces from production

14、ftrace 簡介

15、atrace源碼

16、AndroidAdvanceWithGeektime
/ Chapter06

17、AndroidAdvanceWithGeektime
/ Chapter06-plus

讚賞

如果這個庫對您有很大幫助,您願意支持這個項目的進一步開發和這個項目的持續維護。你可以掃描下面的二維碼,讓我喝一杯咖啡或啤酒。非常感謝您的捐贈。謝謝!


Contanct Me

● 微信:

歡迎關注我的微信:bcce5360

● 微信羣:

微信羣如果不能掃碼加入,麻煩大家想進微信羣的朋友們,加我微信拉你進羣。

● QQ羣:

2千人QQ羣,Awesome-Android學習交流羣,QQ羣號:959936182, 歡迎大家加入~

About me

很感謝您閱讀這篇文章,希望您能將它分享給您的朋友或技術羣,這對我意義重大。

希望我們能成爲朋友,在 Github掘金上一起分享知識。

發佈了16 篇原創文章 · 獲贊 0 · 訪問量 3910
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章