(Please understand that this article was written a little late.)

preface

In the above “How to design an elegant and robust Android WebView? (1)”, the author analyzed the current situation of WebView in China, as well as some pits encountered in the process of WebView development. On the basis of stepping pits, this paper focuses on the WebView in the development process of the need to pay attention to the problems, most of these problems can not find the standard answer on the Internet, but it is WebView development process almost will encounter. In addition, WebView optimization will be discussed, aiming to bring better WebView experience to users.

WebView actual combat operation

WebView will encounter a variety of problems in the use of the process, the following for the production of a few WebView problems may occur.

The WebView initialization

Most developers will probably stop at this code for an Action to open a web page:

WebView webview = new WebView(context);
webview.loadUrl(url);
Copy the code

This should be the shortest code to open a normal web page. However, in most cases, you need to do some additional configuration, such as zoom support, Cookie management, password storage, DOM storage, etc. Most of these configurations are in the WebSettings. The details of the configuration are described in the previous article and will not be covered in this article.

Next, imagine if the return request to visit the web page is 30X, such as using HTTP to visit baidu link (www.baidu.com), then the page is blank, GG. Why is that? Since the WebView only loads the first page, it doesn’t matter what happens next. To solve this problem, we need a WebViewClient to let the system handle the redirection for us.

webview.setWebViewClient(new WebViewClient());
Copy the code

In addition to handling redirects, we can override methods in WebViewClient like:

public boolean shouldOverrideUrlLoading(WebView view, String url) 
public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request)
public void onPageStarted(WebView view, String url, Bitmap favicon) 
public void onPageFinished(WebView view, String url) 
public void onLoadResource(WebView view, String url) 
public void onPageCommitVisible(WebView view, String url) 
public WebResourceResponse shouldInterceptRequest(WebView view, String url) 
public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) 
public void onTooManyRedirects(WebView view, Message cancelMsg, Message continueMsg) 
public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) 
public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) 
public void onReceivedHttpError(WebView view, WebResourceRequest request, WebResourceResponse errorResponse) 
public void onFormResubmission(WebView view, Message dontResend, Message resend) 
public void doUpdateVisitedHistory(WebView view, String url, boolean isReload) 
public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) 
public void onReceivedClientCertRequest(WebView view, ClientCertRequest request) 
public void onReceivedHttpAuthRequest(WebView view, HttpAuthHandler handler, String host, String realm) 
public boolean shouldOverrideKeyEvent(WebView view, KeyEvent event) 
public void onUnhandledKeyEvent(WebView view, KeyEvent event) 
public void onScaleChanged(WebView view, float oldScale, float newScale) 
public void onReceivedLoginRequest(WebView view, String realm, String account, String args) 
public boolean onRenderProcessGone(WebView view, RenderProcessGoneDetail detail) 
Copy the code

These methods can refer to the article “WebView use in detail (2) – WebViewClient and common event listener”. There are several methods that may be necessary to override to handle some of the client logic, as I’ll see later.

In addition, the title of a WebView is not static, and the title varies from page to page. Load of the title of the page in WebView will callback WebChromeClient. OnReceivedTitle () method, for developers to set the title. Therefore, it is necessary to set up a WebChromeClient.

webview.setWebChromeClient(new WebChromeClient());
Copy the code

Also, we can override methods in WebChromeClient:

public void onProgressChanged(WebView view, int newProgress) public void onReceivedTitle(WebView view, String title) public void onReceivedIcon(WebView view, Bitmap icon) public void onReceivedTouchIconUrl(WebView view, String url, boolean precomposed) public void onShowCustomView(View view, int requestedOrientation, CustomViewCallback callback) public void onHideCustomView() public boolean onCreateWindow(WebView view, boolean isDialog, boolean isUserGesture, Message resultMsg) public void onRequestFocus(WebView view) public void onCloseWindow(WebView window) public boolean onJsAlert(WebView view, String url, String message, JsResult result) public boolean onJsConfirm(WebView view, String url, String message, JsResult result) public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) public void onExceededDatabaseQuota(String url, String databaseIdentifier, long quota, long estimatedDatabaseSize, long totalQuota, WebStorage.QuotaUpdater quotaUpdater) public void onReachedMaxAppCacheSize(long requiredStorage, long quota, WebStorage.QuotaUpdater quotaUpdater) public void onGeolocationPermissionsShowPrompt(String origin, GeolocationPermissions.Callback callback) public void onGeolocationPermissionsHidePrompt() public void onPermissionRequest(PermissionRequest request) public void onPermissionRequestCanceled(PermissionRequest request) public  boolean onJsTimeout() public void onConsoleMessage(String message, int lineNumber, StringsourceID)
public boolean onConsoleMessage(ConsoleMessage consoleMessage)
public Bitmap getDefaultVideoPoster()
public void getVisitedHistory(ValueCallback<String[]> callback)
public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback, FileChooserParams fileChooserParams)
public void openFileChooser(ValueCallback<Uri> uploadFile, String acceptType, String capture)
public void setupAutoFill(Message msg)
Copy the code

These methods can be specifically described in the article “WebView use in detail (iii) – WebChromeClient and LoadData supplement”. In addition to receiving headers, progress bar changes, WebView requests for local files, requests for geo-location permissions, etc., are implemented through WebChromeClient’s callbacks.

During the initialization phase, if Javascript is enabled, you need to remove the associated security holes, as mentioned in the previous article. Finally, in the kaolawebView.init () method, the following is done:

protected void init() { mContext = getContext(); mWebJsManager = new WebJsManager(); // Initialize the Js managerif(build.version.sdk_int >= build.version_codes.kitkat) {// Enable Chrome debugging according to the local debugging switch WebView.setWebContentsDebuggingEnabled(WebSwitchManager.isDebugEnable()); } / / WebSettings configuration WebViewSettings setDefaultWebSettings (this); / / get deviceId list, safety related WebViewHelper requestNeedDeviceIdUrlList (null); // Set the listener for downloadsetDownloadListener(this); // The front-end control is rolled back to the stack by default. mBackStep = 1; // Redirection protection to prevent blank pages mRedirectProtected =true; // Screenshots are usedsetDrawingCacheEnabled(true); // Initialize the concrete Jsbridge classenableJsApiInternal(); // Initialize WebCache to load the static resource initWebCache(); // Initialize WebChromeClient, overwriting part of the method super.setWebChromeclient (mChromeClient); // Initialize WebViewClient, overriding part of the method super.setWebViewClient(mWebViewClient); }Copy the code

What should a WebView do when loading a web page?

If loading a web page is as simple as calling webView.loadURL (URL), the programmer is in no trouble. Often things are not so simple. Loading a web page is a complex process, during which we may need to perform some operations, including:

  1. Before loading the web page, reset the WebView state and the state of variables bound to the business. WebView state includes redirection state (mTouchByUser), the back stack of front-end control (mBackStep), business status includes progress bar, shared content of current page, display and hide of share button, etc.
  2. Before loading web pages, local client parameters should be combined according to different domains, including basic model information, version information, login information and Refer information used by buried sites, etc. Sometimes additional configuration is needed when transactions and property are involved.
  3. A callback is called when the page load operation beginsWebViewClient.onPageStarted(webview, url, favicon). In this method, you can reset the variable of the redirection protection (mRedirectProtected), of course, you can also reset before the page loads, but optimizations have not been eliminated here due to legacy code issues.
  4. As the page loads, the WebView calls back several methods.
    • WebChromeClient.onReceivedTitle(webview, title)To set the title. Note that in some Versions of the Android system, this method may be called multiple times, and sometimes the callback title is a URL. In this case, the client can take special measures to avoid displaying unnecessary links in the title bar.
    • WebChromeClient.onProgressChanged(webview, progress)Based on this callback, you can control the progress of the progress bar (both show and hide). In general, it takes a long time to reach 100% progress (especially the first load), and users will feel anxious waiting for the progress bar to not disappear for a long time, which will affect the experience. By the time Progress reaches 80, the page is almost ready to load. In fact, most domestic manufacturers will hide the progress bar in advance, letting users think that the web page loads quickly.
    • WebViewClient.shouldInterceptRequest(webview, request)Whether it is a normal page request (using GET/POST), an asynchronous request in the page, or a resource request in the page, this method is called back to give the developer a chance to intercept the request. In this approach, we can intercept static resources and use cached data instead, or we can intercept pages and use our own network framework to request data. Including the WebView streamless solution described later, is also related to this approach.
    • WebViewClient.shouldOverrideUrlLoading(webview, request)This method is called back if a redirect is encountered, or if the A TAB in the page is clicked to jump to the page. This is probably one of the most important callbacks in WebViewWebView interacts with Native pagesThis method is described in detail in the next section.
    • WebViewClient.onReceived**Error(webview, handler, error)An error occurred while loading the page, and this method is called back. These are mainly HTTP errors and SSL errors. In these two callbacks, we can report anomalies, monitor abnormal pages and expired pages, and timely feedback to the operation or front-end modification. When handling SSL errors, you can perform special operations on untrusted certificates. For example, you can determine the domain name and allow the domain name of your own company to avoid the ugly certificate error page. The SSL certificate query window can also be displayed, just as in Chrome, giving users a choice.
  5. When the page is finished loading, it is called backWebViewClient.onPageFinished(webview, url). In this case, you can determine whether to display the close WebView button according to the situation of the rollback. throughmActivityWeb.canGoBackOrForward(-1)Check whether the rollback can be performed.

WebView interacting with JavaScript – JsBridge

Android WebView and JavaScript communication scheme, the industry has a more mature scheme. Common ones are LzyzSD /JsBridge, Pengwei1024 /JsBridge, etc. See this link.

In general, Java calls JS methods in one of two ways:

  • WebView.loadUrl("javascript:" + javascript);
  • WebView.evaluateJavascript(javascript, callbacck);

The first method is no longer recommended. The second method is more convenient and provides a callback to the result, but only supports systems after API 19.

Js invokes Java in four ways:

  • JavascriptInterface
  • WebViewClient.shouldOverrideUrlLoading()
  • WebChromeClient.onConsoleMessage()
  • WebChromeClient.onJsPrompt()

These four methods are no longer described in detail in this article on the Nuggets.

Here’s the JsBridge protocol used by koalas. Java calls JS methods needless to say, call the first method and the second method according to the Android system version. In js invokes the Java methods, the koala is used in the fourth kind of schemes, namely intrusion WebChromeClient. OnJsPrompt (webview, url, message, defaultValue, result) to realize communication.

@Override
public boolean onJsPrompt(WebView view, String url, String message, String defaultValue,
        JsPromptResult result) {
    if(! ActivityUtils. ActivityIsAlive (mContext)) {/ / page is turned off, returned directly try {result. The cancel (); } catch (Exception ignored) { }return true;
    }
    if(mJsApi ! = null && mJsApi.hijackJsPrompt(message)) { result.confirm();return true;
    }
    return super.onJsPrompt(view, url, message, defaultValue, result);
}
Copy the code

The onJsPrompt method is ultimately called back in the main thread, and it is necessary to determine the lifecycle of the container in which the WebView resides. Js and Java method calls are mainly in mjsapi.hijackjsPrompt (message).

public boolean hijackJsPrompt(String message) {
    if (TextUtils.isEmpty(message)) {
        return false;
    }

    boolean handle = message.startsWith(YIXIN_JSBRIDGE);

    if (handle) {
        call(message);
    }

    return handle;
}
Copy the code

First, determine whether the information should be intercepted. If it is allowed to be intercepted, then take out the methods and parameters passed by JS and throw the message to the business layer through Handler.

private void call(String message) {
    // PREFIX
    message = message.substring(KaolaJsApi.YIXIN_JSBRIDGE.length());
    // BASE64
    message = new String(Base64.decode(message));

    JSONObject json = JSONObject.parseObject(message);
    String method = json.getString("method");
    String params = json.getString("params");
    String version = json.getString("jsonrpc");

    if ("2.0".equals(version)) {
        int id = json.containsKey("id")? json.getIntValue("id") : 1; call(id, method, params); } callJS("window.jsonRPC.invokeFinish()"); } private void call(int id, String method, String params) { Message msg = Message.obtain(); msg.what = MsgWhat.JSCall; msg.obj = new KaolaJSMessage(id, method, params); // The message is sent by handler for the receiver to process.if (handler != null) {
	    handler.sendMessage(msg);
	}
}
Copy the code

Jsbridge implements a CommandQueue that stores JsBridge instructions. Each time you need to call jsBridge, you only need to join the queue.

function CommandQueue() {
    this.backQueue = [];
    this.queue = [];
};

CommandQueue.prototype.dequeue = function() {
    if(this.queue.length <=0 && this.backQueue.length > 0) {
        this.queue = this.backQueue.reverse();
        this.backQueue = [];
    }
    return this.queue.pop();
};

CommandQueue.prototype.enqueue = function(item) {
    this.backQueue.push(item);
};

Object.defineProperty(CommandQueue.prototype, 'length',
{get: function() {return this.queue.length + this.backQueue.length; }});

var commandQueue = new CommandQueue();

function _nativeExec(){
    var command = commandQueue.dequeue();
    if(command) {
        nativeReady = false;
        var jsoncommand = JSON.stringify(command);
        var _temp = prompt(jsoncommand,' ');
        return true;
    } else {
        return false; }}Copy the code

The above code has been truncated and some additional configuration is required to perform full JSBridge functionality. For example, tell the front end that the JS code has been injected successfully.

When is it appropriate to inject JS?

If done WebView development, and the need to interact with the js classmate, most will think js in WebViewClient. OnPageFinished () method into the most appropriate, the dom tree has been built, the page has been completely show ^ 1 ^ 3. But if done page loading speed test, will find WebViewClient. OnPageFinished () method will usually need to wait for a long time the callback (load is usually more than 3 s) for the first time, this is because the WebView finish need to load a web page in the main document and all the resources can callback this method. Can the WebViewClient. OnPageStarted injection in ()? The answer is no. After testing, some models work and some don’t. In the WebViewClient. OnPageStarted () to inject and a fatal problem, this method may be callback for many times, can cause js code injection for many times.

On the other hand, since 7.0, there have been some minor changes to the way webViews load JS, and the official recommendation is to put js injection timing after the page has started loading. Quoting official document ^4:

Javascript run before page load

Starting with apps targeting Android 7.0, the Javascript context will be reset when a new page is loaded. Currently, the context is carried over for the first page loaded in a new WebView instance.

Developers looking to inject Javascript into the WebView should execute the script after the page has started to load.

In this article, we also mentioned that the timing of JS injection can be implemented in multiple callbacks, including:

  • onLoadResource
  • doUpdateVisitedHistory
  • onPageStarted
  • onPageFinished
  • onReceivedTitle
  • onProgressChanged

Although the author of this article has done tests to prove that the above timing injection works, he cannot be completely sure that there are no problems. In fact, many of these callbacks will be called multiple times, and there is no guarantee of a successful injection.

WebViewClient. OnPageStarted () too early, WebViewClient. OnPageFinished () too late again, what do you have a more appropriate injection timing? Try the WebViewClient. OnProgressChanged ()? This method calls back several times during the DOM tree rendering process, each time telling us the current loading progress. Doesn’t that tell us that the page has started loading? The koala is using the WebViewClient. OnProgressChanged () method to inject javascript code.

@Override
public void onProgressChanged(WebView view, int newProgress) {
    super.onProgressChanged(view, newProgress);
    if(null ! = mIWebViewClient) { mIWebViewClient.onProgressChanged(view, newProgress); }if (mCallProgressCallback && newProgress >= mProgressFinishThreshold) {
        DebugLog.d("WebView"."onProgressChanged: " + newProgress);
        mCallProgressCallback = false; // mJsApi is not null and allows JS injection.if(mJsApi ! = null && WebJsManager.enableJs(view.getUrl())) { mJsApi.loadLocalJsCode(); }if(mIWebViewClient ! = null) { mIWebViewClient.onPageFinished(view, newProgress); }}}Copy the code

As you can see, we use the mProgressFinishThreshold variable to control the timing of the injection. This corresponds to the previous mention that when progress reaches 80, the loaded page is almost available.

Getting to 80% is easy, but getting to 100% is hard.

For this reason, by the time the page is 80% loaded, the DOM tree is actually rendered, indicating that the WebView has parsed the < HTML > tag, and the injection must be successful. In the WebViewClient. OnProgressChanged () implementation js injection where there are several need to pay attention to:

  1. For the multi-injection control mentioned above, we used the mCallProgressCallback variable control
  2. Before reloading a URL, you need to reset the mCallProgressCallback so that the reloaded page is injected with JS again
  3. The injected progress threshold is free to be customized, theoretically 10%-100% is reasonable, and we used 80%.

H5 page, Weex page and Native page interaction – KaolaRouter

The interaction between H5 page, Weex page and Native page is realized through URL interception. In the WebView WebViewClient. ShouldOverrideUrlLoading () method to obtain the current loading URL, and then put the URL passed to the koala routing framework, you can tell whether the URL can jump to other pages the H5, The Koala routing framework is described in detail in the article “Koala Android Client Routing Bus Design”, but Weex page was not introduced at that time. On how to integrate the communication of the three, there will be detailed introduction in the subsequent article.

In the WebViewClient. ShouldOverrideUrlLoading (), depending on the type of URL did judgment:

public boolean shouldOverrideUrlLoading(WebView view, String url) {
    if (StringUtils.isNotBlank(url) && url.equals("about:blank"Url = getUrl(); } url = WebViewUtils.removeBlank(url); mCallProgressCallback =true; // Enable the third-party application client to be startedif (WebViewUtils.canHandleUrl(url)) {
        boolean handleByCaller = false; // If the operation is not triggered by the user, there is no need to hand it over to the upper layer.if(null ! = mIWebViewClient && isTouchByUser ()) {/ / to the business layer intercept processing handleByCaller = mIWebViewClient. ShouldOverrideUrlLoading (view, url); }if(! HandleByCaller = handleOverrideUrl(url); handleByCaller = handleOverrideUrl; } mRedirectProtected =true;
        return handleByCaller || super.shouldOverrideUrlLoading(view, url);
    } else {
        try {
            notifyBeforeLoadUrl(url);
            // https://sumile.cn/archives/1223.html
            Intent intent = Intent.parseUri(url, Intent.URI_INTENT_SCHEME);
            intent.addCategory(Intent.CATEGORY_BROWSABLE);
            intent.setComponent(null);
            intent.setSelector(null);
            mContext.startActivity(intent);
            if(! mIsBlankPageRedirect) { back(); } } catch (Exception e) { ExceptionUtils.printExceptionTrace(e); }return true;
    }
}

private boolean handleOverrideUrl(final String url) {
   RouterResult result =  WebActivityRouter.startFromWeb(
            new IntentBuilder(mContext, url).setRouterActivityResult(new RouterActivityResult() {
                @Override
                public void onActivityFound() {
                    if(! MIsBlankPageRedirect) {// After successfully intercepting the route, we added a protection mechanism back() to prevent a blank screen when entering the WebView for the first time; } } @Override public voidonActivityNotFound() {}}));return result.isSuccess();
}
Copy the code

There are comments in the code, so I won’t explain them.

WebView pull down refresh implementation

Because the pull-down refresh used by Koala is not the same as the pull-down refresh used by Material Design, SwipeRefreshLayout cannot be applied directly. Koala is using a modified Android PullToRefresh. The PullToRefresh base of the WebView is inherited from the PullToRefreshBase.

/** * Created by: Square Xu * Date: 2017/2/23 * */ public class PullToRefreshWebView extends PullToRefreshBase<KaolaWebview> {public PullToRefreshWebView(Context context) { super(context); } public PullToRefreshWebView(Context context, AttributeSet attrs) { super(context, attrs); } public PullToRefreshWebView(Context context, AttributeSet attrs, int defStyleAttr) { this(context, attrs); } public PullToRefreshWebView(Context context, Mode mode) { super(context, mode); } public PullToRefreshWebView(Context context, Mode mode, AnimationStyle animStyle) { super(context, mode, animStyle); } @Override public OrientationgetPullToRefreshScrollDirection() {
        returnOrientation.VERTICAL; } @Override protected KaolaWebview createRefreshableView(Context context, AttributeSet attrs) { KaolaWebview kaolaWebview = new KaolaWebview(context, attrs); // Solve the problem of flashing when the keyboard is upsetGravity(AXIS_PULL_BEFORE);
        return kaolaWebview;
    }

    @Override
    protected boolean isReadyForPullEnd() {
        return false;
    }

    @Override
    protected boolean isReadyForPullStart() {
        returngetRefreshableView().getScrollY() == 0; }}Copy the code

Koala uses the full-screen mode to realize the immersive status bar and slide back, and the combination of the full-screen mode and WebView pull-down refresh produces a flashing effect on the keyboard. After the research and debugging of the group’s great god (thanks @Junjun), it is found that setGravity(AXIS_PULL_BEFORE) can solve the flashing problem.

How to handle load errors (Http, SSL, Resource)?

There are three types of error callbacks that occur when a WebView loads a web page:

  • WebViewClient.onReceivedHttpError(webView, webResourceRequest, webResourceResponse)

This method is called back for any HTTP request error, including the HTML document request on the main page, iframe, image, and other resource requests. In this callback, because of the many requests mixed up, it is not suitable for displaying the page that loaded the error, but for monitoring the alarm. When a URL or resource receives a large number of alarms, it indicates that the page or resource may have problems. In this case, relevant operations can respond to modification in time.

  • WebViewClient.onReceivedSslError(webview, sslErrorHandler, sslError)

Any HTTPS request that encounters an SSL error will call back to this method. The right thing to do is to let the user choose whether or not to trust the site, and then pop up a trust box for the user to choose from (most regular browsers do this). But people are selfish, let alone when they encounter their own website. We can make certain sites trusted, regardless of whether their credentials are questionable or not. At this point, share a little pit. Koala’s SSL certificate uses GeoTrust’s GeoTrust SSL CA-G3, but on some models, koala’s page will prompt a certificate error. This is where the “magic trick” has to be used – to make all of the koala’s secondary domains trusted.

@Override
public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
    if (UrlUtils.isKaolaHost(getUrl())) {
        handler.proceed();
    } else{ super.onReceivedSslError(view, handler, error); }}Copy the code
  • WebViewClient.onReceivedError(webView, webResourceRequest, webResourceError)

This method is called back only if there is an error with the main page load. This is the perfect way to show loading error pages. However, if you show the error page directly, it is likely to misjudge, causing the user to often load the illusion of page failure. Since different WebView implementations can be different, we first need to rule out a few examples of misjudgment:

  1. The url that failed to load is not the same as the URL in the WebView, exclude;
  2. ErrorCode =-1: indicates that the error is ERROR_UNKNOWN. To ensure no misjudgment, exclude the error
  3. FailingUrl =null&errorCode=-12 because the error URL is null instead of ERROR_BAD_URL, exclude
@Override
public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
    super.onReceivedError(view, errorCode, description, failingUrl);

    // -12 == EventHandle.ERROR_BAD_URL, a hide return code inside android.net.http package
    if((failingUrl ! = null && ! failingUrl.equals(view.getUrl()) && ! failingUrl.equals(view.getOriginalUrl())) /* not subresource error*/ || (failingUrl == null && errorCode ! = - 12) / * not bad url * / | | errorCode = = 1) {/ / when the errorCode = 1 and error information for net: : ERR_CACHE_MISSreturn;
    }

    if(! TextUtils.isEmpty(failingUrl)) {if (failingUrl.equals(view.getUrl())) {
            if(null ! = mIWebViewClient) { mIWebViewClient.onReceivedError(view); }}}}Copy the code

How do I manipulate cookies?

Cookies do not need to be handled by default. If you have special requirements, such as setting extra Cookie fields for a page, you can control them with code. Here are a few useful interfaces:

  • Get all cookies at a url:CookieManager.getInstance().getCookie(url)
  • Determine whether the WebView accepts cookies:CookieManager.getInstance().acceptCookie()
  • Clear Session cookies:CookieManager.getInstance().removeSessionCookies(ValueCallback<Boolean> callback)
  • Clear all cookies:CookieManager.getInstance().removeAllCookies(ValueCallback<Boolean> callback)
  • Cookie persistence:CookieManager.getInstance().flush()
  • Set cookies for a host:CookieManager.getInstance().setCookie(String url, String value)

How to debug WebView loaded pages?

After Android 4.4, you can debug WebView content ^5 using Chrome Developer Tools. Debugging requires setting the debug switch on in the code.

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
    WebView.setWebContentsDebuggingEnabled(true);
}
Copy the code

After this function is enabled, connect to your PC using USB. When loading the URL, open the Chrome Developer tool and enter the URL in the browser

chrome://inspect
Copy the code

To see what page you are currently viewing, click Inspect to see what the WebView loaded.

The WebView optimization

In addition to the basic operations mentioned above to achieve a complete browser function, WebView loading speed, stability and security can be further enhanced and improved. The following WebView optimization scheme is introduced from several aspects. These schemes may not be suitable for all scenarios, but the ideas can be used for reference.

CandyWebCache

We know that js, CSS, and image resources consume a lot of traffic during page loading. Wouldn’t it be nice if these resources were stored locally from the start, or only downloaded once and reused later? Although WebView has several caching schemes ^6, the overall effect is not satisfactory. Based on the idea of self-built cache system, the CandyWebCache project developed by Netease Hangzhou emerged at the historic moment. CandyWebCache is a solution that can cache WebView resources offline and update remote resources in real time. It can download the latest resource files to integrate into APK and update resources online in real time. In the WebView, we need to intercept the WebViewClient. ShouldInterceptRequest () method, to test whether the cache exists, there is direct local cache data, reduce network traffic requests.

@Override
public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
    if (WebSwitchManager.isWebCacheEnabled()) {
        try {
            WebResourceResponse resourceResponse = CandyWebCache.getsInstance().getResponse(view, request);
            returnWebViewUtils.handleResponseHeader(resourceResponse); } catch (Throwable e) { ExceptionUtils.uploadCatchedException(e); }}return super.shouldInterceptRequest(view, request);
}

@Override
public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
    if (WebSwitchManager.isWebCacheEnabled()) {
        try {
            WebResourceResponse resourceResponse = CandyWebCache.getsInstance().getResponse(view, url);
            returnWebViewUtils.handleResponseHeader(resourceResponse); } catch (Throwable e) { ExceptionUtils.uploadCatchedException(e); }}return super.shouldInterceptRequest(view, url);
}
Copy the code

In addition to the caching solution, Tencent’s QQ member team has also launched VasSonic, an open source solution designed to improve the H5’s page-visiting experience, but preferably with the front and back. This whole solution has a lot to learn from, and koalas are learning from.

Https, HttpDns, and CDN

Switching an HTTP request to an HTTPS request reduces the probability of network hijacking, such as JS hijacking and image hijacking. In particular, http2 improves Web performance, reduces network latency, and reduces request traffic.

HttpDns: Sends domain name resolution requests to specific DNS servers over HTTP instead of sending resolution requests to carriers’ Local DNS based on DNS, reducing access failures caused by CARRIERS ‘DNS hijacking. At present, there are still some problems in using HttpDns on WebView, and there is no good solution online (Ali Cloud Android WebView+HttpDns best practice, Tencent cloud HttpDns SDK access, WebView access HttpDns practice), so it is still under investigation.

On the other hand, static resources can be deployed to the multi-channel CDN, which can be directly accessed through the CDN address to reduce the network delay. Multi-channel CDN ensures that the access of large area nodes of a single CDN can be switched to the standby CDN.

WebView independent process

WebView instances on Android7.0 now have the option to run on a separate process. After 8.0, the default is to run in a separate sandbox process ^8. Google is also moving in this direction in the future. For a detailed history of WebView, please refer to section 1 of the previous article “How to Design an Elegant and Robust Android WebView?

Android7.0 system, WebView is relatively stable, regardless of whether the container hosting WebView is in the main process, do not need to worry about WebView crash caused by the application also crash. However, systems under 7.0 are not so lucky, especially older versions of WebView. For application stability, we can wrap webViews under 7.0 with a separate process Activity, so that even if the WebView crashes, only the WebView process crashes, and the main process is not affected.

public static Intent getWebViewIntent(Context context) {
    Intent intent;
    if (isWebInMainProcess()) {
        intent = new Intent(context, MainWebviewActivity.class);
    } else {
        intent = new Intent(context, WebviewActivity.class);
    }
    return intent;
}

public static boolean isWebInMainProcess() {
    return android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N;
}
Copy the code

WebView free flow

From last year, a number of Internet package cards emerged in the market, such as Tencent Wangcard, Ant Bao Card, JINGdong Qiangcard, Aliyu card, netease Platinum card, etc. Compared with traditional carrier packages, these Internet packages have lower rates, more data, and some cards even have special rights — free streaming of certain applications. For example, netease Platinum card can realize stream-free for some applications of netease and Baidu.

Principle of free flow

The principle of common no-stream applications in the market is nothing more than a “special channel”, so that this part of the traffic is not counted in the carrier’s traffic statistics platform. There are several ways to implement this “special channel” in Android.

  1. Small fart. Operators don’t seem to be using this right now, but it can work. Due to the national conditions, not much introduction, understand natural understand.
  2. Global proxy. All traffic is forwarded to the proxy server, and the proxy server determines whether the traffic is driving-free.
  3. IP directly connected. The server determines whether all traffic from this IP address is free of traffic.

WebView stream free solution

For the schemes mentioned above, all the requests of native pages are initiated by the application layer, which is actually easier to implement. However, WebView pages and resource requests are initiated by JNI, so it takes some effort to intercept the requests. Net net all plan, now think there are two kinds of feasible, respectively is the global agent and intercept WebViewClient. ShouldInterceptRequest ().

The global agent

Since WebView does not provide an interface to set up a proxy for a specific WebView instance, we can only do global proxy. Setting up a global proxy requires notifying the system that the proxy environment has changed. Unfortunately, Android does not provide a public interface, so we have to hook the system interface to implement notifications depending on the system version. The system after 6.0 has not yet been tried whether it is feasible. According to the feedback of colleagues in the company, it is consistent with the scheme of 5.0 system.

/**
 * Set Proxy forAndroid 4.1-4.3. */ @suppressWarnings ("all")
private static boolean setProxyJB(WebView webview, String host, int port) {
    Log.d(LOG_TAG, "Setting Proxy with 4.1-4.3 API.");

    try {
        Class wvcClass = Class.forName("android.webkit.WebViewClassic");
        Class wvParams[] = new Class[1];
        wvParams[0] = Class.forName("android.webkit.WebView");
        Method fromWebView = wvcClass.getDeclaredMethod("fromWebView", wvParams);
        Object webViewClassic = fromWebView.invoke(null, webview);

        Class wv = Class.forName("android.webkit.WebViewClassic");
        Field mWebViewCoreField = wv.getDeclaredField("mWebViewCore");
        Object mWebViewCoreFieldInstance = getFieldValueSafely(mWebViewCoreField, webViewClassic);

        Class wvc = Class.forName("android.webkit.WebViewCore");
        Field mBrowserFrameField = wvc.getDeclaredField("mBrowserFrame");
        Object mBrowserFrame = getFieldValueSafely(mBrowserFrameField, mWebViewCoreFieldInstance);

        Class bf = Class.forName("android.webkit.BrowserFrame");
        Field sJavaBridgeField = bf.getDeclaredField("sJavaBridge");
        Object sJavaBridge = getFieldValueSafely(sJavaBridgeField, mBrowserFrame);

        Class ppclass = Class.forName("android.net.ProxyProperties");
        Class pparams[] = new Class[3];
        pparams[0] = String.class;
        pparams[1] = int.class;
        pparams[2] = String.class;
        Constructor ppcont = ppclass.getConstructor(pparams);

        Class jwcjb = Class.forName("android.webkit.JWebCoreJavaBridge");
        Class params[] = new Class[1];
        params[0] = Class.forName("android.net.ProxyProperties");
        Method updateProxyInstance = jwcjb.getDeclaredMethod("updateProxy", params);

        updateProxyInstance.invoke(sJavaBridge, ppcont.newInstance(host, port, null));
    } catch (Exception ex) {
        Log.e(LOG_TAG, "Setting proxy with >= 4.1 API failed with error: + ex.getMessage());
        return false;
    }

    Log.d(LOG_TAG, "Setting proxy with 4.1-4.3 API successful!");
    return true;
}

/**
 * Set Proxy for Android 5.0.
 */
public static void setWebViewProxyL(Context context, String host, int port) {
    System.setProperty("http.proxyHost", host);
    System.setProperty("http.proxyPort", port + "");
    try {
        Context appContext = context.getApplicationContext();
        Class applictionClass = Class.forName("android.app.Application");
        Field mLoadedApkField = applictionClass.getDeclaredField("mLoadedApk");
        mLoadedApkField.setAccessible(true);
        Object mloadedApk = mLoadedApkField.get(appContext);
        Class loadedApkClass = Class.forName("android.app.LoadedApk");
        Field mReceiversField = loadedApkClass.getDeclaredField("mReceivers");
        mReceiversField.setAccessible(true);
        ArrayMap receivers = (ArrayMap) mReceiversField.get(mloadedApk);
        for (Object receiverMap : receivers.values()) {
            for (Object receiver : ((ArrayMap) receiverMap).keySet()) {
                Class clazz = receiver.getClass();
                if (clazz.getName().contains("ProxyChangeListener")) {
                    Method onReceiveMethod = clazz.getDeclaredMethod("onReceive", Context.class, Intent.class); Intent intent = new Intent(Proxy.PROXY_CHANGE_ACTION); onReceiveMethod.invoke(receiver, appContext, intent); } } } } catch (Exception e) { e.printStackTrace(); }}Copy the code

Note that the agent needs to be reset when the WebView exits.

interceptWebViewClient.shouldInterceptRequest()

Intercept WebViewClient. ShouldInterceptRequest () the purpose of the third way is to use free flow – IP to replace. Just look at the code.

@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
@Override
public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
    WebResourceResponse resourceResponse = CandyWebCache.getsInstance().getResponse(view, request);
    if(request.getUrl() ! = null && request.getMethod().equalsIgnoreCase("get")) {
        Uri uri = request.getUrl();
        String url = uri.toString();
        String scheme = uri.getScheme().trim();
        String host = uri.getHost();
        String path = uri.getPath();
        if (TextUtils.isEmpty(path) || TextUtils.isEmpty(host)) {
            returnnull; } // HttpDns resolves network requests and image requests for CSS filesif ((scheme.equalsIgnoreCase("http") || scheme.equalsIgnoreCase("https")) && (path.endsWith(".css")
                || path.endsWith(".png")
                || path.endsWith(".jpg")
                || path.endsWith(".gif")
                || path.endsWith(".js"))) { try { URL oldUrl = new URL(uri.toString()); URLConnection connection; / / get HttpDns DNS results List < String > ips = HttpDnsManager. GetInstance (). GetIPListByHostAsync (host);if(! ListUtils.isEmpty(ips)) { String ip = ips.get(0); String newUrl = url.replaceFirst(oldUrl.getHost(), ip); connection = new URL(newUrl).openConnection(); / / set the HTTP request header Host domain connection. The setRequestProperty ("Host", oldUrl.getHost());
                } else{ connection = new URL(url).openConnection(); / / set the HTTP request header Host domain} String fileExtension = MimeTypeMap. GetFileExtensionFromUrl (url); String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(fileExtension);return new WebResourceResponse(mimeType, "UTF-8", connection.getInputStream()); } catch (MalformedURLException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); }}}return super.shouldInterceptRequest(view, request);
}

@Override
public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
    if(! TextUtils.isEmpty(url) && Uri.parse(url).getScheme() ! = null) { Uri uri = Uri.parse(url); String scheme = uri.getScheme().trim(); String host = uri.getHost(); String path = uri.getPath();if (TextUtils.isEmpty(path) || TextUtils.isEmpty(host)) {
            returnnull; } // HttpDns resolves network requests and image requests for CSS filesif ((scheme.equalsIgnoreCase("http") || scheme.equalsIgnoreCase("https")) && (path.endsWith(".css")
                || path.endsWith(".png")
                || path.endsWith(".jpg")
                || path.endsWith(".gif")
                || path.endsWith(".js"))) { try { URL oldUrl = new URL(uri.toString()); URLConnection connection; / / get HttpDns DNS results List < String > ips = HttpDnsManager. GetInstance (). GetIPListByHostAsync (host);if(! ListUtils.isEmpty(ips)) { String ip = ips.get(0); String newUrl = url.replaceFirst(oldUrl.getHost(), ip); connection = new URL(newUrl).openConnection(); / / set the HTTP request header Host domain connection. The setRequestProperty ("Host", oldUrl.getHost());
                } else{ connection = new URL(url).openConnection(); / / set the HTTP request header Host domain} String fileExtension = MimeTypeMap. GetFileExtensionFromUrl (url); String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(fileExtension);return new WebResourceResponse(mimeType, "UTF-8", connection.getInputStream()); } catch (MalformedURLException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); }}}return super.shouldInterceptRequest(view, url);
}
Copy the code

By using this scheme, the framework used by WebView network request and Native network request can be unified for convenient management.

conclusion

This paper introduces WebView in the development of some practical experience and optimization process. In order to meet business requirements, WebView really provides a very rich interface for the application layer to process business logic. Aiming at the secondary development of WebView, this paper introduces some common callback processing logic and summarizes the experience in the development process. As it is experience, it may not be accurate, if there is any mistake, please point out correction, very grateful!


Refer to the link

  1. medium.com/@filipe.bat…
  2. Stackoverflow.com/questions/2…
  3. Stackoverflow.com/questions/2…
  4. Developer.android.com/about/versi…
  5. Developers.google.com/web/tools/c…
  6. www.jianshu.com/p/5e7075f48…
  7. Developer.android.com/about/versi…
  8. Developer.android.com/about/versi…
  9. Stackoverflow.com/questions/2…
  10. Stackoverflow.com/questions/4…