原理:WebView加载Url完成后,注入js脚本,脚本代码使用W3C的PerformanceTimingAPI,
往js脚本传入一个Android对象(代码中为AndroidObject),在js脚本中调用AndroidObject中的接口,以此方式将结果传回到Android代码中。
可获取的信息:
坑(注意):
1、WebViewClent的onPageFinished()方法在不同的机型下会有不同的回调情况,在所测机型中魅族Pro6只会在全部网页资源加载完成以及 webView.getProgress()==100 的情况下才会回调,且只会回调一次,华为Mate7则会在所有加载网页资源尚未加载完成( webView.getProgress()<100 )时就会回调,而且即使 webView.getProgress()==100 的情况下也会回调多次,时间难以把握,此时需自己另外加判断,只有在 webView.getProgress()==100 的情况下才执行脚本,且只能执行一次。造成此种情况的原因为WebViewClent底层JNI对不同浏览器内核的适应情况较差,WebChromeClient的onProgressChanged()方法不会出现这种情况。
2、注入js脚本之后需要延时一段时间才能销毁WebView,否则将收不到js返回来的结果,不能在onPageFinished()中(主线程)做耗时操作,需要另外开启一个线程去做延时关闭,然后通过消息机制将销毁WebView的msg发送给Handler处理,在主线中才能销毁WebView。
3、在初始化WebView时是通过 WebView mWebView = new WebView(mContext); 方式,所以在销毁WebView时出现了坑,一开始是使用如下方式进行WebView销毁,但是发现run()方法不被调用,但是hasEnqueue却返回的true,查看文档发现即使在enqueue的情况下该Runnable也并不一定会调用,最后使用 mHandler = new Handler(Lopper.getMainLooper()){...} 方法解决了问题。

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); } } };
执行结果:
可通过修改注入的js脚本获取更多详细信息。
代码:

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
Android 拦截WebView加载URL,控制其加载CSS、JS资源,WebView缓存