原理:WebView加載Url完成後,注入js腳本,腳本代碼使用W3C的PerformanceTimingAPI,javascript
往js腳本傳入一個Android對象(代碼中爲AndroidObject),在js腳本中調用AndroidObject中的接口,以此方式將結果傳回到Android代碼中。html
可獲取的信息:java
坑(注意):android
一、WebViewClent的onPageFinished()方法在不一樣的機型下會有不一樣的回調狀況,在所測機型中魅族Pro6只會在所有網頁資源加載完成以及 webView.getProgress()==100 的狀況下才會回調,且只會回調一次,華爲Mate7則會在全部加載網頁資源還沒有加載完成( webView.getProgress()<100 )時就會回調,並且即便 webView.getProgress()==100 的狀況下也會回調屢次,時間難以把握,此時需本身另外加判斷,只有在 webView.getProgress()==100 的狀況下才執行腳本,且只能執行一次。形成此種狀況的緣由爲WebViewClent底層JNI對不一樣瀏覽器內核的適應狀況較差,WebChromeClient的onProgressChanged()方法不會出現這種狀況。git
二、注入js腳本以後須要延時一段時間才能銷燬WebView,不然將收不到js返回來的結果,不能在onPageFinished()中(主線程)作耗時操做,須要另外開啓一個線程去作延時關閉,而後經過消息機制將銷燬WebView的msg發送給Handler處理,在主線中才能銷燬WebView。github
三、在初始化WebView時是經過 WebView mWebView = new WebView(mContext); 方式,因此在銷燬WebView時出現了坑,一開始是使用以下方式進行WebView銷燬,可是發現run()方法不被調用,可是hasEnqueue卻返回的true,查看文檔發現即便在enqueue的狀況下該Runnable也並不必定會調用,最後使用 mHandler = new Handler(Lopper.getMainLooper()){...} 方法解決了問題。web
1 boolean hasEnqueue = mWebView.postDelayed(new Runnable() { 2 @Override 3 public void run() { 4 //didn't step into here 5 if (mWebView != null) { 6 mWebView.clearCache(true); 7 mWebView.clearHistory(); 8 mWebView.destroy(); 9 mWebView = null; 10 } 11 } 12 }, 500); 13 if(hasEnqueue){ 14 Logger.d("the Runnable was successfully placed in to the message queue."); 15 }else{ 16 Logger.d("the Runnable was failed to be placed in to the message queue."); 17 }
Handler mHandler = new Handler(Looper.getMainLooper()) { @Override public void handleMessage(Message msg) { switch (msg.what) { case MSG_DESTROY: { destroyWebView(); break; } default: super.handleMessage(msg); } } };
執行結果:chrome
可經過修改注入的js腳本獲取更多詳細信息。json
代碼:瀏覽器
1 final MyWebView mWebView = new MyWebView(mContext); 2 mWebView.setVisibility(View.GONE); 3 4 WebSettings setting = mWebView.getSettings(); 5 setting.setJavaScriptEnabled(true); 6 setting.setCacheMode(WebSettings.LOAD_NO_CACHE); 7 setting.setLoadsImagesAutomatically(false); 8 9 MyWebViewClient myWebViewClient = new MyWebViewClient(); 10 myWebViewClient.setTimeOut(timingCheck.getTimeout()); 11 mWebView.setWebViewClient(myWebViewClient); 12 13 mWebView.setAndroidObject(new AndroidObject() { 14 @Override 15 public void handleError(String msg) { 16 Logger.d("AndroidObject,錯誤信息:" + msg); 17 } 18 19 @Override 20 public void handleResource(String jsonStr) { 21 Logger.d("AndroidObject,Timing信息:" + jsonStr); 22 } 23 }); 24 mWebView.loadUrl("http:www.qq.com/");
1 import android.graphics.Bitmap; 2 import android.net.http.SslError; 3 import android.os.Handler; 4 import android.os.Looper; 5 import android.os.Message; 6 import android.webkit.SslErrorHandler; 7 import android.webkit.WebResourceError; 8 import android.webkit.WebResourceRequest; 9 import android.webkit.WebResourceResponse; 10 import android.webkit.WebView; 11 import android.webkit.WebViewClient; 12 13 import com.gomo.health.plugin.plugin.Constants; 14 import com.gomo.health.plugin.utils.Logger; 15 16 import java.util.Timer; 17 import java.util.TimerTask; 18 import java.util.concurrent.atomic.AtomicBoolean; 19 20 21 /** 22 * Created by s_x_q on 2017/4/11. 23 */ 24 public class MyWebViewClient extends WebViewClient { 25 26 private WebView mWebView; 27 private AndroidObject mAndroidObject; 28 29 /** 30 * WebView不支持修改Timeout , 這裏自定義 31 */ 32 private int mTimeOut = 3000; 33 private int mJsTimeout = 500; 34 35 private Timer mTimer = new Timer(); 36 37 /** 38 * 避免重複執行mWebsiteLoadTimeoutTask 39 */ 40 private boolean isWebTimeoutTaskScheduling = false; 41 42 /** 43 * 避免重複執行mJsInjectTimeoutTask 44 */ 45 private boolean isJsTimeoutTaskScheduling = false; 46 47 /** 48 * 判斷網頁加載是否完成 49 */ 50 private AtomicBoolean isWebLoadFinished = new AtomicBoolean(false); 51 52 private TimerTask mWebsiteLoadTimeoutTask = new TimerTask() { 53 @Override 54 public void run() { 55 if (mWebView != null && !isWebLoadFinished.get()) { 56 sendWebsiteLoadTimeoutMsg(); 57 } 58 } 59 }; 60 61 private TimerTask mJsInjectTimeoutTask = new TimerTask() { 62 @Override 63 public void run() { 64 if (mWebView != null && mAndroidObject != null) { 65 if (!mAndroidObject.isDataReturn()) { 66 sendJsInjectTimeoutMsg(); 67 } else { 68 sendDestroyMsg(); 69 } 70 } 71 } 72 }; 73 74 final Handler handler = new Handler(Looper.getMainLooper()) { 75 @Override 76 public void handleMessage(Message msg) { 77 switch (msg.what) { 78 case Constants.HandlerMessage.MSG_DESTROY: { 79 destroyWebView(); 80 break; 81 } 82 case Constants.HandlerMessage.MSG_WEBSITE_LOAD_TIMEOUT: { 83 if (mWebView != null) { 84 Logger.d("網頁加載超時 , WebView進度:" + mWebView.getProgress() + " , url:" + mWebView.getUrl()); 85 if (mWebView.getProgress() < 100) { 86 mAndroidObject.handleError("LoadUrlTimeout"); 87 destroyWebView(); 88 } 89 } 90 break; 91 } 92 case Constants.HandlerMessage.MSG_JS_INJECT_TIMEOUT: { 93 if (mWebView != null) { 94 if (mAndroidObject != null) { 95 if (!mAndroidObject.isDataReturn()) { 96 Logger.d("JS注入腳本執行超時"); 97 String format = "ExecuteJsTimeout(%dms)"; 98 mAndroidObject.handleError(String.format(format, mJsTimeout)); 99 destroyWebView(); 100 } 101 } 102 } 103 break; 104 } 105 106 default: 107 super.handleMessage(msg); 108 } 109 } 110 }; 111 112 @Override 113 public void onPageStarted(WebView view, String url, Bitmap favicon) { 114 super.onPageStarted(view, url, favicon); 115 Logger.d("網頁開始加載:" + url); 116 117 if (mWebView == null) { 118 mWebView = view; 119 if (mWebView instanceof MyWebView) { 120 mAndroidObject = ((MyWebView) mWebView).getAndroidObject(); 121 } 122 } 123 setupWebLoadTimeout(); 124 } 125 126 @Override 127 public boolean shouldOverrideUrlLoading(WebView view, String url) { 128 //只會重定向時回調,而後回調onPageStarted 129 // Logger.d("回調舊版shouldOverrideUrlLoading , url :" + url); 130 return super.shouldOverrideUrlLoading(view, url); 131 } 132 133 @Override 134 public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { 135 //只會重定向時回調,而後回調onPageStarted 136 // Logger.d("回調新版shouldOverrideUrlLoading , request method :" + request.getMethod() + "\t是否爲重定向: " + request.isRedirect() + "\trequest url :" + request.getUrl()); 137 return super.shouldOverrideUrlLoading(view, request); 138 } 139 140 @Override 141 public WebResourceResponse shouldInterceptRequest(WebView view, String url) { 142 //每次請求資源的時候都會在onLoadResource前回調,可用於攔截資源加載,修改request 143 // Logger.d("回調舊版shouldInterceptRequest , url :" + url); 144 return super.shouldInterceptRequest(view, url); 145 } 146 147 @Override 148 public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) { 149 //每次請求資源的時候都會在onLoadResource前回調,可用於攔截資源加載,修改request 150 // Logger.d("回調新版shouldInterceptRequest " ); 151 return super.shouldInterceptRequest(view, request); 152 } 153 154 @Override 155 public void onLoadResource(WebView view, String url) { 156 super.onLoadResource(view, url); 157 // Logger.d("加載網頁資源 , url:" + url + " , WebView進度:" + view.getProgress()); 158 } 159 160 public void onPageFinished(WebView view, String url) { 161 super.onPageFinished(view, url); 162 163 Logger.d("網頁加載完成,WebView進度:" + view.getProgress()); 164 165 166 //可能會在進度<100或==100的狀況下出現屢次onPageFinished回調 167 if (view.getProgress() == 100 && !isWebLoadFinished.get()) { 168 Logger.d("注入js腳本"); 169 //可能會回調屢次 170 isWebLoadFinished.set(true); 171 String format = "javascript:%s.sendResource(JSON.stringify(window.performance.timing));"; 172 String injectJs = String.format(format, MyWebView.ANDROID_OBJECT_NAME); 173 view.loadUrl(injectJs); 174 175 setupJsInjectTimeout(); 176 } 177 } 178 179 @Deprecated 180 @Override 181 public void onReceivedError(WebView view, int errorCode, 182 String description, String failingUrl) { 183 super.onReceivedError(view, errorCode, description, failingUrl); 184 Logger.d("回調舊版本onReceivedError():" + "錯誤描述:" + description + "\t錯誤代碼:" + errorCode + "失敗的Url:" + failingUrl); 185 186 handleError(description); 187 } 188 189 @Override 190 public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) { 191 super.onReceivedError(view, request, error); 192 Logger.d("回調新版本onReceivedError:"); 193 } 194 195 @Override 196 public void onReceivedHttpError(WebView view, WebResourceRequest request, WebResourceResponse errorResponse) { 197 super.onReceivedHttpError(view, request, errorResponse); 198 Logger.d("回調onRecivedHttpError:"); 199 handleError("onReceivedHttpError"); 200 } 201 202 @Override 203 public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) { 204 super.onReceivedSslError(view, handler, error); 205 Logger.d("回調onReceivedSslError():" + "\terror:" + error.toString()); 206 handleError("onReceivedSslError"); 207 } 208 209 private void handleError(String msg) { 210 isWebLoadFinished.set(true); 211 sendDestroyMsg(); 212 if (mAndroidObject != null) { 213 mAndroidObject.handleError(msg); 214 } 215 216 } 217 218 219 public int getTimeOut() { 220 return mTimeOut; 221 } 222 223 public void setTimeOut(int timeOut) { 224 mTimeOut = timeOut; 225 } 226 227 /** 228 * 網頁加載計時 229 */ 230 private void setupWebLoadTimeout() { 231 if (!isWebTimeoutTaskScheduling) { 232 isWebTimeoutTaskScheduling = true; 233 mTimer.schedule(mWebsiteLoadTimeoutTask, mTimeOut); 234 } 235 } 236 237 /** 238 * 注入js腳本執行計時 239 * <p> 240 * 注入js以後等待一段時間,若是這段時間內js不回調AndroidObject.handleResource(),則再銷燬WebView 241 * 過早銷燬WebView,js不回調AndroidObject.handleResource() 242 */ 243 private void setupJsInjectTimeout() { 244 if (!isJsTimeoutTaskScheduling) { 245 isJsTimeoutTaskScheduling = true; 246 if (mAndroidObject != null) { 247 mAndroidObject.setStartTime(System.currentTimeMillis()); 248 } 249 mTimer.schedule(mJsInjectTimeoutTask, mJsTimeout); 250 } 251 } 252 253 private void sendDestroyMsg() { 254 handler.sendEmptyMessage(Constants.HandlerMessage.MSG_DESTROY); 255 } 256 257 private void sendWebsiteLoadTimeoutMsg() { 258 handler.sendEmptyMessage(Constants.HandlerMessage.MSG_WEBSITE_LOAD_TIMEOUT); 259 } 260 261 private void sendJsInjectTimeoutMsg() { 262 handler.sendEmptyMessage(Constants.HandlerMessage.MSG_JS_INJECT_TIMEOUT); 263 } 264 265 private void destroyWebView() { 266 if (mWebView != null) { 267 mWebView.clearCache(true); 268 mWebView.clearHistory(); 269 mWebView.destroy(); 270 mWebView = null; 271 Logger.d("成功銷燬WebView"); 272 } else { 273 Logger.d("銷燬失敗,WebView爲空"); 274 } 275 } 276 277 }
1 import android.webkit.JavascriptInterface; 2 3 4 /** 5 * Created by s_x_q on 2017/4/11. 6 */ 7 8 public abstract class AndroidObject { 9 10 11 private volatile boolean mIsDataReturn = false ; 12 private long startTime ; 13 private long endTime ; 14 15 /** 16 *用於收集Timing信息 17 * 18 * @param jsonStr 19 */ 20 @JavascriptInterface 21 public void sendResource(String jsonStr) { 22 mIsDataReturn = true ; 23 endTime = System.currentTimeMillis(); 24 Logger.d("js成功執行時間:" + (endTime-startTime)); 25 handleResource(jsonStr); 26 } 27 28 29 /** 30 * 用於收集js的執行錯誤 31 * @param msg 32 */ 33 @JavascriptInterface 34 public void sendError(String msg) { 35 handleError(msg); 36 } 37 38 39 /** 40 * 處理錯誤信息,可能會被回調屢次 41 * @param msg 42 */ 43 public abstract void handleError(String msg) ; 44 45 /** 46 * 47 * @param jsonStr 48 */ 49 public abstract void handleResource(String jsonStr); 50 51 public boolean isDataReturn() { 52 return mIsDataReturn; 53 } 54 55 public long getStartTime() { 56 return startTime; 57 } 58 59 public void setStartTime(long startTime) { 60 this.startTime = startTime; 61 } 62 63 public long getEndTime() { 64 return endTime; 65 } 66 67 public void setEndTime(long endTime) { 68 this.endTime = endTime; 69 } 70 }
package com.sxq.webviewperformancemonitor; import android.content.Context; import android.util.AttributeSet; import android.webkit.WebView; /** * Created by shixiaoqiangsx on 2017/4/11. */ public class MyWebView extends WebView { public final static String ANDROID_OBJECT_NAME = "android"; private AndroidObject mAndroidObject = null; public MyWebView(Context context) { this(context, null, 0); } public MyWebView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public MyWebView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); } public void setAndroidObject(AndroidObject object) { if (object == null) { Logger.d("AndroidObject can not be null !"); this.mAndroidObject = new AndroidObject() { @Override public void handleError(String msg) { } @Override public void handleResource(String jsonStr) { } }; } else { this.mAndroidObject = object; } super.addJavascriptInterface(mAndroidObject, ANDROID_OBJECT_NAME); } protected AndroidObject getAndroidObject() { return this.mAndroidObject; } }
項目地址:
拓展:
Performance Timing API :
W3C:A Primer for Web Performance Timing APIs
WebView開發:
WebView攔截過濾Url:
GoogleChrome高級WebView應用實例,官方Chromium WebView Sample