Android如何设计一个H5容器

APP端使用WebView的场景主要是加载H5页面、富文本展示和编辑、图表库(echart)等。当业务对APP动态化有相应需求时,H5作为一个老牌跨平台技术,是最常用的动态化技术之一。本文对WebView的技术需求和使用场景进行了整理,其中大部分是本人工作中遇到过的,部分是在查阅资料过程中发现的,希望对今后的开发有所帮助。

先来谈一谈H5容器的设计目标:

  • 良好的js与原生通讯支持:支持自定义通讯接口;
  • 性能尽可能好,加载速度尽量快:H5本地模版和webview缓存;
  • 可配置,扩展性好:默认配置合理,满足多数场景需求,同时支持丰富的自定义配置;
  • 用户交互的全面支持:状态展示(loading/error/success)、权限申请、文件选择、大图查看、视频全屏、内容复制粘贴、输入框键盘等等;

本文将围绕这些目标展开,结合WebView的基本用法介绍,力求尽量全面地了解WebView的使用。

WebView基本用法

WebView

查看android api 27及之前版本的WebView源码,发现出于线程安全的考虑,WebView的公开接口内部都做了android.webkit.WebView#checkThread处理,以确保所有相关接口的调用都在同一个线程中。api 28及之后WebView继承MockView成为傀儡类,虽然无法直接看到内部逻辑,但是有理由相信这个准则还是不变的,所以推荐在调用WebView相关接口时要注意在主线程调用。

生命周期相关接口

初始化:

mWebView = new WebView(getContext());
// 可选:自定义配置
WebSettings webSettings = mWebView.getSettings();
// 可选:自定义WebViewClient
mWebView.setWebViewClient(new WebViewClient(){...});
// 可选:自定义WebChromeClient
mWebView.setWebChromeClient(new WebChromeClient(){...});
// 可选:添加JS回调
mWebView.addJavascriptInterface(new Object(){ @JavascriptInterface...}, namespace);

前台:

    @Override
    public void onResume() {
    	// 继续被中断的操作
        mWebView.onResume();
        super.onResume();
    }

后台:

    @Override
    public void onPause() {
        super.onPause();
        // Pauses any extra processing associated with this WebView and its 
        // associated DOM, plugins, JavaScript etc. For example, if this 
        // WebView is taken offscreen, this could be called to reduce 
        // unnecessary CPU or network traffic. When this WebView is again 
        // "active", call onResume(). Note that this differs from pauseTimers(),
        // which affects all WebViews.
        mWebView.onPause();
    }

销毁:

// 必须,防止内存泄漏,参见:https://www.jianshu.com/p/eada9b652d99
((ViewGroup) mWebView.getParent()).removeView(this);
// 可选
mWebView.loadDataWithBaseURL(null, "", "text/html", "utf-8", null);
// 可选,停止JS加载,如果前面已经调用了onPause则这里不用
mWebView.stopLoading();
// 可选,退出时调用此方法,移除绑定的服务,否则某些特定系统会报错
mWebView.getSettings().setJavaScriptEnabled(false);
// 可选
mWebView.clearHistory();
mWebView.clearView();
mWebView.removeAllViews();
// 必须,需要在最后调用,此方法调用不能再调用mWebView的任何方法
mWebView.destroy();

加载数据

// 加载指定url,url可以是以下:
// 1. [http_url]:比如http://www.baidu.com
// 2. [assets_url]:比如file:///android_asset/content.html
// 3. [file_url]:本地文件绝对路径
// 4. [js_url]:javascript:getText(),详见原生调用JS
// 5. ["about:blank"]:空页面,用于清空webview内容并释放资源
mWebView.loadUrl("url");
// 加载指定http url,并添加自定义请求头
mWebView.loadUrl("url", Collections.<String, String>emptyMap());

// 使用post请求加载指定url,bytes为urlencode过的数据,将作为post请求body,
// 如果url非http地址,则等同于loadUrl("url")
mWebView.postUrl("url", bytes);

// 加载HTML文本
// 注意data须经UrlEncoder进行编码,不能出现英文字符:’#’, ‘%’, ‘\’ , ‘?’ 这四个字符
mWebView.loadData("data","mimeType","encoding");
// 加载HTML文本,data没有上面的限制,因此使用更多
// String baseUrl:基准URL,不需要可以传null。如果data中的url是相对地址,则就会加上基准url来拼接出完整的地址
// String mimeType:MIME类型,通常是“text/html”
// String encoding:编码方式
// String historyUrl:插入历史记录的值,不需要传Null
mWebView.loadDataWithBaseURL("baseUrl","data","mimeType","encoding","historyUrl");

// 停止加载
mWebView.stopLoading();

// 重新加载当前URL
mWebView.reload();
  • loadData()与loadDataWithBaseURL()
    通过loadUrl()来加载本地页面和在线地址的方式需要很长的加载时间,而这两个方法不是用来加载整个页面文件的,而是用来加载一段代码片。比如可以先通过http将网页HTML数据从服务器下载回来,在通过这两个方法就可以很快把页面加载出来,没有很长的访问网络CDN解析等时间。两者比较推荐使用后者,虽然loadData的历史记录不需要我们自己来实现,但在使用时,加载上后者比前者快一到两倍。另外loadData不能加载图片,而loadDataWithBaseURL是可以加载图片的。
  • loadDataWithBaseURL()
    loadDataWithBaseURL它本身并不会向历史记录中存储数据,要想实现历史记录,需要我们自己来实现;有关历史记录的实现方式是比较复杂的,历史记录是以Key/value的方式存储在一个historyList里的,当前进后退时,会用Key来取出对应的value值来加载进webview中。而Key就是这里的baseUrl,Value就是这里的historyUrl;history所指向的必须是一个页面,并且页面存在于SD卡中或程序中(assets);

想要了解更多webview支持的url类型,参见:android.webkit.URLUtil

页面导航

// 获取当前页面URL
String url = mWebView.getUrl();
// 获取当前初始实际加载的URL,比如重定向后的URL
String originalUrl = mWebView.getOriginalUrl();
boolean b = mWebView.canGoBack();
mWebView.canGoBackOrForward(steps);
boolean b1 = mWebView.canGoForward();
mWebView.goBack();
mWebView.goForward();
mWebView.goBackOrForward(steps);

对于常规的多页H5应用,那么上述webview接口已经可以实现activity对webview页面的管控了,但是目前很多H5应用是单页应用,回退操作需要APP端和H5端协调好接口,比如JS仿照webview的已有接口:canGoBack()和goBack()进行实现,由原生APP端进行调用。

其他接口

// 取消已经选中的webview里的文字,需要搭配View#clearFocus()使用
mWebView.clearMatches();

mWebView.clearFormData();

mWebView.clearHistory();

mWebView.clearSslPreferences();

// 已废弃,情况网页内容,显示空白页
mWebView.clearView();

// 这里比较难理解的可能是realm,它是指服务端配置用户认证配置中的AuthName(Apache httpd http://httpd.apache.org/docs/2.0/howto/auth.html)
mWebView.setHttpAuthUsernamePassword("host","realm","username","password");
String[] usernamePassword = mWebView.getHttpAuthUsernamePassword("host","realm");

SslCertificate certificate = mWebView.getCertificate();
mWebView.setCertificate(certificate);

mWebView.setNetworkAvailable(true);

// 获取html内容宽高
int contentHeight = mWebView.getContentHeight();
int contentWidth = mWebView.getContentWidth();

Tricks

浏览器用户认证

  • setHttpAuthUsernamePassword(“host”,“realm”,“username”,“password”)使用场合
    一个需要用户认证的网址:http://api.test.com/userinfo/vid?=1234
    使用curl访问方式:
curl -u username:password http://api.test.com/userinfo/vid?=1234

使用webview访问:

// 错误,无法访问
// realm不能为空,该参数可以从:
// 1. 到服务器配置>>用户认证配置获取
// 2. 在WebViewClient#onReceivedHttpAuthRequest回调中,也有realm参数
mWebView.setHttpAuthUsernamePassword("api.test.com", "", "username", "password");
mWebView.loadUrl("http://api.test.com/userinfo/vid?=1234");

或使用webview WebViewClient回调:

webview.setWebViewClient(new MyWebViewClient ());

private class MyWebViewClient extends WebViewClient {
    @Override
    public void onReceivedHttpAuthRequest(WebView view, HttpAuthHandler handler, String host, String realm) {
    	// 这里正确的做法是弹窗对话框让用户输入用户名密码
        // 如果直接调用接口,则必须使用正确的用户名密码,否则webview会一直发送请求
        // 当出现请求错误时,可以尝试使用mWebView.stopLoading()来终止retry行为
        handler.proceed("username", "password");
        
    }
}

