一、混合开发的优势与缺陷

在混合开发大行其道的今天,很多页面和功能都转由前端实现,客户端只要在APP中嵌入一个WebView即可,同时前端开发的页面对于Android和iOS端的效果是统一的,省去了适配的困扰。

适合前端开发的界面主要有以下两种:
1、新闻咨询类页面,这类页面布局比较复杂,通过前端实现相对原生更为简单。
2、运营活动类界面,这类页面更新较为频繁,前端迭代后可以直接上线,跳过了客户端的发版流程。

前端开发的优势显而易见:开发敏捷、发版灵活。但是它的缺点同样明显,我认为主要有以下3点。
第一个问题就是性能问题,冷启APP后第一次新建WebView的耗时会让用户感受到明显的卡顿,而WebView.loadUrl(String url)的耗时更为严重,很容易出现白屏的现象。
第二个问题是浪费流量,虽然WebView内核有缓存机制(例如两次打开相同的页面,第二次会快很多),但是对于新闻咨询页面来说,很多css、js甚至图片资源都是重复的,但是内核的缓存机制不一定能准确地复用这些资源。
第三个问题就是安全问题,部分前端页面需要通过JSBridge调用原生API,如果JavaScriptInterface不对调用方的域名进行限制,某些恶意网址就可能调用原生的方法进行入侵。

安全问题是依赖业务去解决的,如果没有暴露相应的方法,恶意网址也就无从下手。而性能问题与流量问题则会影响WebView的加载速度,比较影响用户体验,下面从WebView的加载流程来看我们应该如何解决这两个问题来提高加载速度。

二、WebView加载页面的流程

假设WebView嵌入在Activity中,从Activity启动到显示出前端页面的第一帧,大致要经历以下阶段。
① Activity启动
② WebView新建与初始化
③ WebView.loadUrl(String url)
而当调用WebView.loadUrl(String url)之后,所有的资源请求都会经过WebViewClient的shouldInterceptRequest(...)方法,这些资源请求包括但不局限于主html内容、css、js和图片资源等,应用可以在该方法拦截资源请求并加载别的内容。

可以看到加载页面的流程是串行的,主要分为两个部分,一是WebView的新建与初始化,二是WebView.loadUrl(String url),那么只要我们缩短其中任一阶段的耗时,就可以达到缩短耗时的目的。

三、WebView加载速度优化方案

这里以新闻资讯类页面为例,目前这类页面加载最快的就是头条系APP,例如“懂车帝”,在信息流内点击咨询跳转后直接展示咨询详情,不会像其他APP那样出现过渡界面或白屏。

那么懂车帝究竟做了什么才能让WebView加载地如此之快?
首先对于新闻咨询页面来说,不同的页面之间有很多重复的资源,这些资源可以直接保存在本地复用。反编译懂车帝APK之后会发现Assets中有很多的css、js和图片文件,应用可以拦截这部分的请求转而加载这些文件。
其次,WebView的新建与初始化也是比较耗时的,因此可以使用WebView缓存池进行复用。
当然还有最重要的一点就是预加载,在懂车帝信息流内,即使你断网后点开一条咨询,你会发现文字内容还是正常加载。也就是说,在信息流内时,url的主html内容已经被下载到了内存中,可以直接通过WebView.loadDataWithBaseUrl(...)加载。

接下来介绍这3种方案的具体实现以及对加载速度的影响,我在懂车帝中挑选了7篇咨询,编写Demo并统计其加载时间(这里统计的是点击item到WebView加载进度为100所需的时间),在没有使用任何优化的情况下,平均加载时间为1203ms。

3.1 资源缓存

上面提到,WebView 请求任何的资源时都会回调shouldInterceptRequest()方法,此时可以将 WebView 需要请求的资源替换为本地资源以提升加载速度。
本地资源可以保存在Assets或文件系统中,如果保存在Assets中,文件的安全性会得到保证,没有出错或者被修改的风险,但是无法实时更新,只能依赖客户端发版更新文件;如果保存在文件系统中,可以做到实时更新,但是下载文件时可能出错,使用时需要对文件进行校验。

这里以保存在Assets中为例,当WebView回调shouldInterceptRequest(…)时,如果发现当前的文件可以使用Assets中的缓存,即可将其包装成WebResourceResponse,如下所示。

