再学Android之WebView

WebView

最近一直在作web前端开发,作了预约酒店系统,后台管理系统,小程序等,正好趁机复习一下Android的WebViewjavascript

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

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

WebView自身的一些方法html

//方式1. 加载一个网页:
  webView.loadUrl("http://www.google.com/");

  //方式2:加载apk包中的html页面
  webView.loadUrl("file:///android_asset/test.html");

  //方式3:加载手机本地的html页面
   webView.loadUrl("content://com.android.htmlfileprovider/sdcard/test.html");
复制代码

正常状况下,在WebView界面,用户点击返回键是直接退出该页面的,着固然不是咱们想要的,咱们想要的是网页本身的前进和后退,因此下面介绍网页前进和后退的一些API前端

//判断是否能够后退
Webview.canGoBack() 
//后退网页
Webview.goBack()

//判断是否能够前进                     
Webview.canGoForward()
//前进网页
Webview.goForward()

// 参数传负的话表示后退,传正值的话表示的是前进
Webview.goBackOrForward(int steps) 
复制代码
对返回键的监听,来实现网页的后退
public boolean onKeyDown(int keyCode, KeyEvent event) {
    if ((keyCode == KEYCODE_BACK) && mWebView.canGoBack()) { 
        mWebView.goBack();
        return true;
    }
    return super.onKeyDown(keyCode, event);
}
复制代码

如何防止WebView内存泄漏java

防止内存泄漏的一个原则就是:生命周期长的不要跟生命周期短的玩。为了防止WebView不形成内存泄漏,android

  • 不要在xml里面定义WebView,而是在Activity选中使用代码去构建,而且Context使用ApplicationContext
  • 在Activity销毁的时候,先让WebView加载空内容,而后重rootView中移除WebView,再销毁WebView,最后置空
override fun onDestroy() {
        if (webView != null) {
            webView!!.loadDataWithBaseURL(null, "", "text/html", "utf-8", null)
            webView!!.clearHistory()
            (webView!!.parent as ViewGroup).removeView(webView)
            webView!!.destroy()
            webView = null
        }
        super.onDestroy()

    }
复制代码

WebSetting和WebViewClient,WebChromeClient

  • WebSetting

做用:对WebView进行配置和管理css3

WebSettings webSettings = webView.getSettings();
// 设置能够与js交互,为了防止资源浪费,咱们能够在Activity
// 的onResume中设置为true,在onStop中设置为false
webSettings.setJavaScriptEnabled(true); 

//设置自适应屏幕,二者合用
//将图片调整到适合webview的大小 
webSettings.setUseWideViewPort(true); 
 // 缩放至屏幕的大小
webSettings.setLoadWithOverviewMode(true);

//设置编码格式
webSettings.setDefaultTextEncodingName("utf-8");

// 设置容许JS弹窗
webSettings.setJavaScriptCanOpenWindowsAutomatically(true);

//设置缓存的模式
webSettings.setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK);

复制代码

关于缓存的设置:es6

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

缓存模式以下:
 //LOAD_CACHE_ONLY: 不使用网络,只读取本地缓存数据
 //LOAD_DEFAULT: (默认)根据cache-control决定是否从网络上取据。
 //LOAD_NO_CACHE: 不使用缓存,只从网络获取数据.
 //LOAD_CACHE_ELSE_NETWORK,只要本地有,不管是否过时,或no-cache,都使用缓存中的数据。


复制代码

离线加载chrome

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 做用:处理各类通知,请求事件,主要有,网页开始加载,记载结束,加载错误(如404),处理https请求,具体使用请看下面代码,注释清晰
webView!!.webViewClient = object : WebViewClient() {
    // 启用WebView,而不是系统自带的浏览器
            override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean {
                view.loadUrl(url)
                return true
            }

// 页面开始加载,咱们能够在这里设置loading
            override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
                super.onPageStarted(view, url, favicon)
                tv_start.text = "开始加载了..."
            }
// 页面加载结束,关闭loading
            override fun onPageFinished(view: WebView?, url: String?) {
                super.onPageFinished(view, url)
                tv_end.text = "加载结束了..."
            }

            // 只要加载html,js,css的资源,每次都会回调到这里
            override fun onLoadResource(view: WebView?, url: String?) {
                loge("onLoadResource invoked")
            }

// 在这里咱们能够加载咱们本身的404页面
            override fun onReceivedError(view: WebView?, request: WebResourceRequest?, error: WebResourceError?) {
                loge("加载错误:${error.toString()}")
            }

// webview默认设计是不开启https的,下面的设置是容许使用https
            override fun onReceivedSslError(view: WebView?, handler: SslErrorHandler?, error: SslError?) {
                handler?.proceed()
            }

            // js调用Android的方法,在这里能够,该方法不存在经过注解的方式的内存泄漏,可是想拿到Android的返回值的话很难,
            // 能够经过Android调用js的代码的形式来传递返回值,例以下面的方式
            // Android:MainActivity.java
            //  mWebView.loadUrl("javascript:returnResult(" + result + ")");
            // JS:javascript.html
            //  function returnResult(result){
            //    alert("result is" + result);
            //   }

            override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean {
                val uri = Uri.parse(request?.url.toString())
                // 通常根据scheme(协议格式) & authority(协议名)判断(前两个参数)
                //假定传入进来的 url = "js://webview?arg1=111&arg2=222"(同时也是约定好的须要拦截的)
                if (uri.scheme == "js") {
                    if (uri.authority == "webview") {
                        toast_custom("js调用了Android的方法")
                        val queryParameterNames = uri.queryParameterNames
                        queryParameterNames.forEach {
                            loge(it + ":" + uri.getQueryParameter(it))
                        }
                    }
                    return true
                }
                return super.shouldOverrideUrlLoading(view, request)
            }

            // 拦截资源 一般用于h5的首页页面,将经常使用的一些资源,放到本地
            override fun shouldInterceptRequest(view: WebView?, request: WebResourceRequest?): WebResourceResponse? {
                if(request?.url.toString().contains("logo.gif")){
                    var inputStream: InputStream? = null
                    inputStream = applicationContext.assets.open("images/test.png")
                    return WebResourceResponse("image/png","utf-8", inputStream)
                }
                return super.shouldInterceptRequest(view, request)
            }
        }
复制代码

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

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
mWebView.getSettings().setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
}
复制代码
  • WebChromeClient 做用:辅助webview的一下回调方法,能够获得网页加载的进度,网页的标题,网页的icon,js的一些弹框,直接看代码,注释清晰:
webView!!.webChromeClient = object : WebChromeClient() {

            // 网页加载的进度
            override fun onProgressChanged(view: WebView?, newProgress: Int) {
                tv_progress.text = "$newProgress%"
            }

// 得到网页的标题
            override fun onReceivedTitle(view: WebView?, title: String?) {
                tv_title.text = title
            }

//js Alert
            override fun onJsAlert(view: WebView?, url: String?, message: String?, result: JsResult?): Boolean {

                AlertDialog.Builder(this@WebActivity)
                    .setTitle("JsAlert")
                    .setMessage(message)
                    .setPositiveButton("OK") { _, _ -> result?.confirm() }
                    .setCancelable(false)
                    .show()
                return true
            }

// js Confirm
            override fun onJsConfirm(view: WebView?, url: String?, message: String?, result: JsResult?): Boolean {
                return super.onJsConfirm(view, url, message, result)
            }

//js Prompt
            override fun onJsPrompt(
                view: WebView?,
                url: String?,
                message: String?,
                defaultValue: String?,
                result: JsPromptResult?
            ): Boolean {
                return super.onJsPrompt(view, url, message, defaultValue, result)
            }


        }
复制代码

Android和js的交互

  • Android调用js

    1.经过webview的loadUrl

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

    js代码:
    function callJs(){
        alert("Android 调用了 js代码) } kotlin代码: webView?.loadUrl("javascript:callJs()") 复制代码

    2.经过webview的evaluateJavaScript

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

    js代码:
    function callJs(){
       //  alert("Android 调用了 js代码) return {name:'wfq',age:25} } kotlin代码: webView?.evaluateJavascript("javascript:callJs()") { // 这里直接拿到的是js代码的返回值 toast(it) // {name:'wfq',age:25} } 复制代码
  • js调用Android

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

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

    kotlin中定义被js调用的方法
     @JavascriptInterface
    fun hello(name: String) {
        toast("你好,我是来自js的消息:$msg")
    }
    js代码
    function callAndroid(){
        android.hello("我是js的,我来调用你了")
    }
    kotlin中们在webview里面设置Android与js的代码的映射
    webView?.addJavascriptInterface(this, "android")
    复制代码

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

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

    首先在js中约定号协议
    function callAndroid(){
            // 约定的url协议为:js://webview?name=wfq&age=24
            document.location = "js://webview?name=wfq&age=24"
         }
    在kotlin里面,当loadurl的时候就会回调到shouldOverrideUrlLoading()里面
    
    override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean {
                val uri = Uri.parse(request?.url.toString())
                // 通常根据scheme(协议格式) & authority(协议名)判断(前两个参数)
                //假定传入进来的 js://webview?name=wfq&age=24
                if (uri.scheme == "js") {
                    if (uri.authority == "webview") {
                        toast_custom("js调用了Android的方法")
                        val queryParameterNames = uri.queryParameterNames
                        queryParameterNames.forEach {
                            loge(it + ":" + uri.getQueryParameter(it))
                        }
                    }
                    return true
                }
                return super.shouldOverrideUrlLoading(view, request)
            }
    
    复制代码

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

    经过拦截js对话框,获得他们的消息,而后解析便可,为了安全,建议内容采用上面介绍的url协议, 经常使用的拦截的话就是拦截prompt,由于它能够返回任意值,alert没有返回值,confirm只能返回两种类型,肯定和取消

    js代码
    function clickprompt(){
        var result=prompt("wfq://demo?arg1=111&arg2=222");
        alert("demo " + result);
    }
    kotlin代码
    override fun onJsPrompt(
                view: WebView?,
                url: String?,
                message: String?,
                defaultValue: String?,
                result: JsPromptResult?
            ): Boolean {
                val uri = Uri.parse(message)
                if (uri.scheme == "wfq") {
                    if (uri.authority == "demo") {
                        toast_custom("js调用了Android的方法")
                        val queryParameterNames = uri.queryParameterNames
                        queryParameterNames.forEach {
                            loge(it + ":" + uri.getQueryParameter(it))
                        }
                        // 将须要返回的值经过该方式返回
                        result?.confirm("js调用了Android的方法成功啦啦啦啦啦")
                    }
                    return true
                }
                return super.onJsPrompt(view, url, message, defaultValue, result)
            }
    
    因为拦截了弹框,因此js代码的alert须要处理 这里的message即是上面代码的返回值经过alert显示出来的信息
    override fun onJsAlert(view: WebView?, url: String?, message: String?, result: JsResult?): Boolean {
                AlertDialog.Builder(this@WebActivity)
                    .setTitle("JsAlert")
                    .setMessage(message)
                    .setPositiveButton("OK") { _, _ -> result?.confirm() }
                    .setCancelable(false)
                    .show()
                return true
            }
    
    复制代码

    上面三种方式的区别: addJavascriptInterface() 方便简洁,4.0如下存在漏洞,4.0以上经过@JavascriptInterface注解修复漏洞 WebViewClient.shouldOverrideUrlLoading()回调,不存在漏洞,使用复杂,须要定义协议的约束,可是返回值的话有些麻烦,在不须要返回值的状况下可使用这个方式 经过WebChromeClient的onJsAlerta,onJsConfirm,onJsPrompt,不存在漏洞问题,使用复杂,须要进行协议的约束,能够返回值,能知足大多数状况下的互调通讯

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

参考腾讯大神Carson_Ho的简书 www.jianshu.com/p/3a345d27c…

密码明文存储漏洞

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

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

WebView 任意代码执行漏洞

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

因此怎么解决

对于Android 4.2之前,须要采用拦截prompt()的方式进行漏洞修复
对于Android 4.2之后,则只须要对被调用的函数以 @JavascriptInterface进行注解
复制代码

域控制不严格漏洞

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

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

// 设置是否容许 WebView 使用 File 协议
// 默认设置为true,即容许在 File 域下执行任意 JavaScript 代码
webView.getSettings().setAllowFileAccess(true);     
若是设置为false的话,便不会存在威胁,可是,webview也没法使用本地的html文件


复制代码

setAllowFileAccessFromFileURLs()

// 设置是否容许经过 file url 加载的 Js代码读取其余的本地文件
// 在Android 4.1前默认容许
// 在Android 4.1后默认禁止
webView.getSettings().setAllowFileAccessFromFileURLs(true);

咱们应该明确的设置为false,禁止读取其余文件

复制代码

setAllowUniversalAccessFromFileURLs()

// 设置是否容许经过 file url 加载的 Javascript 能够访问其余的源(包括http、https等源)
// 在Android 4.1前默认容许(setAllowFileAccessFromFileURLs()不起做用)
// 在Android 4.1后默认禁止
webView.getSettings().setAllowUniversalAccessFromFileURLs(true);
复制代码

WebView预加载以及资源预加载

为何须要预加载

h5页面加载慢,慢的缘由:页面渲染慢,资源加载慢

如何优化?

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

  • h5的缓存 Android WebView自带的缓存 1.浏览器缓存

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

    2.App Cache

    方便构建Web App的缓存,存储静态文件(如JS、CSS、字体文件)
     WebSettings settings = getSettings();
     String cacheDirPath = context.getFilesDir().getAbsolutePath()+"cache/";
     settings.setAppCachePath(cacheDirPath);
     settings.setAppCacheMaxSize(20*1024*1024);
     settings.setAppCacheEnabled(true);
    复制代码

    3.Dom Storage

    WebSettings settings = getSettings();
        settings.setDomStorageEnabled(true);
    复制代码

    4.Indexed Database

    // 只需设置支持JS就自动打开IndexedDB存储机制
     // Android 在4.4开始加入对 IndexedDB 的支持,只需打开容许 JS 执行的开关就行了。
    WebSettings settings = getSettings();
    settings.setJavaScriptEnabled(true);
       
    复制代码
  • 资源预加载 预加载webview对象,首次初始化WebView会比第二次慢不少的缘由:初始化后,即便webview已经释放,可是WebView的一些共享的对象依然是存在的,咱们能够在Application里面提早初始化一个Webview的对象,而后能够直接loadurl加载资源

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

// 拦截资源 一般用于h5的首页页面,将经常使用的一些资源,放到本地
            override fun shouldInterceptRequest(view: WebView?, request: WebResourceRequest?): WebResourceResponse? {

                if(request?.url.toString().contains("logo.jpg")){
                    var inputStream: InputStream? = null
                    inputStream = applicationContext.assets.open("images/test.jpg")
                    return WebResourceResponse("image/png","utf-8", inputStream)
                }

                return super.shouldInterceptRequest(view, request)
            }
复制代码

常见的使用注意事项:

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

在manifest的Application标签下面使用:android:usesCleartextTraffic="true"

开启混淆以后,Android没法与h5交互?

#保留annotation, 例如 @JavascriptInterface 等 annotation
-keepattributes *Annotation*

#保留跟 javascript相关的属性
-keepattributes JavascriptInterface

#保留JavascriptInterface中的方法
-keepclassmembers class * {
    @android.webkit.JavascriptInterface <methods>;
}
#这个类是用来与js交互,因此这个类中的 字段 ,方法, 不能被混淆、全路径名称.类名
-keepclassmembers public class com.youpackgename.xxx.H5CallBackAndroid{
   <fields>;
   <methods>;
   public *;
   private *;
}
复制代码

如何调试?

1.在WebViewActivity里面,开启调试

// 开启调试
WebView.setWebContentsDebuggingEnabled(true)
复制代码

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

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

相关文章
相关标签/搜索