UI-WebView

Webview

基本使用

WebView

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 获取当前页面的URL
public String getUrl();
// 获取当前页面的原始URL(重定向后可能当前url不同)
// 就是http headers的Referer参数,loadUrl时为null
public String getOriginalUrl();
// 获取当前页面的标题
public String getTitle();
// 获取当前页面的favicon
public Bitmap getFavicon();
// 获取当前页面的加载进度
public int getProgress();

// 通知WebView内核网络状态
// 用于设置JS属性`window.navigator.isOnline`和产生HTML5事件`online/offline`
public void setNetworkAvailable(boolean networkUp)

// 设置初始缩放比例
public void setInitialScale(int scaleInPercent)

1
2
3
4
5
6
7
8
9
10
webview.setScrollContainer(false); 			  //内容是否可以滚动
webview.setHorizontalScrollBarEnabled(false); //是否显示水平方向的滚动条
webview.setVerticalScrollBarEnabled(false); //是否显示垂直方向的滚动条

//加载内容
if (data.length() > 7 && data.contains("<p>") && data.contains("</p>")){
data = data.replaceAll("<p>", "<p style=\"word-break:break-all\">");
}
webview.loadDataWithBaseURL(null, data, "text/html", "utf-8", null); //加载html字符串

WebSettings

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
WebSettings settings = web.getSettings();

// 存储(storage)
// 启用HTML5 DOM storage API,默认值 false
settings.setDomStorageEnabled(true);
// 启用Web SQL Database API,这个设置会影响同一进程内的所有WebView,默认值 false
// 此API已不推荐使用,参考:https://www.w3.org/TR/webdatabase/
settings.setDatabaseEnabled(true);
// 启用Application Caches API,必需设置有效的缓存路径才能生效,默认值 false
// 此API已废弃,参考:https://developer.mozilla.org/zh-CN/docs/Web/HTML/Using_the_application_cache
settings.setAppCacheEnabled(true);
settings.setAppCachePath(context.getCacheDir().getAbsolutePath());

// 定位(location)
settings.setGeolocationEnabled(true);

// 是否保存表单数据
settings.setSaveFormData(true);
// 是否当webview调用requestFocus时为页面的某个元素设置焦点,默认值 true
settings.setNeedInitialFocus(true);

// 是否支持viewport属性,默认值 false
// 页面通过`<meta name="viewport" ... />`自适应手机屏幕
settings.setUseWideViewPort(true);
// 是否使用overview mode加载页面,默认值 false
// 当页面宽度大于WebView宽度时,缩小使页面宽度等于WebView宽度
settings.setLoadWithOverviewMode(true);
// 布局算法【支持内容重新布局】
settings.setLayoutAlgorithm(WebSettings.LayoutAlgorithm.NORMAL); //NORMAL 正常不做任何渲染
//NARROW_COLUMNS 可能的话所有列的宽度不超过屏幕宽度
//SINGLE_COLUMN 把所有内容放到webview等宽的一列中(会导致超出屏幕的部分不显示)

// 是否支持Javascript,默认值false
settings.setJavaScriptEnabled(true);
// 是否支持多窗口,默认值false
settings.setSupportMultipleWindows(false);
// 是否可用Javascript(window.open)打开窗口,默认值 false
settings.setJavaScriptCanOpenWindowsAutomatically(false);

// 资源访问
settings.setAllowContentAccess(true); // 是否可访问Content Provider的资源,默认值 true
settings.setAllowFileAccess(true); // 是否可访问本地文件,默认值 true
// 是否允许通过file url加载的Javascript读取本地文件,默认值 false
settings.setAllowFileAccessFromFileURLs(false);
// 是否允许通过file url加载的Javascript读取全部资源(包括文件,http,https),默认值 false
settings.setAllowUniversalAccessFromFileURLs(false);

// 资源加载
settings.setLoadsImagesAutomatically(true); // 是否自动加载图片
settings.setBlockNetworkImage(false); // 禁止加载网络图片
settings.setBlockNetworkLoads(false); // 禁止加载所有网络资源

