如果我不说奇技淫巧,你们估计就不点进来了
WebView在现在的项目中使用的频率应该还是非常高的。
HTML5因为其便捷性以及低成本性 是现在乃至未来的一种趋势。
我们来看看 Google 官网关于 WebView 的介绍:
A View that displays web pages. This class is the basis upon which you can roll your own web browser
or simply display some online content within your Activity. It uses the WebKit rendering engine
to display web pages and includes methods to navigate forward and backward through a history,
zoom in and out, perform text searches and more.
在Android4.4(API level 19)系统以前,Android使用了原生自带的Android Webkit内核,这个内核对HTML5的支持不是很好,现在使用4.4以下机子的也不多了,就不对这个内核做过多介绍了,有兴趣可以看下这篇文章。
从Android4.4系统开始,Chromium内核取代了Webkit内核,正式地接管了WebView的渲染工作。Chromium是一个开源的浏览器内核项目,基于Chromium开源项目修改实现的浏览器非常多,包括最著名的Chrome浏览器,以及一众国内浏览器(360浏览器、QQ浏览器等)。其中Chromium在Android上面的实现是Android System WebView
从Android5.0系统开始,WebView移植成了一个独立的apk,可以不依赖系统而独立存在和更新,我们可以在系统->设置->Android System WebView看到WebView的当前版本。
从Android7.0系统开始,如果系统安装了Chrome (version>51),那么Chrome将会直接为应用的WebView提供渲染,WebView版本会随着Chrome的更新而更新,用户也可以选择WebView的服务提供方(在开发者选项->WebView Implementation里),WebView可以脱离应用,在一个独立的沙盒进程中渲染页面(需要在开发者选项里打开)
从Android8.0系统开始,默认开启WebView多进程模式,即WebView运行在独立的沙盒进程中
一、WebView的基本使用
WebView 加载页面
WebView 有四个用来加载页面的方法:
使用起来较为简单,loadData 方法会有一些坑,在下面的内容会介绍到。
同时在使用 WebView 上有遇到一个坑,loadUrl() 方法在安卓 4.1.2 系统上有概率空指针异常,原因未知,应该是系统的 bug,所以也可以简单地用 try…catch 防止崩溃
WebView 常见设置
使用 WebView 的时候,一般都会对其进行一些设置,我们来看看常见的设置:
名称 | 功能 | 建议值 |
javaScriptEnabled | 是否可运行 JavaScript 脚本 | true |
pluginState | 是否可使用插件,插件未来将不会得到支持 | PluginState.ON |
supportZoom | 是否可放大画面 | true |
builtInZoomControls | 是否用内置的缩放算法 | true |
supportZoom | allowFileAccess | true |
layoutAligorithm | 版本号大于19使用TEXT_AUTOSIZING,小于19使用NORMAL 底层布局算法 | |
useWideViewPort | 是否允许使用 <viewport> 标签 | true |
loadWithOverviewMode | 是否使用概览模式 | true |
javaScriptCanOpenWindowsAutomatically | 是否可以运行 JavaScript 的 window.open() 方法来自动打开窗口 | true |
displayZoomControls | 是否在使用内容缩放算法时可以显示缩放控制 | 版本号大于11设为 false |
databaseEnabled | 是否可使用数据库存储 | true |
domStorageEnabled | 是否使用文档存储 | true |
geolocationEnabled | 是否使用地理位置 | true |
appCacheEnabled | 是否使用应用缓存 | true |
设置说明:
WebSettings webSettings=webView.getSettings();
//设置了这个属性后我们才能在 WebView 里与我们的 Js 代码进行交互,对于 WebApp 是非常重要的,默认是 false,
//因此我们需要设置为 true,这个本身会有漏洞,具体的下面我会讲到
webSettings.setJavaScriptEnabled(true);
//设置 JS 是否可以打开 WebView 新窗口
webSettings.setJavaScriptCanOpenWindowsAutomatically(true);
//WebView 是否支持多窗口,如果设置为 true,需要重写
//WebChromeClient#onCreateWindow(WebView, boolean, boolean, Message) 函数,默认为 false
webSettings.setSupportMultipleWindows(true);
//这个属性用来设置 WebView 是否能够加载图片资源,需要注意的是,这个方法会控制所有图片,包括那些使用 data URI 协议嵌入
//的图片。使用 setBlockNetworkImage(boolean) 方法来控制仅仅加载使用网络 URI 协议的图片。需要提到的一点是如果这
//个设置从 false 变为 true 之后,所有被内容引用的正在显示的 WebView 图片资源都会自动加载,该标识默认值为 true。
webSettings.setLoadsImagesAutomatically(false);
//标识是否加载网络上的图片(使用 http 或者 https 域名的资源),需要注意的是如果 getLoadsImagesAutomatically()
//不返回 true,这个标识将没有作用。这个标识和上面的标识会互相影响。
webSettings.setBlockNetworkImage(true);
//显示WebView提供的缩放控件
webSettings.setDisplayZoomControls(true);
webSettings.setBuiltInZoomControls(true);
//设置是否启动 WebView API,默认值为 false
webSettings.setDatabaseEnabled(true);
//打开 WebView 的 storage 功能,这样 JS 的 localStorage,sessionStorage 对象才可以使用
webSettings.setDomStorageEnabled(true);
//打开 WebView 的 LBS 功能,这样 JS 的 geolocation 对象才可以使用
webSettings.setGeolocationEnabled(true);
webSettings.setGeolocationDatabasePath("");
//设置是否打开 WebView 表单数据的保存功能
webSettings.setSaveFormData(true);
//设置 WebView 的默认 userAgent 字符串
webSettings.setUserAgentString("");
//设置是否 WebView 支持 “viewport” 的 HTML meta tag,这个标识是用来屏幕自适应的,当这个标识设置为 false 时,
//页面布局的宽度被一直设置为 CSS 中控制的 WebView 的宽度;如果设置为 true 并且页面含有 viewport meta tag,那么
//被这个 tag 声明的宽度将会被使用,如果页面没有这个 tag 或者没有提供一个宽度,那么一个宽型 viewport 将会被使用。
webSettings.setUseWideViewPort(false);
//设置 WebView 的字体,可以通过这个函数,改变 WebView 的字体,默认字体为 "sans-serif"
webSettings.setStandardFontFamily("");
//设置 WebView 字体的大小,默认大小为 16
webSettings.setDefaultFontSize(20);
//设置 WebView 支持的最小字体大小,默认为 8
webSettings.setMinimumFontSize(12);
//设置页面是否支持缩放
webSettings.setSupportZoom(true);
//设置文本的缩放倍数,默认为 100
webSettings.setTextZoom(100);
----------------------------------------------------------------------
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.KITKAT){
// 用户是否需要通过手势播放媒体(不会自动播放),默认值 true
settings.setMediaPlaybackRequiresUserGesture(true);
}
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.LOLLIPOP){
// 5.0以上允许加载http和https混合的页面(5.0以下默认允许,5.0+默认禁止)
settings.setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
}
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.M){
// 是否在离开屏幕时光栅化(会增加内存消耗),默认值 false
settings.setOffscreenPreRaster(false);
}
if(isNetworkConnected(context)){
// 根据cache-control决定是否从网络上取数据
settings.setCacheMode(WebSettings.LOAD_DEFAULT);
}else{
// 没网,离线加载,优先加载缓存(即使已经过期)
settings.setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK);
}
// deprecated
settings.setRenderPriority(WebSettings.RenderPriority.HIGH);
settings.setDatabasePath(context.getDir("database",Context.MODE_PRIVATE).getPath());
settings.setGeolocationDatabasePath(context.getFilesDir().getPath());
设置WebView缓存
(当加载 html 页面时,WebView会在/data/data/包名目录下生成 database 与 cache 两个文件夹,请求的 URL记录保存在 WebViewCache.db,而 URL的内容是保存在 WebViewCache 文件夹下)
//优先使用缓存: WebView.getSettings().setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK);
//缓存模式如下: //LOAD_CACHE_ONLY: 不使用网络,只读取本地缓存数据
//LOAD_DEFAULT: (默认)根据cache-control决定是否从网络上取数据。
//LOAD_NO_CACHE: 不使用缓存,只从网络获取数据.
//LOAD_CACHE_ELSE_NETWORK,只要本地有,无论是否过期,或者no-cache,都使用缓存中的数据。
//不使用缓存: WebView.getSettings().setCacheMode(WebSettings.LOAD_NO_CACHE);
清空缓存和清空历史记录,
CacheManager 来处理 webview 缓存相关:mWebView.clearCache(true);;清空历史记录mWebview.clearHistory();,这个方法要在 onPageFinished()的方法之后调用。
//结合使用(离线加载)(注意:每个 Application 只调用一次 WebSettings.setAppCachePath(),WebSettings.setAppCacheMaxSize())
if(NetStatusUtil.isConnected(getApplicationContext())){//判断网络是否连接
webSettings.setCacheMode(WebSettings.LOAD_DEFAULT);//根据cache-control决定是否从网络上取数据。
}else{
webSettings.setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK);//没网,则从本地获取,即离线加载
}
webSettings.setDomStorageEnabled(true); // 开启 DOM storage API 功能
webSettings.setDatabaseEnabled(true); //开启 database storage API 功能
webSettings.setAppCacheEnabled(true);//开启 Application Caches 功能
String cacheDirPath=getFilesDir().getAbsolutePath()+APP_CACAHE_DIRNAME;
webSettings.setAppCachePath(cacheDirPath); //设置 Application Caches 缓存目录
然后还有最常用的 WebViewClient 和 WebChromeClient,
WebViewClient主要辅助WebView执行处理各种响应请求事件的,比如:
-
onLoadResource
-
onPageStart
-
onPageFinish
-
onReceiveError
-
onReceivedHttpAuthRequest
-
shouldOverrideUrlLoading
// 拦截页面加载,返回true表示宿主app拦截并处理了该url,否则返回false由当前WebView处理
// 此方法在API24被废弃,不处理POST请求
import android.annotation.TargetApi;
import android.os.Build;
public boolean shouldOverrideUrlLoading(WebView view,String url){
return false;
}
// 拦截页面加载,返回true表示宿主app拦截并处理了该url,否则返回false由当前WebView处理
// 此方法添加于API24,不处理POST请求,可拦截处理子frame的非http请求
@TargetApi(Build.VERSION_CODES.N)
public boolean shouldOverrideUrlLoading(WebView view,WebResourceRequest request){
return shouldOverrideUrlLoading(view,request.getUrl().toString());
}
// 此方法废弃于API21,调用于非UI线程
// 拦截资源请求并返回响应数据,返回null时WebView将继续加载资源
// 注意:API21以下的AJAX请求会走onLoadResource,无法通过此方法拦截
public WebResourceResponse shouldInterceptRequest(WebView view,String url){
return null;
}
// 此方法添加于API21,调用于非UI线程
// 拦截资源请求并返回数据,返回null时WebView将继续加载资源
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public WebResourceResponse shouldInterceptRequest(WebView view,WebResourceRequest request){
return shouldInterceptRequest(view,request.getUrl().toString());
}
// 页面(url)开始加载
public void onPageStarted(WebView view,String url,Bitmap favicon){
}
// 页面(url)完成加载
public void onPageFinished(WebView view,String url){
}
// 将要加载资源(url)
public void onLoadResource(WebView view,String url){
}
// 这个回调添加于API23,仅用于主框架的导航
// 通知应用导航到之前页面时,其遗留的WebView内容将不再被绘制。
// 这个回调可以用来决定哪些WebView可见内容能被安全地回收,以确保不显示陈旧的内容
// 它最早被调用,以此保证WebView.onDraw不会绘制任何之前页面的内容,随后绘制背景色或需要加载的新内容。
// 当HTTP响应body已经开始加载并体现在DOM上将在随后的绘制中可见时,这个方法会被调用。
// 这个回调发生在文档加载的早期,因此它的资源(css,和图像)可能不可用。
// 如果需要更细粒度的视图更新,查看 postVisualStateCallback(long, WebView.VisualStateCallback).
// 请注意这上边的所有条件也支持 postVisualStateCallback(long ,WebView.VisualStateCallback)
public void onPageCommitVisible(WebView view,String url){
}
// 此方法废弃于API23
// 主框架加载资源时出错
public void onReceivedError(WebView view,int errorCode,String description,String failingUrl){
}
// 此方法添加于API23
// 加载资源时出错,通常意味着连接不到服务器
// 由于所有资源加载错误都会调用此方法,所以此方法应尽量逻辑简单
@TargetApi(Build.VERSION_CODES.M)
public void onReceivedError(WebView view,WebResourceRequest request,WebResourceError error){
if(request.isForMainFrame()){
onReceivedError(view,error.getErrorCode(),error.getDescription().toString(),request.getUrl().toString());
}
}
// 此方法添加于API23
// 在加载资源(iframe,image,js,css,ajax...)时收到了 HTTP 错误(状态码>=400)
public void onReceivedHttpError(WebView view,WebResourceRequest request,WebResourceResponse errorResponse){
}
// 是否重新提交表单,默认不重发
public void onFormResubmission(WebView view,Message dontResend,Message resend){
dontResend.sendToTarget();
}
// 通知应用可以将当前的url存储在数据库中,意味着当前的访问url已经生效并被记录在内核当中。
// 此方法在网页加载过程中只会被调用一次,网页前进后退并不会回调这个函数。
public void doUpdateVisitedHistory(WebView view,String url,boolean isReload){
}
// 加载资源时发生了一个SSL错误,应用必需响应(继续请求或取消请求)
// 处理决策可能被缓存用于后续的请求,默认行为是取消请求
public void onReceivedSslError(WebView view,SslErrorHandler handler,SslError error){
handler.cancel();
}
// 此方法添加于API21,在UI线程被调用
// 处理SSL客户端证书请求,必要的话可显示一个UI来提供KEY。
// 有三种响应方式:proceed()/cancel()/ignore(),默认行为是取消请求
// 如果调用proceed()或cancel(),Webview 将在内存中保存响应结果且对相同的"host:port"不会再次调用 onReceivedClientCertRequest
// 多数情况下,可通过KeyChain.choosePrivateKeyAlias启动一个Activity供用户选择合适的私钥
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public void onReceivedClientCertRequest(WebView view,ClientCertRequest request){
request.cancel();
}
// 处理HTTP认证请求,默认行为是取消请求
public void onReceivedHttpAuthRequest(WebView view,HttpAuthHandler handler,String host,String realm){
handler.cancel();
}
// 通知应用有个已授权账号自动登陆了
public void onReceivedLoginRequest(WebView view,String realm,String account,String args){
}
// 给应用一个机会处理按键事件
// 如果返回true,WebView不处理该事件,否则WebView会一直处理,默认返回false
public boolean shouldOverrideKeyEvent(WebView view,KeyEvent event){
return false;
}
// 处理未被WebView消费的按键事件
// WebView总是消费按键事件,除非是系统按键或shouldOverrideKeyEvent返回true
// 此方法在按键事件分派时被异步调用
public void onUnhandledKeyEvent(WebView view,KeyEvent event){
super.onUnhandledKeyEvent(view,event);
}
// 通知应用页面缩放系数变化
public void onScaleChanged(WebView view,float oldScale,float newScale){
}
WebChromeClient 主要辅助 WebView 处理J avaScript 的对话框、网站 Logo、网站 title、load 进度等处理:
-
onCloseWindow(关闭WebView)
-
onCreateWindow
-
onJsAlert
-
onJsPrompt
-
onJsConfirm
-
onProgressChanged
-
onReceivedIcon
-
onReceivedTitle
-
onShowCustomView
// 获得所有访问历史项目的列表,用于链接着色。
import android.annotation.TargetApi;
import android.os.Build;
public void getVisitedHistory(ValueCallback<String[]> callback) {
}
// <video /> 控件在未播放时,会展示为一张海报图,HTML中可通过它的'poster'属性来指定。
// 如果未指定'poster'属性,则通过此方法提供一个默认的海报图。
public Bitmap getDefaultVideoPoster() {
return null;
}
// 当全屏的视频正在缓冲时,此方法返回一个占位视图(比如旋转的菊花)。
public View getVideoLoadingProgressView() {
return null;
}
// 接收当前页面的加载进度
public void onProgressChanged(WebView view, int newProgress) {
}
// 接收文档标题
public void onReceivedTitle(WebView view, String title) {
}
// 接收图标(favicon)
public void onReceivedIcon(WebView view, Bitmap icon) {
}
// Android中处理Touch Icon的方案
// http://droidyue.com/blog/2015/01/18/deal-with-touch-icon-in-android/index.html
public void onReceivedTouchIconUrl(WebView view, String url, boolean precomposed) {
}
// 通知应用当前页进入了全屏模式,此时应用必须显示一个包含网页内容的自定义View
public void onShowCustomView(View view, CustomViewCallback callback) {
}
// 通知应用当前页退出了全屏模式,此时应用必须隐藏之前显示的自定义View
public void onHideCustomView() {
}
// 显示一个alert对话框
public boolean onJsAlert(WebView view, String url, String message, JsResult result) {
return false;
}
// 显示一个confirm对话框
public boolean onJsConfirm(WebView view, String url, String message, JsResult result) {
return false;
}
// 显示一个prompt对话框
public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
return false;
}
// 显示一个对话框让用户选择是否离开当前页面
public boolean onJsBeforeUnload(WebView view, String url, String message, JsResult result) {
return false;
}
// 指定源的网页内容在没有设置权限状态下尝试使用地理位置API。
// 从API24开始,此方法只为安全的源(https)调用,非安全的源会被自动拒绝
public void onGeolocationPermissionsShowPrompt(String origin, GeolocationPermissions.Callback callback) {
}
// 当前一个调用 onGeolocationPermissionsShowPrompt() 取消时,隐藏相关的UI。
public void onGeolocationPermissionsHidePrompt() {
}
// 通知应用打开新窗口
public boolean onCreateWindow(WebView view, boolean isDialog, boolean isUserGesture, Message resultMsg) {
return false;
}
// 通知应用关闭窗口
public void onCloseWindow(WebView window) {
}
// 请求获取取焦点
public void onRequestFocus(WebView view) {
}
// 通知应用网页内容申请访问指定资源的权限(该权限未被授权或拒绝)
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public void onPermissionRequest(PermissionRequest request) {
request.deny();
}
// 通知应用权限的申请被取消,隐藏相关的UI。
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public void onPermissionRequestCanceled(PermissionRequest request) {
}
// 为'<input type="file" />'显示文件选择器,返回false使用默认处理
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback, FileChooserParams fileChooserParams) {
return false;
}
// 接收JavaScript控制台消息
public boolean onConsoleMessage(ConsoleMessage consoleMessage) {
return false;
}
回调顺序
页面加载回调顺序:
shouldOverrideUrlLoading
onProgressChanged[10]
shouldInterceptRequest
onProgressChanged[...]
onPageStarted
onProgressChanged[...]
onLoadResource
onProgressChanged[...]
onReceivedTitle/onPageCommitVisible
onProgressChanged[100]
onPageFinished
onReceivedIcon
资源加载回调:
shouldInterceptRequest() -> onLoadResource()
发生重定向时回调:
onPageStarted() -> shouldOverrideUrlLoading()
直接loadUrl的回调:
// 无重定向
onPageStarted() -> onPageFinished()
// 有重定向,shouldOverrideUrlLoading 返回 true 时 onPageFinished 仍会执行
onPageStarted() -> redirection -> ... -> onPageFinished()
用户点击链接的回调:
// shouldOverrideUrlLoading 返回 true 时不执行onPageStarted/onPageFinished
shouldOverrideUrlLoading() -> ...
// 无重定向
shouldOverrideUrlLoading() -> onPageStarted() -> onPageFinished()
// 有重定向
shouldOverrideUrlLoading() -> onPageStarted() -> redirection -> ... -> onPageFinished()
// 有重定向(A->B->C)
shouldOverrideUrlLoading(A) -> onPageStarted(A) ->
onPageStarted(B) -> shouldOverrideUrlLoading(B) ->
onPageStarted(C) -> shouldOverrideUrlLoading(C) -> onPageFinished(C)
后退/前进/刷新 时回调:
onPageStarted() -> onPageFinished()
关于 window.location
假设从A页面跳转到B页面
如果页面B中直接输出 window.location="http://example.com",那页面B不会被加入回退栈,回退将直接回到A页
如果页面B加载完成后,比如用setTimeout延迟了,那页面B会被加入回退栈,当回退到页面A时会再执行跳转,这会导致回退功能看起来不正常,需要快速回退两次才能回到A页面
缓存机制
Android WebView自带的缓存机制有5种:
浏览器 缓存机制
Application Cache 缓存机制
Dom Storage 缓存机制
Web SQL Database 缓存机制
Indexed Database 缓存机制
File System 缓存机制(H5页面新加入的缓存机制,Android WebView暂时不支持)
二、安卓通过WebView和js交互
使用 Hybrid 开发的 APP 基本都需要 Native 和 web 页面的 JS 进行交互,下面介绍一下交互的方式。
对于 Android调用JS代码 的方法有2种:
通过WebView的loadUrl()
通过WebView的evaluateJavascript()
对于 JS调用Android代码 的方法有3种:
通过WebView的addJavascriptInterface()进行对象映射
通过 WebViewClient 的shouldOverrideUrlLoading ()方法回调拦截 url
通过 WebChromeClient 的onJsAlert()、onJsConfirm()、onJsPrompt()方法回调拦截JS对话框alert()、confirm()、prompt() 消息
js 调用 native
如何让 web 页面调用 native 的代码呢,有三种方式:
第一种方式:通过 addJavascriptInterface 方法进行添加对象映射
这种是使用最多的方式了,首先第一步我们需要设置一个属性:
mWebView.getSettings().setJavaScriptEnabled(true);
这个函数会有一个警告,因为在特定的版本之下会有非常危险的漏洞,我们下面将会着重介绍到,设置完这个属性之后,Native 需要定义一个类:
import android.content.Context;
import android.webkit.JavascriptInterface;
import android.widget.Toast;
public class JSObject {
private Context mContext;
public JSObject(Context context) {
mContext = context;
}
@JavascriptInterface
public String showToast(String text) {
Toast.show(mContext, text, Toast.LENGTH_SHORT).show();
return "success";
}
}
...
//特定版本下会存在漏洞
mWebView.addJavascriptInterface(new JSObject(this), "myObj");
需要注意的是在 API17 版本之后,需要在被调用的地方加上 @addJavascriptInterface 约束注解,因为不加上注解的方法是没有办法被调用的,JS 代码也很简单:
function showToast(){
var result = myObj.showToast("我是来自web的Toast");
}
可以看到,这种方式的好处在于使用简单明了,本地和 JS 的约定也很简单,就是对象名称和方法名称约定好即可,缺点就是下面要提到的漏洞问题。
第二种方式:利用 WebViewClient 接口回调方法拦截 url
这种方式其实实现也很简单,使用的频次也很高,上面我们介绍到了 WebViewClient ,其中有个回调接口 shouldOverrideUrlLoading (WebView view, String url)) ,我们就是利用这个拦截 url,然后解析这个 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".
注意这个方法在 API24 版本已经废弃了,需要使用 shouldOverrideUrlLoading (WebView view, WebResourceRequest request)) 替代,使用方法很类似,我们这里就使用 shouldOverrideUrlLoading (WebView view, String url)) 方法来介绍一下:
public boolean shouldOverrideUrlLoading(WebView view, String url) {
//假定传入进来的 url = "js://openActivity?arg1=111&arg2=222",代表需要打开本地页面,并且带入相应的参数
Uri uri = Uri.parse(url);
String scheme = uri.getScheme();
//如果 scheme 为 js,代表为预先约定的 js 协议
if (scheme.equals("js")) {
//如果 authority 为 openActivity,代表 web 需要打开一个本地的页面
if (uri.getAuthority().equals("openActivity")) {
//解析 web 页面带过来的相关参数
HashMap<String, String> params = new HashMap<>();
Set<String> collection = uri.getQueryParameterNames();
for (String name : collection) {
params.put(name, uri.getQueryParameter(name));
}
Intent intent = new Intent(getContext(), MainActivity.class);
intent.putExtra("params", params);
getContext().startActivity(intent);
}
//代表应用内部处理完成
return true;
}
return super.shouldOverrideUrlLoading(view, url);
}
代码很简单,这个方法可以拦截 WebView 中加载 url 的过程,得到对应的 url,我们就可以通过这个方法,与网页约定好一个协议,如果匹配,执行相应操作,我们看一下 JS 的代码:
function openActivity(){
document.location = "js://openActivity?arg1=111&arg2=222";
}
这个代码执行之后,就会触发本地的 shouldOverrideUrlLoading 方法,然后进行参数解析,调用指定方法。这个方式不会存在第一种提到的漏洞问题,但是它也有一个很繁琐的地方是,如果 web 端想要得到方法的返回值,只能通过 WebView 的 loadUrl 方法去执行 JS 方法把返回值传递回去,相关的代码如下:
//java
mWebView.loadUrl("javascript:returnResult(" + result + ")");
//javascriptfunction returnResult(result){
alert("result is" + result);
}
所以说第二种方式在返回值方面还是很繁琐的,但是在不需要返回值的情况下,比如打开 Native 页面,还是很合适的,制定好相应的协议,就能够让 web 端具有打开所有本地页面的能力了。
第三种方式:利用 WebChromeClient 回调接口的三个方法拦截消息
这个方法的原理和第二种方式原理一样,都是拦截相关接口,只是拦截的接口不一样:
@Override public boolean onJsAlert(WebView view,String url,String message,JsResult result){
return super.onJsAlert(view,url,message,result);
}
@Override public boolean onJsConfirm(WebView view,String url,String message,JsResult result){
return super.onJsConfirm(view,url,message,result);
}
@Override public boolean onJsPrompt(WebView view,String url,String message,String defaultValue,JsPromptResult result){
//假定传入进来的 message = "js://openActivity?arg1=111&arg2=222",代表需要打开本地页面,并且带入相应的参数
Uri uri=Uri.parse(message);
String scheme=uri.getScheme();
if(scheme.equals("js")){
if(uri.getAuthority().equals("openActivity")){
HashMap<String, String> params=new HashMap<>();
Set<String> collection=uri.getQueryParameterNames();
for(String name:collection){
params.put(name,uri.getQueryParameter(name));
}
Intent intent=new Intent(getContext(),MainActivity.class);
intent.putExtra("params",params);
getContext().startActivity(intent);
//代表应用内部处理完成
result.confirm("success");
}
return true;
}
return super.onJsPrompt(view,url,message,defaultValue,result);
}
和 WebViewClient 一样,这次添加的是 WebChromeClient 接口,可以拦截 JS 中的几个提示方法,也就是几种样式的对话框,在 JS 中有三个常用的对话框方法:
-
onJsAlert 方法是弹出警告框,一般情况下在 Android 中为 Toast,在文本里面加入\n就可以换行;
-
onJsConfirm 弹出确认框,会返回布尔值,通过这个值可以判断点击时确认还是取消,true表示点击了确认,false表示点击了取消;
-
onJsPrompt 弹出输入框,点击确认返回输入框中的值,点击取消返回 null。
但是这三种对话框都是可以本地拦截到的,所以可以从这里去做一些更改,拦截这些方法,得到他们的内容,进行解析,比如如果是 JS 的协议,则说明为内部协议,进行下一步解析然后进行相关的操作即可,prompt 方法调用如下所示:
function clickprompt(){
var result=prompt("js://openActivity?arg1=111&arg2=222");
alert("open activity " + result);
}
这里需要注意的是 prompt 里面的内容是通过 message 传递过来的,并不是第二个参数的 url,返回值是通过 JsPromptResult 对象传递。为什么要拦截 onJsPrompt 方法,而不是拦截其他的两个方法,这个从某种意义上来说都是可行的,但是如果需要返回值给 web 端的话就不行了,因为 onJsAlert 是不能返回值的,而 onJsConfirm 只能够返回确定或者取消两个值,只有 onJsPrompt 方法是可以返回字符串类型的值,操作最全面方便。
以上三种方案的总结和对比
调用方式 | 优点 | 缺点 | 使用场景 |
通过addJavascriptInterface()进行添加对象映射 | 方便简洁 | Android4.2以下存在漏洞问题 | Android4.2以上相对简单应用场景 |
通过WebView的方法shouldOverrideUrlLoading()回调拦截url | 不存在漏洞问题 | 使用复杂,需要进行协议约束,从native层到web层传递值比较繁琐 | 不需要返回值情况下互调场景 |
通过 WebChromeClient 的onJsAlert()、onJsConfirm()、onJsPrompt()方法回调拦截JS对话框消息 | 不存在漏洞问题 | 使用复杂,需要进行协议约束 | 能满足大多数情况下互调场景 |
以上三种方案都是可行的,在这里总结一下
-
第一种方式:是现在目前最普遍的用法,方便简洁,但是唯一的不足是在 4.2 系统以下存在漏洞问题;
-
第二种方式:通过拦截 url 并解析,如果是已经约定好的协议则进行相应规定好的操作,缺点就是协议的约束需要记录一个规范的文档,而且从 Native 层往 Web 层传递值比较繁琐,优点就是不会存在漏洞,iOS7 之下的版本就是使用的这种方式。
-
第三种方式:和第二种方式的思想其实是类似的,只是拦截的方法变了,这里拦截了 JS 中的三种对话框方法,而这三种对话框方法的区别就在于返回值问题,alert 对话框没有返回值,confirm 的对话框方法只有两种状态的返回值,prompt 对话框方法可以返回任意类型的返回值,缺点就是协议的制定比较麻烦,需要记录详细的文档,但是不会存在第二种方法的漏洞问题。
native 调用 js
第一种方式
native 调用 js 的方法上面已经介绍到了,方法为:
//java
mWebView.loadUrl("javascript:show(" + result + ")");
//javascript
<script type="text/javascript">
function show(result){
alert("result"=result);
return "success";
}
</script>
需要注意的是名字一定要对应上,要不然是调用不成功的,而且还有一点是 JS 的调用一定要在 onPageFinished 函数回调之后才能调用,要不然也是会失败的。
第二种方式
final int version = Build.VERSION.SDK_INT;
if (version < 18) {
mWebView.loadUrl(jsStr);
} else {
mWebView.evaluateJavascript(jsStr, new ValueCallback<String>() {
@Override
public void onReceiveValue(String value) {
//此处为 js 返回的结果
}
});
}
如果现在有需求,我们要得到一个 Native 调用 Web 的回调怎么办,Google 在 Android4.4 为我们新增加了一个新方法,这个方法比 loadUrl 方法更加方便简洁,而且比 loadUrl 效率更高,因为 loadUrl 的执行会造成页面刷新一次,这个方法不会,因为这个方法是在 4.4 版本才引入的,所以我们使用的时候需要添加版本的判断:
两种方式的对比
调用方式 | 优点 | 缺点 | 使用场景 |
调用loadUrl() | 方便简洁 | 效率低,获取返回值麻烦 | 不需要获取返回值,对性能要求较低时 |
调用evaluateJavascript() | 效率高 | 向下兼容性差(仅用于4.4+) | 适用于4.4+ |
一般最常使用的就是第一种方法,但是第一种方法获取返回的值比较麻烦,而第二种方法由于是在 4.4 版本引入的,所以局限性比较大。
三、webview排坑优化
可以说,如果是初次接触WebView,不踩坑几乎是不可能的。笔者在接触到前人留下来的WebView代码时很有可能就会掉进坑里。下面介绍几个可能遇到的坑。
内存泄漏
直接 new WebView 并传入 application context 代替在 XML 里面声明以防止 activity 引用被滥用,能解决90+%的 WebView 内存泄漏。
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT);
mWebView = new WebView(getApplicationContext());
mWebView.setLayoutParams(params);
container.addView(mWebView);
注:此方法会导致select无法弹出,因为select默认会弹出一个原生的框,需要activity承载。
销毁 WebView
if (vWeb != null) {
vWeb.setWebViewClient(null);
vWeb.setWebChromeClient(null);
vWeb.loadDataWithBaseURL(null, "", "text/html", "utf-8", null);
vWeb.clearHistory();
((ViewGroup) vWeb.getParent()).removeView(vWeb);
vWeb.destroy();
vWeb = null;
}
为什么Webview打开一个页面,播放一段音乐,退出Activity时音乐还在后台播放?
◆◆ 解决方案 1:
//销毁Webview@Overrideprotected void onDestroy() {
if (mWebview != null) {
mWebview.loadDataWithBaseURL(null, "", "text/html", "utf-8", null);
mWebview.clearHistory();
((ViewGroup) mWebview.getParent()).removeView(mWebview);
mWebview.destroy();
mWebview = null;
}
super.onDestroy();
}
还有别问我为什么要移除,等你Error: WebView.destroy() called while still attached!之后你就知道了。
◆◆ 解决方案 2:
@Overrideprotected void onPause() {
h5_webview.onPause();
h5_webview.pauseTimers();
super.onPause();
}
@Overrideprotected void onResume() {
h5_webview.onResume();
h5_webview.resumeTimers();
super.onResume();
}
Webview的onPause()方法官网是这么解释的:
Does a best-effort attempt to pause any processing that can be paused safely, such as animations and geolocation. Note that this call does not pause JavaScript. To pause JavaScript globally, use pauseTimers(). To resume WebView, call onResume(). 【翻译:】通知内核尝试停止所有处理,如动画和地理位置,但是不能停止Js,如果想全局停止Js, 可以调用pauseTimers()全局停止Js,调用onResume()恢复。
怎么用网页的标题来设置自己的标题栏?
◆◆ 解决方案:
WebChromeClient mWebChromeClient = new WebChromeClient() {
@Override
public void onReceivedTitle(WebView view, String title) {
super.onReceivedTitle(view, title);
txtTitle.setText(title);
}
};
mWedView.setWebChromeClient(mWebChromeClient());
★★ 注意事项:
● 1.可能当前页面没有标题,获取到的是null,那么你可以在跳转到该Activity的时候自己带一个标题,或者有一个默认标题。 ● 2.在一些机型上面,Webview.goBack()后,这个方法不一定会调用,所以标题还是之前页面的标题。那么 你就需要用一个ArrayList来保持加载过的url,一个HashMap保存url及对应的title.然后就是用WebView.canGoBack()来做判断处理了。
为什么打包之后JS调用失败(或者WebView与JavaScript相互调用时,如果是debug没有配置混淆时,调用时没问题的,但是当设置混淆后发现无法正常调用了)?
◆◆ 解决方案:在proguard-rules.pro中添加混淆。 -keepattributes *Annotation* -keepattributes *JavascriptInterface* -keep public class org.mq.study.webview.DemoJavaScriptInterface{ public <methods>; } #假如是内部类,混淆如下: -keepattributes *JavascriptInterface* -keep public class org.mq.study.webview.webview.DemoJavaScriptInterface$InnerClass{ public <methods>; } 其中org.mq.study.webview.DemoJavaScriptInterface 是不需要混淆的类名
5.0 以后的WebView加载的链接为Https开头,但是链接里面的内容,比如图片为Http链接,这时候,图片就会加载不出来,怎么解决?
★★ 原因分析:原因是Android 5.0上Webview默认不允许加载Http与Https混合内容:
◆◆ 解决方案:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { //两者都可以 webSetting.setMixedContentMode(webSetting.getMixedContentMode()); //mWebView.getSettings().setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW); } ★★ 参数说明: ● MIXED_CONTENT_ALWAYS_ALLOW 允许从任何来源加载内容,即使起源是不安全的; ● MIXED_CONTENT_NEVER_ALLOW 不允许Https加载Http的内容,即不允许从安全的起源去加载一个不安全的 资源; ● MIXED_CONTENT_COMPLTIBILITY_MODE 当涉及到混合式内容时,WebView会尝试去兼容最新Web浏览器的 风格; 另外:在认证证书不被Android所接受的情况下,我们可以通过设置重写WebViewClient的onReceivedSslError方法在其中设置接受所有网站的证书来解决,具体代码如下:
webView.setWebViewClient(new WebViewClient() {
@Override
public void onReceivedSslError(WebView view,
SslErrorHandler handler, SslError error) {
//super.onReceivedSslError(view, handler, error);注意一定要去除这行代码,否则设置无效。
// handler.cancel();// Android默认的处理方式
handler.proceed();// 接受所有网站的证书
// handleMessage(Message msg);// 进行其他处理
}
});
WebView 开启硬件加速导致的问题?
WebView有很多问题,比如:不能打开pdf,播放视屏也只能打开硬件加速才能支持,在某些机型上会崩溃。
下面看一下硬件加速, 硬件加速 分为四个级别:
Application级别
<application android:hardwareAccelerated="true"...>
Activity级别
<activity android:hardwareAccelerated="true"...>
window级别(目前为止,Android还不支持在Window级别关闭硬件加速。)
getWindow().setFlags(
WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED,
WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED);
View级别
view.setLayerType(View.LAYER_TYPE_HARDWARE, null);
WebView开启硬件加速导致屏幕花屏问题的解决:
★★ 原因分析: 4.0以上的系统我们开启硬件加速后,WebView渲染页面更加快速,拖动也更加顺滑。但有个副作用就是,当WebView视图被整体遮住一块,然后突然恢复时(比如使用SlideMenu将WebView从侧边滑出来时),这个过渡期会出现白块同时界面闪烁。 ◆◆ 解决方案: 在过渡期前将WebView的硬件加速临时关闭,过渡期后再开启,代码如下: if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { webview.setLayerType(View.LAYER_TYPE_SOFTWARE, null); }
Android 4.0+ 版本中的EditText字符重叠问题:
做的软件,在一些机器上,打字的时候,EditText中的内容会出现重叠,而大部分机器没有,所以感觉不是代码的问题,一直没有头绪。 出现原因:JellyBean的硬件加速bug,在此我们关掉硬件加速即可。 解决方案:在EditText中加入一句: android:layerType=”software” 图片无法显示: 做的程序里有的时候会需要加载大图,但是硬件加速中 OpenGL对于内存是有限制的。如果遇到了这个限制,LogCat只会报一个 Warning:Bitmap too large to be uploaded into a texture(587x7696,max=2048x2048) 这时我们就需要把硬件加速关闭了。
ViewPager里非首屏WebView点击事件不响应是什么原因?
如果你的多个WebView是放在ViewPager里一个个加载出来的,那么就会遇到这样的问题。ViewPager首屏WebView的创建是在前台,点击时没有问题;而其他非首屏的WebView是在后台创建,滑动到它后点击页面会出现如下错误日志: 20955-20968/xx.xxx.xxx E/webcoreglue﹕ Should not happen: no rect-based-test nodes found ◆◆ 解决方案: 这个问题的办法是继承WebView类,在子类覆盖onTouchEvent方法,填入如下代码:
@Overridepublic boolean onTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onScrollChanged(getScrollX(), getScrollY(), getScrollX(), getScrollY());
}
return super.onTouchEvent(ev);
}
WebView白屏是什么原因?
◆◆ 解决方案: WebView设置setLayerType(View.LAYER_TYPE_SOFTWARE,null); 示例代码如此下: if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { setLayerType(View.LAYER_TYPE_SOFTWARE, null); } ◆◆ 相关源码分析: WebView继承View,View中有三种layer type分别为LAYER_TYPE_NONE,LAYER_TYPE_SOFTWARE,LAYER_TYPE_HARDWARE。 1.LAYER_TYPE_NONE:表明视图没有多余渲染层。 2.LAYER_TYPE_SOFTWARE:表明视图有一个软件渲染层。无论是否开启硬件加速,都会有一张 Bitmap(software layer),并在上面对 WebView 进行渲染。 好处:在进行动画,使用software可以只画一次ViewTree,很省。 不适合使用场景:View树经常更新时不要用。尤其是在硬件加速打开时,每次更新消耗的时间更多。因为渲染完这张Bitmap后还需要再把这张Bitmap渲染到hardware layer上面去。 LAYER_TYPE_HARDWARE: 表明视图有一个硬件渲染层。硬件加速关闭时,作用同software。硬件加速打开时会在FBO(Framebuffer Object)上做渲染,在进行动画时,View树也只需要画一次。 ◆◆ LAYER_TYPE_SOFTWARE 和 LAYER_TYPE_HARDWARE的区别: 1.前者是渲染到Bitmap,后者是渲染到FB上。 2.hardware可能会有一些操作不支持(出现白屏)。 ◆◆ LAYER_TYPE_SOFTWARE 和 LAYER_TYPE_HARDWARE的相同: 都是开了一个buffer,把View画到这个buffer上面去
WebSettings.setJavaScriptEnabled问题
我相信99%的应用都会调用下面这句 WebSettings.setJavaScriptEnabled(true); 在Android 4.3版本调用WebSettings.setJavaScriptEnabled()方法时会调用一下reload方法,同时会回调多次WebChromeClient.onJsPrompt()。如果有业务逻辑依赖于这两个方法,就需要注意判断回调多次是否会带来影响了。 同时,如果启用了JavaScript,务必做好安全措施,防止远程执行漏洞4。
@TargetApi(11)
private static final void removeJavascriptInterfaces(WebView webView) {
try {
if (Build.VERSION.SDK_INT >= 11 && Build.VERSION.SDK_INT < 17) {
webView.removeJavascriptInterface("searchBoxJavaBridge_");
webView.removeJavascriptInterface("accessibility");
webView.removeJavascriptInterface("accessibilityTraversal");
}
} catch (Throwable tr) {
tr.printStackTrace();
}
}
301/302重定向问题
WebView的301/302重定向问题,绝对在踩坑排行榜里名列前茅。。。随便搜了几个解决方案,要么不能满足业务需求,要么清一色没有彻底解决问题。
https://stackoverflow.com/questions/4066438/android-webview-how-to-handle-redirects-in-app-instead-of-opening-a-browser http://blog.csdn.net/jdsjlzx/article/details/51698250http://www.cnblogs.com/pedro-neer/p/5318354.html http://www.jianshu.com/p/c01769ababfa
301/302业务场景及白屏问题
先来分析一下业务场景。对于需要对url进行拦截以及在url中需要拼接特定参数的WebView来说,301和302发生的情景主要有以下几种:
-
首次进入,有重定向,然后直接加载H5页面,如http跳转https
-
首次进入,有重定向,然后跳转到native页面,如扫一扫短链,然后跳转到native
-
二次加载,有重定向,跳转到native页面
-
对于考拉业务来说,还有类似登录后跳转到某个页面的需求。如我的拼团,未登录状态下点击我的拼团跳转到登录页面,登录完成后再加载我的拼团页。
第一种情况属于正常情况,暂时没遇到什么坑。
第二种情况,会遇到WebView空白页问题,属于原始url不能拦截到native页面,但301/302后的url拦截到native页面的情况,当遇到这种情况时,需要把WebView对应的Activity结束,否则当用户从拦截后的页面返回上一个页面时,是一个WebView空白页。
第三种情况,也会遇到WebView空白页问题,原因在于加载的第一个页面发生了重定向到了第二个页面,第二个页面被客户端拦截跳转到native页面,那么WebView就停留在第一个页面的状态了,第一个页面显然是空白页。
第四种情况,会遇到无限加载登录页面的问题。考拉的登录链接是类似下面这种格式:
https://m.kaola.com/login.html?target=登录后跳转的url
如果登录成功后还重新加载这个url,那么就会循环跳转到登录页面。第四点解决起来比较简单,登录成功以后拿到target后的跳转url再重新加载即可。
301/302回退栈问题
无论是哪种重定向场景,都不可避免地会遇到回退栈的处理问题,如果处理不当,用户按返回键的时候不一定能回到重定向之前的那个页面。 很多开发者在覆写WebViewClient.shouldOverrideUrlLoading()方法时, 会简单地使用以下方式粗暴处理:
WebView.setWebViewClient(new WebViewClient() {
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
view.loadUrl(url);
return true;
}
...
)
这种方法最致命的弱点就是如果不经过特殊处理,那么按返回键是没有效果的,还会停留在302之前的页面。现有的解决方案无非就几种: 手动管理回退栈,遇到重定向时回退两次 ( http://qbeenslee.com/article/android-webview-302-redirect/ ) 通过HitTestResult判断是否是重定向,从而决定是否自己加载url (https://juejin.im/entry/5977598d51882548c0045bde / http://www.cnblogs.com/zimengfang/p/6183869.html ) 通过设置标记位,在onPageStarted和onPageFinished分别标记变量避免重定向( http://blog.csdn.net/dg_summer/article/details/78105582) 可以说,这几种解决方案都不是完美的,都有缺陷。
安卓8.0关于WebView的新特性
WebView新增了一些非常有用的API,可以使用和chrome浏览器类似的API来实现对恶意网站的检测来保护web浏览的安全性,为此需要在manifest中添加如下meta-data标签:
<manifest>
<meta-data
android:name="android.webkit.WebView.EnableSafeBrowing"
android:value="true" /><!-- ... --></manifest>
WebView还增加了关于多进程的API,可以使用多进程来增强安全性和健壮性,如果render进程崩溃了,你还可以使用Termination Handler API来检测到崩溃并做出相应处理。
四、其他概念
视口(viewport)
https://developer.android.com/guide/webapps/targeting.html https://developer.mozilla.org/en-US/docs/Mozilla/Mobile/Viewport_meta_tag https://developer.mozilla.org/zh-CN/docs/Web/CSS/@viewport 视口是一个为网页提供绘图区域的矩形。 你可以指定数个视口属性,比如尺寸和初始缩放系数(initial scale)。其中最重要的是视口宽度,它定义了网页水平方向的可用像素总数(可用的CSS像素数)。 多数 Android 上的网页浏览器(包括 Chrome)设置默认视口为一个大尺寸(被称为"wide viewport mode",宽约 980px)。 也有许多浏览器默认会尽可能缩小以显示完整的视口宽度(被称为"overview mode")。
// 是否支持viewport属性,默认值 false// 页面通过`<meta name="viewport" ... />`自适应手机屏幕// 当值为true且viewport标签不存在或未指定宽度时使用 wide viewport mode
settings.setUseWideViewPort(true);
// 是否使用overview mode加载页面,默认值 false// 当页面宽度大于WebView宽度时,缩小使页面宽度等于WebView宽度
settings.setLoadWithOverviewMode(true);
viewport 语法
<meta name="viewport"
content="
height = [pixel_value | "device-height"] ,
width = [pixel_value | "device-width"] ,
initial-scale = float_value ,
minimum-scale = float_value ,
maximum-scale = float_value ,
user-scalable = ["yes" | "no"]
" />
指定视口宽度精确匹配设备屏幕宽度同时禁用了缩放
<head>
<title>Example</title>
<meta name="viewport" content="width=device-width, user-scalable=no" />
</head>
通过WebView设置初始缩放(initial-scale)
// 设置初始缩放百分比// 0表示依赖于setUseWideViewPort和setLoadWithOverviewMode// 100表示不缩放
web.setInitialScale(0)
管理 Cookies
https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Cookies
Cookie 是服务器发送到用户浏览器并保存在浏览器上的一块数据,它会在浏览器下一次发起请求时被携带并发送到服务器上。
可通过Cookie保存浏览信息来获得更轻松的在线体验,比如保持登录状态、记住偏好设置,并提供本地的相关内容。
会话Cookie 与 持久Cookie
-
会话cookie不需要指定Expires和Max-Age,浏览器关闭之后它会被自动删除。
-
持久cookie指定了Expires或Max-Age,会被存储到磁盘上,不会因浏览器而失效。
第一方Cookie 与 第三方Cookie
每个Cookie都有与之关联的域,与页面域一样的就是第一方Cookie,不一样的就是第三方Cookie。
// 设置接收第三方Cookieif (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
CookieManager.getInstance().setAcceptThirdPartyCookies(vWeb, true);
}
读取/写入/移除 Cookie
// 获取指定url关联的所有Cookie// 返回值使用"Cookie"请求头格式:"name=value; name2=value2; name3=value3"
CookieManager.getInstance().getCookie(url);
// 为指定的url设置一个Cookie// 参数value使用"Set-Cookie"响应头格式,参考:https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Set-Cookie
CookieManager.getInstance().setCookie(url, value);
// 移除指定url下的指定Cookie
CookieManager.getInstance().setCookie(url, cookieName + "=");
webkit cookie 工具类
import android.os.Build;
import android.webkit.CookieManager;
import android.webkit.CookieSyncManager;
public class WebkitCookieUtil {
// 移除指定url关联的所有cookie
public static void remove(String url) {
CookieManager cm = CookieManager.getInstance();
for (String cookie : cm.getCookie(url).split("; ")) {
cm.setCookie(url, cookie.split("=")[0] + "=");
}
flush();
}
// sessionOnly 为true表示移除所有会话cookie,否则移除所有cookie
public static void remove(boolean sessionOnly) {
CookieManager cm = CookieManager.getInstance();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
if (sessionOnly) {
cm.removeSessionCookies(null);
} else {
cm.removeAllCookies(null);
}
} else {
if (sessionOnly) {
cm.removeSessionCookie();
} else {
cm.removeAllCookie();
}
}
flush();
}
// 写入磁盘
public static void flush() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
CookieManager.getInstance().flush();
} else {
CookieSyncManager.getInstance().sync();
}
}
}
同步系统Cookie 与 Webkit Cookie
// 将系统级Cookie(比如`new URL(...).openConnection()`的Cookie) 同步到 WebViewpublic class WebkitCookieHandler extends CookieHandler {
private static final String TAG = WebkitCookieHandler.class.getSimpleName();
private CookieManager wcm;
public WebkitCookieHandler() {
this.wcm = CookieManager.getInstance();
}
@Override
public void put(URI uri, Map<String, List<String>> headers) throws IOException {
if ((uri == null) || (headers == null)) {
return;
}
String url = uri.toString();
for (String headerKey : headers.keySet()) {
if ((headerKey == null) || !(headerKey.equalsIgnoreCase("set-cookie2") || headerKey.equalsIgnoreCase("set-cookie"))) {
continue;
}
for (String headerValue : headers.get(headerKey)) {
Log.e(TAG, headerKey + ": " + headerValue);
this.wcm.setCookie(url, headerValue);
}
}
}
@Override
public Map<String, List<String>> get(URI uri, Map<String, List<String>> headers) throws IOException {
if ((uri == null) || (headers == null)) {
throw new IllegalArgumentException("Argument is null");
}
String url = uri.toString();
String cookie = this.wcm.getCookie(url);
Log.e(TAG, "cookie: " + cookie);
if (cookie != null) {
return Collections.singletonMap("Cookie", Arrays.asList(cookie));
} else {
return Collections.emptyMap();
}
}
}
缓存(Cache)
设置缓存模式
-
WebSettings.LOAD_DEFAULT 根据cache-control决定是否从网络上取数据
-
WebSettings.LOAD_CACHE_ELSE_NETWORK 无网,离线加载,优先加载缓存(即使已经过期)
-
WebSettings.LOAD_NO_CACHE 仅从网络加载
-
WebSettings.LOAD_CACHE_ONLY 仅从缓存加载
// 网络正常时根据cache-control决定是否从网络上取数据
if (isNetworkConnected(mActivity)) {
settings.setCacheMode(WebSettings.LOAD_DEFAULT);
} else {
settings.setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK);
}
清除缓存
// 传入true表示同时内存与磁盘,false表示仅清除内存// 由于内核缓存是全局的因此这个方法不仅仅针对webview而是针对整个应用程序
web.clearCache(true);
预加载(Preload)
一个简单的预加载示例(shouldInterceptRequest) 点击 assets/demo.xml 里的链接"hello"时会加载本地的 assets/hello.html assets/demo.xml
<html><body><a href="http://demo.com/assets/hello.html">hello</a></body></html>
assets/hello.html
<html><body>
hello world!
</body></html>
重载 shouldInterceptRequest
@Overridepublic WebResourceResponse shouldInterceptRequest(WebView view, String url) {
return preload("assets/", url);
}
WebResourceResponse preload(String path, String url) {
if (!url.contains(path)) {
return null;
}
String local = url.replaceFirst("^http.*" + path, "");
try {
InputStream is = getApplicationContext().getAssets().open(local);
String ext = MimeTypeMap.getFileExtensionFromUrl(local);
String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext);
return new WebResourceResponse(mimeType, "UTF-8", is);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
与Javascript交互
启用Javascript // 是否支持Javascript,默认值false
settings.setJavaScriptEnabled(true);
注入对象到Javascript
// 注入对象'jsobj',在网页中通过`jsobj.say(...)`调用
web.addJavascriptInterface(new JSObject(), "jsobj")
在API17后支持白名单,只有添加了@JavascriptInterface注解的方法才会注入JS
public class JSObject {
@JavascriptInterface
public void say(String words) {
// todo
}
}
移除已注入Javascript的对象
web.removeJavascriptInterface("jsobj")
执行JS表达式
// 弹出提示框
web.loadUrl("javascript:alert('hello')");
// 调用注入的jsobj.say方法
web.loadUrl("javascript:jsobj.say('hello')");
在API19后可异步执行JS表达式,并通过回调返回值
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
vWeb.evaluateJavascript("111+222", new ValueCallback<String>() {
@Override
public void onReceiveValue(String value) {
// value => "333"
}
});
}
地理位置(Geolocation)
https://developer.mozilla.org/zh-CN/docs/Web/API/Geolocation/Using_geolocation 需要以下权限 <uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/> 默认可用 settings.setGeolocationEnabled(true); 当H5调用地理位置API时,会先通过WebChromeClient.onGeolocationPermissionsShowPrompt申请授权
// 指定源的网页内容在没有设置权限状态下尝试使用地理位置API。 @Overridepublic void onGeolocationPermissionsShowPrompt(String origin, GeolocationPermissions.Callback callback) {
boolean allow = true; // 是否允许origin使用定位API
boolean retain = false; // 内核是否记住这次制授权
callback.invoke(origin, true, false);
}
// 之前调用 onGeolocationPermissionsShowPrompt() 申请的授权被取消时,隐藏相关的UI。@Overridepublic void onGeolocationPermissionsHidePrompt() {
}
注:从API24开始,仅支持安全源(https)的请求,非安全源的请求将自动拒绝且不调用 onGeolocationPermissionsShowPrompt 与 onGeolocationPermissionsHidePrompt
弹框(alert/confirm/prompt/onbeforeunload)
在javascript中使用 alert/confirm/prompt 会弹出对话框,可通过重载 WebChromeClient 的下列方法控制弹框的交互,比如替换系统默认的对话框或屏蔽这些对话框
@Overridepublic boolean onJsAlert(WebView view, String url, String message, JsResult result) {
// 这里处理交互逻辑
// result.cancel(); 表示用户取消了操作(点击了取消按钮)
// result.confirm(); 表示用户确认了操作(点击了确认按钮)
// ...
// 返回true表示自已处理,返回false表示由系统处理
return false;
}
@Overridepublic boolean onJsConfirm(WebView view, String url, String message, JsResult result) {
return false;
}
@Overridepublic boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
return false;
}
@Overridepublic boolean onJsBeforeUnload(WebView view, String url, String message, JsResult result) {
return false;
}
全屏(Fullscreen)
Fullscreen API https://developer.mozilla.org/zh-CN/docs/DOM/Using_fullscreen_mode 当H5请求全屏时,会回调 WebChromeClient.onShowCustomView 方法 当H5退出全屏时,会回调 WebChromeClient.onHideCustomView 方法 1.manifest 自己处理屏幕尺寸方向的变化(切换屏幕方向时不重建activity) WebView播放视频需要开启硬件加速
<activity
android:name=".WebViewActivity"
android:configChanges="orientation|screenSize"
android:hardwareAccelerated="true"
android:screenOrientation="portrait" />
2.页面布局
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
style="@style/Toolbar.Back"/>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<WebView
android:id="@+id/web"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
...
</FrameLayout>
</LinearLayout>
3.处理全屏回调
CustomViewCallback mCallback;
View vCustom;
@Overridepublic void onShowCustomView(View view, CustomViewCallback callback) {
setFullscreen(true);
vCustom = view;
mCallback = callback;
if (vCustom != null) {
ViewGroup parent = (ViewGroup) vWeb.getParent();
parent.addView(vCustom);
}
}
@Overridepublic void onHideCustomView() {
setFullscreen(false);
if (vCustom != null) {
ViewGroup parent = (ViewGroup) vWeb.getParent();
parent.removeView(vCustom);
vCustom = null;
}
if (mCallback != null) {
mCallback.onCustomViewHidden();
mCallback = null;
}
}
4.设置全屏,切换屏幕方向
void setFullscreen(boolean fullscreen) {
if (fullscreen) {
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);
vToolbar.setVisibility(View.GONE);
vWeb.setVisibility(View.GONE);
} else {
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN, WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN);
vToolbar.setVisibility(View.VISIBLE);
vWeb.setVisibility(View.VISIBLE);
}
if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT) {
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
} else {
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
}
}
参考:
如何设计一个优雅健壮的Android WebView https://blog.klmobile.app/2018/02/16/design-an-elegant-and-powerful-android-webview-part-one/
WebView深度学习 https://www.jianshu.com/p/2b2e5d417e10
WebView使用详解 https://blog.csdn.net/harvic880925/article/details/51523983
美团WebView性能、体验分析与优化 https://tech.meituan.com/WebViewPerf.html
Android WebView独立进程解决方案 https://www.jianshu.com/p/b66c225c19e2
Android WebView 详解 https://www.jianshu.com/p/a6f7b391a0b8
WebView实现全屏播放的一种方法 https://segmentfault.com/a/1190000007561455
WebView中的视频全屏4种方法,真正解决全屏问题 https://www.jianshu.com/p/4aed5c1230dc
JsBridge 实现 JavaScript 和 Java 的互相调用 https://juejin.im/entry/573534f82e958a0069b27646
Android 的 WebView 中 WebSettings 的常规配置 https://blog.csdn.net/firefile/article/details/52449596
详细的Webview使用攻略 https://www.jianshu.com/p/3c94ae673e2a
WebView想说爱你不容易啊 https://www.jianshu.com/p/79d79b8cbcfc
WebView全面总结WebView遇到的坑及优化 https://www.jianshu.com/p/b9164500d3fb
android WebView详解,常见漏洞详解和安全源码 https://juejin.im/post/58a037df86b599006b3fade4
构建 WebView 的缓存机制 & 资源预加载方案 https://blog.csdn.net/carson_ho/article/details/71402764