或将用户名密码拼接到url进行访问,类似curl:

mWebView.loadUrl("http://username:password@api.test.com/userinfo/vid?=1234");

参考资料:Using WebView setHttpAuthUsernamePassword?

WebSettings

API

  • void setSupportZoom(boolean support)

Sets whether the WebView should support zooming using its on-screen zoom controls and gestures. The particular zoom mechanisms that should be used can be set with setBuiltInZoomControls. This setting does not affect zooming performed using the WebView.zoomIn() and WebView.zoomOut() methods. The default is true.

Params:
support – whether the WebView should support zoom

  • void setMediaPlaybackRequiresUserGesture(boolean require)

Sets whether the WebView requires a user gesture to play media. The default is true.

Params:
require – whether the WebView requires a user gesture to play media

  • void setBuiltInZoomControls(boolean enabled)

Sets whether the WebView should use its built-in zoom mechanisms. The built-in zoom mechanisms comprise on-screen zoom controls, which are displayed over the WebView’s content, and the use of a pinch gesture to control zooming. Whether or not these on-screen controls are displayed can be set with setDisplayZoomControls. The default is false.
The built-in mechanisms are the only currently supported zoom mechanisms, so it is recommended that this setting is always enabled.

Params:
enabled – whether the WebView should use its built-in zoom mechanisms

  • void setDisplayZoomControls(boolean enabled)

Sets whether the WebView should display on-screen zoom controls when using the built-in zoom mechanisms. See setBuiltInZoomControls. The default is true.

Params:
enabled – whether the WebView should display on-screen zoom controls

  • void setAllowFileAccess(boolean allow)

Enables or disables file access within WebView. File access is enabled by default. Note that this enables or disables file system access only. Assets and resources are still accessible using file:///android_asset and file:///android_res.

  • void setAllowContentAccess(boolean allow)

Enables or disables content URL access within WebView. Content URL access allows WebView to load content from a content provider installed in the system. The default is enabled.

  • void setAllowUniversalAccessFromFileURLs(boolean flag)

Sets whether JavaScript running in the context of a file scheme URL should be allowed to access content from any origin. This includes access to content from other file scheme URLs. See setAllowFileAccessFromFileURLs. To enable the most restrictive, and therefore secure policy, this setting should be disabled. Note that this setting affects only JavaScript access to file scheme resources. Other access to such resources, for example, from image HTML elements, is unaffected. To prevent possible violation of same domain policy on android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH and earlier devices, you should explicitly set this value to false.
The default value is true for API level android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1 and below, and false for API level android.os.Build.VERSION_CODES.JELLY_BEAN and above.

Params:
flag – whether JavaScript running in the context of a file scheme URL should be allowed to access content from any origin

  • void setLoadsImagesAutomatically(boolean flag)

Sets whether the WebView should load image resources. Note that this method controls loading of all images, including those embedded using the data URI scheme. Use setBlockNetworkImage to control loading only of images specified using network URI schemes. Note that if the value of this setting is changed from false to true, all images resources referenced by content currently displayed by the WebView are loaded automatically. The default is true.

Params:
flag – whether the WebView should load image resources

  • void setBlockNetworkImage(boolean flag)

Sets whether the WebView should not load image resources from the network (resources accessed via http and https URI schemes). Note that this method has no effect unless getLoadsImagesAutomatically returns true. Also note that disabling all network loads using setBlockNetworkLoads will also prevent network images from loading, even if this flag is set to false. When the value of this setting is changed from true to false, network images resources referenced by content currently displayed by the WebView are fetched automatically. The default is false.

Params:
flag – whether the WebView should not load image resources from the network

  • void setBlockNetworkLoads(boolean flag)

Sets whether the WebView should not load resources from the network. Use setBlockNetworkImage to only avoid loading image resources. Note that if the value of this setting is changed from true to false, network resources referenced by content currently displayed by the WebView are not fetched until WebView.reload is called. If the application does not have the android.Manifest.permission.INTERNET permission, attempts to set a value of false will cause a SecurityException to be thrown. The default value is false if the application has the android.Manifest.permission.INTERNET permission, otherwise it is true.

Params:
flag – whether the WebView should not load any resources from the network

  • void setLoadWithOverviewMode(boolean overview)

Sets whether the WebView loads pages in overview mode, that is, zooms out the content to fit on screen by width. This setting is taken into account when the content width is greater than the width of the WebView control, for example, when getUseWideViewPort is enabled. The default is false.

  • void setSaveFormData(boolean save)

Sets whether the WebView should save form data. The default is true.

  • void setTextZoom(int textZoom)

Sets the text zoom of the page in percent. The default is 100.

  • void setAcceptThirdPartyCookies(boolean accept)

Sets policy for third party cookies. Developers should access this via CookieManager.setShouldAcceptThirdPartyCookies.

  • void setUseWideViewPort(boolean use)

Sets whether the WebView should enable support for the “viewport” HTML meta tag or should use a wide viewport. When the value of the setting is false, the layout width is always set to the width of the WebView control in device-independent (CSS) pixels. When the value is true and the page contains the viewport meta tag, the value of the width specified in the tag is used. If the page does not contain the tag or does not provide a width, then a wide viewport will be used.

Params:
use – whether to enable support for the viewport meta tag

  • void setSupportMultipleWindows(boolean support)

Sets whether the WebView whether supports multiple windows. If set to true, WebChromeClient.onCreateWindow must be implemented by the host application. The default is false.

Params:
support – whether to suport multiple windows

  • void setJavaScriptCanOpenWindowsAutomatically(boolean flag)

Tells JavaScript to open windows automatically. This applies to the JavaScript function window.open(). The default is false.

Params:
flag – true if JavaScript can open windows automatically

  • void setLayoutAlgorithm(LayoutAlgorithm l)

Sets the underlying layout algorithm. This will cause a relayout of the WebView. The default is WebSettings.LayoutAlgorithm.NARROW_COLUMNS.

Params:
l – the layout algorithm to use, as a WebSettings.LayoutAlgorithm value

  • void setStandardFontFamily(String font)

Sets the standard font family name. The default is “sans-serif”.

Params:
font – a font family name

  • void setFixedFontFamily(String font)

Sets the fixed font family name. The default is “monospace”.

Params:
font – a font family name

  • void setSansSerifFontFamily(String font)

Sets the sans-serif font family name. The default is “sans-serif”.

Params:
font – a font family name

  • void setSerifFontFamily(String font)
    Sets the serif font family name. The default is “sans-serif”.

Params:
font – a font family name

  • void setCursiveFontFamily(String font)

Sets the cursive font family name. The default is “cursive”.

Params:
font – a font family name

  • void setFantasyFontFamily(String font)

Sets the fantasy font family name. The default is “fantasy”.

Params:
font – a font family name

  • void setMinimumFontSize(int size)

Sets the minimum font size. The default is 8.

Params:
size – a non-negative integer between 1 and 72. Any number outside the specified range will be pinned.

  • void setMinimumLogicalFontSize(int size)

Sets the minimum logical font size. The default is 8.

Params:
size – a non-negative integer between 1 and 72. Any number outside the specified range will be pinned.

  • void setDefaultFontSize(int size)

Sets the default font size. The default is 16.

Params:
size – a non-negative integer between 1 and 72. Any number outside the specified range will be pinned.

  • void setDefaultFixedFontSize(int size)

Sets the default fixed font size. The default is 16.

Params:
size – a non-negative integer between 1 and 72. Any number outside the specified range will be pinned.

  • void setJavaScriptEnabled(boolean flag)

Tells the WebView to enable JavaScript execution. The default is false.

Params:
flag – true if the WebView should execute JavaScript

  • void setGeolocationDatabasePath(String databasePath)

Sets the path where the Geolocation databases should be saved. In order for Geolocation permissions and cached positions to be persisted, this method must be called with a path to which the application can write.

Params:
databasePath – a path to the directory where databases should be saved.

  • void setAppCacheEnabled(boolean flag)

Sets whether the Application Caches API should be enabled. The default is false. Note that in order for the Application Caches API to be enabled, a valid database path must also be supplied to setAppCachePath.

Params:
flag – true if the WebView should enable Application Caches

  • void setAppCachePath(String appCachePath)

Sets the path to the Application Caches files. In order for the Application Caches API to be enabled, this method must be called with a path to which the application can write. This method should only be called once: repeated calls are ignored.

Params:
appCachePath – a String path to the directory containing Application Caches files.

  • void setDatabaseEnabled(boolean flag)

Sets whether the database storage API is enabled. The default value is false. See also setDatabasePath for how to correctly set up the database storage API. This setting is global in effect, across all WebView instances in a process. Note you should only modify this setting prior to making any WebView page load within a given process, as the WebView implementation may ignore changes to this setting after that point.

