1. 程式人生 > >Android WebView 頁面效能監控實現

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

  1. 目前只是完成了對資料的採集,採集之後對資料的處理還沒有進行,但這個跟具體的業務掛鉤,不是這篇文章的重點;
  2. 現在還只能監控到頁面效能資料以及js錯誤等,下一步考慮支援Ajax,獲取ajax請求過程中各個階段的耗時。