一个加载网页的过程中,native、网络、后端处理、CPU都会参与,各自都有必要的工作和依赖关系;让他们相互并行处理而不是相互阻塞才可以让网页加载更快:
· WebView初始化慢,可以在初始化同时先请求数据,让后端和网络不要闲着。
· 后端处理慢,可以让服务器分trunk输出,在后端计算的同时前端也加载网络静态资源。
· 脚本执行慢,就让脚本在最后运行,不阻塞页面解析。
· 同时,合理的预加载、预缓存可以让加载速度的瓶颈更小。
· WebView初始化慢,就随时初始化好一个WebView待用。
· DNS和链接慢,想办法复用客户端使用的域名和链接。
· 脚本执行慢,可以把框架代码拆分出来,在请求页面之前就执行好。
WebView内存消耗
为了测试WebView会消耗多少内存,我们设计了如下的测试方案:
1. 客户端启动后,记录消耗的内存。
2. 打开空页面,记录内存的上涨。
3. 退出。
4. 打开空页面,记录内存上涨。
5. 退出。
6. 打开加载了代码的页面,记录内存的额外增加。
得到如下测试结果:
测试系统: iOS模拟器,Titans 10.0.7
测试系统: OPPO R829T Android 4.2.2
测试方式:测试10次取平均值
|
首次打开增加内存 |
二次打开增加内存 |
加载KNB+VUE+灵犀 |
iOS UIWebView |
31.1M |
5.52M |
2M |
iOS WKWebView |
1.95M |
1.6M |
2M |
Android |
32.2M |
6.62M |
1.7M |
WKWebView的内存消耗相比其他低了一个数量级,在此方面相当占优。
UIWebView和Android的WebView在首次初始化时都要消耗大量内存,之后每次新建WebView会额外增加一些。
UIWebView的内存占用不会在关闭WebView时主动回收,每次新开WebView都会消耗额外内存。
相比于性能,对于内存的优化可以做的还是比较有限的。
· WKWebView的内存占用优势比较大(代价是初始化比较慢)。
· 页面内代码消耗的内存相比与WebView系统的内存消耗相比可以说是很低。
WebView体验
除了打开的速度,WebView通常体验也没有native的实现更好,我们可以找到以下几个例子:
在WebView中,长按文字会使得WebView默认开始选择文字;长按链接会弹出提示是否在新页面打开。
解决方法:可以通过给body增加CSS来禁止这些默认规则。
在WebView中,click通常会有大约300ms的延迟(同时包括链接的点击,表单的提交,控件的交互等任何用户点击行为)。
唯一的例外是设置的meta:viewpoint为禁止缩放的Chrome(然而并不是Android默认的浏览器)。
解决方法:使用fastclick一般可以解决这个问题。
在很多需求中会有一些吸顶的元素,例如导航条,购买按钮等;当页面滚动超出元素高度后,元素吸附在屏幕顶部。
这个功能在PC和native中都能够实现,然而在WebView中却成了难题:
在页面滚动期间,Scroll Event不触发
不仅如此,WebView在滚动期间还有各种限定:
· setTimeout和setInterval不触发。
· GIF动画不播放。
· 很多回调会延迟到页面停止滚动之后。
· background-position:fixed不支持。
· 这些限制让WebView在滚动期间很难有较好的体验。
这些限制大部分是不可突破的,但至少对于吸顶功能还是可以做一些支持:
解决方法:
· 在iOS上,使用position:sticky可以做到元素吸顶。
· 在Android上,监听touchmove事件可以在滑动期间做元素的position切换(惯性运动期间就无效了)。
WebView对键盘的控制能力很弱,无法直接调起或者隐藏键盘,而且键盘的确认文案是无法自定义的。
我们以百度为例:
当你打开百度搜索时,点击【换行】就完成了输入并开始了搜索。
为什么是【换行】而不是【搜索】呢?
当然不是bug……而是……臣妾做不到啊!
解决方法:
目前只能通过由与App通过桥协议的方式,由App代为唤起键盘(但是实际操作过于复杂)。
通常WebView并不能直接接触到底层的API,因此比较稳定;但仍然有使用不当造成整个App崩溃的情况。
目前发现的案例包括:
· 使用过大的图片(2M)
· 不正常使用WebGL
WebView安全
由于WebView加载的页面代码是从服务器动态获取的,这些代码将会很容易被中间环节所窃取或者修改,其中最主要的问题出自地方运营商(浙江尤其明显)和一些WiFi。
我们监测到的问题包括:
· 无视通信规则强制缓存页面。
· header被篡改。
· 页面被注入广告。
· 页面被重定向。
· 页面被重定向并重新iframe到新页面,框架嵌入广告。
· HTTPS请求被拦截。
· DNS劫持。
这些问题轻则影响用户体验,重则泄露数据,或影响公司信誉。
针对页面注入的行为,有一些解决方案:
CSP可以有效的拦截页面中的非白名单资源,而且兼容性较好。在美团移动版的使用中,能够阻止大部分的页面内容注入。
但在使用中还是存在以下问题:
· 由于业务的需要,通常inline脚本还是在白名单中,会导致完全依赖内联的页面代码注入可以通过检测。
· 如果注入的内容是纯HTML+CSS的内容,则CSP无能为力。
· 无法解决页面被劫持的问题。
· 会带来额外的一些维护成本。
总体来说CSP是一个行之有效的防注入方案,但是如果对于安全要求更高的网站,这些还不够。
HTTPS可以防止页面被劫持或者注入,然而其副作用也是明显的,网络传输的性能和成功率都会下降,而且HTTPS的页面会要求页面内所有引用的资源也是HTTPS的,对于大型网站其迁移成本并不算低。
HTTPS的一个问题在于:一旦底层想要篡改或者劫持,会导致整个链接失效,页面无法展示。这会带来一个问题:本来页面只是会被注入广告,而且广告会被CSP拦截,而采用了HTTPS后,整个网页由于受到劫持完全无法展示。
对于安全要求不高的静态页面,就需要权衡HTTPS带来的利与弊了。
如果HTTP请求容易被拦截,那么让App将其转换为一个Socket请求,并代理WebView的访问也是一个办法。
通常不法运营商或者WiFi都只能拦截HTTP(S)请求,对于自定义的包内容则无法拦截,因此可以基本解决注入和劫持的问题。
Socket代理请求也存在问题。
· 首先,使用客户端代理的页面HTML请求将丧失边下载边解析的能力;根据前面所述,浏览器在HTML收到部分内容后就立刻开始解析,并加载解析出来的外链、图片等,执行内联的脚本……而目前WebView对外并没有暴露这种流式的HTML接口,只能由客户端完全下载好HTML后,注入到WebView中。因此其性能将会受到影响。
· 其次,其技术问题也是较多的,例如对跳转的处理,对缓存的处理,对CDN的处理等等……稍不留神就会埋下若干大坑。
此外还有一些其他的办法,例如页面的MD5检测,页面静态页打包下载等等方式,具体如何选择还要根据具体的场景抉择。
一般来说,客户端内的WebView都是可以通过客户端的某个schema打开的,而要打开页面的URL很多都并不写在客户端内,而是可以由URL中的参数传递过去的。
那么,一旦此URL可以通过外界输入自定义,那么就有可能在客户端内部打开一个外部的网页。
例:作案过程
· 某个App有个WebView,打开的schema为appxx://web?url={weburl}。
· App中有个扫码的功能,可以扫描某个二维码并打开对应的schema链接。
· 某个坏人制作了一个二维码并张贴到街上,内容符合:appxx://web?url={some_hack_weburl}。
· 用户扫码打开了some_hack_weburl。
· 如果some_hack_weburl是一个高仿的登录页面,那么用户将会很可能将用户名密码提交到其他网站。
解决方法:在内嵌的WebView中应该限制允许打开的WebView的域名,并设置运行访问的白名单。或者当用户打开外部链接前给用户强烈而明显的提示。
发展
在一个客户端内,native目前主要功能是提供高效而基础的功能;内部的WebView则添加一些性能体验要求不高但动态化要求高的能力。
提高客户端的动态能力,或者提高WebView的性能,都是提升App功能覆盖的方式。
而目前的各种框架,ReactNative、Week包括微信小程序,都是这个趋势的尝试。
随着技术的发展,WebView的性能、体验和安全问题也将会逐渐的改善,在App中占有越来越多比重的同时,也将会为App开拓新的能力,为用户带来更优质的体验。