Params:
flag – true if the WebView should use the database storage API

  • void setDomStorageEnabled(boolean flag)

Sets whether the DOM storage API is enabled. The default value is false.

Params:
flag – true if the WebView should use the DOM storage API

  • void setGeolocationEnabled(boolean flag)

Sets whether Geolocation is enabled. The default is true.
Please note that in order for the Geolocation API to be usable by a page in the WebView, the following requirements must be met:
– an application must have permission to access the device location, see android.Manifest.permission.ACCESS_COARSE_LOCATION, android.Manifest.permission.ACCESS_FINE_LOCATION;
– an application must provide an implementation of the WebChromeClient.onGeolocationPermissionsShowPrompt callback to receive notifications that a page is requesting access to location via the JavaScript Geolocation API.
As an option, it is possible to store previous locations and web origin permissions in a database. See setGeolocationDatabasePath.

Params:
flag – whether Geolocation should be enabled

  • void setDefaultTextEncodingName(String encoding)

Sets the default text encoding name to use when decoding html pages. The default is “UTF-8”.

Params:
encoding – the text encoding name

  • void setUserAgentString(String ua)

Sets the WebView’s user-agent string. If the string is null or empty, the system default value will be used. Note that starting from android.os.Build.VERSION_CODES.KITKAT Android version, changing the user-agent while loading a web page causes WebView to initiate loading once again.

Params:
ua – new user-agent string

  • void setNeedInitialFocus(boolean flag)

Tells the WebView whether it needs to set a node to have focus when WebView.requestFocus(int, android.graphics.Rect) is called. The default value is true.

Params:
flag – whether the WebView needs to set a node

  • void setCacheMode(int mode)
    Overrides the way the cache is used. The way the cache is used is based on the navigation type. For a normal page load, the cache is checked and content is re-validated as needed. When navigating back, content is not revalidated, instead the content is just retrieved from the cache. This method allows the client to override this behavior by specifying one of LOAD_DEFAULT, LOAD_CACHE_ELSE_NETWORK, LOAD_NO_CACHE or LOAD_CACHE_ONLY. The default value is LOAD_DEFAULT.

Params:
mode – the mode to use

  • void setMixedContentMode(int mode)
    Configures the WebView’s behavior when a secure origin attempts to load a resource from an insecure origin. By default, apps that target android.os.Build.VERSION_CODES.KITKAT or below default to MIXED_CONTENT_ALWAYS_ALLOW. Apps targeting android.os.Build.VERSION_CODES.LOLLIPOP default to MIXED_CONTENT_NEVER_ALLOW. The preferred and most secure mode of operation for the WebView is MIXED_CONTENT_NEVER_ALLOW and use of MIXED_CONTENT_ALWAYS_ALLOW is strongly discouraged.

Params:
mode – The mixed content mode to use. One of MIXED_CONTENT_NEVER_ALLOW, MIXED_CONTENT_ALWAYS_ALLOW or MIXED_CONTENT_COMPATIBILITY_MODE.

  • void setOffscreenPreRaster(boolean enabled)

Sets whether this WebView should raster tiles when it is offscreen but attached to a window. Turning this on can avoid rendering artifacts when animating an offscreen WebView on-screen. Offscreen WebViews in this mode use more memory. The default value is false. Please follow these guidelines to limit memory usage:
WebView size should be not be larger than the device screen size.
Limit use of this mode to a small number of WebViews. Use it for visible WebViews and WebViews about to be animated to visible.

Tricks

常用移动端H5设置

WebSettings settings = webview.getSettings();
settings.setJavaScriptEnabled(true);//启用js
settings.setJavaScriptCanOpenWindowsAutomatically(true);//js和android交互
settings.setAppCachePath(cacheDirPath); //设置缓存的指定路径
settings.setAllowFileAccess(true); // 允许访问文件
settings.setAppCacheEnabled(true); //设置H5的缓存打开,默认关闭
settings.setUseWideViewPort(true);//设置webview自适应屏幕大小
settings.setLayoutAlgorithm(WebSettings.LayoutAlgorithm.NARROW_COLUMNS);//设置,可能的话使所有列的宽度不超过屏幕宽度
settings.setLoadWithOverviewMode(true);//设置webview自适应屏幕大小
settings.setDomStorageEnabled(true);//设置可以使用localStorage
settings.setSupportZoom(false);//关闭zoom按钮
settings.setBuiltInZoomControls(false);//关闭zoom
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
	webview.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
}

缓存模式

  • LOAD_CACHE_ONLY
    不使用网络,只读取本地缓存数据
  • LOAD_DEFAULT:
    根据cache-control决定是否从网络上取数据。
  • LOAD_CACHE_NORMAL
    API level 17中已经废弃, 从API level 11开始作用同LOAD_DEFAULT模式
  • LOAD_NO_CACHE
    不使用缓存,只从网络获取数据.
  • LOAD_CACHE_ELSE_NETWORK
    只要本地有,无论是否过期,或者no-cache,都使用缓存中的数据。

如:www.taobao.com的cache-control为no-cache,在模式LOAD_DEFAULT下,无论如何都会从网络上取数据,如果没有网络,就会出现错误页面;在LOAD_CACHE_ELSE_NETWORK模式下,无论是否有网络,只要本地有缓存,都使用缓存。本地没有缓存时才从网络上获取。
www.360.com.cn的cache-control为max-age=60,在两种模式下都使用本地缓存数据。

根据以上两种模式,建议缓存策略为,判断是否有网络,有的话,使用LOAD_DEFAULT,无网络时,使用LOAD_CACHE_ELSE_NETWORK。

WebViewClient

API

java doc copied from android api 23.

  • boolean shouldOverrideUrlLoading(WebView view, String url)

Give the host application a chance to take over the control when a new url is about to be loaded in the current WebView. If WebViewClient is not provided, by default WebView will ask Activity Manager to choose the proper handler for the url. If WebViewClient is provided, return true means the host application handles the url, while return false means the current WebView handles the url. This method is not called for requests using the POST “method”.

Params:
view – The WebView that is initiating the callback.
url – The url to be loaded.
Returns:
True if the host application wants to leave the current WebView and handle the url itself, otherwise return false.

注意:代码调用WebView.loadUrl(url)时此回调是不会被调用的,在网页中点击才会走这个方法

  • void onPageStarted(WebView view, String url, Bitmap favicon)

Notify the host application that a page has started loading. This method is called once for each main frame load so a page with iframes or framesets will call onPageStarted one time for the main frame. This also means that onPageStarted will not be called when the contents of an embedded frame changes, i.e. clicking a link whose target is an iframe, it will also not be called for fragment navigations (navigations to #fragment_id).

Params:
view – The WebView that is initiating the callback.
url – The url to be loaded.
favicon – The favicon for this page if it already exists in the database.

  • void onPageFinished(WebView view, String url)

Notify the host application that a page has finished loading. This method is called only for main frame. When onPageFinished() is called, the rendering picture may not be updated yet. To get the notification for the new Picture, use WebView.PictureListener.onNewPicture.

Params:
view – The WebView that is initiating the callback.
url – The url of the page.

  • void onl oadResource(WebView view, String url)

Notify the host application that the WebView will load the resource specified by the given url.

Params:
view – The WebView that is initiating the callback.
url – The url of the resource the WebView will load.

  • void onPageCommitVisible(WebView view, String url)

Notify the host application that WebView content left over from previous page navigations will no longer be drawn.
This callback can be used to determine the point at which it is safe to make a recycled WebView visible, ensuring that no stale content is shown. It is called at the earliest point at which it can be guaranteed that WebView.onDraw will no longer draw any content from previous navigations. The next draw will display either the background color of the WebView, or some of the contents of the newly loaded page.
This method is called when the body of the HTTP response has started loading, is reflected in the DOM, and will be visible in subsequent draws. This callback occurs early in the document loading process, and as such you should expect that linked resources (for example, css and images) may not be available.
For more fine-grained notification of visual state updates, see WebView.postVisualStateCallback.
Please note that all the conditions and recommendations applicable to WebView.postVisualStateCallback also apply to this API.
This callback is only called for main frame navigations.

Params:
view – The WebView for which the navigation occurred.
url – The URL corresponding to the page navigation that triggered this callback.

  • WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request)

Notify the host application of a resource request and allow the application to return the data. If the return value is null, the WebView will continue to load the resource as usual. Otherwise, the return response and data will be used. NOTE: This method is called on a thread other than the UI thread so clients should exercise caution when accessing private data or the view system.

Params:
view – The WebView that is requesting the resource.
request – Object containing the details of the request.
Returns:
A WebResourceResponse containing the response information or null if the WebView should load the resource itself.

  • void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error)
    Report web resource loading error to the host application. These errors usually indicate inability to connect to the server. Note that unlike the deprecated version of the callback, the new version will be called for any resource (iframe, image, etc), not just for the main page. Thus, it is recommended to perform minimum required work in this callback.

Params:
view – The WebView that is initiating the callback.
request – The originating request.
error – Information about the error occured.

  • void onReceivedHttpError(WebView view, WebResourceRequest request, WebResourceResponse errorResponse)

Notify the host application that an HTTP error has been received from the server while loading a resource. HTTP errors have status codes >= 400. This callback will be called for any resource (iframe, image, etc), not just for the main page. Thus, it is recommended to perform minimum required work in this callback. Note that the content of the server response may not be provided within the errorResponse parameter.

Params:
view – The WebView that is initiating the callback.
request – The originating request.
errorResponse – Information about the error occured.

  • void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error)

Notify the host application that an SSL error occurred while loading a resource. The host application must call either handler.cancel() or handler.proceed(). Note that the decision may be retained for use in response to future SSL errors. The default behavior is to cancel the load.

Params:
view – The WebView that is initiating the callback.
handler – An SslErrorHandler object that will handle the user’s response.
error – The SSL error object.

  • void onFormResubmission(WebView view, Message dontResend, Message resend)

As the host application if the browser should resend data as the requested page was a result of a POST. The default is to not resend the data.

Params:
view – The WebView that is initiating the callback.
dontResend – The message to send if the browser should not resend
resend – The message to send if the browser should resend data

  • void doUpdateVisitedHistory(WebView view, String url, boolean isReload)

Notify the host application to update its visited links database.

Params:
view – The WebView that is initiating the callback.
url – The url being visited.
isReload – True if this url is being reloaded.

  • void onReceivedClientCertRequest(WebView view, ClientCertRequest request)

Notify the host application to handle a SSL client certificate request. The host application is responsible for showing the UI if desired and providing the keys. There are three ways to respond: proceed(), cancel() or ignore(). Webview stores the response in memory (for the life of the application) if proceed() or cancel() is called and does not call onReceivedClientCertRequest() again for the same host and port pair. Webview does not store the response if ignore() is called. This method is called on the UI thread. During the callback, the connection is suspended. For most use cases, the application program should implement the android.security.KeyChainAliasCallback interface and pass it to android.security.KeyChain.choosePrivateKeyAlias to start an activity for the user to choose the proper alias. The keychain activity will provide the alias through the callback method in the implemented interface. Next the application should create an async task to call android.security.KeyChain.getPrivateKey to receive the key. An example implementation of client certificates can be seen at AOSP Browser The default behavior is to cancel, returning no client certificate.

Params:
view – The WebView that is initiating the callback
request – An instance of a ClientCertRequest

  • void onReceivedHttpAuthRequest(WebView view, HttpAuthHandler handler, String host, String realm)

Notifies the host application that the WebView received an HTTP authentication request. The host application can use the supplied HttpAuthHandler to set the WebView’s response to the request. The default behavior is to cancel the request.

Params:
view – the WebView that is initiating the callback
handler – the HttpAuthHandler used to set the WebView’s response
host – the host requiring authentication
realm – the realm for which authentication is required
See Also:
WebView.getHttpAuthUsernamePassword

  • void onReceivedLoginRequest(WebView view, String realm, String account, String args)

Notify the host application that a request to automatically log in the user has been processed.

Params:
view – The WebView requesting the login.
realm – The account realm used to look up accounts.
account – An optional account. If not null, the account should be checked against accounts on the device. If it is a valid account, it should be used to log in the user.
args – Authenticator specific arguments used to log in the user.

  • public boolean shouldOverrideKeyEvent(WebView view, KeyEvent event)

Give the host application a chance to handle the key event synchronously. e.g. menu shortcut key events need to be filtered this way. If return true, WebView will not handle the key event. If return false, WebView will always handle the key event, so none of the super in the view chain will see the key event. The default behavior returns false.

Params:
view – The WebView that is initiating the callback.
event – The key event.
Returns:
True if the host application wants to handle the key event itself, otherwise return false

  • void onUnhandledInputEvent(WebView view, InputEvent event)

Notify the host application that a input event was not handled by the WebView. Except system keys, WebView always consumes input events in the normal flow or if shouldOverrideKeyEvent returns true. This is called asynchronously from where the event is dispatched. It gives the host application a chance to handle the unhandled input events. Note that if the event is a android.view.MotionEvent, then it’s lifetime is only that of the function call. If the WebViewClient wishes to use the event beyond that, then it must create a copy of the event. It is the responsibility of overriders of this method to call onUnhandledKeyEvent(WebView, KeyEvent) when appropriate if they wish to continue receiving events through it.

Params:
view – The WebView that is initiating the callback.
event – The input event.

  • void onScaleChanged(WebView view, float oldScale, float newScale)

Notify the host application that the scale applied to the WebView has changed.

Params:
view – the WebView that is initiating the callback.
oldScale – The old scale factor
newScale – The new scale factor

Tricks

高频拦截接口

  • shouldOverrideUrlLoading:拦截除资源请求的url,此处可以解析做自定义跳转
  • shouldInterceptRequest:拦截所有url请求,包括超链接、JS文件、CSS文件、图片等,注意此回调非主线程,此处可以做资源替换和预加载

WebChromeClient

API

  • void onProgressChanged(WebView view, int newProgress)

Tell the host application the current progress of loading a page.

Params:
view – The WebView that initiated the callback.
newProgress – Current page loading progress, represented by an integer between 0 and 100.

  • void onReceivedTitle(WebView view, String title)

Notify the host application of a change in the document title.

Params:
view – The WebView that initiated the callback.
title – A String containing the new title of the document.

  • void onReceivedIcon(WebView view, Bitmap icon)

Notify the host application of a new favicon for the current page.

Params:
view – The WebView that initiated the callback.
icon – A Bitmap containing the favicon for the current page.

  • void onReceivedTouchIconUrl(WebView view, String url, boolean precomposed)

Notify the host application of the url for an apple-touch-icon.

Params:
view – The WebView that initiated the callback.
url – The icon url.
precomposed – True if the url is for a precomposed touch icon.

  • void onShowCustomView(View view, CustomViewCallback callback)

Notify the host application that the current page has entered full screen mode. The host application must show the custom View which contains the web contents — video or other HTML content — in full screen mode. Also see “Full screen support” documentation on WebView.

Params:
view – is the View object to be shown.
callback – invoke this callback to request the page to exit full screen mode.

当 H5 页面中点击播放的 flash video 的全屏按钮时,会调用这个方法

  • void onHideCustomView()

Notify the host application that the current page has exited full screen mode. The host application must hide the custom View, ie. the View passed to onShowCustomView when the content entered fullscreen. Also see “Full screen support” documentation on WebView.

与 onShowCustomView() 对应的取消全屏时会调用的方法

  • boolean onCreateWindow(WebView view, boolean isDialog, boolean isUserGesture, Message resultMsg)

Request the host application to create a new window. If the host application chooses to honor this request, it should return true from this method, create a new WebView to host the window, insert it into the View system and send the supplied resultMsg message to its target with the new WebView as an argument. If the host application chooses not to honor the request, it should return false from this method. The default implementation of this method does nothing and hence returns false.

Params:
view – The WebView from which the request for a new window originated.
isDialog – True if the new window should be a dialog, rather than a full-size window.
isUserGesture – True if the request was initiated by a user gesture, such as the user clicking a link.
resultMsg – The message to send when once a new WebView has been created. resultMsg.obj is a WebView.WebViewTransport object. This should be used to transport the new WebView, by calling WebView.WebViewTransport.setWebView(WebView).
Returns:
This method should return true if the host application will create a new window, in which case resultMsg should be sent to its target. Otherwise, this method should return false. Returning false from this method but also sending resultMsg will result in undefined behavior.

类似于浏览器open with new tab

  • void onCloseWindow(WebView window)

Notify the host application to close the given WebView and remove it from the view system if necessary. At this point, WebCore has stopped any loading in this window and has removed any cross-scripting ability in javascript.

Params:
window – The WebView that needs to be closed.

  • void onRequestFocus(WebView view)

Request display and focus for this WebView. This may happen due to another WebView opening a link in this WebView and requesting that this WebView be displayed.

Params:
view – The WebView that needs to be focused.

  • boolean onJsAlert(WebView view, String url, String message, JsResult result)

Tell the client to display a javascript alert dialog. If the client returns true, WebView will assume that the client will handle the dialog. If the client returns false, it will continue execution.

Params:
view – The WebView that initiated the callback.
url – The url of the page requesting the dialog.
message – Message to be displayed in the window.
result – A JsResult to confirm that the user hit enter.
Returns:
boolean Whether the client will handle the alert dialog.

alert弹窗:只有一个确认按钮

  • boolean onJsConfirm(WebView view, String url, String message, JsResult result)

Tell the client to display a confirm dialog to the user. If the client returns true, WebView will assume that the client will handle the confirm dialog and call the appropriate JsResult method. If the client returns false, a default value of false will be returned to javascript. The default behavior is to return false.

Params:
view – The WebView that initiated the callback.
url – The url of the page requesting the dialog.
message – Message to be displayed in the window.
result – A JsResult used to send the user’s response to javascript.

confirm弹窗:确认和取消按钮

  • boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result)

Tell the client to display a prompt dialog to the user. If the client returns true, WebView will assume that the client will handle the prompt dialog and call the appropriate JsPromptResult method. If the client returns false, a default value of false will be returned to to javascript. The default behavior is to return false.

Params:
view – The WebView that initiated the callback.
url – The url of the page requesting the dialog.
message – Message to be displayed in the window.
defaultValue – The default value displayed in the prompt dialog.
result – A JsPromptResult used to send the user’s reponse to javascript.
Returns:
boolean Whether the client will handle the prompt dialog.

Prompt弹窗:通常带有用户输入

  • boolean onJsBeforeUnload(WebView view, String url, String message, JsResult result)

Tell the client to display a dialog to confirm navigation away from the current page. This is the result of the onbeforeunload javascript event. If the client returns true, WebView will assume that the client will handle the confirm dialog and call the appropriate JsResult method. If the client returns false, a default value of true will be returned to javascript to accept navigation away from the current page. The default behavior is to return false. Setting the JsResult to true will navigate away from the current page, false will cancel the navigation.

Params:
view – The WebView that initiated the callback.
url – The url of the page requesting the dialog.
message – Message to be displayed in the window.
result – A JsResult used to send the user’s response to javascript.
Returns:
boolean Whether the client will handle the confirm dialog.

  • void onGeolocationPermissionsShowPrompt(String origin, GeolocationPermissions.Callback callback)

Notify the host application that web content from the specified origin is attempting to use the Geolocation API, but no permission state is currently set for that origin. The host application should invoke the specified callback with the desired permission state. See GeolocationPermissions for details.

Params:
origin – The origin of the web content attempting to use the Geolocation API.
callback – The callback to use to set the permission state for the origin.

  • void onGeolocationPermissionsHidePrompt()

Notify the host application that a request for Geolocation permissions, made with a previous call to onGeolocationPermissionsShowPrompt() has been canceled. Any related UI should therefore be hidden.

  • void onPermissionRequest(PermissionRequest request)

Notify the host application that web content is requesting permission to access the specified resources and the permission currently isn’t granted or denied. The host application must invoke PermissionRequest.grant(String[]) or PermissionRequest.deny(). If this method isn’t overridden, the permission is denied.

Params:
request – the PermissionRequest from current web content.

  • void onPermissionRequestCanceled(PermissionRequest request)

Notify the host application that the given permission request has been canceled. Any related UI should therefore be hidden.

Params:
request – the PermissionRequest that needs be canceled.

  • boolean onConsoleMessage(ConsoleMessage consoleMessage)

Report a JavaScript console message to the host application. The ChromeClient should override this to process the log message as they see fit.

Params:
consoleMessage – Object containing details of the console message.
Returns:
true if the message is handled by the client.

  • Bitmap getDefaultVideoPoster()

When not playing, video elements are represented by a ‘poster’ image. The image to use can be specified by the poster attribute of the video tag in HTML. If the attribute is absent, then a default poster will be used. This method allows the ChromeClient to provide that default image.

Returns:
Bitmap The image to use as a default poster, or null if no such image is available.

  • View getVideoLoadingProgressView()

Obtains a View to be displayed while buffering of full screen video is taking place. The host application can override this method to provide a View containing a spinner or similar.

Returns:
View The View to be displayed whilst the video is loading.

  • void getVisitedHistory(ValueCallback<String[]> callback)

Obtains a list of all visited history items, used for link coloring

  • boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback, FileChooserParams fileChooserParams)

Tell the client to show a file chooser. This is called to handle HTML forms with ‘file’ input type, in response to the user pressing the “Select File” button. To cancel the request, call filePathCallback.onReceiveValue(null) and return true.

Params:
webView – The WebView instance that is initiating the request.
filePathCallback – Invoke this callback to supply the list of paths to files to upload, or NULL to cancel. Must only be called if the showFileChooser implementations returns true.
fileChooserParams – Describes the mode of file chooser to be opened, and options to be used with it.
Returns:
true if filePathCallback will be invoked, false to use default handling.

Tricks

WebChromeClientWebViewClient区别

相信你跟我一样十分不理解为什么安卓webview要搞两个client来处理相关事务,其实WebView的内部实现并不是完全使用Chrome的内核,而是部分使用Chome内核,其它都是与Chrome不相同的。

  • WebViewClient:在影响View的事件到来时,会通过WebViewClient中的方法回调通知用户
  • WebChromeClient:当影响浏览器的事件到来时,就会通过WebChromeClient中的方法回调通知用法。

总之,WebViewClientWebChromeClient都是一堆针对不同事件的回调,而google将这些回调进行分类集合,就产生了WebViewClientWebChromeClient这两个大类,其中管理着针对不同类型的回调而已。

例如:

  • 只需要加上mWebView.setWebChromeClient(new WebChromeClient());就可以实现confrim()、alert()、prompt()的弹出效果了。除了alert,prompt,confirm以外,其它时候都不需要强制设置WebChromeClient。
  • 页面中含有连接,点击链接如果想继续在当前浏览器中浏览网页则需要重写 WebView 的 WebViewClient ,并实现shouldOverrideUrlLoading接口,否则默认的行为是会在手机系统自带的浏览器中打开新的链接。

CookieManager

WebStorage

原生通讯

原生调JS

// 调用JS
// 调用一个已定义的JS方法:
String jsUri="javascript:getText()";
// 调用一个未定义过的JS方法:
// String jsUri="javascript:(function getText(){...})()";
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
	// 相较于loadUrl,evaluateJavascript的优势在于异步加载,还可以将执行JS代码的结果带回来
	mWebView.evaluateJavascript(jsUri, null);
} else {
	mWebView.loadUrl(jsUri);
}

原生调用JS很关键的一点是注入的时机,在 Activity 的 onCreate() 方法中直接调用 WebView.loadUrl() 方法是不会生效的,需要在WebView 加载完成之后再调用 Js 的代码才会生效。通常推荐的自动注入入口是:

  • WebClient#OnPageStarted()
  • WebClient#OnPageFinished()

JS调原生

JavascriptInterface方案

首先原生定义一个通讯接口:

webView.addJavascriptInterface(new Object(){
	@JavascriptInterface
    public void toast(String msg) {
        ToastUtils.toast(msg);
    }
}, "namespace");

然后H5端调用:

function testPrompt(){
      window.namespace.toast("jsbridge");
}

iframe+schema拦截方案

url拦截这部分内容我们比较熟悉了,就是前面提到的WebViewClient#shouldOverrideUrlLoading回调以及WebChromeClient#onJsPrompt回调。

还是直接看例子:
一个只有button的H5页面如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <style>
         *{
            margin: 0;
            padding: 0;
        }
        html,body{
            width: 100%;
            height: 100%;
        }
        div{
            width:100%;
            height: 100%;
            background: aquamarine;
        }
    </style>
</head>
<body>
<div style="bac">
<button  onclick="callAndroid()" style="height: 26px ;width:160px; text-align: center; vertical-align: middle ">JS 调用Native</button>
</div>
</body>
<script>
    function callAndroid() {
      location.href= "jsbridge://webview?&arg1=hello&arg2=world"
    }
</script>
</html>

然后在原生端实现拦截,以响应JS:

@Override
public boolean shouldOverrideUrlLoading(WebView webView, String s) {
	Uri uri = Uri.parse(s);
    Log.d("test112", s);
    if(uri.getScheme().startsWith("jsbridge")) {
        String arg1 = uri.getQueryParameter("arg1");
        String arg2 = uri.getQueryParameter("arg2");
        String s1 = "JS调用Native,参数1:"+arg1+"参数2:"+arg2;
        Toast.makeText(MainActivity.this, s1, Toast.LENGTH_LONG).show();
    }
    return true;
}

我们发现url scheme拦截的核心是利用了H5的路由跳转功能,这就不可避免会遭遇跨域问题,此时可以通iframe技术绕过这种限制,有名的JSBridge项目就是利用了这个技术进行实现的

接口设计