mWebView.setWebViewClient(new WebViewClient() {@Overridepublic WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {// 根据url得到文件名String fileName = getFileNameByUrl(request.getUrl().toString());if (!fileInCache(fileName)) {// 如果当前文件不在缓存列表中, 不使用缓存return super.shouldInterceptRequest(view, request);} else {// 当前文件可以使用缓存, 根据后缀判断 mimetypeInputStream inputStream = null;String mimeType = null;if (fileName.endsWith("css")) {mimeType = "text/css";} else if (fileName.endsWith("js")) {mimeType = "text/javascript";} else if (fileName.endsWith("png")) {mimeType = "image/png";}if (mimeType == null) {return null;}try {inputStream = App.getContext().getAssets().open(fileName);} catch (IOException e) {Log.e(LOG_TAG, "read file IOException: " + e.getMessage());}if (inputStream != null) {WebResourceResponse response = new WebResourceResponse(mimeType, "utf-8", inputStream);// 解决css、js的跨域问题Map<String, String> headers = new HashMap<>();headers.put("Access-Control-Allow-Origin", "*");headers.put("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE, PUT");headers.put("Access-Control-Max-Age", "3600");headers.put("Access-Control-Allow-Headers", "Content-Type,Access-Token,Authorization");response.setResponseHeaders(headers);return response;}return null;}}
});

使用资源缓存后,平均的加载时间为1001ms,相较于初始时提升了200ms左右。

3.2 WebView缓存池

平时使用WebView的时候我们都是动态新建并添加到ViewGroup中,新建时就传入了Context,那如果使用缓存池,Context该怎么传呢?

一种方案是直接用ApplicationContext新建WebView;另一种方案是使用MutableContextWrapper,如果在某个Activity中被使用了就改为该Activity的Context,回收时改为ApplicationContext。一个简易版的WebView缓存池实现如下。

注意在APP启动时应该就要调用WebViewPool的初始化方法新建一个WebView,不然启动第一个包含WebView的页面时耗时也会很久。

public class WebViewPool {private List<DetailWebView> mIdleWebViewList;private List<DetailWebView> mUsingWebViewList;private static class Holder {private static WebViewPool sInstance = new WebViewPool();}public static WebViewPool getInstance() {return Holder.sInstance;}private WebViewPool() {}/*** 在 APP 启动时调用, 直接新建一个备用的WebView*/public void init() {mIdleWebViewList = new CopyOnWriteArrayList<>();mUsingWebViewList = new ArrayList<>();MutableContextWrapper contextWrapper = new MutableContextWrapper(App.getContext());DetailWebView detailWebView = new DetailWebView(contextWrapper);mIdleWebViewList.add(detailWebView);}public DetailWebView acquireWebView(Context context) {if (mIdleWebViewList != null && mIdleWebViewList.size() > 0) {DetailWebView webView = mIdleWebViewList.remove(0);MutableContextWrapper contextWrapper = (MutableContextWrapper) webView.getContext();contextWrapper.setBaseContext(context);mUsingWebViewList.add(webView);return webView;} else {MutableContextWrapper contextWrapper = new MutableContextWrapper(context);DetailWebView webView = new DetailWebView(contextWrapper);mUsingWebViewList.add(webView);return webView;}}public void recycleWebView(DetailWebView webView) {if (webView == null) {return;}ViewGroup viewParent = (ViewGroup) webView.getParent();if (viewParent != null) {viewParent.removeView(webView);}webView.loadUrl("about:blank");if (mUsingWebViewList != null && mUsingWebViewList.contains(webView)) {mUsingWebViewList.remove(webView);MutableContextWrapper contextWrapper = (MutableContextWrapper) webView.getContext();contextWrapper.setBaseContext(App.getContext());webView.setWebViewClient(null);webView.setWebChromeClient(null);mIdleWebViewList.add(webView);} else {webView.clearHistory();webView.destroy();}}
}

在使用资源缓存的基础上再使用WebView缓存池,平均的加载时间为918ms。

3.3 预加载

预加载主体的实现比较简单,在信息流的RecyclerView进入IDLE状态时,判断当前有多少个条目曝光,然后启动对应数量的子线程开始下载数据为String,用户进入某个条目时,如果数据下载完毕就可以直接通过WebView.loadDataWithBaseURL(mUrl, data, "text/html", "utf-8", null)展示数据。
应用对预加载下载的数据是需要统一管理的,可以考虑使用LRU缓存。不过由于是多线程下载数据,需要对在对LRU缓存读取时加锁保证线程安全,这里的锁可以选择读写锁。

通过实验,使用预加载之后的平均加载时间为698ms。

3.4 总结

这里的Demo对于加载时间的统计其实是存在误差的,首先选取的咨询较少;其次前端页面的加载时间受网速影响是最大的。所以这个统计仅供参考,但是也能看出来每个优化方法都是有作用的,在实际项目中可以根据项目实际情况使用。

四、其余优化方案

腾讯有一个WebView加载优化的方案VasSonic,将WebView初始化和WebView加载数据这两个操作由原本的串行改为并行,缩短整体的加载时间。

VasSonic大概的流程为:WebView开始初始化时启动一个子线程去下载html数据,当WebView初始化完的时候通知子线程已经初始化完毕,此时数据下载有3种情况:1、子线程还没开始下载数据;2、子线程下载了一部分数据;3、子线程已经下载完了数据。收到WebView初始化完的消息后,子线程将已经下载的数据和没有下载的数据拼接为桥接流返回给内核渲染。

当然这个框架还有很多功能,例如将html内容分为模板和数据,并且为模板和数据分别提供更新的功能,适合用于更新频繁的运营类界面,具体可见参考3。

参考

  1. 常用文件的mime和mimetype的对应关系
  2. 跨域详解
  3. 腾讯祭出大招VasSonic,让你的H5页面首屏秒开

Android—WebView加载速度优化工程实践相关推荐

  1. Android开发——H5容器加载速度优化方案

    1. 背景介绍 在偏重活动运营的电商App中,受制于App版本审核,具备开发周期短.可灵活发布等特点的H5页面受到青睐,承载了很多重要业务.但App Webview存在令人烦恼的性能问题,特别突出的是 ...

  2. android WebView加载网页白屏问题优化处理

    android WebView加载网页白屏问题优化处理 问题描述: 使用webview加载Web界面时,会出现界面白屏的情况.http下通常问题不大,通常https白屏的可能性很大,而且通常没有任何报 ...

  3. 初级前端小程序项目加载速度优化

    这份文字是根据近期团队做来问丁香医生 SPA 和 丁香医生小程序 加载速度优化的经历整理而成. 效果 古人有一句话叫做:治感冒看疗效.既然是关于速度优化的,我们就先来看一下优化的效果. 来问丁香医生 ...

  4. 页面加载速度优化的12个建议

    1.合并Js文件和CSS 将JS代码和CSS样式分别合并到一个共享的文件,这样不仅能简化代码,而且在执行JS文件的时候,如果JS文件比较多,就需要进行多次"Get"请求,延长加载速 ...

  5. android 双 webview,Android webview加载页面

    释放双眼,带上耳机,听听看~! Android webview加载页面 private WebView webView; public void init() { webView = (WebView ...

  6. android webView加载网络视频

    之前,我写过webView加载本地网页的博客,今天,就写写webView加载网络视频的内容. 一.加载网页 1.WebView用来显示网页,使用必须时刻注意我们需要添加网络权限 <uses-pe ...

  7. 分享网页加载速度优化的一些技巧?

    日期:2013-2-17  来源:GBin1.com 不管你是不是相信,在最近的几年里,互联网网页的大小已经显著增大了.由HTTP Archive研究得出的结果表明,目前平均一个页面的大小是1.25M ...

  8. 网站加载速度 优化_您肯定要优化网站的加载速度。 这是如何做。

    网站加载速度 优化 Do you remember this iconic scene from a very famous Star Wars Parody? 您还记得著名的<星球大战> ...

  9. android webview 太大,Android应用开发之Android WebView加载图片显示过大的处理教程(代码教程)...

    本文将带你了解Android应用开发Android  WebView加载图片显示过大的处理教程(代码教程),希望本文对大家学Android有所帮助. Webview加载图片时,经常会遇到图片显示不符合 ...

最新文章

  1. 你哪来这么多事(一):学生信息插入
  2. 使用 PHP 在站点上构建类似 Twitter 的系统
  3. 关于从基于Mult-Org的视图中查询数据的问题(转)
  4. [Recompose] Pass a React Prop to a Stream in RxJS
  5. 全球权威声纹识别竞赛斩获双料冠军 网易AI Lab智能技术领先国际
  6. 电商的「穿衣AI」用得好,剁手根本停不了 | CVPR 2020
  7. PHP 开发环境和组织管理
  8. 人脸识别成创业热门,统计企业超1万家,刷脸支付项目将迎来热潮
  9. 五个人+三个月=美摄云非编1.0 | 我们采访到了“工期很紧“的美摄研发总监
  10. nagios监控三部曲之——nagios实现飞信报警(3)
  11. SpringBoot项目中,获取配置文件信息
  12. Android view.settran,Android RecyclerView从入门到玩坏
  13. PHP 数字转化为自定义长度的字符串[前插后入]
  14. 机器人对话系统的单轮对话和多轮对话
  15. 用户体验与可用性测试
  16. 浩海技术GHOSTXPSP3_2010浪漫圣诞纯净版
  17. php元换成万元,人民币单位换算器(元换算成万元换算器)
  18. 计算机ip地址会变吗,电脑IP地址会变吗?
  19. matlab 自制闹钟程序,想编一个每半小时提示的闹钟程序
  20. 三门问题与神奇的贝叶斯大脑

热门文章

  1. 研发组织中的“长尾类”问题如何看待和消除?
  2. Canvas判断线段是否重叠
  3. 140斤减脂到90斤需要多久?
  4. 联想R520 安装 AS4.4 +apache+mysql+php
  5. 如何在给排水CAD图纸中绘制任意喷头?
  6. LogStash~LogStash的output(输出)
  7. 微型四轴飞行器(4)通讯设计
  8. JavaScript的条件判断语句以及三元运算符
  9. 写一个打开cmd窗口并执行cmd命令的Windows脚本(.bat文件)
  10. CentOS服务器高负载状态重启