Android WebView 頁面效能監控實現
在上一篇 Android WebView 開發使用筆記 中記錄了WebView的一些使用方法以及注意事項,在這一篇,我將對WebView中頁面資源載入以及JS錯誤的監控實現進行詳細的介紹。
使用方法
首先貼一下程式碼 https://github.com/jwcqc/WebViewMonitor
核心其實就是 https://github.com/jwcqc/WebViewMonitor/blob/master/app/src/main/assets/collector.js 這個js檔案,當WebView中頁面載入完成後,通過重寫WebViewClient的onPageFinished(WebView view, String url) 方法,呼叫WebView的loadUrl方法載入一段JS,新加一個script標籤到head標籤中,並在script中包含要注入的collecotr.js的url地址,再為加入的script標籤新增onload事件,確保該script已載入完成後呼叫js檔案中編寫好的的startWebViewMonitor()方法即可,程式碼如下:
String inject = "javascript:" +
" (function() { " +
" var script=document.createElement('script'); " +
" script.setAttribute('type','text/javascript'); " +
" script.setAttribute('src', '" + injectJsUrl + "'); " +
" document.head.appendChild(script); " +
" script.onload = function() {" +
" startWebViewMonitor();" +
" }; " +
" }" +
" )();";
webview.loadUrl(inject );
在collecor.js中,分別寫有兩個方法,用來發送監控資訊到Android本地物件中對應的方法上:
function sendResourceTiming(e) {
myObj.sendResource(JSON.stringify(e))
};
function sendErrors() {
var err = errorMonitor.getError();
if (err.length > 0) {
var errorInfo = {
type: "monitor_error",
payload: {
url: hrefUrl,
domain: hostname,
uri: pathname,
error_list: err
}
};
myObj.sendError(JSON.stringify(errorInfo))
}
};
如上面程式碼所示,myObj是通過呼叫WebView的addJavascriptInterface方法新增的一個對映物件,新增的程式碼如下所示:
webview.addJavascriptInterface(new JSObject(), "myObj");
在JSObject類中分別有相應的方法:
public class JSObject {
@JavascriptInterface
public void sendResource(String msg) {
//handleResource(msg);
}
@JavascriptInterface
public void sendError(String msg) {
//handleError(msg);
}
}
到此便可以在sendResource和sendError兩個方法中分別對監控到的資源請求資料、js錯誤資料進行處理,比如儲存到資料庫或傳送給後臺伺服器等,這個則跟具體的業務有關。
可以發現,整個監控過程只需注入一段js到頁面標籤中即可,便會在頁面中自動引入collector.js檔案實現功能,並不需要頁面程式碼進行多餘的操作,整個過程非常的方便。
JS程式碼的實現
頁面耗時、資原始檔耗時的獲得
在collecor.js中主要用到了頁面載入Navigation Timing和頁面資源載入Resource Timing,這兩個API非常有用,可以幫助我們獲取頁面的domready時間、onload時間、白屏時間等,以及單個頁面資源在從傳送請求到獲取到response各階段的效能引數。需要注意的是使用這兩個API需要在頁面完全載入完成之後,但是由於我們是在onPageFinished方法中才插入的js,因此這一點完全不用擔心。
下圖是列出了PerformanceTiming物件包含的頁面效能屬性,其中包括各種與瀏覽器效能有關的時間資料,可以提供瀏覽器處理網頁各個階段的耗時
下圖能更加直觀的展示,這些資料直接的先後次序關係
至於Resource Timing API,這個主要用來獲取到單個靜態資源(JS,CSS,圖片,音訊視訊等等)從開始發出請求到獲取響應之間各個階段的Timing,可以在Chrome的console中輸入performance.getEntries()即可看到效果,它列出了所有靜態資源的陣列列表,如下圖所示:
明白這個原理之後,我們要做的,只需在collector.js中獲得這個performance物件,然後取得需要的屬性進行格式化然後返回即可,程式碼實現如下:
var performanceTiming = function() {
function navigationTiming() {
if (!e.performance || !e.performance.timing) return {};
var time = e.performance.timing;
return {
navigationStart: time.navigationStart,
redirectStart: time.redirectStart,
redirectEnd: time.redirectEnd,
fetchStart: time.fetchStart,
domainLookupStart: time.domainLookupStart,
domainLookupEnd: time.domainLookupEnd,
connectStart: time.connectStart,
secureConnectionStart: time.secureConnectionStart ? time.secureConnectionStart: time.connectEnd - time.secureConnectionStart,
connectEnd: time.connectEnd,
requestStart: time.requestStart,
responseStart: time.responseStart,
responseEnd: time.responseEnd,
unloadEventStart: time.unloadEventStart,
unloadEventEnd: time.unloadEventEnd,
domLoading: time.domLoading,
domInteractive: time.domInteractive,
domContentLoadedEventStart: time.domContentLoadedEventStart,
domContentLoadedEventEnd: time.domContentLoadedEventEnd,
domComplete: time.domComplete,
loadEventStart: time.loadEventStart,
loadEventEnd: time.loadEventEnd,
pageTime: pageTime || (new Date).getTime()
}
}
function resourceTiming() {
if (!e.performance || !e.performance.getEntriesByType) return [];
for (var time = e.performance.getEntriesByType("resource"), resArr = [], i = 0; i < time.length; i++) {
var i = time[i].secureConnectionStart ? time[i].secureConnectionStart: time[i].connectEnd - time[i].secureConnectionStart,
res = {
connectEnd: time[i].connectEnd,
connectStart: time[i].connectStart,
domainLookupEnd: time[i].domainLookupEnd,
domainLookupStart: time[i].domainLookupStart,
duration: time[i].duration,
entryType: time[i].entryType,
fetchStart: time[i].fetchStart,
initiatorType: time[i].initiatorType,
name: time[i].name,
redirectEnd: time[i].redirectEnd,
redirectStart: time[i].redirectStart,
requestStart: time[i].requestStart,
responseEnd: time[i].responseEnd,
responseStart: time[i].responseStart,
secureConnectionStart: i,
startTime: time[i].startTime
};
resArr.push(res);
}
return resArr;
}
return {
cacheResourceTimingLength: 0,
getNavigationTiming: function() {
return navigationTiming();
},
getResourceTiming: function() {
var timing = resourceTiming();
var len = timing.length;
return timing.length != this.cacheResourceTimingLength ?
(timing = timing.slice(this.cacheResourceTimingLength, len), this.cacheResourceTimingLength = len, timing) : []
}
}
}();
最後呼叫performanceTiming.getNavigationTiming()或者performanceTiming.getResourceTiming()便能返回所有資料。
如果需要獲得其他對我們比較有用的頁面效能資料,比如DNS查詢耗時、TCP連結耗時、request請求耗時、解析dom樹耗時、白屏時間、domready時間、onload時間等,可以通過上面的performance.timing各個屬性的差值計算得到,方法如下:
DNS查詢耗時 :domainLookupEnd - domainLookupStart
TCP連結耗時 :connectEnd - connectStart
request請求耗時 :responseEnd - responseStart
解析dom樹耗時 : domComplete- domInteractive
白屏時間 :responseStart - navigationStart
domready時間 :domContentLoadedEventEnd - navigationStart
onload時間 :loadEventEnd - navigationStart
JS錯誤的捕獲
var errorMonitor = function() {
var errors = [];
return e.addEventListener && e.addEventListener("error",
function(e) {
var eInfo = {};
eInfo.time = e.timeStamp || (new Date).getTime(),
eInfo.url = e.filename,
eInfo.msg = e.message,
eInfo.line = e.lineno,
eInfo.column = e.colno,
e.error ? (eInfo.type = e.error.name, eInfo.stack = e.error.stack) : (eInfo.msg.indexOf("Uncaught ") > -1 ? eInfo.stack = eInfo.msg.split("Uncaught ")[1] + " at " + eInfo.url + ":" + eInfo.line + ":" + eInfo.column: eInfo.stack = eInfo.msg + " at " + eInfo.url + ":" + eInfo.line + ":" + eInfo.column, eInfo.type = eInfo.stack.slice(0, eInfo.stack.indexOf(":"))),
eInfo.type.toLowerCase().indexOf("script error") > -1 && (eInfo.type = "ScriptError"),
errors.push(eInfo);
}, !1), {
getError: function() {
return errors.splice(0, errors.length);
}
}
}();
TODO
- 目前只是完成了對資料的採集,採集之後對資料的處理還沒有進行,但這個跟具體的業務掛鉤,不是這篇文章的重點;
- 現在還只能監控到頁面效能資料以及js錯誤等,下一步考慮支援Ajax,獲取ajax請求過程中各個階段的耗時。