Android WebView的性能问题及缓存机制、资源加载方案

WebView的重要类

类名作用常用方法
WebView创建对象、加载URL、生命周期管理、状态管理loadUrl()加载网页
WebSettings配置和管理WebViewsetCacheMode()设置缓存模式、setJavaScriptEnabled()与JS交互
WebViewClient处理各种通知和请求事件shouldOverrideUrlLoading()打开网页在WebView显示、onPageStarted()载入页面时调用如加载进度、onPageFinished()页面加载结束时调用
WebChromeClient辅助WebView处理Javascript对话框(如网站标题等)onProgressChanged()获得网页的加载进度并显示、onReceivedTitle()获取网页中的标题、onJsAlert()支持javascript警告框、onJsConfirm()支持javascript的确认框、onJsPrompt()支持javascript输入框

Android 4.4以后的 WebView 浏览器版本内核都是Chrome,这样就解决了内核不统一的问题。

WebView显示H5页面存在一个很明显的性能问题: WebView加载H5页面很慢

加载H5页面慢的原因有:

  1. 渲染速度慢:
    (1)首先是JS本身的解析过程复杂、解析速度慢;
    (2)前端页面又涉及较多的JS代码文件,叠加起来就造成了JS解析效率低;
    (3)其次是Android机型碎片化,导致手机硬件设备的性能不可控,有些表现良好,有些表现就较差。
  2. 页面资源加载慢,每加载一个H5页面都会产生较多网络请求:
    (1)HTML的主URL请求;
    (2)HTML引用外部的JS、CSS、字体文件、图片文件等都会构造一个独立的HTTP请求。
    注:每次加载都会产生这么多的网络请求,会相当耗费流量。

解决方案

可以通过以下三种方案来解决WebView的性能问题:

  1. WebView的缓存机制
  2. 资源预加载
  3. 资源拦截

WebView的缓存机制

缓存就是离线存储,即H5网页加载后会存储在缓存里。即使在无网络连接的情况下也可以访问网页。

当再次访问H5网页时,可以直接使用已缓存的资源,不需要访问服务器。缓存可以有效提高页面的加载速度。
在WebView中缓存机制和缓存模式是两个不同的概念。缓存机制是告诉浏览器如何将加载过的网页数据保存到本地;缓存模式则是告诉WebView如何读取之前保存到本地缓存里的网页数据。

Android WebView 的缓存模式有以下4种:

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

在代码中的具体使用:

// 如果导航类型没有强制指定行为,那么当缓存中的资源是可用且还没有过期的就使用缓存数据,否则就向原始服务器发送请求。(就是根据cache-control决定是否从网络上取数据)
webView.getSettings().setCacheMode(WebSettings.LOAD_DEFAULT);
// 只要缓存中有的,就使用缓存中的数据,否则就向网络原始服务器发请求
webView.getSettings().setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK);
// 无论如何都只使用缓存中的数据,没有也不会向原始服务器发送请求
webView.getSettings().setCacheMode(WebSettings.LOAD_CACHE_ONLY);
// 这种缓存模式就是告诉WebView不要使用缓存数据,直接向原始服务器请求数据
webView.getSettings().setCacheMode(WebSettings.LOAD_NO_CACHE);

Android WebView常用的缓存机制有:

  • 浏览器缓存机制
  • Application Cache 缓存机制
  • Dom Storage 缓存机制
  • Indexed Database 缓存机制

1.浏览器缓存机制
浏览器缓存机制是浏览器内核的机制,Android WebView内置实现了,不需要我们额外设置或实现。
Android WebView会将静态资源文件如JS、CSS、字体、图片等文件缓存在以下路径:

/data/data/包名/cache/org.chromium.android_webview

浏览器缓存机制是由服务器响应回来的HTTP协议头信息来控制的。《HTTP协议头》
浏览器根据 HTTP 协议头里的 Cache-Control或Expires等字段来控制文件缓存的机制。
Cache-Control的可能取值及意义:

# 当资源一旦过期,如max-age已过期,缓存就不能用这些没有与原始服务器成功验证过的资源去响应后面的请求
Cache-Control: must-revalidate
# 同must-revalidate一样,只是它用于共享缓存如代理,同样会被私有缓存忽略
Cache-Control: proxy-revalidate
# 在使用缓存副本的内容前,强制提交请求到原始服务做验证,检查是否是最新的。
Cache-Control: no-cache
# 无论服务端还是浏览器(客户端)都不缓存客户的请求或服务端的响应
Cache-Control: no-store
# Content-Encoding, Content-Range, Content-Type 等头信息,禁止代理修改
Cache-Control: no-transform
# 表示响应的内容可以被缓存到任何地方,即使此响应是不能缓存的(如响应中没有max-age或Expires等头信息)也可以被缓存。
Cache-Control: public
# 表示响应的内容只能为单个用户(此处的用户应该理解成浏览器的一个会话)使用,禁止将响应缓存到共享缓存区。一个私有的缓存可以缓存响应。换言之,当你返回这个页面时,它会用私有缓存的内容来为你服务,除非私有缓存中没有缓存任何内容,否则它不会向原始服务器发送请求。但当你关了它再开,它就要重新向服务器请求了。因为之前的私有缓存并不是当前再打开这个用户实例的。
Cache-Control: private
# 指定一个最大的时间,在此时间内缓存的资源都将被认为是最新的与Expires刚好相反,Expires是相对于发出请求的时间的。
Cache-Control: max-age=<seconds>
# 这个时间与max-age或Expires相比,会被优先考虑,但它只能用于共享缓存如代理缓存,而将会被私有缓存忽略
Cache-Control: s-maxage=<seconds>

Expires

Expires: <http-date>,如:
Expires: Thu, 25 Jul 2019 12:29:10 GMT

它表示资源的过期时间,相对请求而言的。但是请注意:如果HTTP响应头信息里有Cache-Control指定了 max-age 或 s-maxage 命令,那么Expires头将会被忽略。

上面解决了网页数据缓存到本地的问题,使用本地缓存数据可以通过设置WebView的缓存模式来决定,如:

// 只要缓存中有的,就使用缓存中的数据,否则就向网络原始服务器发请求
webView.getSettings().setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK);

2.Dom Storage 缓存机制
Dom Storage缓存机制通过在本地存储Key - Value 对来提供缓存信息功能。存储空间大( 5MB),在不同浏览器大小会有所不同,相比之下,Cookies 大小只有4KB。Dom Storage适用于存储临时的、简单的数据的场景。
DOM Storage 分为 sessionStorage 和 localStorage; 二者使用方法基本相同,区别在于作用范围不同:
(1)sessionStorage:是临时性的,即存储与页面相关的数据,它在页面关闭后无法使用
(2)localStorage:是持久性的,即保存的数据在页面关闭后也可以使用。

启用方式:

// 开启DOM storage
webView.getSettings().setDomStorageEnabled(true);

3. IndexedDB缓存机制
IndexedDB属于NoSQL数据库,通过存储Key - Value对来提供缓存信息功能。类似于Dom Storage的存储方式。适用于存储复杂、数据量大的结构化数据的场景。
启用方式:

// 只需设置支持JS就自动打开IndexedDB存储机制
// Android 在4.4开始加入对 IndexedDB 的支持,只需打开允许 JS 执行的开关就好了。
webView.getSettings().setJavaScriptEnabled(true);

4.Application Cache 缓存机制
以文件为单位进行缓存。要使用AppCache,就必须在网页的html标签通过 manifest 属性引用 manifest 文件,manifest 文件是一个普通文件,列出了需要缓存的文件。浏览器在首次加载 HTML 文件时,会解析 manifest 属性,并读取 manifest 文件,获取 Section:CACHE MANIFEST 下要缓存的文件列表,再对文件进行缓存,网页要按如下来写


// manifest 文件:就是下面以 appcache 结尾的文件,
<!DOCTYPE html>
<html manifest="http://www.hello.com/demo_html.appcache">
<body>
...
</body>
</html>

AppCache 在首次加载生成后,如果要更新被缓存的文件,需要更新 manifest 文件。
因为浏览器在下次加载时,除了会默认使用缓存外,还会在后台检查 manifest 文件有没有修改。
发现有修改,就会重新获取 manifest 文件,对 Section:CACHE MANIFEST 下文件列表检查更新。
manifest 文件与缓存文件的检查更新也遵守浏览器缓存机制。如果用户手动清了 AppCache 缓存,下次加载时,浏览器会重新生成缓存,这也算是一种缓存的更新。

AppCache 的缓存文件,与浏览器的缓存文件分开存储的,因为 AppCache 在本地有 5MB的空间限制。适用于存储静态文件如JS、CSS、字体文件等。AppCache 的缓存是作为对浏览器缓存机制的一种补充。

WebView配置如下:

webView.getSettings().setAppCachePath(appCachePath);
webView.getSettings().setAllowFileAccess(true);
webView.getSettings().setAppCacheEnabled(true);
// 通过设置WebView的settings来实现
WebSettings settings = webView.getSettings();
// 1. 设置缓存路径
String appCachePath = getApplicationContext().getCacheDir().getAbsolutePath()+"appcache/";
settings.setAppCachePath(appCachePath);
// 2. 设置缓存大小
settings.setAppCacheMaxSize(Long.MAX_VALUE);
// 3. 开启Application Cache存储机制
settings.setAppCacheEnabled(true);  
// 特别注意
// 每个 Application 只调用一次 WebSettings.setAppCachePath()和WebSettings.setAppCacheMaxSize()

资源预加载

提早加载将需使用的H5页面,即提前构建缓存,使用时直接取过来用即可。可以预加载WebView对象,减少使用时再初始化时的耗时,同时预加载H5资源,提前构建缓存。

预加载WebView对象

类型理由实现思路具体实现
首次使用的WebView对象初始化WebView对象比较慢在程序开始运行时,就创建一个WebView对象。当要用时就可以直接使用,免去了初始化的时间在程序的Application子类里初始化一个全局WebView对象
后续使用的WebView对象多次创建WebView对象会相当慢且消耗资源初始化一批WebView对象,建立起WebView对象池,要用WebView的就来这里取在程序启动后,初始化一批WebView对象,通过一个单例类来管理这个池子。

预加载H5资源
在应用启动后,在Application子类BaseApplication里初始化一个WebView对象时,直接开始网络请求加载H5页面,构建本地缓存,因为加载后就有缓存了。后续需打开H5页面时就可以直接从本地缓存加载数据。

对于用Android WebView作为首页的App,建议使用这种方案,它能有效提高首页加载的效率。

我在GitHub上分享了一个Demo,感觉十分有趣

我在BaseApplication里预加载“百度知道”。当app进入百度的首页,尔后,关闭网络,点击“知道”,你会发现它居然成功打开了,因为它在本地缓存好了,这归功于前面做H5页面资源在BaseApplication中提供构建了。

构建自己的资源缓存

  1. 事先将更新频率较低又常用又固定的H5静态资源如Js文件、CSS文件、图片等放到本地
  2. 拦截H5页面的资源网络请求,并对其进行检测:如果检测到本地具有相同的静态资源,则直接从本地读取,否则发送该资源的网络请求到原始服务器获取

具体实现
重写WebViewClient 的 shouldInterceptRequest 方法,拦截H5页面的资源网络请求,并检测所请求的资源在本地是否存在,存在就用本地资源代替,否则就让其向服务器请求资源。

webView.setWebViewClient(new WebViewClient(){
       @Override
       public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {

       // 步骤1:拦截资源请求URL,并判断URL里的资源的文件名是否包含事先缓存在本地的文件名
       // 假设拦截到的资源请求URL为:https://mms-secr.cdn.bcebos.com/xuanfa1/wenzi1.png
       // 资源文件名为:wenzi1.png
       if (request.getUrl().toString().contains("wenzi1.png")) {
              // 步骤2:创建一个输入流
              InputStream ins = null;
              try {
              // 步骤3:获得需要替换的资源(存放在assets文件夹里)
              // a. 先在app/src/main下创建一个assets文件夹
              // b. 在assets文件夹里再创建一个images文件夹
              // c. 在images文件夹放上我们要缓存的资源wenzi1.png
              ins = getApplicationContext().getAssets().open("images/wenzi1.png");
              } catch (IOException e) {
                    e.printStackTrace();
              }
              // 步骤4:替换资源
              // 参数1:http请求里该图片的Content-Type,此处图片为image/png
              // 参数2:编码类型
              // 参数3:存放着替换资源的输入流
              return new WebResourceResponse("image/png","utf-8", ins);
              }
              return super.shouldInterceptRequest(view, request);
            }
        });

Demo已在GitHub上了
这种缓存机制有效解决 H5页面静态资源加载速度慢和流量消耗多的问题。该方法只是更好地加快H5加载速度,即使失效,也不会对H5页面产生任何负面影响。

上述放到本地的静态资源的更新方式:

  • 发布新版本安装包apk时一并更新
  • 增量更新:在用户处于WIFI环境时让服务器推送到本地,著名的微信应用就是采用小范围更新本地资源的。

谢谢阅读!

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值