// 缩放(zoom)
settings.setSupportZoom(true); // 是否支持缩放
settings.setBuiltInZoomControls(false); // 是否使用内置缩放机制。为true的话 setSupportZoom默认为true
settings.setDisplayZoomControls(true); // 是否显示内置缩放控件

// 默认文本编码,默认值 "UTF-8"
settings.setDefaultTextEncodingName("UTF-8");
settings.setDefaultFontSize(16); // 默认文字尺寸,默认值16,取值范围1-72
settings.setDefaultFixedFontSize(16); // 默认等宽字体尺寸,默认值16
settings.setMinimumFontSize(8); // 最小文字尺寸,默认值 8
settings.setMinimumLogicalFontSize(8); // 最小文字逻辑尺寸,默认值 8
settings.setTextZoom(100); // 文字缩放百分比,默认值 100

// 字体
settings.setStandardFontFamily("sans-serif"); // 标准字体,默认值 "sans-serif"
settings.setSerifFontFamily("serif"); // 衬线字体,默认值 "serif"
settings.setSansSerifFontFamily("sans-serif"); // 无衬线字体,默认值 "sans-serif"
settings.setFixedFontFamily("monospace"); // 等宽字体,默认值 "monospace"
settings.setCursiveFontFamily("cursive"); // 手写体(草书),默认值 "cursive"
settings.setFantasyFontFamily("fantasy"); // 幻想体,默认值 "fantasy"


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());

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
// 拦截页面加载,返回true表示宿主app拦截并处理了该url,否则返回false由当前WebView处理
// 此方法在API24被废弃,不处理POST请求
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将继续加载资源
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

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
// 获得所有访问历史项目的列表,用于链接着色。
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;
}

Webview 加载优化

  • 使用本地资源替代

可以 将一些资源文件放在本地的 asset s目录, 然后重 写WebViewClient 的 shouldInterceptRequest 方法,对访问地址进行拦截,当 url 地址命中本地配置的url时,使用本地资源替代,否则就使用网络上的资源。

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
mWebview.setWebViewClient(new WebViewClient() {   
 // 设置不用系统浏览器打开,
@Override    
public boolean shouldOverrideUrlLoading(WebView view, String url) {      
view.loadUrl(url);     
return true;    
}   
 
@Override    
public WebResourceResponse shouldInterceptRequest(WebView view, String url) {      // 如果命中本地资源, 使用本地资源替代      
if (mDataHelper.hasLocalResource(url)){         
 WebResourceResponse response = mDataHelper.getReplacedWebResourceResponse(getApplicationContext(), url);          
if (response != null) {              
return response;
}      
}      
return super.shouldInterceptRequest(view, url);    
}   

@TargetApi(VERSION_CODES.LOLLIPOP)@Override    
public WebResourceResponse shouldInterceptRequest(WebView view,WebResourceRequest request) {      
String url = request.getUrl().toString();      
if (mDataHelper.hasLocalResource(url)) {         
WebResourceResponse response =  mDataHelper.getReplacedWebResourceResponse(getApplicationContext(), url);          
if (response != null) {              
return response;          
}      
}      
return super.shouldInterceptRequest(view, request);    
}
}); 
  • WebView初始化慢,可以在初始化同时先请求数据,让后端和网络不要闲着。

  • 后端处理慢,可以让服务器分trunk输出,在后端计算的同时前端也加载网络静态资源。

  • 脚本执行慢,就让脚本在最后运行,不阻塞页面解析。

  • 同时,合理的预加载、预缓存可以让加载速度的瓶颈更小。

  • WebView初始化慢,就随时初始化好一个WebView待用。

  • DNS和链接慢,想办法复用客户端使用的域名和链接。

  • 脚本执行慢,可以把框架代码拆分出来,在请求页面之前就执行好。

内存泄漏

与JS交互

JS调用原生

原生操作JS

加日志

  1. setWebChromeClient中添加方法:

    1
    2
    3
    4
    5
    @Override
    public boolean onConsoleMessage(ConsoleMessage consoleMessage) {
    DebugLog.log("onConsoleMessage:"+ consoleMessage.message());
    return super.onConsoleMessage(consoleMessage);
    }
  2. setWebViewClient中添加方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @Override
    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种:

  1. 浏览器 缓存机制
  2. Application Cache缓存机制
  3. Dom Storage缓存机制
  4. Web SQL Database缓存机制
  5. 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同时使用时,只要满足其中一个条件,就认为文件没有更新。