原生生命周期事件

返回栈

安全相关

调试安全

// 是否允许JS代码,开启后控制台会输出详细的网页日志
// 此项仅能在debug开启,否则会报安全漏洞
WebView.setWebContentsDebuggingEnabled(true);

JS安全

webView.addJavascriptInterface这个接口允许JavaScript 控制宿主应用程序,这是个很强大的特性,但同时,在4.2的版本前存在重大安全隐患,因为JavaScript 可以使用反射访问注入webview的java对象的public fields,在一个包含不信任内容的WebView中使用这个方法,会允许攻击者去篡改宿主应用程序,使用宿主应用程序的权限执行java代码。因此4.2以后,任何为JS暴露的接口,都需要加@JavascriptInterface。

同时,为防止JS在后台执行危险动作,建议在页面onStart()中开启,onStop()中关闭:

	@Override
	protected void onStart() {
		super.onStart();
		mWebView.getSettings().setJavaScriptEnabled(true);
	}

	@Override
	protected void onStop() {
		mWebView.getSettings().setJavaScriptEnabled(false);
		super.onStop();
	}

跨域访问本地资源

在WebView中加载出从web服务器上拿取的内容时,是无法访问本地资源的,如assets目录下的图片资源,因为这样的行为属于跨域行为(Cross-Domain),而WebView是禁止的。解决这个问题的方案是把html内容先下载到本地,然后使用loadDataWithBaseURL加载html。这样就可以在html中使用 file:///android_asset/xxx.png 的链接来引用包里面assets下的资源了。

mWebView.setAllowFileAccess(boolean allow)

用户交互

状态展示(loading/error/success)

进度监听:

new WebChromeClient(){
    @Override
    public void onProgressChanged(WebView view, int newProgress) { 
		if (newProgress >= 100) {
			// 切换页面
		}else {
			// 自定义loading页面
		}
	}
}

监听状态:

new WebViewClient(){
	@Override
    public void onPageStarted(WebView view, String url, Bitmap favicon) { ... }
    
	@Override
    public void onPageFinished(WebView view, String url) { ... }
    
    @Override
    public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) { 
		super.onReceivedError(view, errorCode, description, failingUrl);
		//清除掉默认错误页内容
		loadDataWithBaseURL(null, "", "text/html", "utf-8", null);
		//显示自定义的error画面
		mErrorView.setVisibility(View.VISIBLE);
	}
}

权限申请

文件选择

回调在WebChromeClient中,具体如下:

mWebView.setWebChromeClient(new WebChromeClient() {
        //以下是在各个Android版本中 WebView调用文件选择器的方法
        // For Android < 3.0
        public void openFileChooser(ValueCallback<Uri> valueCallback) { ... }
        // For Android  >= 3.0
        public void openFileChooser(ValueCallback valueCallback, 
        							String acceptType) { ... }

        //For Android  >= 4.1
        @Override
        public void openFileChooser(ValueCallback<Uri> valueCallback,
                                    String acceptType, 
                                    String capture) { ... }

        // For Android >= 5.0
        @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
        @Override
        public boolean onShowFileChooser(WebView webView,
                                         ValueCallback<Uri[]> filePathCallback,
                                         FileChooserParams fileChooserParams) {
                
           //Intent intent = fileChooserParams.createIntent();
           //startActivityForResult(intent, CHOOSE_ANDROID_5);
           return true;
        }
    });

输入框键盘

  • 无法弹出软键盘
    解决办法:需要调用mWebView.requestFocus(),如果还是不行,检查构造方法中Webview(Context context, AttributeSet attrs, int defStyleAttr)defStyleAttr是否传了0,这里不能传0;

  • 软键盘遮挡input输入框
    如果是非沉浸式状态栏,设置activity的windowSoftInputMode=adjustResize
    如果是沉浸式状态栏,采用AndroidBug5497Workaround解决方案,原理是监听页面android.R.id.content容器高度,与屏幕高度之差大于一个阈值时表明键盘是弹出状态,则将android.R.id.content容器的第一个child(即我们的页面内容)高度设置成合适的高度。

大图查看

    /**
     * Inject img click logic. For example:
     * <pre>
     * // add native handler
     * view.addJavascriptInterface(new Object(){
     *     <code>@JavascriptInterface</code>
     *     <code>@Keep</code>
     *     public void openImage(String src,int position){
     *         // view image
     *     }
     * },"Android");
     *
     * // inject when page load finish
     * H5WebView.injectImgClick(view,"Android");
     * </pre>
     *
     * @param view   the view
     * @param bridge the bridge, use with {@link WebView#addJavascriptInterface(Object, String)}
     */
    public static void injectImgClick(WebView view, String bridge) {
        String js = "var imgs=document.getElementsByTagName(\"img\");";
        js += "for(var i=0;i<imgs.length;i++){";
        js += "imgs[i].pos = i;";
        js += "imgs[i].οnclick=function(){";
        js += "window." + bridge + ".openImage(this.src,this.pos);}}";
        view.loadUrl("javascript:" + js);
    }

图片长按下载

WebView.HitTestResult 这个函数可获取到我们点击的目标类型来得到当前点击的资源是什么,可实现长按下载图片。

视频全屏

详见以下两个接口说明:

  • WebChromeClient#onShowCustomView
  • WebChromeClient#onHideCustomView

文字复制粘贴

启用

Android如何设计一个H5容器

如果H5页面中没有禁用复制粘贴功能,则默认长按网页文字会弹出上图的操作菜单。

获取h5页面选中文字代码片段:

	private void getSelection() {
		String js = "(function getSelectedText() {"
				+ "var txt;"
				+ "if (window.getSelection) {"
				+ "txt = window.getSelection().toString();"
				+ "} else if (window.document.getSelection) {"
				+ "txt = window.document.getSelection().toString();"
				+ "} else if (window.document.selection) {"
				+ "txt = window.document.selection.createRange().text;"
				+ "}"
				// KITKAT(api19)以前:只能通过JS回调回传结果:
				// 假设JS回调接口是getSelectedTextResult(String text)
				+ bridgeNameSpace+".getSelectedTextResult(txt);"
				// api19及之后:可以直接返回值,evaluateJavascript支持回调
				// + "return txt;"
				+ "})()";
		// 这里由于不涉及回调
		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
			evaluateJavascript("javascript:" + js, null);
		} else {
			loadUrl("javascript:" + js);
		}
	}

禁用

以下代码来自网络,未经过测试验证

CSS控制页面文字不能被选中:

user-select:none;
body {
-moz-user-select:none;/*火狐*/
-webkit-user-select:none;/*webkit浏览器*/
-ms-user-select:none;/*IE10*/
-khtml-user-select:none;/*早期浏览器*/
user-select:none;
}

JS屏蔽复制粘贴右键菜单:

//屏蔽右键菜单
document.oncontextmenu = function (event){
	if(window.event){
		event = window.event;
	}
	try {
		var the = event.srcElement;
		if (!((the.tagName == "INPUT" && the.type.toLowerCase() == "text") || the.tagName == "TEXTAREA")){
			return false;
		}
		return true;
	}catch (e){
		return false;
	}
}

//屏蔽粘贴
document.onpaste = function (event){
	if(window.event){
		event = window.event;
	}
	try{
		var the = event.srcElement;
		if (!((the.tagName == "INPUT" && the.type.toLowerCase() == "text") || the.tagName == "TEXTAREA")){
			return false;
		}
		return true;
	}catch (e){
		return false;
	}
}

//屏蔽复制
document.oncopy = function (event){
	if(window.event){
		event = window.event;
	}
	try{
		var the = event.srcElement;
		if(!((the.tagName == "INPUT" && the.type.toLowerCase() == "text") || the.tagName == "TEXTAREA")){
			return false;
		}
		return true;
	}catch (e){
		return false;
	}
}

//屏蔽剪切
document.oncut = function (event){
	if(window.event){
		event = window.event;
	}
	try{
		var the = event.srcElement;
		if(!((the.tagName == "INPUT" && the.type.toLowerCase() == "text") || the.tagName == "TEXTAREA")){
			return false;
		}
		return true;
	}catch (e){
		return false;
	}
}

//屏蔽选中
document.onselectstart = function (event){
	if(window.event){
		event = window.event;
	}
	try{
		var the = event.srcElement;
		if (!((the.tagName == "INPUT" && the.type.toLowerCase() == "text") || the.tagName == "TEXTAREA")){
			return false;
		}
		return true;
	} catch (e) {
		return false;
	}
}

H5跳原生

H5跳转原生页面主要有3种方案:

  • JS调用原生
  • 通过uri scheme
  • 实现url自定义解析规则

其中JS调用原生比较简单,这里不做介绍,这里关注一下后面两种。

uri scheme

即Android DeepLink方案,支持当用户点击一个web链接时能直接跳转到特定的APP内某个页面,而不是从启动APP开始。

下面简单介绍下DeepLink的实现:

  • H5端:
    href=‘myapp://home’
  • APP端:
    页面清单:
<intent-filter>
   <data android:scheme="***" /> /* URI Schema 在此进行配置 */
   <action android:name="android.intent.action.VIEW" />
   <category android:name="android.intent.category.DEFAULT" />
   <category android:name="android.intent.category.BROWSABLE" />
</intent-filter>

其中data可配参数:

<data
	 <!-协议类型,必须,我们可以自定义,一般是项目或公司缩写,String !->
     android:scheme="xxxx"  
     <!-域名地址,必须,String !->
     android:host="xxxx"
     <!-端口,int !->
     android:port="xxxx"
     <!-访问的路径,String !->
     android:path="xxxx"
     <!-访问路径的匹配格式,相对于path和pathPrefix更为灵活,String !->
     <!-path,pathPrefix,pathPattern一般指定一个就可以了,pathPattern与host不可同时使用 !->
     android:pathPattern="xxxx"
     <!-访问的路径的前缀,String !->
     android:pathPrefix="xxxx"
     <!-资源类型,例如常见的:video/*, image/png, text/plain,设置了,跳转的时候必须加上mimeType,否则不能匹配到Activity !->
     android:mimeType="xxxx"/>

页面获取参数:

// 获取页面uri
Uri uri = getIntent().getData();

需要注意的是,在android的清单文件中对activity添加自定义intent-filter会使得activity的exported自动变为true,从而在一众移动安全扫描引擎中被标记为高危风险。

扩展阅读:延迟深度链接(Deferred Deep Linking)

url自定义解析

前面介绍WebViewClient的api时就有这么几个接口,可以利用它们实现自定义url解析和跳转逻辑。

webview.setWebViewClient(new WebViewClient() { 
	@Override 
	public boolean shouldOverrideUrlLoading(WebView view, String url) { 
		parserURL(url); //解析url,如果存在有跳转原生界面的url规则,则跳转原生。 
		return super.shouldOverri deUrlLoading(view, url); 
	} 
	@Override 
	public void onPageFinished(WebView view, String url) { 
		super.onPageFinished (view, url); 
	} 
	@Override 
	public void onl oadResource(WebView view, String url) { 
		super.onLoadResource(view, url); 
	} 
});

性能优化

缓存优化

相关HTTP协议

引用Nginx使用介绍中的相关图表:

服务端header 说明 客户端header 说明
Expires 缓存过期的日期和时间,给客户端用 当有缓存时判断缓存是否过期
Cache-Control 设置和缓存相关的配置信息,Cache-Control也可以返回一个过期时间,其功能比Expires更丰富,且优先级更高 配合Expires的值,判断缓存是否过期
Last-Modified 请求资源最后修改时间 If-Modified-SInce 客户端回传Last-Modified给服务器,服务器判断资源是否有更新,无更新返回304,减少网络数据传输
ETag 请求变量的实体标签的当前值,比如文件的MD5值 If-None-Match 客户端回传ETag,服务端判断资源有更新

Android如何设计一个H5容器
补充:浏览器在得到服务端返回的资源时还会根据Cache-Control指定的规则以及浏览器是否开启缓存功能来确定是否缓存。

服务器缓存实现

1、可以在html页面中添加 来给页面设置缓存时间。
2、对于图片、css等文件则需要web服务器配置中进行规则配置实现在请求资源的时候添加在responese的header中。

Android端实现

H5常用的6种缓存模式

  • Dom Storage

Dom Storage 提供 5MB 大小的缓存空间,以键值对的形式存取文件。

  • Web SQL Database

H5 提供了基于 SQL 的数据库存储机制,用于存储一些结构化的数据。

  • Application Cache 存储机制

Application Cache(简称 AppCache)似乎是为支持 Web App 离线使用而开发的缓存机制。它的缓存机制类似于浏览器的缓存(Cache-Control 和 Last-Modified)机制,都是以文件为单位进行缓存,且文件有一定更新机制。AppCache 是对浏览器缓存机制的补充,不是替代。不过根据官方文档,AppCache 已经不推荐使用了,标准也不会再支持。现在主流的浏览器都是还支持AppCache的。

  • Indexed Database 存储机制

IndexedDB 也是一种数据库的存储机制,但不同于已经不再支持的 Web SQL Database。IndexedDB 不是传统的关系数据库,可归为 NoSQL 数据库。IndexedDB 又类似于 Dom Storage 的 key-value 的存储方式,但功能更强大,且存储空间更大。Android 在4.4开始加入对 IndexedDB 的支持,只需打开允许 JS 执行的开关就好了。

  • File System API(暂不支持)

File System API 是 H5 新加入的存储机制。它为 Web App 提供了一个虚拟的文件系统,就像 Native App 访问本地文件系统一样。由于安全性的考虑,这个虚拟文件系统有一定的限制。Web App 在虚拟的文件系统中,可以进行文件(夹)的创建、读、写、删除、遍历等操作。很可惜到目前,Android 系统的 WebView 还不支持 File System API。

代码实现

// 开启Dom Storage(Web Storage)存储机制,默认false
webSettings.setDomStorageEnabled(true);

// 开启Web SQL Database存储机制,默认false,虽然已经不推荐使用了,但是为了兼容性还是开启下
webSettings.setDatabaseEnabled(true);
webSettings.setDatabasePath(getDir("db",Context.MODE_PRIVATE).getPath())

// 开启Application Cache存储机制
webSettings.setAppCacheEnabled(true);
webSettings.setAppCachePath(getDir("cache",Context.MODE_PRIVATE).getPath());
webSettings.setAppCacheMaxSize(5*1024*1024);

// 开启Indexed Database 存储机制,默认false
webSettings.setJavaScriptEnabled(true);

预加载技术

缓存优化可以加快webview二次启动的加载速度,那么首次加载该怎么优化呢?预加载资源就是一个比较好的方案,仅从资源下载的角度先分析一下一个H5网页的加载过程:

  1. 下载html文件
  2. 解析html,并行下载外部依赖的js、css、图片以及其他静态资源

第一阶段和第二阶段是不能并行执行的,可以从这两点入手进行优化。

本地HTML模板

如果页面样式是相对固定的,可以和前端(如果和App不是同一个开发)探讨设计一个HTML/CSS/JS模板,然后动态变化的CSS/JS通过HTML模板进行解析下载,模板文件直接将模板打入安装包中,这个一步优化效果是立竿见影的,不仅将两阶段下载缩减为一个,同时由于通常HTML下载通常耗时占比也比较高,所以可以很直观感受到加载速度的加快,因为用户很快就可以看见一个界面,而不是等待loading。

当然更多的情况是前端和APP之间没有这样的协同开发机制或HTML为了实现动态更新,很难做到HTML本地化,那么可以退而求其次,把H5页面的通用方法封装成JS库打入App中,APP在WebView 调用了onPageFinished() 方法后进行加载。

示例:一个基本的html文件格式+原生端配合

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
<script type="text/javascript">
   function show(jsondata){
      //[{name:"xxx",amount:600,phone:"13988888"},{name:"bb",amount:200,phone:"1398788"}]
           var jsonobjs = eval(jsondata);
           var table = document.getElementById("personTable");
           for(var y=0; y<jsonobjs.length; y++){
              var tr = table.insertRow(table.rows.length); //添加一行
              //添加三列
              var td1 = tr.insertCell(0);
              var td2 = tr.insertCell(1);
              td2.align = "center";
              var td3 = tr.insertCell(2);
              td3.align = "center";
              //设置列内容和属性
              td1.innerHTML = jsonobjs[y].name; 
              td2.innerHTML = jsonobjs[y].amount; 
              td3.innerHTML = "<a href='javascript:contact.call(\""+ jsonobjs[y].phone+ "\")'>"+ jsonobjs[y].phone+ "</a>"; 
         }
   }
</script>
</head>
<!-- js代码通过webView调用其插件中的java代码,即 -->
<body onl oad="javascript:contact.showContacts()">
   <table border="0" width="100%" id="personTable" cellspacing="0">
      <tr>
         <td width="35%">姓名</td><td width="30%" align="center">存款</td><td align="center">电话</td>
      </tr>
   </table>
   <a href="javascript:window.location.reload()">刷新</a>
</body>
</html>

原生端代码:

public class MainActivity extends AppCompatActivity {

    private WebView mWebView;

    private Handler mHandler = new Handler();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mWebView = (WebView) findViewById(R.id.web_view);
        mWebView.loadUrl("file:///android_asset/index.html");
        mWebView.getSettings().setJavaScriptEnabled(true);
        mWebView.addJavascriptInterface(new JSObject(),"contact");
    }

    public class JSObject{
        @JavascriptInterface
        public void call(String phone){
            Log.e("phone", "phone---->" + phone);
        }

        @JavascriptInterface
        public void showContacts(){
            try {
                JSONArray jsonArray = new JSONArray();
                JSONObject jsonObject = new JSONObject();
                jsonObject.put("name", "zhangsan");
                jsonObject.put("amount", "50");
                jsonObject.put("phone", "123465798");
                jsonArray.put(jsonObject);

                JSONObject jsonObject1 = new JSONObject();
                jsonObject1.put("name", "lisi");
                jsonObject1.put("amount", "48");
                jsonObject1.put("phone", "987456123");
                jsonArray.put(jsonObject1);

                final String json = jsonArray.toString();
                // 调用javascript中的show()方法
                mHandler.post(new Runnable() {
                    @Override
                    public void run() {
                        mWebView.loadUrl("javascript:show('" + json + "')");
                    }
                });

            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

本地JS文件

页面加载完成后,调用webView.loadUrl("javascript:"+jsContent)进行JS注入。需要注意的是,在该 JS 文件中需要写入一个 JS 文件载入完毕的事件,这样前端才能接受都到 JS 文件已经种植完毕,可以调用 JS 中的方法了。
示例JS代码:

 ;
(function() {
	try{
		window.JSBridge = {
			'invoke': function(name) {
						var args = [].slice.call(arguments, 1),
						callback = args.pop(),
						params, obj = this[name];
						if (typeof callback !== 'function') {
							params = callback;
							callback = function() {}
						} else {
							params = args[0]
						} if (typeof obj !== 'object' || typeof obj.func !== 'function') {
							callback({
								'err_msg': 'system:function_not_exist'
							});
							return
						}
					obj.callback = callback;
					obj.params = params;
					obj.func(params)
				},
		'on': function(event, callback) {
				var obj = this['on' + event];
				if (typeof obj !== 'object') {
					callback({
						'err_msg': 'system:function_not_exist'
					});
					retrun
				}
				if (typeof callback !== 'undefined') obj.callback = callback
			},
		'login': {
			'func': function(params) {
						prompt("login", JSON.stringify(params))
				},
			'params': {},
			'callback': function(res) {}
		},
		'settitle': {
			'func': function(params) {
						prompt("settitle",JSON.stringify(params))
					},
			'params': {},
			'callback': function(res) {}
		},
	}catch(e){
		alert('demo.js error:'+e);
	}
var readyEvent = document.createEvent('Events');
readyEvent.initEvent('JSBridgeReady', true, true);
document.dispatchEvent(readyEvent)
})();

可以比较一下在onPageFinished() 注入JS和shouldInterceptRequest()中替换JS本地文件哪个更多,择优选择方案

关于JS延迟加载。Android 的 OnPageFinished 事件会在 Javascript 脚本执行完成之后才会触发。如果在页面中使 用JQuery,会在处理完 DOM 对象,执行完 $(document).ready(function() {}); 事件自会后才会渲染并显示页面。而同样的页面在 iPhone 上却是载入相当的快,因为 iPhone 是显示完页面才会触发脚本的执行。一种解决方案是延迟 JS 脚本的载入,这个方面的问题是需要Web前端工程师帮忙优化的。

资源文件预加载

WebView为我们提供了资源请求拦截回调,从而可以实现外部依赖的 JS、CSS、图片等资源提前下载好,等H5 加载时直接替换:

mWebView.setWebViewClient(new WebViewClient() {
	@Override
	public WebResourceResponse shouldInterceptRequest(WebView webView, final String url) {
		WebResourceResponse response = null;
		// 检查该资源是否已经提前下载完成。我采用的策略是在应用启动时,用户在 wifi 的网络环境下 
		// 提前下载 H5 页面需要的资源。
		boolean resDown = JSHelper.isURLDownValid(url);
		if (resDown) {
			jsStr = JsjjJSHelper.getResInputStream(url);
			if (url.endsWith(".png")) {
				response = getWebResourceResponse(url, "image/png", ".png");
			} else if (url.endsWith(".gif")) {
				response = getWebResourceResponse(url, "image/gif", ".gif");
			} else if (url.endsWith(".jpg")) {
				response = getWebResourceResponse(url, "image/jepg", ".jpg");
			} else if (url.endsWith(".jepg")) {
				response = getWebResourceResponse(url, "image/jepg", ".jepg");
			} else if (url.endsWith(".js") && jsStr != null) {
				response = getWebResourceResponse("text/javascript", "UTF-8", ".js");
			} else if (url.endsWith(".css") && jsStr != null) {
				response = getWebResourceResponse("text/css", "UTF-8", ".css");
			} else if (url.endsWith(".html") && jsStr != null) {
				response = getWebResourceResponse("text/html", "UTF-8", ".html");
			}
		}
		// 若 response 返回为 null , WebView 会自行请求网络加载资源。
		return response;
	}
});

private WebResourceResponse getWebResourceResponse(String url, String mime, String style) {
	WebResourceResponse response = null;
	try {
		response = new WebResourceResponse(mime, "UTF-8", new FileInputStream(new File(getJSPath() + MD5.md5String(url) + style)));
	} catch (FileNotFoundException e) {
		e.printStackTrace();
	}
	return response;
}

public String getJSPath() {
	String splashTargetPath = context.getFilesDir().getPath() + "/JS";
	if (!FileUtil.isDirFileExist(splashTargetPath)) {
		FileUtil.createDir(splashTargetPath);
	}
	return splashTargetPath + "/";
}


图片延迟加载

指降低图片下载优先级,是为了把带宽让步给JS/CSS,使页面框架先下载好渲染出来呈现给用户,缩短页面空白期的持续时间,而图片等可以后面逐步加载。

public void int () {
	if(Build.VERSION.SDK_INT >= 19) {
		webView.getSettings().setLoadsImagesAutomatically(true);
	} else {
		webView.getSettings().setLoadsImagesAutomatically(false);
	}
}

//同时在WebView的WebViewClient实例中的onPageFinished()方法添加如下代码:
@Override
public void onPageFinished(WebView view, String url) {
	if(!webView.getSettings().getLoadsImagesAutomatically()) {
		webView.getSettings().setLoadsImagesAutomatically(true);
	}
}

上面代码对系统API在19以上的版本作了兼容。因为4.4以上系统在onPageFinished时再恢复图片加载时,如果存在多张图片引用的是相同的src时,会只有一个image标签得到加载,因而对于这样的系统我们就先直接加载。

内存泄漏优化

webview实例的创建需要占用大量内存(即使不加载任内容),而且即便销毁含有webview的页面,相关内存也无法被完全释放。关于webview内存泄漏的问题早在2009年就被人提出(链接),问题提到了泄漏的源头是PlguinManager持有了context引用,导致activity内存泄漏,但是采用applicationContext又会导致flash内容奔溃。

目前解决webview内存泄漏主要有三个方案:

  • 不在xml使用webview,采用new的方式并传入applicationContext进行创建;
  • 使用第三方的webview,比如腾讯X5内核;
  • 为 WebView 开启独立进程,通过 AIDL 与主进程进行通信,WebView 所在的进程根据业务的需要选择合适的时机进行销毁;

使用独立进程的好处是,webview各种内存占用、泄漏甚至OOM的问题不会影响到主进程。WebView 开启独立进程的方法如下:

<activity
	android:name=".ui.webview.CommWebviewActivity"
	android:configChanges="orientation|keyboardHidden|screenSize"
	android:process=":webview"   
	android:screenOrientation="portrait"
	android:windowSoftInputMode="stateHidden">
		<intent-filter>
			<category android:name="android.intent.category.BROWSABLE" />
			<category android:name="android.intent.category.DEFAULT" />
			<action android:name="android.intent.action.VIEW" /> 
			<data android:host="xxxx.com" android:scheme="myapp" /> 
		</intent-filter>
</activity>

重点是android:process=":webview",后续通讯可以通过aidl实现,这里就不过多介绍了。

其他细节优化

硬件加速

开启硬件加速后WebView渲染页面更加快速,拖动也更加顺滑。开启方法:

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { 
	webview.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
}

当WebView视图被整体遮住一块,然后突然恢复时(比如使用SlideMenu将WebView从侧边滑出来时),这个过渡期会出现白块同时界面闪烁。解决这个问题的方法是在过渡期前将WebView的硬件加速临时关闭,过渡期后再开启。

参考资料

添加链接描述

webview开源项目

上一篇:uni-app项目打包成H5部署到服务器(超详细步骤)


下一篇:初识h5