58商家通Android端WebView加载优化方案
导语
本文从实际需求出发,通过分析Android端的Webview加载流程以及加载过程中可以优化的耗时点,分阶段优化加载速度,最终实现在一秒内加载H5页面,希望对有此需求的开发者有所启发和帮助。
背景
目前58商家通对58来说是一个连接B端平台,对于商家来说是一个运营管理工具,商家可以在58商家通上进行商业服务(精准、置顶)、信息沟通、帖子管理等基本运营操作而获取服务保障、访客足迹、会员权益等服务。由于58商家通是一个平台软件,随着规模的扩大,接入兄弟部门服务也越来越多,如推荐有奖,服务保障,放心服务,到家精选,福利商城等,而接入兄弟部门的服务都是通过H5的形式接入,因此,58商家通上的H5页面比例已经超过了Native 页面的比例,由于H5页面的加载效率远远低于Native的加载效率,所以对于58商家通的H5加载效率成为了重中之重的问题,优化这个问题,首先可提高用户体验和APP的活跃度、流量,其次接入各个服务之后能够提高用户的体验意愿,对于新服务在58商家通的推广也有很大的意义,故此将优化过程中遇到的问题及解决方案跟大家分享一下,希望能给大家一些帮助。
webview默认加载流程分析
1、webview默认加载流程
在优化webview加载H5页面之前我们需要了解默认webview加载H5页面的流程,并针对特定的耗时流程做出符合我们开发技术的方案。首先,webview首次加载流程大概分为以下几个阶段:
A、webview的初始化
B、浏览器内核初始化(全部webview共享,第一次初始化)
D、下载解析过程中需要下载的js,css,图片等资源文件
E、生成h5的domTree
F、根据上文的domTree渲染页面
String resultUrl = jsurl.replace(suffix, "_v" + version + suffix);//替换前端文件后缀,拼成html
//举例用法:getVersion("https://test.58.com/test.js", ".js");
//https://test.58.com/test_v2019555555555.js
然后我们看下之前说的流程中,其中A-D步骤都是页面白屏,本文中的测试页面是我们58商家通中相对资源比较多的页面,所以首次加载耗时相当严重,白屏超过3秒,二次加载也需要大概2秒多,所以这对于使用者是难以忍受的,优化过程大致分为以下几个阶段:
A、Webview缓存的优化,使用自定义缓存替代webview自带缓存
webview默认加载过程中不可避免的需要使用到缓存,由于我们h5页面的开发行为以及使用到的一些技术,如果使用webview自带的缓存api去实现缓存逻辑,将会有以下一些问题:
API固定,依赖系统自带的API,如果需要扩展,如果系统不支持,很难二次开发。
由于我们的h5页面图片都已经使用了cdn服务,而我们默认配置了8台服务器,所以相同的资源文件在客户端可能重复下载多次,造成流量和存储的浪费。
现在大多数公司都会对资源文件做版本控制(为了解决资源文件内容修改之后,前端不能及时更新资源的问题),其中我们58就使用了相应的usdt服务,对于js,css文件使用具有usdt相同功能的服务,自带版本号,如果使用默认webview的缓存,新版本更新之后不能及时删除老版本的js,css文件问题。
默认缓存,对于缓存策略和文件的操作都不可扩展,对于我们来说是个黑盒子。
由于以上等原因我们做了第一阶段的优化,使用自定义缓存替代webview自带的缓存。
B、预先初始化Webview,可提前初始化浏览器内核,并预加载页面,在父页面提前根据策略加载需要加载的页面。
APP启动之后就定义一个全局的webview对象实例,因为在我们第一次使用webview初始化过程中,需要初始化浏览器内核大概500ms左右,可以对此进行优化,其次,在我们第一次进入某页面之后,在页面初始化View组件的同时,使用默认的webview将资源文件缓存到本地,等到View组件初始好之后,可以直接使用本地缓存的资源进行渲染,以提高页面加载速度。并且使用该全局webview对象在父页面提前缓存子页面,这样在加载子页面的时候提前从本地缓存加载页面。减少第一次下载页面资源以及解析生成中间数据的时间。
C、特定的场景使用H5离线包,首次请求进行下载,二次进入场景如果需要显示直接从本地显示。
特定的场景:例如,显示推广活动的H5页面,定期宣传的服务H5页面等等,
服务器会提供相关接口告知APP,APP在首次启动的时候下载资源,当下载完成之后,App第二次进入时候,直接从本地加载。
优化之后的整体架构如下:
下一章节将具体说明优化方案的全过程。
Webview加载优化方案实践过程
1、默认加载速度测试过程
由于第一章节说了使用webview自带缓存的问题,所以默认测试速度的时候是禁用系统的缓存的,并且加载了的测试页面(58商家通中的商学院页面),关键代码如下:
webView.getSettings().setCacheMode(WebSettings.LOAD_NO_CACHE);
webView.loadUrl("https://hyapp.58.com/app/school/open/articles/tohome");
然后测试10次,并记录每次各项数据如下:
分析:
创建页面时间:onCreate()开始时间
页面加载时间:onPageStarted()开始时间
页面加载完成时间:onPageFinished()开始时间
初始化耗时:从onCreate()到onPageStarted的时间
加载资源耗时:从onPageStarted()到onPageFinished()的时间
总耗时:从onCreate()到onPageFinished()的时间
由数据可以看出:
第一次资源缓存时间大概3.1s左右,第二次资源缓存大概2s左右
第一次总耗时大概3.9s,第二次大概2.3s左右
第一次初始化耗时大概800ms,第二次初始化耗时300ms
所以为了缩短总耗时,首先需要优化缓存这个最耗时的步骤,下面我们将说明缓存优化的过程。
2、缓存优化的过程
首页我们来开看缓存优化的切入点以及具体的流程如下图所示:
A、切入点:当webview 需要加载资源的时候,会使用下面两个api进行拦截。
/**
* 发生资源加载,拦截顺序
*
* 此方法添加于API21,调用于非UI线程,拦截资源请求并返回数据,返回null时WebView将继续加载资源
* @param view
* @param request
* @return
*/
public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request)
/**
* 此方法废弃于API21,调用于非UI线程拦截资源请求并返回响应数据,返回null时WebView将继续加载资源
* @param view
* @param url
* @return
*/
public WebResourceResponse shouldInterceptRequest(WebView view, String url)
B、当拦截到需要下载网页资源的url后,我们需要以下几点需要明确:
哪些url文件需要缓存?
对于js,css文件等如何更新?
多台cdn服务图片如何只下载一份?
解决这些问题之前:首先我们内部开发约定如下:
js,css文件上线需要继承usdt等相近的服务。
图片资源应上传至cdn服务器,从cdn服务器应用。
之后,开始解决上面的问题,首先我们app端只缓存了符合我们规定的资源文件
大概占95%以上,对于不符合规定的资源文件依旧是从网络获取,来保证我们的页面的正确性。所以我们只针对具有版本号的js,css文件,已经cdn服务器的图片进行缓存。其次来看看文件的更新策略,由于我们需要缓存的js,css文件都是携带版本号的(https://j1.58cdn.com.cn/shangjiatong/sdk/sj_app_v20190327110116.js)我们会以携带的版本号来判断文件是否需要更新,如果需要更新,则直接异步缓存文件,并删除之前的旧版本,并同时让webview从网略加载需要更新的资源。最后对于多台cdn服务器缓存的同一个图片资源的url是不同的例如:
https://pic1.58cdn.com.cn/nowater/sjtnw/n_v2d2dd3ffb95d84cc8ae2dad24e8bd4a5b.jpg
https://pic2.58cdn.com.cn/nowater/sjtnw/n_v2d2dd3ffb95d84cc8ae2dad24e8bd4a5b.jpg
https://pic3.58cdn.com.cn/nowater/sjtnw/n_v2d2dd3ffb95d84cc8ae2dad24e8bd4a5b.jpg
所以可以根据这个特点,做细节处理,对于后缀相同的图片值缓存一次,避免重复下载。后期服务端会做路由和分发,可以直接避免此问题。我们在优化之后使用相同的手机和相同的页面进行测试,测试结果如下:
由测试结果可以看出,第一次加载资源耗时因为是从网络缓存文件1709ms,第二次由于直接从缓存获取已经降低到了416ms。而页面初始化的时间第一次依旧需要800ms左右,第二次需要300ms左右,但可以看出如果第二次初始化都是300ms,比第一次少了500ms左右,这因为第二次加载页面首先不需要初始化浏览器内核,第二是第一次加载页面之后,会对一些临时、简单数据进行缓存,Cookies的扩展。具体的API如下
webView.getSettings().setDomStorageEnabled(true);
正因为第二次比第一次加载明显变快,所以能不能将第一次加载也做成是第二次加载呢?因此带着这个问题我们进入了下一个流程的优化。
3、初始化全局webview阶段,并提前预加载页面
我们在APP使用Webview加载一个页面总感觉比在手机浏览器中打开同一个页面会慢,这主要是因为当我们在手机浏览器中打开页面之前,我们已经打开了手机浏览器这个APP,打开完成之后,它已经对浏览器内核进行了初始化。而当我们打开自己的APP去加载页面时候,当加载页面的时候才会去初始化webview,然后第一个初始化webview 就会初始化浏览器内核,所以会比浏览器慢,为了解决这个问题,我们可以如下优化。
在App启动之后,定义了一个全局的WebviewProxy单例对象,它会持有一个webview对象,首先,在初始化它的时候,会初始化浏览器内核,下次进入页面初始化webview时会更快,由之前的数据可以看出大概会提高500ms;其次,通过这个webview对象可以在父页面提前缓存子页面,这样加载子页面时候可以快速显示子页面。
针对上面的方案,实践过程中需要注意以下几个问题?
初始化webview持有上下文环境?
当父页面加载子页面还没有完成时,点击子页面如何处理?
父页面为H5页面,子页面很多,如何动态配置加载子页面?
启动APP的时候如何将本地文件加载到内存?
首先,webview持有的上下文环境如果直接传当前页面的上下文环境,如果当前需要退出,由于被全局webview对象持有,所以会导致内存泄漏,如果使用MutableContextWrapper类去持有当前页面Context,需要在销毁页面时候去主动调用setBaseContext()方法去释放当前Context,由于我们的webview本来就是全局唯一的单例对象,所以我们为其分配了Applciation对象作为Context对象。
具体的定义全部的webview的代理对象:
public class WebviewProxy implements IWebviewProxy{
private WebView webView;
private static volatile WebviewProxy INSTANCE;
private WebviewProxy(){
webView = new WebView(MyApplication.getInstance());
initWebView();
}
public static WebviewProxy getInstatnce(){
if(INSTANCE == null){
synchronized (WebviewProxy.class){
if(INSTANCE == null){
INSTANCE = new WebviewProxy();
}
}
}
return INSTANCE;
}
@Override
public void load(String url) {
webView.loadUrl(url);
}
...
}
其中在需要提前加载页面时可以通过load方法,提前缓存页面以及生成domTree,如下所示。
private void initWebview() {
WebviewProxy.getInstatnce().load("https://hyapp.58.com/app/school/open/articles/tohome");
}
第二个问题当父页面加载子页面还没有完成时,点击子页面时如何处理,这里主要关注的点是缓存资源可能存在重复下载的问题,所以在做此处的时候需要对上面的下载组件进行了重构,加入了任务队列模块,所以,在点击子页面的时候,如果已经缓存了则直接从缓存获取,如果没有,则判断是否在缓存队列,如果在,则不需要重新缓存,如果不在,才会下载,这样就避免了同一资源重复下载的问题。
第三个问题父页面为H5页面,如何动态加载子页面,对于这个问题,我们对H5页面提供了jsbridge协议,当父页面需要缓存时,直接调用Native提供的协议即可,这样H5开发过程中,会自己根据判断是否需要加载子页面,而动态的调用协议去缓存。
第四个问题,启动的时候如何将本地文件加载到内存中,如果将本地的缓存文件全部加载到内存中,如果缓存文件过多,太消耗内存,所以我们做了动态配置,以及优先级等策略,首先,文件保存到本地会设置文件优先级,核心页面为1-10,普通一级页面为10-100,二级页面为100-1000,其次,配置加载的内存大小,所以,首次启动APP之后,我们按优先级最高的一个一个文件加载到内存中,并判断是否到达最大内存限制,对需要初始化的资源进行管控。
最后还是使用我们商学院的的页面做测试,我们再首页启动的时候就预先使用全局的WebviewProxy这个对象去加载商学院url,然后,点击商学院页面,进入商学院页面,对其进行了测试,数据结果如下:
可以看出初始化全局webview并且预加载页面之后,我们的58商家通中资源消耗最多的商学院页面加载速度也达到了1秒以内,并且第二次和第一次加载耗时基本相近。
4、特定场景下,支持离线包
之前的流程在一般场景下都是可以适用的,最后针对我们特定的业务需求,又做了离线包加载模块,首先,来看下场景:我们需要动态的在APP启动的时候显示可配置的H5广告,因为是APP启动,所以应该以最快速度显示页面,所以需要将所有的H5页面以及资源打包,当我们启动的时候,首次先请求接口,如果有需要展示的广告,先下载本地并解压,之后启动APP的时候判断如果还需要显示,就直接从本地加载,这样可以以最快速度加载我们的H5页面,而且是否显示,显示的广告内容,都是服务器可配置的,方便产品做运营推广,这一节其实只是一个场景的补充,与上面几部的优化没有什么关联,但是提供了另一种优化方案(APP提供浏览器的壳,所有的资源可动态加载到本地,之后直接加载本地页面和资源)。具体流程如下图所示:
持续优化计划
上文优化过程中还有许多需要后期优化的点,后期持续优化计划如下:
首先会对缓存文件的初始化逻辑优化,缓存策略的优化,减少运行过程中对File的操作,能直接命中内存中的缓存。其次是对架构中各模块的封装,降低各模块之间的耦合性,最后,希望能够封装成sdk,直接在其他项目中集成使用。
总结
作者简介
赵兵,58商家通全栈开发工程师,主要负责58商家通前后端业务开发、性能优化、架构设计、重点项目和版本迭代,长期参与58商家通需求开发迭代,并偶尔参与本部门其他服务端开发如推荐有奖、广告平台、企管家等服务。
END
阅读推荐
58App-Android端的动态化框架实践与思考
API管理平台之SCF服务测试篇
API管理平台之系统设计篇
开源|Magpie可视化圈选埋点实践
开源|Magpie:组件库详解