WebView

Recently, I have been doing web front-end development, including hotel reservation system, background management system and small programs. I just took the opportunity to review Android WebView

For a brief introduction, Android has adopted Chrome kernel since 4.4, so we can use ES6 syntax, CSS3 style and so on when developing web pages

I will be divided into the following modules to introduce Android WebView

Some methods of WebView itself

1. Load a web page: webview.loadurl ("http://www.google.com/"); // Method 2: load the HTML page in the APK package webview.loadurl ("file:///android_asset/test.html"); // Method 3: Load the phone's local HTML page webview. loadUrl("content://com.android.htmlfileprovider/sdcard/test.html");
Copy the code

Under normal circumstances, in the WebView interface, the user click the back button is directly exit the page, of course not what we want, we want is the page itself forward and back, so here is a page forward and back some API

Webview.cangoback (); webView.goback (); webView.cangoforward () Webview.goBackOrForward(int steps) webView. goBackOrForward(int steps)Copy the code
Public Boolean onKeyDown(int keyCode, KeyEvent event) {public Boolean onKeyDown(int keyCode, KeyEvent event) {if ((keyCode == KEYCODE_BACK) && mWebView.canGoBack()) { 
        mWebView.goBack();
        return true;
    }
    return super.onKeyDown(keyCode, event);
}
Copy the code

How do I prevent WebView memory leaks

One rule of thumb to prevent memory leaks is: Don’t play with a long life cycle with a short one. To prevent WebView from causing memory leaks,

  • Instead of defining a WebView in XML, build it using code in the Activity selection and use ApplicationContext
  • When the Activity is destroyed, the WebView is loaded with empty content, the WebView is removed from rootView, the WebView is destroyed, and the content is empty
override fun onDestroy() {
        if(webView ! = null) { webView!! .loadDataWithBaseURL(null,""."text/html"."utf-8", null) webView!! .clearHistory() (webView!! .parent as ViewGroup).removeView(webView) webView!! .destroy() webView = null } super.onDestroy() }Copy the code

WebSetting and WebViewClient, WebChromeClient

  • WebSetting

Function: Configures and manages the WebView

WebSettings webSettings = webView.getSettings(); // Settings can interact with JS. To prevent resource waste, we can set the Activity // onResume totrue, set to onStopfalse
webSettings.setJavaScriptEnabled(true); / / set the adaptive screen, both share / / adjust the images to fit the size of the webview webSettings. SetUseWideViewPort (true); / / resized to the size of the screen webSettings. SetLoadWithOverviewMode (true); / / set the coding format webSettings. SetDefaultTextEncodingName ("utf-8"); / / setting allows JS pop-up webSettings. SetJavaScriptCanOpenWindowsAutomatically (true); Websettings. setCacheMode(websettings. LOAD_CACHE_ELSE_NETWORK);Copy the code

About the cache setup:

When the HTML page is loaded, the WebView generates two folders, database and cache, in the /data/data/ package name directory. The requested URL records are stored in webviewCache.db. The contents of the URL are stored in the WebViewCache folder

The cache modes are as follows: //LOAD_CACHE_ONLY: reads only the local cache data without using the network. //LOAD_DEFAULT: determines whether to fetch data from the network according to cache-control. //LOAD_NO_CACHE: no cache is used, only data is obtained from the network.Copy the code

Off-line load

if(NetStatusUtil.isConnected(getApplicationContext())) { webSettings.setCacheMode(WebSettings.LOAD_DEFAULT); Cache-control determines whether to fetch data from the network. }else{ webSettings.setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK); / / not net, is obtained from the local, the off-line load} webSettings. SetDomStorageEnabled (true); / / open the DOM storage API function webSettings. SetDatabaseEnabled (true); / / open the database storage API function webSettings. SetAppCacheEnabled (true); String cacheDirPath = getFilesDir().getabsolutePath () + APP_CACAHE_DIRNAME; // Enable Application Caches. webSettings.setAppCachePath(cacheDirPath); // Sets the Application Caches cache directoryCopy the code
  • WebViewClient: handle all kinds of notification, request events, mainly have, web page start load, record end, load error (such as 404), handle HTTPS request, see the following code for specific use, annotated clear
webView!! .webViewClient = object :WebViewClientOverride fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean { view.loadUrl(url)return true} // We can set loading override fun onPageStarted(view: WebView? , url: String? , favicon: Bitmap?) { super.onPageStarted(view, url, favicon) tv_start.text ="Loading started..."
            }
// 页面加载结束,关闭loading
            override fun onPageFinished(view: WebView?, url: String?) {
                super.onPageFinished(view, url)
                tv_end.text = "Loading finished..."} // Override fun onLoadResource(view: WebView? , url: String?) { loge("onLoadResource invoked"} // Here we can load our own 404 page override Fun onReceivedError(view: WebView? , request: WebResourceRequest? , error: WebResourceError?) { loge("Loading error:${error.toString()}"Override Fun onReceivedSslError(view: webview? , handler: SslErrorHandler? , error: SslError?) { handler? .proceed()} // this method does not have memory leaks via annotations, but it is difficult to get the return value from Android. // This method can be passed in the form of Android calling js code. // Android: mainactivity.java // mwebView.loadurl ("javascript:returnResult(" + result + ")"); // JS: javascript. HTML //function returnResult(result){
            //    alert("result is"+ result); // } override fun shouldOverrideUrlLoading(view: WebView? , request: WebResourceRequest?) : Boolean { val uri = Uri.parse(request? .url.tostring ()).url.tostring ()).url.tostring ()"js://webview? arg1=111&arg2=222"(also agreed to intercept)if (uri.scheme == "js") {
                    if (uri.authority == "webview") {
                        toast_custom("Js calls Android methods")
                        val queryParameterNames = uri.queryParameterNames
                        queryParameterNames.forEach {
                            loge(it + ":" + uri.getQueryParameter(it))
                        }
                    }
                    return true
                }
                returnSuper. ShouldOverrideUrlLoading (view, request)} / / intercept resource Usually used for h5 home page, will commonly used some of the resources, Override fun shouldInterceptRequest(view: WebView? , request: WebResourceRequest?) : WebResourceResponse? {if(request? .url.toString().contains("logo.gif")){
                    var inputStream: InputStream? = null
                    inputStream = applicationContext.assets.open("images/test.png")
                    return WebResourceResponse("image/png"."utf-8", inputStream)
                }
                return super.shouldInterceptRequest(view, request)
            }
        }
Copy the code

Note: in 5.1, HTTPS and HTTP are disabled by default. The following Settings are enabled

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
mWebView.getSettings().setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
}
Copy the code
  • WebChromeClient function: auxiliary webView a callback method, you can get the progress of the page loading, the title of the page, the icon of the page, some js popboxes, directly see the code, clear notes:
webView!! .webChromeClient = object :WebChromeClientOverride fun onProgressChanged(view: WebView? , newProgress: Int) { tv_progress.text ="$newProgress%"Override fun onReceivedTitle(view: WebView? , title: String?) { tv_title.text = title } //js Alert override fun onJsAlert(view: WebView? , url: String? , message: String? , result: JsResult?) : Boolean { AlertDialog.Builder(this@WebActivity) .setTitle("JsAlert")
                    .setMessage(message)
                    .setPositiveButton("OK") { _, _ -> result? .confirm() } .setCancelable(false)
                    .show()
                return true} // js Confirm override fun onJsConfirm(view: WebView? , url: String? , message: String? , result: JsResult?) : Boolean {returnsuper.onJsConfirm(view, url, message, result) } //js Prompt override fun onJsPrompt( view: WebView? , url: String? , message: String? , defaultValue: String? , result: JsPromptResult? ) : Boolean {return super.onJsPrompt(view, url, message, defaultValue, result)
            }


        }
Copy the code

Interaction between Android and JS

  • Android called js

    1. Pass the loadUrl of the WebView

    Note: This method must be invoked after the WebView has loaded, after the onPageFinished() callback of the webviewClient, and it is inefficient because it refreshes the interface

    Js code:function callJs(){
        alert("Android calls js code)} Kotlin code: webView? .loadUrl("javascript:callJs()")
    Copy the code

    EvaluateJavaScript via WebView

    It is more efficient than the first method, but won’t be used until after 4.4

    Js code:function callJs(){
       //  alert(Return {name:' WFQ ',age:25}} Kotlin code: webView? .evaluateJavascript("javascript:callJs()Toast (it) {name:' WFQ ',age:25}}Copy the code
  • Js calls the Android

    1. Use the addJavaScriptInterface of the webview to map objects

    We can define a separate class, all the interactive methods can be written in this class, of course, can also be directly written in the Activity, the following directly defined in the Activity as an example, advantages: easy to use, disadvantages: There are bugs (before 4.2), see “Some WebView bugs and How to Prevent them” below

    @javascriptInterface fun hello(name: String) {toast("Hello, this is a message from JS:$msg"} js codefunction callAndroid(){
        android.hello("I'm js, I'm calling you."} Kotlin set the Android and JS code in the webView mapping webView? .addJavascriptInterface(this,"android")
    Copy the code

    2. Intercept url via shouldOverrideUrlLoading callback of webviewClient

    Specific use: parse the protocol of the URL, if the detection is a pre-agreed protocol, then call the corresponding method. It is safe to use javascript to get the return value from Android, but it is cumbersome to use javascript to get the return value from Android. The only way to pass the return value is to execute javascript code to loadURL ()

    First, the number protocol is specified in JSfunction callAndroid(){// The url protocol is: js://webview? name=wfq&age=24 document.location ="js://webview? name=wfq&age=24"Override fun shouldOverrideUrlLoading(view: WebView?) , request: WebResourceRequest?) : Boolean { val uri = Uri.parse(request? .url.tostring ()).url.tostring ()).url.tostring (); name=wfq&age=24if (uri.scheme == "js") {
                    if (uri.authority == "webview") {
                        toast_custom("Js calls Android methods")
                        val queryParameterNames = uri.queryParameterNames
                        queryParameterNames.forEach {
                            loge(it + ":" + uri.getQueryParameter(it))
                        }
                    }
                    return true
                }
                return super.shouldOverrideUrlLoading(view, request)
            }
    
    Copy the code

    3. By onJsAlert webChromeClient onJsConfirm, intercept onJsPrompt callbacks dialog

    Intercept the JS dialog box to get their message, and then parse it. For security, it is recommended to use the URL protocol introduced above. The commonly used intercept is prompt, because it can return any value, alert does not return a value, and Confirm can only return two types, CONFIRM and cancel

    Js codefunction clickprompt(){
        var result=prompt("wfq://demo? arg1=111&arg2=222");
        alert("demo "+ result); } Kotlin code override fun onJsPrompt(view: WebView? , url: String? , message: String? , defaultValue: String? , result: JsPromptResult? ) : Boolean { val uri = Uri.parse(message)if (uri.scheme == "wfq") {
                    if (uri.authority == "demo") {
                        toast_custom("Js calls Android methods")
                        val queryParameterNames = uri.queryParameterNames
                        queryParameterNames.forEach {
                            loge(it + ":"+ uri.getQueryParameter(it))} // Return the value that needs to be returned. .confirm("Js called the Android method successfully la la la la la la")}return true
                }
                returnSuper.onjsprompt (view, URL, message, defaultValue, result)} Override Fun onJsAlert(view: WebView? , url: String? , message: String? , result: JsResult?) : Boolean { AlertDialog.Builder(this@WebActivity) .setTitle("JsAlert")
                    .setMessage(message)
                    .setPositiveButton("OK") { _, _ -> result? .confirm() } .setCancelable(false)
                    .show()
                return true
            }
    
    Copy the code

    The difference between the above three ways: AddJavascriptInterface () convenient and concise, loopholes in 4.0 the following, above 4.0 by @ JavascriptInterface annotations to fix WebViewClient. ShouldOverrideUrlLoading () callback. There is no vulnerability, it is complicated to use, it needs to define the constraints of the protocol, but there is some trouble to return the value, when there is no need to return the value, you can use this method through WebChromeClient onJsAlerta, onJsConfirm,onJsPrompt, there is no vulnerability problem, the use of complex, The need for protocol constraints, can return values, can meet the intermodulation communication in most cases

Some WebView vulnerabilities and how to prevent

Refer to tencent great god Carson_Ho Jane books www.jianshu.com/p/3a345d27c…

Password plaintext storage vulnerability

Webview default opens the password function, will pop up after the user to enter the password prompt dialog box asking the user whether to save the password, save the password will be clear after stored in the/data/data/com. Package. The name/databases/webview. Db below, Mobile phone root can be viewed, so how to solve?

WebSettings.setSavePassword(false) // Turn off the password save reminder functionCopy the code

WebView arbitrary code execution vulnerability

AddJavascriptInterface vulnerability. First of all, when JAVASCRIPT calls Android code, we often use addJavascriptInterface. One of the ways that JAVASCRIPT calls Android is to map objects through addJavascriptInterface. Before Android4.2, all methods in this object can be called. After 4.2, Functions that need to be called by JS are annotated with @javascriptInterface to avoid this vulnerability

So how do you solve it?

Before Android 4.2, intercept prompt () is required to fix the vulnerability. After Android 4.2, only @javascriptInterface annotations are required for the called functionCopy the code

Lax domain control vulnerability

  • In the Applilcation, Android :exported=”true”, an application exported by AN application B can use an Activity exported by an application B to load A malicious FILE URL. In this way, the internal private files of APPLICATION B can be obtained, which brings the threat of data leakage.

Let’s look at how WebView security is affected by the methods of getSettings in WebView. SetAllowFileAccess ()

// Set whether to allow WebView to use File protocoltrueWebview.getsettings ().setallowFileAccess ()true); If set tofalseThere's no threat, but webViews can't use native HTML files eitherCopy the code

SetAllowFileAccessFromFileURLs ()

// Set whether to allow javascript code loaded by file URL to read other local files // Default allowed before Android 4.1 // default prohibited after Android 4.1 webView.getSettings().setAllowFileAccessFromFileURLs(true); We should explicitly set it tofalseTo prohibit reading other filesCopy the code

SetAllowUniversalAccessFromFileURLs ()

// Set whether to allow Javascript loaded through file URL to access other sources (including HTTP, HTTPS, etc.)setAllowFileAccessFromFileURLs () doesn't work) / / default is prohibited after the Android 4.1 webView. GetSettings () setAllowUniversalAccessFromFileURLs (true);
Copy the code

WebView preloading and resource preloading

Why is preloading required

H5 page loading is slow. Causes: Page rendering is slow and resource loading is slow

How to optimize?

H5 cache, resource preloading, resource interception

  • H5 cache Android WebView built-in cache 1

    The mechanism for controlling file caching based on cache-Control (or Expires) and Last-Modified (or Etag) fields in the HTTP header is implemented by the browser itself, and we need to deal with itCopy the code

    2.App Cache

    Easy to build the cache of Web App, store static files (such as JS, CSS, font files). String cacheDirPath = context.getFilesDir().getAbsolutePath()+"cache/";
     settings.setAppCachePath(cacheDirPath);
     settings.setAppCacheMaxSize(20*1024*1024);
     settings.setAppCacheEnabled(true);
    Copy the code

    3.Dom Storage

        WebSettings settings = getSettings();
        settings.setDomStorageEnabled(true);
    Copy the code

    4.Indexed Database

    // Android added support for IndexedDB in 4.4. Simply turn on the JS switch to allow JS execution. WebSettings settings = getSettings(); settings.setJavaScriptEnabled(true);
       
    Copy the code
  • Resource preloading WebView object preloading, the first webView initialization is much slower than the second one reasons: After initialization, even if the WebView has been released, some shared objects of the WebView still exist. We can initialize a WebView object in advance in the Application, and then load resources directly loadURL

  • Resource interception can store some static resource files with low frequency in the local, intercept h5 resource network request and detect, if detected, directly replace with local resources

Override fun shouldInterceptRequest(view: WebView? , request: WebResourceRequest?) : WebResourceResponse? {if(request? .url.toString().contains("logo.jpg")){
                    var inputStream: InputStream? = null
                    inputStream = applicationContext.assets.open("images/test.jpg")
                    return WebResourceResponse("image/png"."utf-8", inputStream)
                }

                return super.shouldInterceptRequest(view, request)
            }
Copy the code

Common precautions for use:

Android9.0 has prohibited webview from using HTTP.

Use: under the labels in the manifest the Application of android: usesCleartextTraffic = “true”

Android can’t interact with H5 after obfuscation is enabled?

Save annotations, such as @javascriptInterface annotations
-keepattributes *Annotation*

Keep javascript-related attributes
-keepattributes JavascriptInterface

Keep methods in JavascriptInterface
-keepclassmembers class * {
    @android.webkit.JavascriptInterface <methods>;
}
# This class is used to interact with JS, so fields and methods in this class cannot be confused with full path names. The name of the class
-keepclassmembers public class com.youpackgename.xxx.H5CallBackAndroid{
   <fields>;
   <methods>;
   public *;
   private *;
}
Copy the code

How to debug?

1. In WebViewActivity, start debugging

/ / open the debug WebView. SetWebContentsDebuggingEnabled (true)
Copy the code

2. Enter Chrome ://inspect in the address bar

3. Mobile phone open USB debugging, open webView page, click on the bottom of chrome page inspect, so that you can enter the Web development, see the console, network request, etc