Webview
基本使用
WebView
1 | // 获取当前页面的URL |
1 | webview.setScrollContainer(false); //内容是否可以滚动 |
WebSettings
1 | WebSettings settings = web.getSettings(); |
WebViewClient
1 | // 拦截页面加载,返回true表示宿主app拦截并处理了该url,否则返回false由当前WebView处理 |
WebChromeClient
1 | // 获得所有访问历史项目的列表,用于链接着色。 |
Webview 加载优化
- 使用本地资源替代
可以 将一些资源文件放在本地的 asset s目录, 然后重 写WebViewClient 的 shouldInterceptRequest
方法,对访问地址进行拦截,当 url 地址命中本地配置的url时,使用本地资源替代,否则就使用网络上的资源。
1 | mWebview.setWebViewClient(new WebViewClient() { |
WebView初始化慢,可以在初始化同时先请求数据,让后端和网络不要闲着。
后端处理慢,可以让服务器分trunk输出,在后端计算的同时前端也加载网络静态资源。
脚本执行慢,就让脚本在最后运行,不阻塞页面解析。
同时,合理的预加载、预缓存可以让加载速度的瓶颈更小。
WebView初始化慢,就随时初始化好一个WebView待用。
DNS和链接慢,想办法复用客户端使用的域名和链接。
脚本执行慢,可以把框架代码拆分出来,在请求页面之前就执行好。
内存泄漏
与JS交互
JS调用原生
原生操作JS
加日志
setWebChromeClient中添加方法:
1
2
3
4
5
public boolean onConsoleMessage(ConsoleMessage consoleMessage) {
DebugLog.log("onConsoleMessage:"+ consoleMessage.message());
return super.onConsoleMessage(consoleMessage);
}setWebViewClient中添加方法:
1
2
3
4
5
6
7
8
9
10
public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) {
super.onReceivedError(view, request, error);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
DebugLog.log("onReceivedError:"
+ request.getUrl().toString() + " "
+ error.getErrorCode() + " "
+ error.getDescription().toString());
}
}
WebView缓存
Android WebView自带的缓存机制有5种:
- 浏览器 缓存机制
- Application Cache缓存机制
- Dom Storage缓存机制
- Web SQL Database缓存机制
- Indexed Database缓存机制
https://blog.csdn.net/Ever69/article/details/104479626
浏览器 缓存机制
根据HTTP协议头里的Cache-Control(或Expires)和Last-Modified(或Etag)等字段来控制文件缓存的机制。
Cache-Control:用于控制文件在本地缓存有效时长
HTTP1.1中新加的字段。如服务器回包:Cache-Control:max-age=600,表示该文件在本地缓存的有效时长是600秒
Expires:与Cache-Control功能相同,即控制缓存的有效时间
HTTP1.0标准中的字段,若和Cache-Control同时出现,则后者优先级更高。
Last-Modified:标识文件在服务器上的最新更新时间
下次请求,如果文件缓存过期,浏览器会通过
if-Modified-Since
这个字段带上这个时间发给服务器,由服务器判断文件是否有修改。若没有修改,服务器返回304告诉浏览器继续使用缓存;若有修改,则返回200同时返回最新的文件
Etag:功能同 Last-Modified,即标识文件在服务器上的最新更新时间。
不同的是,Etag的取值是一个对文件进行标识的特征字串。
在向服务器查询文件是否有更新时,浏览器通过
If-None-Match
字段把特征字串发送给服务器,由服务器判断文件是否有更新。若没有更新回包304,若有更新回包200
注意:Etag和Last-Modified同时使用时,只要满足其中一个条件,就认为文件没有更新。
常见用法:
- Cache-Control和Last-Modified一起使用;
- Expires和Etag一起使用;
特点
优点:支持Http协议层
不足:缓存文件需要首次加载后才产生;浏览器缓存的存储空间有限,缓存有被清除的可能;缓存的文件没有校验。对于上述问题可以参考手Q的离线包。
应用场景
静态资源文件的存储,如JS
、CSS
、字体、图片等。
Android WebView会将缓存的文件记录及文件内容存在当前app的data目录中
具体实现
Android WebView内置自动实现
Application Cache缓存机制
已文件为单位进行缓存,且文件有一定更新机制(类似于浏览器缓存机制)
APPCache原理有两个关键点:manifest属性和manifest文件。
HTML在头中通过 manifest 属性 引用 manifest 文件。
manifest文件:就是以 appcache 结尾的一个普通文件
特点
方便构建Web App的缓存,专门为Web App离线使用而开发的缓存机制,AppCache是对浏览器缓存机制的补充。
应用场景
存储静态文件(如JS、CSS、字体文件等)
具体实现
1 | // 通过设置WebView的settings来实现 |
注意:每个Application只调用一次
WebSettings.setAppCachePath()
和WebSettings.setAppCacheMaxSize()
Dom Storage缓存机制
通过存储字符串的 Key-Value 来提供
Dom Storage分为 sessionStorage 和 localStorage;二者使用方法基本相同,区别在于作用范围不同
- sessionStorage:具备临时性,即存储与页面相关的数据,在页面关闭后无法使用
- localStorage:具备持久性,即 保存的数据在页面关闭后仍可使用
特点
- 存储空间大(5MB):存储空间对于不同浏览器不同,如Cookies才4KB
- 存储安全、便捷:Dom Storage存储的数据在本地,不需要经常和服务器进行交互
- 不像Cookies每次请求一次页面,都会向服务器发送网络请求
应用场景
- 存储临时、简单的数据
- 代替将不需要让服务器知道的信息的存储到cookies这种传统方法
- Dom Storage机制类似于 Android 的 SharedPreference 机制
具体实现
1 | // 通过设置 `WebView`的`Settings`类实现 |
Web SQL Database缓存机制
基于 SQL 的数据库存储机制
特点
充分利用数据库的优势,可方便对数据进行增删查改
应用场景
存储适合数据库的结构化数据
具体实现
1 | // 通过设置WebView的settings实现 |
官方说明,Web SQL Database存储机制不再推荐使用(不再维护),取而代之的是 IndexedDB 缓存机制
Indexed Database缓存机制
数据 NoSQL 数据库,通过存储字符串 key-value 对来提供。
类似于 Dom Storage 存储机制的 key-value 存储方式
特点
- 功能强大、使用简单 ,通过数据的事务机制进行数据操作,可对对象任何熟悉生成索引,方便查询;
- 存储空间大,默认 250MB,比Dom Storage大很多;
- 使用灵活,以 key-value 方式存取对象,可以使任何类型值或对象,包括二进制
- 异步API调用,避免造成等待而影响体验
应用场景
存储复杂、数据量大的结构化数据
具体实现
通过设置WebView的settings实现,只需设置支持JS就自动打开IndexedDB存储机制,Android在4.4开始加入对 IndexedDB 的支持,只需打开允许 JS 执行的开关就好
1 | WebSettings settings = getSettings(); |
File System
为H5页面的数据提供一个虚拟的文件系统
特点
可存储数据体积较大的二进制数据,可预加载资源文件,可直接编辑文件
应用场景
通过文件系统,管理数据
具体使用
由于 File System 是H5 新加入的缓存机制,所以 Android We吧View 暂不支持
WebView对比与使用
WebView | 腾讯X5内核 | JsBridge | AgentWeb | UC内核 | SuperWeb | DSBridge-Android | |
---|---|---|---|---|---|---|---|
地址 | https://github.com/lzyzsd/JsBridge | https://github.com/Justson/AgentWeb | https://github.com/Victory-Over/SuperWeb | https://github.com/wendux/DSBridge-Android | |||
说明 | 基于腾讯X5内核 | 侧重js与原生app交互,写法类似JSBridge | |||||
大小 | |||||||
占内存大小 | |||||||
加载速度 | |||||||
无网是否支持缓存 | |||||||
是否可定制窗口大小 | |||||||
进度条和自定义进度条 | 有进度条 | 支持进度条和自定义进度条 | 支持进度条 | ||||
进度条显隐是否页面抖动 | |||||||
支持原生交互 | true | true | |||||
Easy of use | 麻烦 | 简洁 | |||||
支持文件浏览 | false | true | |||||
支持文件下载 | false | true | true | ||||
支持文件下载断点续传 | true | ||||||
支持下载通知形式提示进度 | true | ||||||
支持文件上传 | false | true | true | ||||
加强web安全 | true | ||||||
全屏播放视频 | 不支持 | true | true | ||||
兼容低版本Js安全通信 | true | ||||||
支持调起微信支付 | true | ||||||
支持调起支付宝 | true | ||||||
支持传入WebLayout(下拉回弹效果) | true | true | |||||
支持定位 | true | ||||||
支持自定义WebView | true | ||||||
支持Js通信 | true | 更简洁 | |||||
支持JsBridge | true | ||||||
是否线程安全 | false | true | |||||
star数 | 8.4k | 8k | 835 | 3k | |||
本地html字体大小适配 | |||||||
大厂使用 |
如果更喜欢腾讯X5内核,可用AgentWebX5,该库3年前作者已通知已不再维护
AgentWeb、SuperWeb上一次在两三年前维护
腾讯X5内核使用
配置
build.gradle
添加依赖。添加ndk
配置
1 | android{ |
自行导入os文件(需要从官网下载sdk文件)
sdk接入示例,复制整个jniLibs到自己的项目中(放在与java、res同一级)
AndroidManifest.xml文件中加入
1
2
3
4
5<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
使用
初始化
X5WebView
,下面方法复制可直接调用,记得在加载WebView之前调用,所以最好是放在打开项目的时候就调用该方法,提前加载X5替换webview配置1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23/**************腾讯X5webview**************/
private void initX5WebView() { //使用腾讯x5 webview,解决安卓原生wenview不适配不同机型问题
//搜集本地tbs内核信息并上报服务器,服务器返回结果决定使用哪个内核。
QbSdk.PreInitCallback cb = new QbSdk.PreInitCallback() {
public void onViewInitFinished(boolean arg0) {
//x5內核初始化完成的回调,为true表示x5内核加载成功,否则表示x5内核加载失败,会自动切换到系统内核。
if(arg0){//true
Log.e("腾讯X5", " onViewInitFinished 加载 成功 "+arg0);
}else{
Log.e("腾讯X5", " onViewInitFinished 加载 失败!!!使用原生安卓webview "+arg0);
}
}
public void onCoreInitFinished() {
}
};
//x5内核初始化接口
QbSdk.initX5Environment(getApplicationContext(), cb);
}
/**************腾讯X5webview**************/替换xml中
webview
布局为com.tencent.smtt.sdk.WebView
1
2
3
4
5
6
7
8
9
10
11<com.tencent.smtt.sdk.WebView
android:id="@+id/TX_X5_webview"
android:layout_width="match_parent"
android:layout_height="match_parent">
</com.tencent.smtt.sdk.WebView>
// <WebView
// android:id="@+id/nromal_webview"
// android:layout_width="match_parent"
// android:layout_height="match_parent">
// </WebView>java中使用 X5 webview
1
2
3
4
5com.tencent.smtt.sdk.WebView TX_X5_webView;//全局变量
TX_X5_webView = findViewById(R.id.TX_X5_webView);
TX_X5_webView.setBackgroundColor(0);
TX_X5_webView.getSettings().setJavaScriptEnabled(true);
TX_X5_webView.loadUrl("https://www.baidu.com/")
使用腾讯X5内核的一些坑
初始化腾讯内核在加载webview之后
AndroidManifest配置权限少了
缺少os文件
本地运行没问题,打包出来运行加载X5失败,查看日志发现
NoClassDefFoundError:com.tencent.smtt.export.extern
错误,需要加混淆配置1
2
3
4
5-dontwarn dalvik.**
-dontwarn com.tencent.smtt.**
-keep class com.tencent.** {
*;
}网络清单配置没有对tbs开放权限,所以网络请求初始化加载X5被阻止了,因为腾讯都是https请求。安卓7.0版本以上需要配置网络清单
1
2
3
4<domain includeSubdomains="true">android.bugly.qq.com</domain>
<domain includeSubdomains="true">cfg.imtt.qq.com</domain>
<domain includeSubdomains="true">tbs.imtt.qq.com</domain>
<domain includeSubdomains="true">x5.tencent.com</domain>将关于腾讯的域名添加到网络清单配置中即可
res
下新建xml
文件夹,文件夹下新建nextwork_security_config.xml
1
2
3
4
5
6
7
8<network-security-config>
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">android.bugly.qq.com</domain>
<domain includeSubdomains="true">cfg.imtt.qq.com</domain>
<domain includeSubdomains="true">tbs.imtt.qq.com</domain>
<domain includeSubdomains="true">x5.tencent.com</domain>
</domain-config>
</network-security-config>在项目的清单文件
AndroidManifest.xml
的<application
中添加1
android:networkSecurityConfig="@xml/network_security_config"
另,可以在测试的时候用省事的写法,把
nextwork_security_config.xml
内容写为以下(正式的不要这么写,不安全)1
2
3
4
<network-security-config>
<base-config cleartextTrafficPermitted="true" />
</network-security-config>其中还可以加其他参数
1
2
3
4
5
6<base-config cleartextTrafficPermitted="true">
<trust-anchors>
<certificates src="system" />
<certificates src="user" />
</trust-anchors>
</base-config>system 设备中预装的系统证书
user 用户自装证书
resourceID /raw文件下的证书
使用了以下写法,需去除
1
2
3
4
5import android.*;
import android.webkit.*;
import android.webkit.WebStorage.*;
import android.net.*;
import android.net.http.*;回调是false,但是加载显示webview内容没问题,那可能是版本过低导致X5自动切回了原生webview
JsBridge使用
添加依赖
1
2
3
4
5
6repositories {
maven {url "https://jitpack.io"}
}
dependencies {
compile 'com.github.lzyzsd:jsbridge:1.0.4'
}或者下载源码拷贝到自己的工程内当类库
提供给JS调用
1
2
3
4
5
6webView.registerHandler("submitFromWeb", new BridgeHandler(){
public void handler(String data, CallBackFunction function){
function.onCallBack("submitFrom web exe, response data from java");
}
}Js调用Java提供的方法【值要写“submitFromWeb”,这个是Java定的】
1
2
3
4
5
6
7
8WebViewJavascriptBridge.callHandler(
'submitFromWeb',
{'param':str1},
function(responseData){
//这里打印的应该是上面Handler实现方法中的callback的入参:submitFrom web exe, response data from java
document.getElementById("show").innerHTML = "response data from java, data = "+responseData
}
)还有一种简单的没有回调的调用方式:
1
2//Java中提供的方法
webView.setDefaultHandler(new DefaultHandler());1
2
3
4
5
6
7//Js中的调用方式
WebViewJavascriptBridge.send(
data,
function(responseData){
//java中DefaultHandler所实现的方法中callback所定义的入参
}
)提供给Java调用
1
2
3
4
5
6//Js中定义方法
WebViewJavascriptBridge.registerHandler("functionInJs", function(data, responseCallback) {
document.getElementById("show").innerHTML = ("data from Java: = " + data);
var responseData = "Javascript Says Right back aka!";
responseCallback(responseData);
});1
2
3
4
5
6
7
8//Java调用Handler,与Js的一样
webView.callHandler("functionInJs", new Gson().toJson(user),
new CallBackFunction(){
public void onCallBack(String data){
}
}
);同样的,在Js中也可注册默认的Handler,以方便Java调用,通过send方法发送数据
1
2
3
4
5
6
7
8bridge.init(function(message, responseCallback) {
console.log('JS got a message', message);
var data = {
'Javascript Responds': 'Wee!'
};
console.log('JS responding with', data);
responseCallback(data);
});1
2//Java中调用send方法给js发送消息
webView.send("hello");
AgentWeb使用
添加依赖 build.gradle
1
compile 'com.just.agentweb:agentweb:2.0.0'
加载网页,以京东首页为例
1
2
3
4
5
6
7
8mAgentWeb = AgentWeb.with(this)//传入Activity
.setAgentWebParent(mLinearLayout, new LinearLayout.LayoutParams(-1, -1))//传入AgentWeb 的父控件 ,如果父控件为 RelativeLayout , 那么第二参数需要传入 RelativeLayout.LayoutParams ,第一个参数和第二个参数应该对应。
.useDefaultIndicator()// 使用默认进度条
.defaultProgressBarColor() // 使用默认进度条颜色
.setReceivedTitleCallback(mCallback) //设置 Web 页面的 title 回调
.createAgentWeb()//
.ready()
.go("http://www.jd.com");不用配置 Setting , 不用添加 WebChromeClient 就有进度条 。
安卓调用JavaScript方法
1
2
3
4
5
6
7
8//Javascript 方法
function callByAndroid(){
console.log("callByAndroid")
}
//Android 端
mAgentWeb.getJsEntraceAccess().quickCallJs("callByAndroid");
//结果
consoleMessage:callByAndroid lineNumber:27JavaScript调用安卓方法
1
2
3
4//Android 端 , AndroidInterface 是一个注入类 ,里面有一个无参数方法:callAndroid
mAgentWeb.getJsInterfaceHolder().addJavaObject("android",new AndroidInterface(mAgentWeb,this));
//在 Js 里就能通过
window.android.callAndroid() //调用 Java 层的 AndroidInterface 类里 callAndroid 方法跟随 Activity 或者 Fragment 生命周期 , 释放 CPU和资源, 更省电
1
2
3
4
5
6
7
8
9
10
11
protected void onPause() {
mAgentWeb.getWebLifeCycle().onPause();
super.onPause();
}
protected void onResume() {
mAgentWeb.getWebLifeCycle().onResume();
super.onResume();
}
SuperWeb使用
Android和H5进行交互的模板代码
Android
构建用户交互的对象
TestObject
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18public static class TestObject{
private final WeakReference<Activity> wfActivity;
public TestObject(Activity ac) {
this.wfActivity = new WeakReference<>(ac);
}
//处理html按钮触发事件
public void login(String uid) {
if (wfActivity.get() != null) {
}
}
public void loginOut() {
}
}WebView
设置WebViewClient
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35mWebView.setWebViewClient(new WebViewClient() {
public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
return super.shouldOverrideUrlLoading(view, request);
}
public boolean shouldOverrideUrlLoading(WebView view, String url) {
return super.shouldOverrideUrlLoading(view, url);
}
public void onPageStarted(WebView view, String url, Bitmap favicon) {
super.onPageStarted(view, url, favicon);
}
public void onPageFinished(WebView view, String url) {
//把数据传给html
final UserInfo bean = DataSourceManager.getUserInfoBen();
if (bean != null) {
final String token = ACache.get(context).getAsString(Constants.TOKEN);
mWebView.evaluateJavascript("javascript:getUserInfo('" + bean.user_id + "','" + bean.avatar + "','" + token + "')", new ValueC
public void onReceiveValue(String value) {
//Log.e("123", "onReceiveValue getUserInfo : " + value);
}
});
}
super.onPageFinished(view, url);
}
public void doUpdateVisitedHistory(WebView view, String url, boolean isReload) {
super.doUpdateVisitedHistory(view, url, isReload);
}
});
mWebView.addJavascriptInterface(new TestObject(mMainActivity), "testObject");
mWebView.loadUrl("https://xxx.net/xxx/xxx");
Html
1 | <html lang="zh-cn"> |
WebView问题
WebView不支持拨打电话
1 | // Web视图 |
调用
1 | // 设置Web视图 |
让WebView各Android版本正常播放
AndroidManifest.xml
中对应Activity添加硬件加速
1 | <activity ... android:hardwareAccelerated="true" > |
这是针对单个Activity起作用。若要整个App起作用,那么要在application标签内添加。
也可在代码中动态添加:
1
2
3
4 getWindow.setFlags(WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED,
WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED);
//一些View不需要硬件加速,则加上
view.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
WebView无法全屏播放
在onShowCustomView方法中,将获取到的view放到当前Activity的最上方,在onHideCustomView中,将之前的view隐藏或者删除,将原来被覆盖的webview放回来。
1 | public class WebVideoActivity extends Activity { |
WebView明文存储密码带来的安全漏洞
WebView组件默认开启了密码保存功能,会提示用户是否保存密码,当用户选择保存在WebView中输入的用户名和密码,则会被明文保存到应用数据目录的databases/webview.db中。攻击者可能通过root的方式访问该应用的WebView数据库,从而窃取本地明文存储的用户名和密码。
解决方案
开发者调用 WebView.getSettings().setSavePassword(false),显示调用API设置为false,让WebView不存储密码。
WebView远程代码执行漏洞
Android API level 17以及之前的系统版本,由于程序没有正确限制使用addJavascriptInterface方法,远程攻击者可通过使用Java Reflection API利用该漏洞执行任意Java对象的方法。通过addJavascriptInterface给WebView加入一个 JavaScript桥接接口,JavaScript通过调用这个接口可以直接与本地的Java接口进行交互。就有可能出现手机被安装木马程序、发送扣费短信、通信录或者短信被窃取,甚至手机被远程控制等安全问题。
解决方案
如果一定要使用addJavascriptInterface接口,需使用以下方法:
- 设置minSdkVersion值大于或等于17,使应用不能在4.2以下系统上运行
- 允许被JavaScript调用的方法必须以@JavascriptInterface进行注解声明
未移除有风险的WebView系统隐藏接口漏洞
根据CVE披露的WebView远程代码执行漏洞信息(CVE-2012-663、CVE-2014-7224),Android系统中存在一共三个有远程代码执行漏洞的隐藏接口。分别是位于android/webkit/webview中的“searchBoxJavaBridge”接口、android/webkit/AccessibilityInjector.java中的“accessibility”接口和“accessibilityTraversal”接口。调用此三个接口的APP在开启辅助功能选项中第三方服务的Android系统上将面临远程代码执行漏洞。
解决方案
如果应用内使用了WebView组件,那么使用 WebView.removeJavascriptInterface(String name) API时,显示的移除searchBoxJavaBridge、accessibility、accessibilityTraversal这三个接口。
减少webview引起的内存泄漏
直接 new WebView 并传入 application context 代替在 XML 里面声明以防止 activity 引用被滥用,能解决90+%的 WebView 内存泄漏。
1 | vWeb = new WebView(getContext().getApplicationContext()); |
销毁 WebView
1 | if (vWeb != null) { |
升级到API33,pdf无法显示
解决:下载到App的内置目录,再用本地的 assets/pdfjs/web/viewer.html【3.11.174pdj】
进行展示【某些机型无法展示,用pdfjs-dist@2.3.200可以】
其中 assets/pdfjs 从 https://mozilla.github.io/pdf.js/getting_started/ 下载
或从 https://github.com/mozilla/pdf.js/releases/tag/v3.11.174 下载
WebSettings添加
1 | // 支持javascript |
添加下载:
1 | webView.setDownloadListener((url, userAgent, contentDisposition, mimetype, contentLength) -> { |
1 | private DownloadManagerUtils downloadManagerUtils; |
上面的代码在某些手机上无法展示:报 SyntaxError: Unexpected token '.'
把webView.loadUrl("file:///android_asset/pdfjs/web/viewer.html?file=" + filePath);
改成
webView.loadUrl("file:///android_asset/pdf.html?" + filePath);
【其中版本是pdfjs-dist@2.3.200
】
用的 assets/pdf.html、assets/pdf.js 并非带源码的那种
ssets/pdf.html
1 |
|
assets/pdf.js
1 | var url = location.search.substring(1); |
上述的 DownloadManagerUtils.java
1 | import android.app.DownloadManager; |