57

58商家通Android端WebView加载优化方案

 4 years ago
source link: http://mp.weixin.qq.com/s?__biz=MzI1NDc5MzIxMw%3D%3D&%3Bmid=2247487816&%3Bidx=1&%3Bsn=fd686a96c55f1996347e5accad1b2689
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

导语

本文从实际需求出发,通过分析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首次加载流程大概分为以下几个阶段:

Qry6rqi.png!web

A、webview的初始化

  B、浏览器内核初始化(全部webview共享,第一次初始化)

C、请求html页面,并对页面进行解析

D、下载解析过程中需要下载的js,css,图片等资源文件

E、生成h5的domTree

F、根据上文的domTree渲染页面

2、 webview优化过程分析

分析之前,先解释下文中的一个名词(usdt服务:是58自主开发的一个管理web资源版本的scf服务,其主要功能是对资源文件通过添加版本后缀来实现版本管理。主要实现方式:上线资源时,给资源文件自动化加个时间戳后缀,主要实现如下:

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第二次进入时候,直接从本地加载。
优化之后的整体架构如下:

qIb2Mbr.png!web

下一章节将具体说明优化方案的全过程。

Webview加载优化方案实践过程

1、默认加载速度测试过程

由于第一章节说了使用webview自带缓存的问题,所以默认测试速度的时候是禁用系统的缓存的,并且加载了的测试页面(58商家通中的商学院页面),关键代码如下:

webView.getSettings().setCacheMode(WebSettings.LOAD_NO_CACHE);

webView.loadUrl("https://hyapp.58.com/app/school/open/articles/tohome");

然后测试10次,并记录每次各项数据如下:

MZ7zIjJ.png!web

分析:
创建页面时间:onCreate()开始时间
页面加载时间:onPageStarted()开始时间
页面加载完成时间:onPageFinished()开始时间
初始化耗时:从onCreate()到onPageStarted的时间
加载资源耗时:从onPageStarted()到onPageFinished()的时间
总耗时:从onCreate()到onPageFinished()的时间
由数据可以看出:

  • 第一次资源缓存时间大概3.1s左右,第二次资源缓存大概2s左右

  • 第一次总耗时大概3.9s,第二次大概2.3s左右

  • 第一次初始化耗时大概800ms,第二次初始化耗时300ms

所以为了缩短总耗时,首先需要优化缓存这个最耗时的步骤,下面我们将说明缓存优化的过程。

2、 缓存优化的过程

首页我们来开看缓存优化的切入点以及具体的流程如下图所示:

BRzUbii.png!web

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

所以可以根据这个特点,做细节处理,对于后缀相同的图片值缓存一次,避免重复下载。后期服务端会做路由和分发,可以直接避免此问题。我们在优化之后使用相同的手机和相同的页面进行测试,测试结果如下:

fI3QNnb.png!web

由测试结果可以看出,第一次加载资源耗时因为是从网络缓存文件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,然后,点击商学院页面,进入商学院页面,对其进行了测试,数据结果如下:

BZbQraa.png!web

可以看出初始化全局webview并且预加载页面之后,我们的58商家通中资源消耗最多的商学院页面加载速度也达到了1秒以内,并且第二次和第一次加载耗时基本相近。

4、 特定场景下,支持离线包

之前的流程在一般场景下都是可以适用的,最后针对我们特定的业务需求,又做了离线包加载模块,首先,来看下场景:我们需要动态的在APP启动的时候显示可配置的H5广告,因为是APP启动,所以应该以最快速度显示页面,所以需要将所有的H5页面以及资源打包,当我们启动的时候,首次先请求接口,如果有需要展示的广告,先下载本地并解压,之后启动APP的时候判断如果还需要显示,就直接从本地加载,这样可以以最快速度加载我们的H5页面,而且是否显示,显示的广告内容,都是服务器可配置的,方便产品做运营推广,这一节其实只是一个场景的补充,与上面几部的优化没有什么关联,但是提供了另一种优化方案(APP提供浏览器的壳,所有的资源可动态加载到本地,之后直接加载本地页面和资源)。具体流程如下图所示:

3eqaYzy.png!web

持续优化计划

上文优化过程中还有许多需要后期优化的点,后期持续优化计划如下:
首先会对缓存文件的初始化逻辑优化,缓存策略的优化,减少运行过程中对File的操作,能直接命中内存中的缓存。其次是对架构中各模块的封装,降低各模块之间的耦合性,最后,希望能够封装成sdk,直接在其他项目中集成使用。

总结

技术服务于业务需求,由于58商家通中的H5页面比例的增多,所以对于我们58商家通平台来说优化H5页面的加载速度显得格外重要,所以经过一系列的实践和方案的实施,使得我们58商家通APP的H5页面加载到达了秒级显示,因此性能优化在实践中得到了验证,所以出此文章供大家参考。我也一直会继续努力优化我们的58商家通这款App,后期计划的优化点还有许多,使得商家能够简单高效的在我们58上做生意,提高我们产品的体验度,对此如果大家还有别的方案,也希望能够多多交流,以后,争取能够发表更多关于前端Android技术的分享。温故而知新,希望这次总结,也能对自己有所帮助。


作者简介

赵兵, 58商家通全栈开发工程师,主要负责58商家通前后端业务开发、性能优化、架构设计、重点项目和版本迭代,长期参与58商家通需求开发迭代,并偶尔参与本部门其他服务端开发如推荐有奖、广告平台、企管家等服务。

阅读推荐


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK