前言
现在很多App里都内置了Web网页(Hybrid App),比如说很多电商平台,淘宝、京东、聚划算等等。那么这种该如何实现呢?其实这是Android里一个叫WebView组件实现。下面将围绕着这点进行详细的整理说明。
简介
android控件中的WebView,一个基于webkit引擎、展现web页面的控件
- Android 4.4前:Android Webview在低版本 & 高版本采用了不同的webkit版本的内核
- Android 4.4后:直接使用了Chrome内核
作用
- 在 Android 客户端上加载h5页面
- 在本地 与 h5页面实现交互 & 调用
- 其他:对 url 请求、页面加载、渲染、对话框 进行额外处理。
具体使用
- 涉及到的类:
- Webview类:
- 创建对象,加载URL,生命周期管理,状态管理
- loadUrl(),goBack()等
- WebSettings类
- 配置 & 管理WebView
- 缓存:setCacheMode()
- 与js交互:setJavascriptEnable()
- WebViewClient类
- 处理各种通知 & 请求事件
- shouldOverrideUrlLoading():打开网页在webview显示
- onPageStarted():载入页面时
- onPageFinished():页面加载结束时
- onLoadResource():在加载页面资源时会调用,每一个资源(比如图片)的加载都会调用一次。
- onReceivedError():加载页面的服务器出现错误时(如404)调用。
- onReceivedSslError(): 处理https请求,webView默认是不处理https请求的,页面显示空白,需要进行如下设置:
webView.setWebViewClient(new WebViewClient() { @Override public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) { handler.proceed(); //表示等待证书响应 // handler.cancel(); //表示挂起连接,为默认方式 // handler.handleMessage(null); //可做其他处理 } }); // 特别注意:5.1以上默认禁止了https和http混用,以下方式是开启 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { mWebView.getSettings().setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW); } - WebChromeClient类
- 辅助WebView处理js对话框,网站title,网站icon等
- onProgressChanged()
- onReceivedTitle()
- onJsAlert() 无返回值 知道了
- onJsConfirm() 返回值 true:确认,false:取消
- onJsPrompt():输入框,确认:返回输入框的内容,取消:返回null
- Webview类:
Native与JS交互
- 交互方式:桥梁WebView
- android调用js代码
-
方式一 通过webview的loadUrl()
- 该方式会引起页面刷新
- 指定url:http:www.baidu.com
- 指定资源文件: file:///android_assets/filename.file
- loadUrl(“javascripr:functionname()”)
-
方式二:通过webView的evaluateJavaScript()
- 该方式不会引起页面刷新,并且在android4.4之后才可使用
- 向下兼容差
-
- js去调用android的代码
- 方式一:通过webView的addJavaScriptInterface()进行对象映射
- 这种方式存在严重的漏洞问题
- 方式二:通过WebViewClient的shouldOverrideUrlLoading()回调拦截url
- 具体原理:第一步,webview通过webviewclient的回调方法shouldOverrideUrlLoading()拦截url,第二步,解析该url的协议;第三步,如果检测到内部约定好的协议,则执行业务流程代码(即js调用android native代码)
- 优点: 不存在方式1的漏洞
- 缺点:js获取android方法的返回值复杂。如果js想要得到android方法的返回值,只能通过webview的loadUrl()去执行js方法把返回值传递回去
// Android:MainActivity.java mWebView.loadUrl("javascript:returnResult(" + result + ")"); // JS:javascript.html function returnResult(result){ alert("result is" + result); } - 方式三:通过WebChromeClient的onJsAlert()onJsConfirm()onJsPrompt()方法拦截js对话框信息
- 常用的拦截是:onJsPrompt()方法,
- 因为该方法可以返回任意类型的值;操作简便;不存在漏洞;
- 方式一:通过webView的addJavaScriptInterface()进行对象映射
- android调用js代码
如何避免webview内存泄漏
- 不在xml中定义webview,而是在需要的时候再activity中创建,并且使用application context;
- 在activity销毁的时候,先让webview加载null内容,然后移除webview,再销毁webview,最后置空
@Override
protected void onDestroy() {
if (mWebView != null) {
mWebView.loadDataWithBaseURL(null, "", "text/html", "utf-8", null);
mWebView.clearHistory();
((ViewGroup) mWebView.getParent()).removeView(mWebView);
mWebView.destroy();
mWebView = null;
}
super.onDestroy();
}
构建全面的webview缓存机制 & 资源加载方案
因为在移动端webview存在较为明显的性能问题,特别突出的是:加载速度慢,流量消耗大。那么有么有解决的方案呢?如下:
-
存在哪些性能问题?
- H5页面加载速度慢
- 渲染速度
- js解析效率:js本身解析过程复杂,解析速度不快。并且页面内涉及较多的js文件,所以叠加起来使得总的js解析效率低
- 手机硬件性能:android机型碎片化,导致手机性能参差不齐。
- 资源加载慢:H5页面需要从服务端下载,并存储在手机内存中
- h5页面资源多:
- 网络请求数量多:每加载一个html页面,都有url自身的请求,以及页面中对外部资源的引用也是一个个独立的http请求;每个请求都是串行
- 渲染速度
- 耗费流量:
- 每次使用h5页面时, 用户都需要重新加载页面
- 每次加载一个h5页面,都会产生较多的网络请求
- 每个请求都是串行的,这么多串行导致流量消耗大
- H5页面加载速度慢
-
针对问题,有哪些解决方案:
- h5自带的缓存方案(也是WebView自带缓存机制):
- h5页面加载后会存储在缓存区域 /data/data/包名/cache 以及 database 其中请求的url记录保存在WebViewCache.db 而URL的内容是保存在WebViewcache文件夹下
- android webview 目前除了新的File System缓存机制还不支持,其他都支持
- 作用:
- 离线浏览
- 提高页面加载速度,以及减少流量消耗
- 应用:此处主要讲解前端H5的缓存机制(或者缓存模式)
-
缓存机制:如何将加载过的网页数据保存到本地(保存)
- 浏览器 缓存机制
- 特点:
- 支持HTTP协议层
- 缓存文件需要首次加载后才会产生;
- 浏览器缓存的存储空间有限;
- 缓存有被清除的可能;
- 缓存的文件没有校验
- 根据Http协议头里的Cache-Control , Expires,Last-Modified,ETag等字段来控制文件缓存的机制
- Cache-Control:用于控制文件在本地缓存有效时长,服务器回包:Cache-Control:max-age=600,则表示文件在本地应该缓存,且有效时长是600秒(从发出请求算起)。在接下来600秒内,如果有请求这个资源,浏览器不会发出 HTTP 请求,而是直接使用本地缓存的文件。
- Expires:与Cache-Control功能相同,即控制缓存的有效时间
- Expires是HTTP1.0标准中的字段,Cache-Control是HTTP1.1标准中新加的字段
- 当两个字段同时出现时,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一起使用;缓存失效后,向服务器查询是否有更新
- 应用场景:
- 静态资源文件的存储,如js,css,字体,图片等,存放在/data/data/包名/…
- 具体实现:
- webview内置自动实现,即不需要设置
- android 4.4后的webview浏览器内核chrome
- 浏览器缓存机制是浏览器内核的机制,一般都是标准的实现
- webview内置自动实现,即不需要设置
- 特点:
- Application cache缓存机制
-
原理:
- 以文件为单位进行缓存,且文件有一定的更新机制
- AppCache原理有两个关键点:manifest属性以及manifest文件
<!DOCTYPE html> <html manifest="demo_html.appcache"> // HTML 在头中通过 manifest 属性引用 manifest 文件 // manifest 文件:就是上面以 appcache 结尾的文件,是一个普通文件文件,列出了需要缓存的文件 // 浏览器在首次加载 HTML 文件时,会解析 manifest 属性,并读取 manifest 文件,获取 Section:CACHE MANIFEST 下要缓存的文件列表,再对文件缓存 <body> ... </body> </html> // 原理说明如下: // AppCache 在首次加载生成后,也有更新机制。被缓存的文件如果要更新,需要更新 manifest 文件 // 因为浏览器在下次加载时,除了会默认使用缓存外,还会在后台检查 manifest 文件有没有修改(byte by byte) 发现有修改,就会重新获取 manifest 文件,对 Section:CACHE MANIFEST 下文件列表检查更新 // manifest 文件与缓存文件的检查更新也遵守浏览器缓存机制 // 如用户手动清了 AppCache 缓存,下次加载时,浏览器会重新生成缓存,也可算是一种缓存的更新 // AppCache 的缓存文件,与浏览器的缓存文件分开存储的,因为 AppCache 在本地有 5MB(分 HOST)的空间限制 - 特点: 方便构建web app的缓存
- 应用场景:存储静态文件(js,css,字体文件)
- 应用场景同浏览器缓存机制
- 但appcache是对浏览器缓存机制的补充,不是替代
- 具体实现
// 通过设置WebView的settings来实现 WebSettings settings = getSettings(); String cacheDirPath = context.getFilesDir().getAbsolutePath()+"cache/"; settings.setAppCachePath(cacheDirPath); // 1. 设置缓存路径 settings.setAppCacheMaxSize(20*1024*1024); // 2. 设置缓存大小 settings.setAppCacheEnabled(true); // 3. 开启Application Cache存储机制 // 特别注意 // 每个 Application 只调用一次 WebSettings.setAppCachePath() 和WebSettings.setAppCacheMaxSize()
-
- DOM Storage缓存机制
- 原理:
- 通过存储字符串的key-value来提供
- Dom storage分为session storage和local storage
- session storage:具备临时性,即存储与页面相关的数据,它在页面关闭后无法使用
- local storage:具备持久性,即保存的数据在页面关闭后也可以使用
- 特点
- 存储空间大(5MB):存储空间对于不同浏览器不同,如cookie才4kb
- 存储安全,便捷:DOM storage存储的数据在本地,不需要经常和服务器进行交互
- 不像cookie每次请求一次页面,都会向服务器发送网络请求
- 应用场景
- 存储临时,简单的数据;
- dom storage 机制类似于android的sharedPreference机制
- 存储临时,简单的数据;
- 具体实现:
- 原理:
// 通过设置
WebView的Settings类实现
WebSettings settings = getSettings();settings.setDomStorageEnabled(true); // 开启DOM storage ```- Web SQL Database缓存机制
- 原理:
- 基于SQL的数据库存储机制
- 特点:
- 充分利用数据库的优势,可方便对数据进行增删改查
- 应用场景:
- 存储适合数据库的结构化数据
- 具体实现
// 通过设置WebView的settings实现 WebSettings settings = getSettings(); String cacheDirPath = context.getFilesDir().getAbsolutePath()+"cache/"; settings.setDatabasePath(cacheDirPath); // 设置缓存路径 settings.setDatabaseEnabled(true); // 开启 数据库存储机制- 特别说明:
- Web SQL Database存储机制不再推荐使用
- 取而代之的是IndexeDB缓存机制
- 原理:
- Indexed Database缓存机制
- 原理:
- 类似NoSQL数据库,通过存储字符串的key-value对来提供
- 类似Dom storage 存储机制的key-value存储方式
- 特点:
- 功能强大,使用简单
- 通过数据库的事物transation机制进行数据操作
- 可对对象的任意属性生成索引,方便查询
- 存储空间大:
- 较大存储空间,默认推荐250MB(以host区分),比Dom storage的5MB要大的多
- 使用灵活:
- 以key-value的方式存储对象,可以是任何类型值或对象,包括二进制
- 异步api调用,避免造成等待而影响体验
- 功能强大,使用简单
- 应用场景:
- 存储复杂,数据量大的结构化数据
- 具体实现:
// 通过设置WebView的settings实现 WebSettings settings = getSettings(); settings.setJavaScriptEnabled(true); // 只需设置支持JS就自动打开IndexedDB存储机制 // Android 在4.4开始加入对 IndexedDB 的支持,只需打开允许 JS 执行的开关就好了。 - 原理:
- File System 缓存机制(H5页面新加入的缓存机制,虽然android webview暂时不支持,但会进行简单介绍)
- 原理:
- 为H5页面的数据,提供一个虚拟的文件系统
- 可以进行文件的创建,读写,删除,遍历等,就想native app访问本地文件系统一样
- 虚拟的文件系统是运行在沙盒中
- 不同web app的虚拟文件系统是相互隔离的,虚拟文件系统与本地文件系统也是互相隔离的
- 虚拟文件系统提供两种类型的存储空间:临时 & 持久
- 临时:由浏览器自动分配,但可能被浏览器回收
- 持久:需要显示申请;自己管理;
- 为H5页面的数据,提供一个虚拟的文件系统
- 特点:
- 可存储数据体积较大的二进制数据
- 可预加载资源文件
- 可直接编辑文件
- 应用场景
- 通过文件系统,管理数据
- 具体使用
- 由于File System是由H5新加入的缓存机制,所以android webview暂不支持
- 原理:
- 浏览器 缓存机制
-
缓存模式:
- 定义:加载网页时webview如何读取之前保存到本地的网页缓存(读取)。
- webview自带缓存模式四种
- LOAD_CACHE_ONLY : 不使用网络,只读取本地缓存数据
- LOAD_NO_CACHE: 不使用缓存,只从网络获取数据
- LOAD_DEFAULT:默认,根据Cache-Control决定是否从网络上获取数据
- LOAD_CACHE_ELSE_NETWORK:只要本地有,无论是否过期,或者no-cache,都使用缓存数据
-
- 资源预加载:
- 定义:提前加载将需要使用的H5页面,即 提前构建缓存;使用时直接取过来,而不是在需要的时候才去加载
- 具体实现:
- 预加载webview对象 & 预加载H5资源
- 预加载webview对象:
- 首次使用 WebView对象
- 原因:
- 首次初始化webview会比第2次初始化慢很多
- 初始化后,及时webview已释放,但是一些多个webview共用的全局服务/资源对象仍未释放
- 第二次初始化时,不需要再生成,从而变快
- 实现思路:
- 应用启动时就初始化1个全局的webview对象
- 当用户需加载H5页面时,则直接使用该WebView对象加载并显示
- 具体实现:
- 在android的BaseApplication中初始化一个webView对象,放入webview对象池
- 原因:
- 后续使用Webview对象
- 原因:
- 多次创建webview对象会消耗很多时间以及资源
- 实现思路:
- 自身构建webview复用池
- 当用户需使用webview加载H5页面时,直接从池中获取webview对象
- 具体实现:
- 采用2个/多个webview的复用,而不需要每次打开H5都需要重新构建
- 原因:
- 首次使用 WebView对象
- 预加载h5资源
- 原理:
- 在应用启动,初始化第一个webview对象的时候,直接开始网络请求加载h5页面
- 后续需要打开这些H5页面时就直接从该本地对象中获取
- 事前加载常用的H5页面资源(加载后就有缓存了)
- 此方法虽然不能减少webview初始化时间,但是数据请求和webview初始化可以并行进行,总体的页面加载时间就缩短了;缩短总体的页面加载时间
- 具体实现:
- 在android的BaseApplication里初始化一个webview对象,(用于加载常用的H5页面资源);当需要使用这些页面时再取出来直接使用
- 应用场景:
- 对于android webview的首页建议使用这种方案,能有效提高首页加载的效率。
- 原理:
- 自身构建缓存
- 除了使用android webview自身的缓存机制外,还可以自己针对某一需求场景构建缓存机制。
- 背景:H5页面有一些更新频率低,常用&固定的静态资源文件,如js,css文件,图片
- 冲突:每次重新加载会浪费很多资源(时间&流量)
- 解决方案:
- 通过拦截H5页面的资源网络请求,若资源相同,则直接从本地读取资源,而不需要发送网络请求到服务器获取
- 实现步骤:
- 提前将更新频率低,常用&固定的H5静态资源文件存放到本地
- 拦截H5页面的网络请求,过滤相关资源的网络请求
- 如果检测到本地有相同资源,则读取本地的
- h5自带的缓存方案(也是WebView自带缓存机制):
webview的那些漏洞
类型
- 任意代码执行漏洞
- addJavascriptInterface()
- webview内置到处的searchBoxJavaBridge 对象
- webview内置导出的accesssibility和accessibilityTraversalObject对象
- 密码明文存储漏洞
- 域控制不严格漏洞
具体分析
- 任意代码执行漏洞
- addJavascriptInterface()接口
- 漏洞产生原因:
webView.addJavascriptInterface(new JSObject(), "myObj"); // 参数1:Android的本地对象 // 参数2:JS的对象 // 通过对象映射将Android中的本地对象和JS中的对象进行关联,从而实现JS调用Android的对象和方法 所以,漏洞产生的原因是:当js拿到android这个对象后,从而实现js调用android对象中所有的方法,包括系统类,从而进行任意代码执行。如可以执行命令获取本地设备的sd卡中的文件等信息从而造成信息泄露。- 具体获取系统类的描述(结合java反射机制)
- android中的对象有一公共的方法:getClass();
- 该方法可以获取到当前类 类型Class
- 该类有一关键的方法:Class.forName()
- 该方法可以加载一个类(java.lang.Runtime)
- 而且该类是可以执行本地命令的
- js攻击核心代码
function execute(cmdArgs) { // 步骤1:遍历 window 对象 // 目的是为了找到包含 getClass ()的对象 // 因为Android映射的JS对象也在window中,所以肯定会遍历到 for (var obj in window) { if ("getClass" in window[obj]) { // 步骤2:利用反射调用forName()得到Runtime类对象 alert(obj); return window[obj].getClass().forName("java.lang.Runtime") // 步骤3:以后,就可以调用静态方法来执行一些命令,比如访问文件的命令 getMethod("getRuntime",null).invoke(null,null).exec(cmdArgs); // 从执行命令后返回的输入流中得到字符串,有很严重暴露隐私的危险。 // 如执行完访问文件的命令之后,就可以得到文件名的信息了。 } } } - 当一些 APP 通过扫描二维码打开一个外部网页时,攻击者就可以执行这段 js 代码进行漏洞攻击。在微信盛行、扫一扫行为普及的情况下,该漏洞的危险性非常大
- 具体解决方案:
- 方案一:在Android 4.2 版本中规定对被调用的函数以 @JavascriptInterface进行注解从而避免漏洞攻击
- 方案二:在Android 4.2版本之前采用拦截prompt()进行漏洞修复。
- 具体步骤:继承WebView,重写addJavascriptInterface()方法,然后在内部自己维护一个对象映射关系的Map;将需要添加的js接口放入该map中
- 每次当WebView加载页面前,加载一段本地代码的js代码。原理是:
- 让js调用一JavaScript方法:该方法是通过调用prompt()把js中的信息(含有特点标识,方法名称,参数等)传递到android端;
- 在android的onJsPrompt()中,解析传递过来的信息,再通过反射机制调用java对象的方法,这样实现安全的js调用android代码。
- 关于android返回给js的值:可以通过prompt()把java中方法的处理结果返回到js中
- webview内置到处的searchBoxJavaBridge_对象
- 原因:在Android 3.0以下,Android系统会默认通过searchBoxJavaBridge_的Js接口给 WebView 添加一个JS映射对象:searchBoxJavaBridge_对象。该接口可能被利用,实现远程任意代码。
- 解决方案:删除searchBoxJavaBridge_的js接口
// 通过调用该方法删除接口 removeJavascriptInterface(); - webview内置导出的accesssibility和accessibilityTraversalObject对象
- 同searchBoxJavaBridge_对象
- addJavascriptInterface()接口
- 密码明文存储漏洞
- WebView默认开启密码保存功能 :mWebView.setSavePassword(true)`
- 开启后,在用户输入密码时,会弹出提示框:询问用户是否保存密码;如果选择”是”,密码会被明文保到 /data/data/com.package.name/databases/webview.db 中,这样就有被盗取密码的危险
- 解决方案:关闭密码保存提醒,WebSettings.setSavePassword(false)
- 域控制不严格漏洞
- 即 A 应用可以通过 B 应用导出的 Activity 让 B 应用加载一个恶意的 file 协议的 url,从而可以获取 B 应用的内部私有文件,从而带来数据泄露威胁。。具体:当其他应用启动此 Activity 时, intent 中的 data 直接被当作 url 来加载(假定传进来的 url 为 file:///data/local/tmp/attack.html ),其他 APP 通过使用显式 ComponentName 或者其他类似方式就可以很轻松的启动该 WebViewActivity 并加载恶意url。
- 下面我们着重分析WebView中getSettings类的方法对 WebView 安全性的影响:
- setAllowFileAccess()
使用 file 域加载的 js代码能够使用进行同源策略跨域访问,从而导致隐私信息泄露// 设置是否允许 WebView 使用 File 协议 webView.getSettings().setAllowFileAccess(true); // 默认设置为true,即允许在 File 域下执行任意 JavaScript 代码; // 如果是false,则不会存在上述的威胁,但同时也限制了 WebView 的功能,使其不能加载本地的 html 文件- 同源策略跨域访问:对私有目录文件进行访问
- 针对IM类产品,泄漏的是聊天信息,以及联系人等
- 针对浏览器类软件,泄漏的是cookie信息
- 解决该问题方案:则需要对不使用file协议的业务启用,对使用file协议的禁用
// 禁止 file 协议加载 JavaScript if (url.startsWith("file://") { setJavaScriptEnabled(false); } else { setJavaScriptEnabled(true); }- setAllowFileAccessFromFileURLs():设置是否允许通过 file url 加载的 Js代码读取其他的本地文件
当AllowFileAccessFromFileURLs()设置为 true 时,攻击者的JS代码为: <script> function loadXMLDoc() { var arm = "file:///etc/hosts"; var xmlhttp; if (window.XMLHttpRequest) { xmlhttp=new XMLHttpRequest(); } xmlhttp.onreadystatechange=function() { //alert("status is"+xmlhttp.status); if (xmlhttp.readyState==4) { console.log(xmlhttp.responseText); } } xmlhttp.open("GET",arm); xmlhttp.send(null); } loadXMLDoc(); </script> // 通过该代码可成功读取 /etc/hosts 的内容数据- 解决方案:设置setAllowFileAccessFromFileURLs(false);当设置成为 false 时,上述JS的攻击代码执行会导致错误,表示浏览器禁止从 file url 中的 javascript 读取其它本地文件。
- setAllowUniversalAccessFromFileURLs(): 设置是否允许通过 file url 加载的 Javascript 可以访问其他的源(包括http、https等源)。// 在Android 4.1前默认允许(setAllowFileAccessFromFileURLs()不起作用)// 在Android 4.1后默认禁止
-
当AllowFileAccessFromFileURLs()被设置成true时,攻击者的JS代码是: // 通过该代码可成功读取 http://www.so.com 的内容 <script> function loadXMLDoc() { var arm = "http://www.so.com"; var xmlhttp; if (window.XMLHttpRequest) { xmlhttp=new XMLHttpRequest(); } xmlhttp.onreadystatechange=function() { //alert("status is"+xmlhttp.status); if (xmlhttp.readyState==4) { console.log(xmlhttp.responseText); } } xmlhttp.open("GET",arm); xmlhttp.send(null); } loadXMLDoc(); </script> 解决方案:设置setAllowUniversalAccessFromFileURLs(false);
-
- setJavaScriptEnabled():设置是否允许 WebView 使用 JavaScript(默认是不允许).但很多应用(包括移动浏览器)为了让 WebView 执行 http 协议中的 JavaScript,都会主动设置为true,不区别对待是非常危险的。
本文详细介绍了Android WebView组件的使用和优化技巧,涵盖WebView的基本概念、关键类与方法、与JavaScript的交互方式、内存泄漏预防、缓存机制及资源加载策略,以及安全性和漏洞防范措施。

1万+

被折叠的 条评论
为什么被折叠?



