App 效能測試揭祕 (Android 篇)

EMAS發表於2020-12-25

阿里雲 雲原生應用研發平臺EMAS 李嘉華(千瞬)

前言

效能測試在移動測試領域一直是一個大難題,它最直觀的表現是使用者在前臺使用 App 時的主觀體驗,然而決定體驗優劣的背後,涉及到了許許多多的技術變遷。

  • 當我們習慣於諾基亞時,智慧機出現了;當我們學會native開發時,hybrid來了;當各種 hybrid 框架下的巨型應用傾向成熟時,小程式出現在了我們眼前;緊接著直播、iot、ar、vr、人工智慧,新的技術與應用場景正在以無法想象的速度向前發展。效能測試技術在快速變化的場景與開發技術面前,面臨著巨大的挑戰,當我們還在糾結如何測試 a 時,b 就已經出來了。
  • 效能測試本身,有發展日漸成熟的解決方案,如線上效能監控APM、線下效能採集工具;有基於各個應用場景衍生的測試技術,如壓力測試、穩定性測試、功耗測試等;也有基於各項效能指標(記憶體、cpu、電量、流量)而來的各種專項測試能力。

我們致力於打造線上線下一體的效能解決方案,希望能夠幫助開發者發現、定位與解決一系列移動端效能問題。本文將著重介紹 EMAS 效能測試平臺的能力與規劃,還是那句話:功能決定現在,效能決定未來

通常我們在採集 Android 裝置效能資料時,都是通過 adb shell 獲取各項系統資料,對採集效率、資料準確度等影響很大。阿里雲移動測試做了大量技術優化創新,目前效能測試採集間隔為1s,並且同時做到了無侵入、低延遲、低功耗。

在介紹技術方案之前,這裡將本文的方案(app_process)與 adb shell 的方案做一組簡單的資料對比。

  • 採集的所有效能資料為:cpu、memory、fps、network
  • 開發環境: java + ddmlib
  • 測試電腦:MacBook Pro (Retina, 15-inch, Mid 2015) 上進行測試
  • 測試裝置:OPPO R17/Android 8.0

image.png

儘管對比的樣本數不多,且不同實現方式也會有些許差異,但基於 app_process 的效能採集方案依然有很明顯的優勢:

  • 效能資料誤差更小。相比之下效能與精度的提升是顯而易見的,在部分手機上app_process的cpu開銷甚至低於1%;
  • 資料採集更及時、響應更快。由於 app_process 介面的高效性,我們在每一秒鐘都會監控被測應用的 pid,實際上效能資料對 APP 重啟等動作的響應是實時的;
  • 相容性更好。ps、top等命令在不同的裝置上可能存在資料格式上的差異,這類不同機型的適配問題在本文方案中是不存在的。

1. APP_PROCESS

在Android系統中,zygote 通過 fork()呼叫一個app_process程式作為App的載體,我們同樣也可以通過app_process執行一個普通的 java 程式,這個java程式可以像 App 一樣通過 binder 跨程式與 system_server 通訊,實現並呼叫一些 Android 系統服務的介面,同時,通過app_process啟動的程式擁有shell等同的許可權,這樣可以完成一些 app 無許可權但是 adb 能夠完成的命令。

通過下圖我們簡單理解一下 Android Binder 與本文的基本原理,更多細節可以自行搜尋學習。通常來說,如果我們的 App 能夠獲取到一個 Manager(如 ActivityManager),那麼 System_Server 中必然存在對應的 Service(如 ActivityManagerService),那麼我們就可以通過 ActivityManagerProxy 與它通訊。
image.png

2. 效能指標

目前 移動測試 效能測試平臺支援採集的效能指標如下:
image.png

2.1 記憶體

指標說明

  • TotalPss: 應用實際佔用實體記憶體
  • NativePss: native程式申請分配的實體記憶體
  • SwapPss: 動態記憶體交換區,zRAM 交換可通過壓縮記憶體頁面並將其放入動態分配的記憶體交換區來增加系統中的可用記憶體量。由於這是以犧牲 CPU 時間為代價來增加少量記憶體,所以 swapPss的異常變化可能對系統效能造成影響。(https://source.android.com/devices/tech/config/low-ram.html)

原理

通過adb shell dumpsys meminfo pid,我們可以獲得如下內容

Applications Memory Usage (in Kilobytes):
Uptime: 543447125 Realtime: 543469686
** MEMINFO in pid 23178 [com.huawei.browser:sandboxed_process0:com.huawei.browser.sandbox.SandboxedProcessService0:6] **
Pss Private Private SwapPss Heap Heap Heap
Total Dirty Clean Dirty Size Alloc Free
------ ------ ------ ------ ------ ------ ------
Native Heap 99 96 0 2028 6656 4327 2328
Dalvik Heap 4 0 0 754 3078 1030 2048
Dalvik Other 4 4 0 366
Stack 8 8 0 26
Other dev 4 0 4 0
.so mmap 535 4 0 319
.jar mmap 114 0 0 0
.apk mmap 2 0 0 0
.dex mmap 622 0 4 2617
.oat mmap 409 0 0 0
.art mmap 259 16 0 2183
Other mmap 14 0 0 6
Unknown 28 28 0 455
TOTAL 10856 156 8 8754 9734 5357 4376
App Summary
Pss(KB)
------
Java Heap: 16
Native Heap: 96
Code: 8
Stack: 8
Graphics: 0
Private Other: 36
System: 10692
TOTAL: 10856 TOTAL SWAP PSS: 8754

在 Android 10 以下的裝置中,我們可以通過activityManager.getProcessMemoryInfo(pids) 獲取程式相關的記憶體資訊,Android 10之後的系統對這個介面加了一些限制,資料更新時間為5分鐘,需要直接呼叫meminfo service 來dump獲取這部分內容。

2.2 CPU

指標說明

ProcessCpu:測試程式CPU使用率
SystemCpu:整機CPU使用率

原理

通過讀取/proc/stat檔案,我們可以看到下面的內容

cpu  2490696 175785 2873834 17973539 12823 680472 230184 0 0 0
cpu0 621631 33199 739364 12893642 10736 365458 86720 0 0 0
cpu1 623944 30576 688904 677748 609 145744 93230 0 0 0
cpu2 519768 33948 650022 685194 703 78117 23873 0 0 0
cpu3 499978 33082 547153 687802 650 81072 21360 0 0 0
cpu4 32586 4853 41910 774975 36 2097 1025 0 0 0
cpu5 30950 5003 40730 776693 19 2060 999 0 0 0
cpu6 99227 22708 109219 722048 23 3970 2140 0 0 0
cpu7 62610 12414 56531 755434 44 1952 836 0 0 0
intr 209333749 0 0 0 0 35952688 0 11796562 7 5 5 17537 80 2431 0 0 0 1069962 0 35 1334360 0 0 0 0 0 11 11 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 34984538 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 505 50695 1174791 345 0 0 0 11301652 24660 0 111 0 0 0 0 0 0 0 0 0 0 0 86153 54 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1099230 0 18 1814 0 0 23 514624 1300943 248469 0 0 0 0 0 97168 60709 1641967 609754 38618 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 519 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1556 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 18 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 5 0 0 0 3548401 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 18 0 0 163911 192365 0 0 0 0 1018 0 1 0 2 0 2 0 2 1 0 0 2 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 56891 4227 147 0 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 751521 0 0 200 0 0 0 0 0 0 0 0 0 0 0 0 0 27 26 26 0 34 50 330 34 0 0 0 0 0 0 0 0 1223 0 11 0 0 0 26

對於上述cpu資料來說,每行CPU的數字依次表示

user (14624) 從系統啟動開始累計到當前時刻,處於使用者態的執行時間 
nice (771) 從系統啟動開始累計到當前時刻
system (8484) 從系統啟動開始累計到當前時刻,處於核心態的執行時間
idle (283052) 從系統啟動開始累計到當前時刻,除IO等待時間以外的其它等待時間
iowait (0) 從系統啟動開始累計到當前時刻,IO等待時間
irq (0) 從系統啟動開始累計到當前時刻,硬中斷時間
softirq (62) 從系統啟動開始累計到當前時刻,軟中斷時間
  • 我們可以得到 cpu 執行時長為 cpu = user + nice + system + iowait + irq + softirq,而總時長為 cpu_total = cpu + idle
  • 由於我們的採集間隔幾乎等於1s,於是過去1s的裝置整體 cpu 使用率為 (cpu - cpu_pre) / (cpu_total - cpu_total_pre)

通過讀取/proc/pid/stat檔案,我們可以看到下面的內容

6873 (a.out) R 6723 6873 6723 34819 6873 8388608 77 0 0 0 41958 31 0 0 25 0 3 0 5882654 1409024 56 4294967295 134512640 134513720 3215579040 0 2097798 0 0 0 0 0 0 0 17 0 0 0

這裡資料較多,依次表示

pid=6873 程式(包括輕量級程式,即執行緒)
comm=a.out 應用程式或命令的名字
task_state=R 任務的狀態,R:runnign, S:sleeping (TASK_INTERRUPTIBLE), D:disk sleep (TASK_UNINTERRUPTIBLE), T: stopped, T:tracing stop,Z:zombie, X:dead
ppid=6723 父程式ID
pgid=6873 執行緒組號
sid=6723 c該任務所在的會話組ID
tty_nr=34819(pts/3) 該任務的tty終端的裝置號,INT34817/256=主裝置號,(34817-主裝置號)=次裝置號
tty_pgrp=6873 終端的程式組號,當前執行在該任務所在終端的前臺任務(包括shell 應用程式)PID
task->flags=8388608 程式標誌位,檢視該任務的特性
min_flt=77 該任務不需要從硬碟拷資料而發生的缺頁(次缺頁)的次數
cmin_flt=0 累計的該任務的所有的waited-for程式曾經發生的次缺頁的次數目
maj_flt=0 該任務需要從硬碟拷資料而發生的缺頁(主缺頁)的次數
cmaj_flt=0 累計的該任務的所有的waited-for程式曾經發生的主缺頁的次數目
utime=41958 該任務在使用者態執行的時間,單位為jiffies
stime=31 該任務在核心態執行的時間,單位為jiffies
cutime=0 累計的該任務的所有的waited-for程式曾經在使用者態執行的時間,單位為jiffies
cstime=0 累計的該任務的所有的waited-for程式曾經在核心態執行的時間,單位為jiffies
priority=25 任務的動態優先順序
nice=0 任務的靜態優先順序
num_threads=3 該任務所在的執行緒組裡執行緒的個數
it_real_value=0 由於計時間隔導致的下一個 SIGALRM 傳送程式的時延,以 jiffy 為單位.
start_time=5882654 該任務啟動的時間,單位為jiffies
vsize=1409024page 該任務的虛擬地址空間大小
rss=56(page) 該任務當前駐留實體地址空間的大小
Number of pages the process has in real memory,minu 3 for administrative purpose.
這些頁可能用於程式碼,資料和棧。
rlim=4294967295bytes 該任務能駐留實體地址空間的最大值
start_code=134512640 該任務在虛擬地址空間的程式碼段的起始地址
end_code=134513720 該任務在虛擬地址空間的程式碼段的結束地址
start_stack=3215579040 該任務在虛擬地址空間的棧的結束地址
kstkesp=0 esp(32 位堆疊指標) 的當前值, 與在程式的核心堆疊頁得到的一致.
kstkeip=2097798 指向將要執行的指令的指標, EIP(32 位指令指標)的當前值.
pendingsig=0 待處理訊號的點陣圖,記錄傳送給程式的普通訊號
block_sig=0 阻塞訊號的點陣圖
sigign=0 忽略的訊號的點陣圖
sigcatch=082985 被俘獲的訊號的點陣圖
wchan=0 如果該程式是睡眠狀態,該值給出排程的呼叫點
nswap swapped的頁數,當前沒用
cnswap 所有子程式被swapped的頁數的和,當前沒用
exit_signal=17 該程式結束時,向父程式所傳送的訊號
task_cpu(task)=0 執行在哪個CPU
task_rt_priority=0 實時程式的相對優先順序別
task_policy=0 程式的排程策略,0=非實時程式,1=FIFO實時程式;2=RR實時程式

通過app_process更優雅

實際上我們並不確定上述資料格式在不同的系統版本或者機型上是否存在相容性,這也是潛在的風險。
而通過 app_process 我們可以直接反射呼叫Process的 readProcFile 介面,很容易獲得 utime 與 stime,這樣就完全消除了相容性問題風險。

  • 由於我們的採集間隔為1s,可以計算程式cpu使用率為 ((utime + stime) - (utime_pre + stime_pre)) / (cpu_total - cpu_total_pre)

介面呼叫虛擬碼如下

Method readProcFile = android.os.Process.class.getMethod("readProcFile", String.class, int[].class, String[].class, long[].class, float[].class);
readProcFile.setAccessible(true);
readProcFile.invoke(null, statFile, PROCESS_STATS_FORMAT, null, statsData, null);
readProcFile.invoke(null, "/proc/stat", SYSTEM_CPU_FORMAT, null, sysCpu, null);

2.3 流量

指標說明

recv:被測應用的下行流量
send:被測應用的上行流量

原理

通過讀取 /proc/pid/net/dev 檔案(低版本直接使用介面TrafficStats.getUidRxBytes()),我們可以獲得如下資料,其中wlan0表示wifi流量,rmnet0表示sim卡流量

解析/proc/%d/net/dev示例結果
Inter-| Receive | Transmit
face |bytes packets errs drop fifo frame compressed multicast|bytes packets errs drop fifo colls carrier compressed
rmnet4: 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
rmnet_tun03: 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
rmnet_r_ims01: 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
rmnet_tun02: 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
dummy0: 0 0 0 0 0 0 0 0 1610 23 0 0 0 0 0 0
rmnet2: 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
rmnet_tun11: 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
rmnet_ims00: 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
rmnet_tun10: 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
rmnet_emc0: 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
rmnet_tun13: 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
rmnet0: 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
rmnet_tun00: 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
rmnet_tun04: 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
rmnet5: 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
wlan0: 1241518561 840807 0 0 0 0 0 7 7225770 73525 0 6 0 0 0 0
rmnet_r_ims00: 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
rmnet3: 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
rmnet_tun01: 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
sit0: 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
rmnet_tun14: 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
ip_vti0: 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
ip6tnl0: 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
rmnet1: 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
ip6_vti0: 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
rmnet_r_ims11: 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
rmnet_r_ims10: 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
rmnet6: 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
rmnet_tun12: 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
lo: 3796620 292 0 0 0 0 0 0 3796620 292 0 0 0 0 0 0
rmnet_ims10: 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0

2.4 流暢度

指標說明

fps:使用者可見的每秒顯示幀數
jank:卡頓發生次數

FPS 原理

關於 fps 的統計存在很多個版本,基於不同方案統計的 fps 其含義完全不一樣。
這裡主要講講 移動測試 在 fps 上的選擇與方案:

Choreographer

Choreographer 需要在 App 中實現,常見於 APM 等效能監控方案上,簡單介紹一下原理:

  1. Vsync 訊號一般來說由硬體產生,負責產生硬體 Vsync 的是 HWC;
  2. DispSync 將 Vsync 生成VSYNC_APP 和 VSYNC_SF 訊號,之後由 Choreographer 和 SurfaceFlinger 使用;
  3. SurfaceFlinger 收到 VSYNC_SF 訊號,開始第 N-1 幀的合成與繪製;
  4. App 收到 VSYNC_APP 訊號,開始第 N 幀渲染;

由於大部分裝置都是60HZ的重新整理頻率,所以VSYNC訊號的週期通常是16.6ms,這個訊號週期的長短可以很直觀的反映應用程式碼實現的效能。但如果App處於“靜止”狀態,VSYNC訊號依然會持續產生,這時GPU繪製可能並未實際發生,這個統計值通常高於我們視覺看到的幀數。fps 的定義為“每秒顯示幀數”或“赫茲”,一般來說FPS用於描述影片、電子繪圖或遊戲每秒播放多少幀。

所以我更加傾向於Choreographer採集的VSYNC訊號是一個流暢度指標(SM),而非真實FPS。

SurfaceFlinger

SurfaceFlinger 接受來自多個資料來源的資料緩衝區資料,通過GPU合成併傳送給顯示裝置。這是我們通常描述的 fps,也是客戶真實可視可體驗到的的幀數資料。

在安卓系統中,WindowManagerService 會對每一個 contentView 建立相關的UI載體Surface,SurfaceFlinger 主要負責將這個 Surface 渲染到手機螢幕上。

除了 Android 主視窗的焦點Activity 與相對應的 ContentView 之外,還存在一種特殊的 SurfaceView, 他會獨享一個 Surface,這個 Surface 獨立渲染非常高效,支援 OpenglES 渲染。也就是說可能會出現兩類視窗fps。一個是Activity視窗幀率和SurfaceView視窗幀率。

一般來說,遊戲、視訊類應用都是通過這種 SurfaceView 來進行繪製,為了能夠儘可能準確的獲取被測應用的幀率,我們預設優先獲取 SurfaceView 的 FPS。

如下是 優酷視訊 我們能獲取到的 Surface 如下:
image.png

然後,再通過dumpsys SurfaceFlinger --latency SurfaceView com.youku.phone/com.youku.ui.activity.DetailActivity 可以準確獲取到視訊視窗的幀繪製資訊。

採用以上方法統計 fps 通常會有以下疑問:

  • SurfaceFlinger必須始終顯示內容,所以當上層並沒有新的快取資料時,SurfaceFlinger會繼續顯示當前資料,因此通過這種方法統計出來的 fps 值一般較低,靜態頁面可能為0,這樣的 fps 值是否具有迷惑性;
  • 比如上圖的視訊應用只有 25fps,我們滑動時可能都有 40+ fps,這個資料能說明什麼樣的問題;
  • fps 波動範圍這麼大,fps 值怎樣才可以描述應用的流暢程度。

回答上面的問題,首先要需要重新定義 FPS != 流暢度。

這裡引用蘋果 WDDC2018 開發者大會的一個分享(https://developer.apple.com/videos/play/wwdc2018/612/)。左圖試圖以 60fps 執行程式,實際只能達到40fps,而右圖實現了穩定的 30fps,右圖的流暢度是明顯要高於左圖的,這種現象稱為 Micro Stuttering。
image.gif

這裡不再深究 Micro Stuttering 的產生原因,回到 FPS 本身,首先 FPS 並不是越高越好,也不是越低就越差。它反映的是一種視覺慣性現象,FPS 值應當是越穩定越好。正如前面優酷視訊的例子中,FPS 基本穩定在25左右,同樣的在各類視訊應用中,我們發現 FPS 幾乎都是穩定在 20+,這已經足夠給我們帶來良好的觀看體驗了。

視覺慣性

視覺預期幀率,使用者潛意識裡認為下幀也應該是當前幀率,比如我們玩遊戲一直是60幀,使用者潛意識裡認為下幀也應該是60幀率。重新整理一直是25幀,使用者潛意識裡認為下幀也應該是25幀率。但是如果60幀一下跳變為25幀,就會產生明顯的卡頓感。

電影幀

電影幀率一般是24幀。電影幀單幀耗時為 1000ms/24≈41.67ms。電影幀率是一個臨界點。低於這個幀率,人眼可以感覺出畫面的不連續性。

JANK 原理

既然 fps 無法完整的描述應用的流暢度,那麼是否可以有一個指標表示應用的流暢程度,換言之,能否描述應用的卡頓程度。答案是 jank。

理解 jank,就一定要理解 google 設計的三重快取機制(如下)。三重快取指的是A、B、C三個快取結構,當 GPU 未能在一次 VSync 時間內完成B的處理,此時display、gpu、cpu 同時在處理A、B、C三個快取,實現資源最大化的利用。
image.png

我們可以通過 dumpsys gfxinfo packageName 獲取到的 janky frames如下。這裡的Janky frames是當一幀的時間大於16.67ms時,就計為一次 Janky frame。

從上文提到的三重快取機制我們可以進行分析,B先導致了一次視覺上的jank,C理論上也是jank(跨VSync),但是由於此時螢幕上顯示的是B,C雖然delay了一幀,但是 C 看起來仍然是緊跟著B顯示在螢幕上,而且 A 順利的在16.67ms完成了繪製,實際上使用者視覺上只少看了一幀,而Janky frames 是 2。我們發現,當 Janky frames 高達近 40% 甚至 50% 時,我們依然感受不到卡頓,這個值並不是理想中的反映流暢度的指標。

Applications Graphics Acceleration Info:
Uptime: 171070276 Realtime: 962775383
** Graphics info for pid 13422 [com.zhongduomei.rrmj.society] **
Stats since: 152741070392878ns
Total frames rendered: 110
Janky frames: 7 (6.36%)
50th percentile: 9ms
90th percentile: 13ms
95th percentile: 18ms
99th percentile: 36ms
Number Missed Vsync: 2
Number High input latency: 0
Number Slow UI thread: 6
Number Slow bitmap uploads: 3
Number Slow issue draw commands: 0

基於以上考慮,我們重新定義 jank 的計算方式:

  • 視覺連續性問題:幀時長 > 前三幀平均時長*2
  • 卡頓問題:幀時長 > 電影幀時長 * 2

假設應用按照電影幀 41.67ms 執行,若幀時長大於 2*41.67ms,意味著在快取機制下,依然必現一次卡頓問題。

3. 其它介面

通過app_process,我們還能夠完成很多其它有趣的事情。

  • 獲得已安裝應用列表。通過 android.content.pm.PackageManager,我們可以獲得所有已安裝應用,同時獲得所有應用的圖示,也可以提前獲得應用程式的所有Service,目前小程式大多使用 Service 層來做邏輯處理、資料請求以及介面呼叫,提前確認要測試的 Service 可以更加精確的完成小程式測試;
  • 獲得更多的裝置硬體資訊,如gpu資訊等等;
  • 獲取裝置視音訊流;
  • 部分實現靜默安裝等。

阿里雲移動研發平臺EMAS https://www.aliyun.com/product/emas
阿里雲移動測試 https://www.aliyun.com/product/mqc
釘釘交流群:11762195

相關文章