常见用法:

  1. Cache-Control和Last-Modified一起使用;
  2. Expires和Etag一起使用;

特点

优点:支持Http协议层

不足:缓存文件需要首次加载后才产生;浏览器缓存的存储空间有限,缓存有被清除的可能;缓存的文件没有校验。对于上述问题可以参考手Q的离线包。

应用场景

静态资源文件的存储,如JSCSS、字体、图片等。

Android WebView会将缓存的文件记录及文件内容存在当前app的data目录中

具体实现

Android WebView内置自动实现

Application Cache缓存机制

已文件为单位进行缓存,且文件有一定更新机制(类似于浏览器缓存机制)

APPCache原理有两个关键点:manifest属性和manifest文件。

HTML在头中通过 manifest 属性 引用 manifest 文件。

manifest文件:就是以 appcache 结尾的一个普通文件

特点

方便构建Web App的缓存,专门为Web App离线使用而开发的缓存机制,AppCache是对浏览器缓存机制的补充。

应用场景

存储静态文件(如JS、CSS、字体文件等)

具体实现

1
2
3
4
5
6
7
8
9
// 通过设置WebView的settings来实现
WebSettings settings = getSettings();
String cacheDirPath = context.getFilesDir().getAbsolutePath()+"cache/";
// 1. 设置缓存路径
settings.setAppCachePath(cacheDirPath);
// 2. 设置缓存大小
settings.setAppCacheMaxSize(20*1024*1024);
// 3. 开启Application Cache存储机制
settings.setAppCacheEnabled(true);

注意:每个Application只调用一次WebSettings.setAppCachePath()WebSettings.setAppCacheMaxSize()

Dom Storage缓存机制

通过存储字符串的 Key-Value 来提供

Dom Storage分为 sessionStorage 和 localStorage;二者使用方法基本相同,区别在于作用范围不同

  1. sessionStorage:具备临时性,即存储与页面相关的数据,在页面关闭后无法使用
  2. localStorage:具备持久性,即 保存的数据在页面关闭后仍可使用

特点

  • 存储空间大(5MB):存储空间对于不同浏览器不同,如Cookies才4KB
  • 存储安全、便捷:Dom Storage存储的数据在本地,不需要经常和服务器进行交互
  • 不像Cookies每次请求一次页面,都会向服务器发送网络请求

应用场景

  • 存储临时、简单的数据
  • 代替将不需要让服务器知道的信息的存储到cookies这种传统方法
  • Dom Storage机制类似于 Android 的 SharedPreference 机制

具体实现

1
2
3
4
// 通过设置 `WebView`的`Settings`类实现
WebSettings settings = getSettings();
// 开启DOM storage
settings.setDomStorageEnabled(true);

Web SQL Database缓存机制

基于 SQL 的数据库存储机制

特点

充分利用数据库的优势,可方便对数据进行增删查改

应用场景

存储适合数据库的结构化数据

具体实现

1
2
3
4
5
6
7
// 通过设置WebView的settings实现
WebSettings settings = getSettings();
String cacheDirPath = context.getFilesDir().getAbsolutePath()+"cache/";
// 设置缓存路径
settings.setDatabasePath(cacheDirPath);
// 开启 数据库存储机制
settings.setDatabaseEnabled(true);

官方说明,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
2
WebSettings settings = getSettings();
settings.setJavaScriptEnabled(true);

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内核使用

配置

  1. build.gradle添加依赖。添加ndk配置
1
2
3
4
5
6
7
8
9
10
11
android{
defaultConfig{
ndk{
abiFilters “armeabi”, “armeabi-v7a”, “x86”, “mips”
}
}
}

dependencies {
api 'com.tencent.tbs.tbssdk:sdk:43903’
}
  1. 自行导入os文件(需要从官网下载sdk文件)

    sdk接入示例,复制整个jniLibs到自己的项目中(放在与java、res同一级)

  2. 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" />

使用

  1. 初始化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() {
    @Override
    public void onViewInitFinished(boolean arg0) {
    //x5內核初始化完成的回调,为true表示x5内核加载成功,否则表示x5内核加载失败,会自动切换到系统内核。
    if(arg0){//true
    Log.e("腾讯X5", " onViewInitFinished 加载 成功 "+arg0);
    }else{
    Log.e("腾讯X5", " onViewInitFinished 加载 失败!!!使用原生安卓webview "+arg0);
    }
    }

    @Override
    public void onCoreInitFinished() {

    }
    };
    //x5内核初始化接口
    QbSdk.initX5Environment(getApplicationContext(), cb);
    }
    /**************腾讯X5webview**************/
  2. 替换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>
  3. java中使用 X5 webview

    1
    2
    3
    4
    5
    com.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内核的一些坑

onViewInitFinished回调一直是false

  1. 初始化腾讯内核在加载webview之后

  2. AndroidManifest配置权限少了

  3. 缺少os文件

  4. 本地运行没问题,打包出来运行加载X5失败,查看日志发现NoClassDefFoundError:com.tencent.smtt.export.extern错误,需要加混淆配置

    1
    2
    3
    4
    5
    -dontwarn dalvik.**
    -dontwarn com.tencent.smtt.**
    -keep class com.tencent.** {
    *;
    }
  5. 网络清单配置没有对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
    <?xml version="1.0" encoding="utf-8"?>
    <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文件下的证书

  6. 使用了以下写法,需去除

    1
    2
    3
    4
    5
    import android.*;
    import android.webkit.*;
    import android.webkit.WebStorage.*;
    import android.net.*;
    import android.net.http.*;
  7. 回调是false,但是加载显示webview内容没问题,那可能是版本过低导致X5自动切回了原生webview

JsBridge使用

JsBridge原理与使用

  1. 添加依赖

    1
    2
    3
    4
    5
    6
    repositories {  
    maven {url "https://jitpack.io"}
    }
    dependencies {
    compile 'com.github.lzyzsd:jsbridge:1.0.4'
    }

    或者下载源码拷贝到自己的工程内当类库

  2. 提供给JS调用

    1
    2
    3
    4
    5
    6
    webView.registerHandler("submitFromWeb", new BridgeHandler(){
    @Override
    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
    8
    WebViewJavascriptBridge.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所定义的入参
    }
    )
  3. 提供给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(){
    @Override
    public void onCallBack(String data){
    }
    }
    );

    同样的,在Js中也可注册默认的Handler,以方便Java调用,通过send方法发送数据

    1
    2
    3
    4
    5
    6
    7
    8
    bridge.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使用

agentweb与自带webview比较

  1. 添加依赖 build.gradle

    1
    compile 'com.just.agentweb:agentweb:2.0.0'
  2. 加载网页,以京东首页为例

    1
    2
    3
    4
    5
    6
    7
    8
    mAgentWeb = 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 就有进度条 。

  3. 安卓调用JavaScript方法

    1
    2
    3
    4
    5
    6
    7
    8
    //Javascript 方法
    function callByAndroid(){
    console.log("callByAndroid")
    }
    //Android 端
    mAgentWeb.getJsEntraceAccess().quickCallJs("callByAndroid");
    //结果
    consoleMessage:callByAndroid lineNumber:27

    JavaScript调用安卓方法

    1
    2
    3
    4
    //Android 端 , AndroidInterface 是一个注入类 ,里面有一个无参数方法:callAndroid 
    mAgentWeb.getJsInterfaceHolder().addJavaObject("android",new AndroidInterface(mAgentWeb,this));
    //在 Js 里就能通过
    window.android.callAndroid() //调用 Java 层的 AndroidInterface 类里 callAndroid 方法
  4. 跟随 Activity 或者 Fragment 生命周期 , 释放 CPU和资源, 更省电

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    @Override
    protected void onPause() {
    mAgentWeb.getWebLifeCycle().onPause();
    super.onPause();
    }

    @Override
    protected void onResume() {
    mAgentWeb.getWebLifeCycle().onResume();
    super.onResume();
    }

SuperWeb使用

Android和H5进行交互的模板代码

Android

  1. 构建用户交互的对象 TestObject

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    public static class TestObject{
    private final WeakReference<Activity> wfActivity;
    public TestObject(Activity ac) {
    this.wfActivity = new WeakReference<>(ac);
    }

    //处理html按钮触发事件
    @JavascriptInterface
    public void login(String uid) {
    if (wfActivity.get() != null) {

    }
    }

    @JavascriptInterface
    public void loginOut() {
    }
    }
  2. 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
    35
    mWebView.setWebViewClient(new WebViewClient() {
    @Override
    public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
    return super.shouldOverrideUrlLoading(view, request);
    }
    @Override
    public boolean shouldOverrideUrlLoading(WebView view, String url) {
    return super.shouldOverrideUrlLoading(view, url);
    }
    @Override
    public void onPageStarted(WebView view, String url, Bitmap favicon) {
    super.onPageStarted(view, url, favicon);
    }
    @Override
    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
    @Override
    public void onReceiveValue(String value) {
    //Log.e("123", "onReceiveValue getUserInfo : " + value);
    }
    });
    }
    super.onPageFinished(view, url);
    }
    @Override
    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
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
<html lang="zh-cn">
<head>
<script>
//h5接收移动端传过来的用户信息
function getUserInfo(uid,pwd,avatar){
return "用户信息:"+ uid+" "+pwd +" "+avatar;
}
//h5接收移动端退出动作
function exitUser(uid){
return "退出账号: "+ uid;
}

</script>
</head>
<body bgcolor="#BCBCBC">
<p>
<button type="button" onclick="testObject.login(uid);">h5把用户信息给移动端,
移动端处理对应的 login 方法进行登录
</button>
</p>
<p>
<button type="button" onclick="testObject.loginOut(uid);">html用户退出操作,
移动端处理 loginOut 方法</button>
</p>
</body>
</html>

WebView问题

WebView不支持拨打电话

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Web视图
private class webViewClient extends WebViewClient {
public boolean shouldOverrideUrlLoading(WebView view, String url) {
if (ShowWebActivity.this == null) {
return false;
}
//调用拨号程序
if (url.startsWith("mailto:") || url.startsWith("geo:") || url.startsWith("tel:") || url.startsWith("smsto:")) {
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
startActivity(intent);
return true;
}
return false;
}
}

调用

1
2
// 设置Web视图
webview.setWebViewClient(new webViewClient());

让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
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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
public class WebVideoActivity extends Activity {

private WebView webView;

/** 视频全屏参数 */
protected static final FrameLayout.LayoutParams COVER_SCREEN_PARAMS = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
private View customView;
private FrameLayout fullscreenContainer;
private WebChromeClient.CustomViewCallback customViewCallback;

@Override
protected void onCreate(Bundle bundle) {
super.onCreate(bundle);
setContentView(R.layout.activity_xx);
webView = (WebView) findViewById(R.id.xx);
initWebView();
}

@Override
protected void onStop() {
super.onStop();
webView.reload();
}

/** 展示网页界面 **/  public void initWebView() {
WebChromeClient wvcc = new WebChromeClient();
WebSettings webSettings = webView.getSettings();
webSettings.setJavaScriptEnabled(true);
webSettings.setUseWideViewPort(true); // 关键点
webSettings.setLoadWithOverviewMode(true);
webSettings.setCacheMode(WebSettings.LOAD_NO_CACHE); // 不加载缓存内容

webView.setWebChromeClient(wvcc);
WebViewClient wvc = new WebViewClient() {
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
webView.loadUrl(url);
return true;
}
};
webView.setWebViewClient(wvc);

webView.setWebChromeClient(new WebChromeClient() {

/*** 视频播放相关的方法 **/

@Override
public View getVideoLoadingProgressView() {
FrameLayout frameLayout = new FrameLayout(WebVideoActivity.this);
frameLayout.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
return frameLayout;
}

@Override
public void onShowCustomView(View view, CustomViewCallback callback) {
showCustomView(view, callback);
}

@Override
public void onHideCustomView() {
hideCustomView();
}
});

// 加载Web地址
webView.loadUrl(webUrl);
}

/** 视频播放全屏 **/
private void showCustomView(View view, CustomViewCallback callback) {
// if a view already exists then immediately terminate the new one
if (customView != null) {
callback.onCustomViewHidden();
return;
}

WebVideoActivity.this.getWindow().getDecorView();

FrameLayout decor = (FrameLayout) getWindow().getDecorView();
fullscreenContainer = new FullscreenHolder(WebVideoActivity.this);
fullscreenContainer.addView(view, COVER_SCREEN_PARAMS);
decor.addView(fullscreenContainer, COVER_SCREEN_PARAMS);
customView = view;
setStatusBarVisibility(false);
customViewCallback = callback;
}

/** 隐藏视频全屏 */
private void hideCustomView() {
if (customView == null) {
return;
}

setStatusBarVisibility(true);
FrameLayout decor = (FrameLayout) getWindow().getDecorView();
decor.removeView(fullscreenContainer);
fullscreenContainer = null;
customView = null;
customViewCallback.onCustomViewHidden();
webView.setVisibility(View.VISIBLE);
}

/** 全屏容器界面 */
static class FullscreenHolder extends FrameLayout {

public FullscreenHolder(Context ctx) {
super(ctx);
setBackgroundColor(ctx.getResources().getColor(android.R.color.black));
}

@Override
public boolean onTouchEvent(MotionEvent evt) {
return true;
}
}

private void setStatusBarVisibility(boolean visible) {
int flag = visible ? 0 : WindowManager.LayoutParams.FLAG_FULLSCREEN;
getWindow().setFlags(flag, WindowManager.LayoutParams.FLAG_FULLSCREEN);
}

@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
switch (keyCode) {
case KeyEvent.KEYCODE_BACK:
/** 回退键 事件处理 优先级:视频播放全屏-网页回退-关闭页面 */
if (customView != null) {
hideCustomView();
} else if (webView.canGoBack()) {
webView.goBack();
} else {
finish();
}
return true;
default:
return super.onKeyUp(keyCode, event);
}
}
}

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接口,需使用以下方法:

  1. 设置minSdkVersion值大于或等于17,使应用不能在4.2以下系统上运行
  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
2
vWeb =  new WebView(getContext().getApplicationContext());
container.addView(vWeb);

销毁 WebView

1
2
3
4
5
6
7
8
9
10
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;
}

升级到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
2
3
4
5
6
// 支持javascript
settings.setJavaScriptEnabled(true);
settings.setAllowFileAccess(true);
//允许加载本地文件
settings.setAllowFileAccessFromFileURLs(true);
settings.setAllowUniversalAccessFromFileURLs(true);

添加下载:

1
2
3
4
5
webView.setDownloadListener((url, userAgent, contentDisposition, mimetype, contentLength) -> {
if (isresumerow) {
downloadPdf(url, "preview", "附件简历", "preview.pdf", userAgent, mimetype);
}
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private DownloadManagerUtils downloadManagerUtils;
private void downloadPdf(String url, String title, String msg, String fileName, String userAgent, String mimetype) {
if (downloadManagerUtils == null) {
downloadManagerUtils = new DownloadManagerUtils(this);
//注册广播和下载完成监听
downloadManagerUtils.registerReceiver(new DownloadManagerUtils.OnDownloadCompleted() {
@Override
public void onDownloadCompleted(long completeDownloadId) {
if (mBinding != null) {
String filePath = downloadManagerUtils.getPathFromId(completeDownloadId);
//展示
webView.loadUrl("file:///android_asset/pdfjs/web/viewer.html?file=" + filePath);
}
}
});
}
long id = downloadManagerUtils.download(url, title, msg, fileName, userAgent, mimetype);
}

上面的代码在某些手机上无法展示:报 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width,initial-scale=1.0,maximum-scale=2.0,user-scalable=yes"/>
<title>PDF查看</title>
<style type="text/css">
canvas {
width: 100%;
height: 100%;
border: 1px solid black;
}
</style>
<script src="https://unpkg.com/pdfjs-dist@2.3.200/build/pdf.min.js"></script>
<script type="text/javascript" src="pdf.js"></script>
</head>
<body>
</body>
</html>

assets/pdf.js

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
var url = location.search.substring(1);
pdfjsLib.cMapUrl = 'https://unpkg.com/pdfjs-dist@2.3.200/cmaps/';
pdfjsLib.cMapPacked = true;
var pdfDoc = null;
function createPage() {
var div = document.createElement("canvas");
document.body.appendChild(div);
return div;
}
function renderPage(num) {
pdfDoc.getPage(num).then(function (page) {
var viewport = page.getViewport({scale:2.0});
var canvas = createPage();
var ctx = canvas.getContext('2d');
canvas.height = viewport.height;
canvas.width = viewport.width;
page.render({
canvasContext: ctx,
viewport: viewport
});
});
}
pdfjsLib.getDocument(url).promise.then(function (pdf) {
pdfDoc = pdf;
for (var i = 1; i <= pdfDoc.numPages; i++) {
renderPage(i)
}
});

上述的 DownloadManagerUtils.java

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
import android.app.DownloadManager;
import android.content.ActivityNotFoundException;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.net.Uri;
import android.os.Environment;
import android.webkit.CookieManager;

import com.xm597.common.debug.DebugLog;

import java.io.FileNotFoundException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class DownloadManagerUtils {


private Context context;

public DownloadManagerUtils(Context context) {
this.context = context;
getService();
downLoadCompleteReceiver = new DownloadCompleteReceiver();
downloadIds = new ArrayList<Long>();
}

/**
* 获取当前下载的所有任务 id
*
* @return
*/
public List<Long> getDownloadIds() {
return downloadIds;
}

/**
* 根据任务id打开安装界面
*
* @param downloadApkId
*/
public void installApk(long downloadApkId) {
Intent install = new Intent(Intent.ACTION_VIEW);
Uri downloadFileUri = downloadManager.getUriForDownloadedFile(downloadApkId);//获取下载文件路径
if (downloadFileUri != null) {
install.setDataAndType(downloadFileUri, "application/vnd.android.package-archive");
install.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
try {
context.startActivity(install);//打开安装界面
} catch (ActivityNotFoundException ex) {
ex.printStackTrace();
}
}
}

/**
* 解除注册广播
*/
public void unregisterReceiver() {
if (downloadIds != null&&!downloadIds.isEmpty()) {
long[] removeList = new long[downloadIds.size()];
for (int i = 0; i < downloadIds.size(); i++) {
removeList[i] = downloadIds.get(i);
}
try{
downloadManager.remove(removeList);
}catch (Exception e){
e.printStackTrace();
}
}
context.unregisterReceiver(downLoadCompleteReceiver);
}

DownloadCompleteReceiver downLoadCompleteReceiver;

/**
* 注册广播并注册监听接口
*
* @param onDownloadCompleted
*/
public void registerReceiver(OnDownloadCompleted onDownloadCompleted) {
context.registerReceiver(downLoadCompleteReceiver,
new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));
this.onDownloadCompleted = onDownloadCompleted;


}

OnDownloadCompleted onDownloadCompleted;

/**
* 下载完成监听
*/
class DownloadCompleteReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (DownloadManager.ACTION_DOWNLOAD_COMPLETE.equals(action)) {
long completeDownloadId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1);//获取下载完成任务的id
if (completeDownloadId != -1) {
if (onDownloadCompleted !=null) {
onDownloadCompleted.onDownloadCompleted(completeDownloadId);//调用下载完成接口方法
}
//下载完成后删除id
for (int i = 0; i < downloadIds.size(); i++) {
if (completeDownloadId == downloadIds.get(i)) {
downloadIds.remove(i);
break;
}
}
}
}
}
}

/**
* 下载完成监听
*/
public interface OnDownloadCompleted {
public void onDownloadCompleted(long completeDownloadId);
}

/**
* 判断下载组件是否可用
*
* @param context
* @return
*/
private boolean canDownloadState(Context context) {
try {
int state = context.getPackageManager().getApplicationEnabledSetting("com.android.providers.downloads");
if (state == PackageManager.COMPONENT_ENABLED_STATE_DISABLED
|| state == PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER
|| state == PackageManager.COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED) {
return false;
}
} catch (Exception e) {
e.printStackTrace();
return false;
}
return true;
}

/**
* 启用下载组件
*
* @param context
*/
private void enableDowaload(Context context) {
String packageName = "com.android.providers.downloads";
Intent intent = new Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
intent.setData(Uri.parse("package:" + packageName));
context.startActivity(intent);
}

List<Long> downloadIds;//记录所有下载任务id

/**
* @param uil 下载地址
* @param title 通知栏标题
* @param description 描述
* @return
*/
public long download(String uil, String title, String description, String fileName, String userAgent, String mimetype) {
Uri uri = Uri.parse(uil);
DownloadManager.Request request = new DownloadManager.Request(uri);//设置下载地址
request.addRequestHeader("userAgent", userAgent);
request.addRequestHeader("mimetype", mimetype);
CookieManager cookieManager = CookieManager.getInstance(); // 获取 CookieManager 实例
String cookie = cookieManager.getCookie(uil); // 获取指定 URL 的 Cookie
request.addRequestHeader("Cookie", cookie);
request.setTitle(title);//设置Notification的title信息
request.setDescription(description);//设置Notification的message信息
request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_HIDDEN);//设置通知栏下载通知不可见状态
//下载到download文件夹下
// request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, fileName);
//保存在app下可以访问
request.setDestinationInExternalFilesDir(context, Environment.DIRECTORY_DOWNLOADS, fileName);
//只能在WiFi下进行下载(这个是两个环境都可以)
request.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI | DownloadManager.Request.NETWORK_MOBILE);
//reference变量是系统为当前的下载请求分配的一个唯一的ID,我们可以通过这个ID重新获得这个下载任务,进行一些自己想要进行的操作或者查询
long id = downloadManager.enqueue(request);
downloadIds.add(id);
return id;
}

public long download(String uil, String title, String description, String fileName) {
Uri uri = Uri.parse(uil);
DownloadManager.Request request = new DownloadManager.Request(uri);//设置下载地址
request.setTitle(title);//设置Notification的title信息
request.setDescription(description);//设置Notification的message信息
request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);//设置通知栏下载通知显示状态
//下载到download文件夹下
request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, fileName);
//只能在WiFi下进行下载(这个是两个环境都可以)
request.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI | DownloadManager.Request.NETWORK_MOBILE);
//reference变量是系统为当前的下载请求分配的一个唯一的ID,我们可以通过这个ID重新获得这个下载任务,进行一些自己想要进行的操作或者查询
long id = downloadManager.enqueue(request);
downloadIds.add(id);
return id;
}

/**
* @param downloadId 实际的下载任务 ID
* @return
*/
public String getPathFromId(long downloadId) {
if (context == null) {
return "";
}
DownloadManager downloadManager = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
// 查询下载任务
DownloadManager.Query query = new DownloadManager.Query();
query.setFilterById(downloadId);
Cursor cursor = downloadManager.query(query);
String filePath = "";
// 获取查询结果
if (cursor.moveToFirst()) {
int columnIndex = cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI);
filePath = cursor.getString(columnIndex);
}
// 关闭 Cursor 和 DownloadManager
try {
cursor.close();
} catch (Exception e) {
e.printStackTrace();
}
return filePath;
}

DownloadManager downloadManager;

/**
* 初始化downloadManager
*/
private void getService() {
String serviceString = Context.DOWNLOAD_SERVICE;
downloadManager = (DownloadManager) context.getSystemService(serviceString);
}


// private void testDownload(){
// DownloadManagerUtils downloadManagerUtils = new DownloadManagerUtils(this);
// long id = downloadManagerUtils.download("https://cdn.597.com/hrTemplate/在职证明.docx","测试下载","这是一次测试下载","在职证明.docx");
// downloadManagerUtils.registerReceiver(new DownloadManagerUtils.OnDownloadCompleted() {//注册广播和下载完成监听
// @Override
// public void onDownloadCompleted(long completeDownloadId) {
// showtoast("在职证明.docx下载完成");
// downloadManagerUtils.installApk(completeDownloadId);//调用这个是下载
// }
// });
// }

//
// @Override
// protected void onDestroy() {
// downloadManagerUtils.unregisterReceiver();//销毁是解除注册
// super.onDestroy();
// }
}