经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » 移动开发 » Android » 查看文章
WebView的坑你别嫌多
来源:掘金网  作者:骑着蜗牛闯红灯  时间:2019/8/28 10:24:41  对本文有异议

先简单介绍一下,Android在4.4之后采用了Chrome内核,所以我们在开发web页面的时候,es6的语法,css3的样式等大可放心使用。

我将分下面几个模块去介绍Android上面WebView。

WebView自身的一些方法//方式1. 加载一个网页:

  1. webView.loadUrl("http://www.google.com/");
  2.  
  3. //方式2:加载apk包中的html页面
  4. webView.loadUrl("file:///android_asset/test.html");
  5.  
  6. //方式3:加载手机本地的html页面
  7. webView.loadUrl("content://com.android.htmlfileprovider/sdcard/test.html");

正常情况下,在WebView界面,用户点击返回键是直接退出该页面的,着当然不是我们想要的,我们想要的是网页自己的前进和后退,所以下面介绍网页前进和后退的一些API

  1. //判断是否可以后退
  2. Webview.canGoBack
  3. //后退网页
  4. Webview.goBack
  5. //判断是否可以前进
  6. Webview.canGoForward
  7. //前进网页
  8. Webview.goForward
  9. // 参数传负的话表示后退,传正值的话表示的是前进
  10. Webview.goBackOrForward(int steps)

对返回键的监听,来实现网页的后退

  1. public boolean onKeyDown(int keyCode, KeyEvent event) {
  2. if ((keyCode == KEYCODE_BACK) && mWebView.canGoBack) {
  3. mWebView.goBack;
  4. return true;
  5. }
  6. return super.onKeyDown(keyCode, event);
  7. }

如何防止WebView内存泄漏

防止内存泄漏的一个原则就是:生命周期长的不要跟生命周期短的玩。

为了防止WebView不造成内存泄漏,

  • 不要在xml里面定义WebView,而是在Activity选中使用代码去构建,并且Context使用ApplicationContext

  • 在Activity销毁的时候,先让WebView加载空内容,然后重rootView中移除WebView,再销毁WebView,最后置空

  1. override fun onDestroy {
  2. if (webView != ) {
  3. webView!!.loadDataWithBaseURL(, "", "text/html", "utf-8", )
  4. webView!!.clearHistory
  5. (webView!!.parent as ViewGroup).removeView(webView)
  6. webView!!.destroy
  7. webView =
  8. }
  9. super.onDestroy
  10.  
  11. }

WebSetting和WebViewClient,WebChromeClien

  • WebSetting

作用:对WebView进行配置和管理

  1. WebSettings webSettings = webView.getSettings;
  2. // 设置可以与js交互,为了防止资源浪费,我们可以在Activity
  3. // 的onResume中设置为true,在onStop中设置为false
  4. webSettings.setJavaScriptEnabled(true);
  5.  
  6. //设置自适应屏幕,两者合用
  7. //将图片调整到适合webview的大小
  8. webSettings.setUseWideViewPort(true);
  9. // 缩放至屏幕的大小
  10. webSettings.setLoadWithOverviewMode(true);
  11.  
  12. //设置编码格式
  13. webSettings.setDefaultTextEncodingName("utf-8");
  14.  
  15. // 设置允许JS弹窗
  16. webSettings.setJavaScriptCanOpenWindowsAutomatically(true);
  17.  
  18. //设置缓存的模式
  19. webSettings.setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK);

关于缓存的设置:

当加载 html 页面时,WebView会在/data/data/包名目录下生成 database 与 cache 两个文件夹,请求的 URL记录保存在 WebViewCache.db,而 URL的内容是保存在 WebViewCache 文件夹下

缓存模式如下:

  1. //LOAD_CACHE_ONLY: 不使用网络,只读取本地缓存数据
  2. //LOAD_DEFAULT: (默认)根据cache-control决定是否从网络上取据。
  3. //LOAD_NO_CACHE: 不使用缓存,只从网络获取数据.
  4. //LOAD_CACHE_ELSE_NETWORK,只要本地有,无论是否过期,或no-cache,都使用缓存中的数据。

离线加载

  1. if (NetStatusUtil.isConnected(getApplicationContext)) {
  2. webSettings.setCacheMode(WebSettings.LOAD_DEFAULT);//根据cache-control决定是否从网络上取数据。
  3. } else {
  4. webSettings.setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK);//没网,则从本地获取,即离线加载
  5. }
  6.  
  7. webSettings.setDomStorageEnabled(true); // 开启 DOM storage API 功能
  8. webSettings.setDatabaseEnabled(true); //开启 database storage API 功能
  9. webSettings.setAppCacheEnabled(true);//开启 Application Caches 功能
  10.  
  11. String cacheDirPath = getFilesDir.getAbsolutePath + APP_CACAHE_DIRNAME;
  12. webSettings.setAppCachePath(cacheDirPath); //设置 Application Caches 缓存目录
  • WebViewClient 作用

  • 处理各种通知,请求事件,主要有,网页开始加载,记载结束,加载错误(如404),处理https请求,具体使用请看下面代码,注释清晰。

  1. webView!!.webViewClient = object : WebViewClient {
  2. // 启用WebView,而不是系统自带的浏览器
  3. override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean {
  4. view.loadUrl(url)
  5. return true
  6. }
  7.  
  8. // 页面开始加载,我们可以在这里设置loading
  9. override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
  10. super.onPageStarted(view, url, favicon)
  11. tv_start.text = "开始加载了..."
  12. }
  13. // 页面加载结束,关闭loading
  14. override fun onPageFinished(view: WebView?, url: String?) {
  15. super.onPageFinished(view, url)
  16. tv_end.text = "加载结束了..."
  17. }
  18.  
  19. // 只要加载html,js,css的资源,每次都会回调到这里
  20. override fun onLoadResource(view: WebView?, url: String?) {
  21. loge("onLoadResource invoked")
  22. }
  23.  
  24. // 在这里我们可以加载我们自己的404页面
  25. override fun onReceivedError(view: WebView?, request: WebResourceRequest?, error: WebResourceError?) {
  26. loge("加载错误:${error.toString}")
  27. }
  28.  
  29. // webview默认设计是不开启https的,下面的设置是允许使用https
  30. override fun onReceivedSslError(view: WebView?, handler: SslErrorHandler?, error: SslError?) {
  31. handler?.proceed
  32. }
  33.  
  34. // js调用Android的方法,在这里可以,该方法不存在通过注解的方式的内存泄漏,但是想拿到Android的返回值的话很难,
  35. // 可以通过Android调用js的代码的形式来传递返回值,例如下面的方式
  36. // Android:MainActivity.java
  37. // mWebView.loadUrl("javascript:returnResult(" + result + ")");
  38. // JS:javascript.html
  39. // function returnResult(result){
  40. // alert("result is" + result);
  41. // }
  42.  
  43. override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean {
  44. val uri = Uri.parse(request?.url.toString)
  45. // 一般根据scheme(协议格式) & authority(协议名)判断(前两个参数)
  46. //假定传入进来的 url = "js://webview?arg1=111&arg2=222"(同时也是约定好的需要拦截的)
  47. if (uri.scheme == "js") {
  48. if (uri.authority == "webview") {
  49. toast_custom("js调用了Android的方法")
  50. val queryParameterNames = uri.queryParameterNames
  51. queryParameterNames.forEach {
  52. loge(it + ":" + uri.getQueryParameter(it))
  53. }
  54. }
  55. return true
  56. }
  57. return super.shouldOverrideUrlLoading(view, request)
  58. }
  59.  
  60. // 拦截资源 通常用于h5的首页页面,将常用的一些资源,放到本地
  61. override fun shouldInterceptRequest(view: WebView?, request: WebResourceRequest?): WebResourceResponse? {
  62. if(request?.url.toString.contains("logo.gif")){
  63. var inputStream: InputStream? =
  64. inputStream = applicationContext.assets.open("images/test.png")
  65. return WebResourceResponse("image/png","utf-8", inputStream)
  66. }
  67. return super.shouldInterceptRequest(view, request)
  68. }
  69. }

注意:

5.1 以上默认禁止了https和http的混用,下面的设置是开启:

  1. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
  2. webView.getSettings.setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
  3. }
  • WebChromeClient 作用

    辅助webview的一下回调方法,可以得到网页加载的进度,网页的标题,网页的icon,js的一些弹框,直接看代码,注释清晰。

  1. webView!!.webChromeClient = object : WebChromeClient {
  2.  
  3. // 网页加载的进度
  4. override fun onProgressChanged(view: WebView?, newProgress: Int) {
  5. tv_progress.text = "$newProgress%"
  6. }
  7.  
  8. // 获得网页的标题
  9. override fun onReceivedTitle(view: WebView?, title: String?) {
  10. tv_title.text = title
  11. }
  12.  
  13. //js Alert
  14. override fun onJsAlert(view: WebView?, url: String?, message: String?, result: JsResult?): Boolean {
  15.  
  16. AlertDialog.Builder(this@WebActivity)
  17. .setTitle("JsAlert")
  18. .setMessage(message)
  19. .setPositiveButton("OK") { _, _ -> result?.confirm }
  20. .setCancelable(false)
  21. .show
  22. return true
  23. }
  24.  
  25. // js Confirm
  26. override fun onJsConfirm(view: WebView?, url: String?, message: String?, result: JsResult?): Boolean {
  27. return super.onJsConfirm(view, url, message, result)
  28. }
  29.  
  30. //js Prompt
  31. override fun onJsPrompt(
  32. view: WebView?,
  33. url: String?,
  34. message: String?,
  35. defaultValue: String?,
  36. result: JsPromptResult?
  37. ): Boolean {
  38. return super.onJsPrompt(view, url, message, defaultValue, result)
  39. }
  40.  
  41. }

Android和js的交互

Android调用js

1. 通过webview的loadUrl

注意:该方式必须在webview加载完毕之后才能调用,也就是webviewClient的onPageFinished方法回调之后,而且该方法的执行 会刷新界面,效率较低

js代码:

  1. function callJs{
  2. alert("Android 调用了 js代码)
  3. }

kotlin代码:

  1. webView?.loadUrl("javascript:callJs")

2. 通过webview的evaluateJavaScript

比起第一种方法,效率更高,但是要在4.4之后才能使用

js代码:

  1. function callJs{
  2. // alert("Android 调用了 js代码)
  3. return {name:'wfq',age:25}
  4. }

kotlin代码:

  1. webView?.evaluateJavascript("javascript:callJs") {
  2. // 这里直接拿到的是js代码的返回值
  3. toast(it) // {name:'wfq',age:25}
  4. }

js调用Android

1. 通过webview的addJavaScriptInterface进行对象映射

我们可以单独定义一个类,所有需要交互的方法可以全部写在这个类里面,当然也可以直接写在Activity里面,下面以直接定义在Activity里面为例,优点:使用方便,缺点:存在漏洞(4.2之前),请看下面的“WebView的一些漏洞以及如何防止”

kotlin中定义被js调用的方法

  1. @JavascriptInterface
  2. fun hello(name: String) {
  3. toast("你好,我是来自js的消息:$msg")
  4. }

js代码

  1. function callAndroid{
  2.  android.hello("我是js的,我来调用你了")
  3. }

kotlin中们在webview里面设置Android与js的代码的映射:

  1. webView?.addJavascriptInterface(this, "android")

2. 通过webviewClient的shouldOverrideUrlLoading的回调来拦截url

具体使用:解析该url的协议,如果监测到是预先约定好的协议,那么就调用相应的方法。比较安全,但是使用麻烦,js获取Android的返回值的话很麻烦,只能通过上面介绍的通过loadurl去执行js代码把返回值通过参数传递回去

首先在js中约定号协议

  1. function callAndroid{
  2. // 约定的url协议为:js://webview?name=wfq&age=24
  3. document.location = "js://webview?name=wfq&age=24"
  4. }
  5. kotlin里面,当loadurl的时候就会回调到shouldOverrideUrlLoading里面
  6.  
  7. override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean {
  8. val uri = Uri.parse(request?.url.toString)
  9. // 一般根据scheme(协议格式) & authority(协议名)判断(前两个参数)
  10. //假定传入进来的 js://webview?name=wfq&age=24
  11. if (uri.scheme == "js") {
  12. if (uri.authority == "webview") {
  13. toast_custom("js调用了Android的方法")
  14. val queryParameterNames = uri.queryParameterNames
  15. queryParameterNames.forEach {
  16. loge(it + ":" + uri.getQueryParameter(it))
  17. }
  18. }
  19. return true
  20. }
  21. return super.shouldOverrideUrlLoading(view, request)
  22. }

3.通过webChromeClient的onJsAlert,onJsConfirm,onJsPrompt回调来拦截对话框

通过拦截js对话框,得到他们的消息,然后解析即可,为了安全,建议内容采用上面介绍的url协议, 常用的拦截的话就是拦截prompt,因为它可以返回任意值,alert没有返回值,confirm只能返回两种类型,确定和取消

js代码

  1. function clickprompt{
  2. var result=prompt("wfq://demo?arg1=111&arg2=222");
  3. alert("demo " + result);
  4. }

kotlin代码

  1. override fun onJsPrompt(
  2. view: WebView?,
  3. url: String?,
  4. message: String?,
  5. defaultValue: String?,
  6. result: JsPromptResult?
  7. ): Boolean {
  8. val uri = Uri.parse(message)
  9. if (uri.scheme == "wfq") {
  10. if (uri.authority == "demo") {
  11. toast_custom("js调用了Android的方法")
  12. val queryParameterNames = uri.queryParameterNames
  13. queryParameterNames.forEach {
  14. loge(it + ":" + uri.getQueryParameter(it))
  15. }
  16. // 将需要返回的值通过该方式返回
  17. result?.confirm("js调用了Android的方法成功啦啦啦啦啦")
  18. }
  19. return true
  20. }
  21. return super.onJsPrompt(view, url, message, defaultValue, result)
  22. }

由于拦截了弹框,所以js代码的alert需要处理 这里的message便是上面代码的返回值通过alert显示出来的信息

  1. override fun onJsAlert(view: WebView?, url: String?, message: String?, result: JsResult?): Boolean {
  2. AlertDialog.Builder(this@WebActivity)
  3. .setTitle("JsAlert")
  4. .setMessage(message)
  5. .setPositiveButton("OK") { _, _ -> result?.confirm }
  6. .setCancelable(false)
  7. .show
  8. return true
  9. }

上面三种方式的区别:

addJavascriptInterface 方便简洁,4.0以下存在漏洞,4.0以上通过@JavascriptInterface注解修复漏洞。

WebViewClient.shouldOverrideUrlLoading回调,不存在漏洞,使用复杂,需要定义协议的约束,但是返回值的话有些麻烦,在不需要返回值的情况下可以使用这个方式。

通过WebChromeClient的onJsAlerta,onJsConfirm,onJsPrompt,不存在漏洞问题,使用复杂,需要进行协议的约束,可以返回值,能满足大多数情况下的互调通信。

WebView的一些漏洞以及如何防止

密码明文存储漏洞

webview默认开启了密码保存功能,在用户输入密码后会弹出提示框询问用户是否保存密码,保存后密码会被明文保存在 /data/data/com.package.name/databases/webview.db 下面,手机root后可以查看,那么如何解决?

  1. WebSettings.setSavePassword(false) // 关闭密码保存提醒功能

WebView 任意代码执行漏洞

addJavascriptInterface漏洞,首先先明白一点,js调用Android代码的时候,我们经常使用的是addJavascriptInterface, JS调用Android的其中一个方式是通过addJavascriptInterface接口进行对象映射,那么Android4.2之前,既然拿到了这个对象,那么这个对象中的所有方法都是可以调用的,4.2之后,需要被js调用的函数加上@JavascriptInterface注解后来避免该漏洞

所以怎么解决

对于Android 4.2以后,则只需要对被调用的函数以 @JavascriptInterface进行注解

域控制不严格漏洞

  • 原因分析 当我们在Applilcation里面,android:exported="true"的时候,A 应用可以通过 B 应用导出的 Activity 让 B 应用加载一个恶意的 file 协议的 url,从而可以获取 B 应用的内部私有文件,从而带来数据泄露威胁,

下面来看下WebView中getSettings类的方法对 WebView 安全性的影响 setAllowFileAccess

  1. // 设置是否允许 WebView 使用 File 协议
  2. webView.getSettings.setAllowFileAccess(true);
  3. 如果设置为false的话,便不会存在威胁,但是,webview也无法使用本地的html文件

setAllowFileAccessFromFileURLs

  1. // 设置是否允许通过 file url 加载的 Js代码读取其他的本地文件
  2. // 在Android 4.1前默认允许
  3. // 在Android 4.1后默认禁止
  4. webView.getSettings.setAllowFileAccessFromFileURLs(true);
  5.  
  6. 我们应该明确的设置为false,禁止读取其他文件

setAllowUniversalAccessFromFileURLs

  1. // 设置是否允许通过 file url 加载的 Javascript 可以访问其他的源(包括http、https等源)
  2. // 在Android 4.1前默认允许(setAllowFileAccessFromFileURLs不起作用)
  3. // 在Android 4.1后默认禁止
  4. webView.getSettings.setAllowUniversalAccessFromFileURLs(true);
  1. WebView预加载以及资源预加载

为什么需要预加载

h5页面加载慢,慢的原因:页面渲染慢,资源加载慢

如何优化?

h5的缓存,资源预加载,资源拦截

h5的缓存 Android WebView自带的缓存

1. 浏览器缓存

  1. 根据 HTTP 协议头里的 Cache-Control(或 Expires)和 Last-Modified(或Etag)等字段来控制文件缓存的机制浏览器自己实现,我需我们处理

2. App Cache

  1. 方便构建Web App的缓存,存储静态文件(如JSCSS、字体文件)WebSettings settings = getSettings;
  2. String cacheDirPath = context.getFilesDir.getAbsolutePath+"cache/";
  3. settings.setAppCachePath(cacheDirPath);
  4. settings.setAppCacheMaxSize(20*1024*1024);
  5. settings.setAppCacheEnabled(true);

3. Dom Storage

  1. WebSettings settings = getSettings;
  2. settings.setDomStorageEnabled(true);

4. Indexed Database

  1. // 只需设置支持JS就自动打开IndexedDB存储机制
  2. // Android 在4.4开始加入对 IndexedDB 的支持,只需打开允许 JS 执行的开关就好了。
  3. WebSettings settings = getSettings;
  4. settings.setJavaScriptEnabled(true);
  • 资源预加载 预加载webview对象,首次初始化WebView会比第二次慢很多的原因:初始化后,即使webview已经释放,但是WebView的一些共享的对象依然是存在的,我们可以在Application里面提前初始化一个Webview的对象,然后可以直接loadurl加载资源

  • 资源拦截 可以将跟新频率低的一些资源静态文件放在本地,拦截h5的资源网络请求并进行检测,如果检测到,就直接拿本地的资源进行替换即可

  1. // 拦截资源 通常用于h5的首页页面,将常用的一些资源,放到本地
  2. override fun shouldInterceptRequest(view: WebView?, request: WebResourceRequest?): WebResourceResponse? {
  3.  
  4. if(request?.url.toString.contains("logo.jpg")){
  5. var inputStream: InputStream? =
  6. inputStream = applicationContext.assets.open("images/test.jpg")
  7. return WebResourceResponse("image/png","utf-8", inputStream)
  8. }
  9.  
  10. return super.shouldInterceptRequest(view, request)
  11. }

常见的使用注意事项

1. Android9.0 已经禁止了webview使用http,怎么解决?

在manifest Application标签下面使用:

  1. android:usesCleartextTraffic="true"

2. 开启混淆之后,Android无法与h5交互?

  1. #保留annotation, 例如 @JavascriptInterface 等 annotation
  2. -keepattributes *Annotation*
  3.  
  4. #保留跟 javascript相关的属性
  5. -keepattributes JavascriptInterface
  6.  
  7. #保留JavascriptInterface中的方法
  8. -keepclassmembers class * {
  9. @android.webkit.JavascriptInterface <methods>;
  10. }
  11. #这个类是用来与js交互,所以这个类中的 字段 ,方法, 不能被混淆、全路径名称.类名
  12. -keepclassmembers public class com.youpackgename.xxx.H5CallBackAndroid{
  13. <fields>;
  14. <methods>;
  15. public *;
  16. private *;
  17. }

3. 如何调试?

3.1 在WebViewActivity里面,开启调试

  1. // 开启调试
  2. WebView.setWebContentsDebuggingEnabled(true)

3.2 chrome浏览器地址栏输入 chrome://inspect

3.3 手机打开USB调试,打开webview页面,点击chrome页面的最下面的inspect,这样,便可以进入了web开发,看控制台,网络请求等

看到WebView是不是很头疼?


 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号