使用WebView監控網頁載入狀況,PerformanceMonitor,WebViewClient生命週期
原理:WebView載入Url完成後,注入js指令碼,指令碼程式碼使用W3C的PerformanceTimingAPI,
往js指令碼傳入一個Android物件(程式碼中為AndroidObject),在js指令碼中呼叫AndroidObject中的介面,以此方式將結果傳回到Android程式碼中。
可獲取的資訊:
坑(注意):
1、WebViewClent的onPageFinished()方法在不同的機型下會有不同的回撥情況,在所測機型中魅族Pro6只會在全部網頁資源載入完成以及 webView.getProgress()==100 的情況下才會回撥,且只會回撥一次,華為Mate7則會在所有載入網頁資源尚未載入完成( webView.getProgress()<100
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使用mWebView.postDelayed銷燬mWebViewRunnable() { 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); } } };使用mHandler.handleMessage()銷燬mWebView
執行結果:
可通過修改注入的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/");Main.java
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 }MyWebViewClient.java
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 }AndroidObject.java
拓展:
Performance Timing API :
WebView開發:
WebView攔截過濾Url: