前言
隨着移動端業務復雜度的提升,開發同學在編寫業務的時候往往容易忽略性能問題,雖然有贊移動端自研了 APM ,但是 APM 采集的都是線上的數據,無法在 QA 與開發階段提前發現問題,為了保障軟件的穩定性,需要補齊線下監控能力,避免性能問題上線對商家經營過程造成影響。
一、架構設計
整體基於 APM 現有框架迭代線下監控能力,並在端上開發 AWACS 可視化工具,通過全局懸浮窗,並結合提醒能力(彈窗與 Toast 提示)實時通知測試人員進行問題查看,同時后台也會定時分析測試環境采集的性能數據,進行管理與分配。
QA 同學基於 Appium 補齊了 UI 主流程 case ,通過自動化測試最大程度確保每次應用發版的穩定。而設備自動化回歸流程可以與線下監控完美的結合起來,首先每個版本可以確保運行在同一款設備上,其次每個版本運行的主流程用例基本上保持一致,這樣就為應用“前后”版本性能數據分析對比提供了兩個參照條件。性能問題上報后,除了微信Robot通知相關干系人解決問題外,還基於移動端 mPaaS 搭建了問題管理與分配平台,便於跟進與追蹤。
二、監控指標分析
性能監控目前對階段、流量、頁面耗時、 ANR 、慢方法、 fps 等數據做了實時監控,本篇文章只會對階段、流量、頁面耗時進行歸納分析,后面“有贊移動性能監控平台系列文章“會對 ANR 、慢方法、 fps 等監控數據進行總結。
2.1 階段數據
移動端每個業務流程都可以統稱為“階段”,比如 App 啟動、商品加購、商品查詢等,業務方可以對自身需要關心的業務階段進行監控,結合“數據分析”與“告警能力”快速協助業務方排查問題。階段分析包括“方法耗時分析”與“網絡狀況分析”兩個部分,下面會具體介紹。
2.1.1 方法耗時分析
在 App 編譯期會對每個方法進行前后打點,確保運行過程中每個階段方法耗時都可以被自動統計出來,節省手動打點統計成本。
原理
開發 Gradle Plugin 插件,在 App 編譯 Transform 階段( .class 轉換為 .dex 過程),對字節碼進行操作,在方法的開始執行( methodEnter )與結束執行( methodExit )通過 ASM 工具分別插入 MethodBeat 的 i 與 o 方法,對每個方法進行首尾打點,運行時自動統計方法耗時。
分析詳情
應用所有版本產生的階段數據都會上傳到后端,后端會通過定式任務對階段數據進行分析,分析角度分為新增、新減、陡增、陡降4個維度,協助開發綜合對問題進行排查。啟動階段舉例:
2.1.2 網絡狀況分析
業務方可以定義是否需要監控階段的網絡狀況,比如啟動階段,除了要監控啟動方法耗時之外,網絡狀態也需要進行監控( App 啟動時,硬件負載比較高,過多的網絡 IO 請求會拖累啟動速度)。
原理
有贊零售 App 網絡通過 OkHttp 進行請求,通過自定義攔截器對網絡進行統一攔截,統計每個階段網絡鏈接數量與請求耗時, intercept 方法實現如下:
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
Response response = null;
String url = getRequestUrl(request.url());
if (!TextUtils.isEmpty(url)) {
AppSegmentCache.INSTANCE.setRequestStart(url);
long startNs = System.nanoTime();
try {
response = chain.proceed(request);
} catch (Exception e) {
throw e;
}
long tookMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNs);
AppSegmentCache.INSTANCE.setRequestEnd(url, tookMs);
}
return chain.proceed(request);
}
分析詳情
分為“全部調用”與"重復調用"兩個統計維度,“全部調用”會統計當前版本該階段網絡請求總數,且會與上個版本請求總數進行比較,計算出升降趨勢(提升/下降了 x%),且列出“新增”與“新減”兩個數據維度,協助對網絡狀況進行分析。“重復調用”會統計所有重復調用的接口狀況,包括網絡重復請求的總數,還會全部列出重復調用鏈接內容,方便業務方進行排查優化。
2.2 流量
App 運行過程中主要涉及到接口、文本、視頻、圖片等各種流量請求,往往在開發過程中不太會注意流量消耗這個指標,最近也經常有商家反饋 App 流量消耗比較大,但目前並不能准確的定位流量消耗主因。
2.2.1 原理
分別對 HttpUrlConnection 和 OkHttp 做 Hook ( .class -> .dex transform 流程插樁),所有的請求都得經過 Hook 層,這樣就能統計每個請求的流量大小與 response 內容,便於業務方進行分析。
OkHttpHook
App 編譯期往 OkHttp 中配置 GlobalNetworkInterceptor 攔截器,統計 response length(流量大小)、response content(請求返回內容,如果是 gzip 需要解壓),然后將流量內容寫入文件中(非線上包才會采集),便於分析。
internal object OkHttpHook {
@JvmField
public val globalNetworkInterceptor = Interceptor { chain ->
... ...
// 計算repsonse length(流量大小)
// 讀取response content(如果是gzip需要解壓)
// 流量內容寫入文件中
val fileUrl = File(file, URLEncoder.encode(SimpleDateFormat("yyyy-MM-dd-HH:mm:ss-SSS").format(Date()) + "-" + netPackInfo.url))
fileUrl.writeText(netPackInfo.toString())
... ...
}
HttpUrlConnectHook
編譯期對 HttpUrlConnection 進行 Hook ,底層網絡請求其實是代理到 OkhttpClient 上,這樣就能保證 HttpUrlConnection 所有網絡請求也能通過 OkHttp 進行處理,這樣流量攔截器( GlobalNetworkInterceptor )就可以進行復用了。
public object HttpUrlConnectHook {
@JvmStatic
fun proxy(httpUrlConnection: URLConnection): URLConnection {
try {
return hookOkHttpURLConnection(httpUrlConnection)
} catch (e: Exception) {
e.printStackTrace()
}
return urlConnection
}
}
@Throws(Exception::class)
private fun hookOkHttpURLConnection(httpUrlConnection: URLConnection): URLConnection {
val builder = OkHttpClient.Builder()
val mClient = builder
.retryOnConnectionFailure(true)
... ...
.build()
val strUrl = httpUrlConnection.url.toString()
val url = URL(strUrl)
val protocol = url.protocol.toLowerCase(Locale.ROOT)
if (protocol.startWith("http", ignoreCase = true)) {
return HttpUrlFactory.OkHttpURLConnection(url, mClient)
} else urlConnection
}
2.2.2 分析詳情
通過展示網絡各個統計指標數據詳情,包括每個接口的請求流量大小、請求次數、請求內容,便於技術人員對流量問題進行分析。
2.3 頁面耗時
有贊零售面向B端的產品,適配了很多低端收銀機,在頁面流暢性有嚴格要求,通過頁面耗時監控,統計每個頁面( Activity | Fragment )的耗時,當頁面耗時超過閾值時,會生成問題,分配給相應的處理人進行修復。
2.3.1 原理
監控 Activity 與 Fragment onCreate() 方法開始執行作為頁面繪制開始時機,頁面 onDraw() 方法第一次回調時機作為頁面繪制結束時機,兩個時機做減法,算出頁面渲染耗時。
頁面監控開始時機
在 ActivityLifecycleCallbacks 全局監聽 Activity 生命周期,在 onActivityCreated() 方法中調用 watchActivity() 方法,watchActivity 除了統一對 Activity 頁面預埋開始時機外,還會區分 Activity 類型,對 Activity 內嵌的 Fragment 注冊 FragmentLifecycleCallbacks 監聽,同樣在 onFragmentViewCreate() 回調中對Fragment頁面開始時機進行預埋。
public void watchActivity(Activity activity) {
watchWithMonitorView(activity.getClass().getName(), activity.getWindow().getDecorView());
... ...
if (activity instanceof android.support.v4.app.FragmentActivity) {
((FragmentActivity) activity).getSupportFragmentManager().registerFragmentLifecycleCallbacks(new FragmentLifecycleCallbacks() {
public void onFragmentViewCreated(android.support.v4.app.FragmentManager fm, final android.support.v4.app.Fragment f, View v,
Bundle savedInstanceState) {
watchWithMonitorView(f.getClass().getName(), v);
}
}), true);
}
... ...
}
頁面監控結束時機
監控頁面根布局 onDraw() 第一次回調,定為頁面繪制結束時機。
public void watchWithMonitorView(final String className, final View view) {
final long startTime = System.currentTimeMillis();
final WeakReference<View> viewWeakReference = new WeakReference<>(view);
final ViewTreeObserver.OnDrawListener onDrawListener = new ViewTreeObserver.OnDrawListener() {
Boolean first = true;
@Override
public void onDraw() {
if (startTime != 0 && first && viewWeakReference.get() != null) {
... ...
}
};
view.getViewTreeObserver().addOnDrawListener(onDrawListener);
}
2.3.2 分析詳情
在設備自動化回歸過程中一個頁面會被多次調用,在線下監控環境中,只有一個頁面3次超過耗時閾值( 200ms )才會算成有效的頁面卡頓問題,防止硬件不穩定造成問題誤報。
三、后台問題分析
設備自動化回歸過程中產生的性能數據存在一定的波動性,后台需要對批量性能數據進行算法校驗,評估出合理的有效問題,減少問題誤報。
分析工作流程(階段數據分析舉例):
設備自動化回歸過程采集到階段數據后,上傳到移動網關,晚上7點啟動定時任務,在后台拉取當前應用版本各個階段最近n條數據,進行數據聚合分析,計算出合理的階段耗時平均值,再同樣拉取當前應用上個版本各個階段的最近n條數據,同樣算出階段耗時平均值,與當前版本階段耗時平均值進行對比,算出漲跌幅度,如果超出閾值就會當成有效問題,進行分配與告警。
四、線下AWACS工具
在 QA 與開發過程中, App 上會懸浮告警 ICON ,開發者可以點擊告警 ICON 打開性能監控中心進行數據查看。性能監控中心會展示階段、 ANR 、慢方法、流量、 FPS 等性能數據,便於開發對問題進行排查。
五、問題管理與分配平台
后台對問題進行分析后,如果是有效問題會落到后台 db 中,前台在 mPaaS 搭建一套問題查看與分配 UI 看板,方便業務方對問題進行處理與狀態跟進。
5.1 問題列表
APM 監控的所有性能指標都可以在性能面板中進行切換篩選, tab 選中后再結合應用、狀態、環境篩選器列出問題列表。
5.2 問題詳情
點擊問題列表后跳轉到問題詳情,問題詳情中包含問題發生次數、進度、詳細信息、設備基本信息等,協助開發定位問題。
點擊問題詳情右上角“變更狀態”按鈕,可以變更問題狀態,並可選擇負責人對問題進行分配與跟進。
六、未來規划
1:監控更多維度的數據,包括 cpu 、線程、子線程更新 UI 等。
2:增加更多的自動化測試用例,針對特定的性能場景進行獨立測試(現在測試用例基本上都是主流程,覆蓋度還不太夠)。
3:補齊自動化設備的數量與型號,通過多機型綜合分析性能問題。
4:推廣到公司內部使用,協助解決有贊其他應用端性能問題。
end