百度APP-Android H5首屏优化实践 - 百度App技术
Posted Android编程精选
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了百度APP-Android H5首屏优化实践 - 百度App技术相关的知识,希望对你有一定的参考价值。
一、背景
二、方法论
-
通过用户反馈、QA测试等多种渠道,发现落地页首屏加载慢问题 -
定义首屏性能指标(首屏含图,以图片加载为准;首屏无图,以文字渲染结束为准) -
NA、内核、H5三方针对自己加载H5的流程进行划分并埋点上报 -
统计侧根据三端上报的数据产出平均值、80分位值的性能报表 -
分析性能报表,找到不合理的耗时点,并进行优化 -
以AB实验方式,对比优化前后的性能报表数据,产出优化效果,同时评估用户体验等相关指标 -
按照长期优化的方式,不断分析定位性能瓶颈点并优化,以AB实验方式评估效果,最终达到我们的落地页秒开目标
三、Hybrid方案简述及性能瓶颈
优化之前,我们与业内大多数的App一样,在落地页的技术选型中,为了满足跨平台和动态性的要求,采用了Hybrid这种比较成熟的方案。Hybrid,顾名思义,即混合开发,也就是半原生半Web的方式。页面中的复杂交互功能采用端能力的方式,调用原生API来实现。成本低,灵活性较好,适合偏信息展示类的H5场景。
下面用一张图来表示百度App中Hybrid的实现机制和加载流程
为了分析Hybrid方案首屏展现较慢的原因,找到具体的性能瓶颈,客户端和前端分别针对各自加载过程中的关键节点进行埋点统计,并借由性能监控平台日志进行展示,下图是截取的某一天全网用户的落地页首屏展现速度80分位数据
我们具体分析下四个阶段的性能损耗主要发生在哪些地方:
从点击到落地页框架初始化完成,主要工作为初始化WebView,尤其是第一次进入(WebView首次创建耗时均值为500ms)
2) Hybrid初始化
这个阶段的工作主要包含两部分,一个是根据调起协议中传入的相关参数,校验解压下发到本地的Hybrid模板,大致需要100ms的时间;此外,WebView.loadUrl执行后,会触发对Hybrid模板头部和Body的解析
3) 正文加载&渲染
执行到这个阶段,内核已经完成了对Hybrid模板头部和body的解析,此时需要加载解析页面所需的JS文件,并通过JS调用端能力发起对正文数据的请求,客户端从Server拿到数据后,用JsCallback的方式回传给前端,前端需要对客户端传来的JSON格式的正文数据进行解析,并构造DOM结构,进而触发内核的渲染流程;
4) 图片加载
第(3)步中,前端获取到的正文数据包含落地页的图片地址集,在完成正文的渲染后,需要前端再次执行图片请求的端能力,客户端这边接收到图片地址集后按顺序请求服务器,完成下载后,客户端会调用一次IO将文件写入缓存,同时将对应图片的本地地址回传给前端,最终通过内核再发起一次IO操作获取到图片数据流,进行渲染;
通过分析,延伸出对Hybrid方案的一些思考:
-
渲染为什么这么慢 -
图片请求能否提前 -
串行逻辑是否可以改为并行 -
WebView初始化时间是否还可以优化
四、百度App落地页优化方案
基于之前对Hybrid性能的分析,我们内部孵化了一个叫做CloudHybrid的项目,用来解决落地页首屏展现慢的痛点;一句话来形容CloudHybrid方案,就是采用后端直出+预取+拦截的方式,简化页面渲染流程,提前化&并行化网络请求逻辑,进而提升H5首屏速度
a. 页面静态直出
对于Hybrid方案来说,端上预置和加载的html文件只是一个模板文件,内部包含一些简单的JS和CSS文件,端上加载HTML后,需要执行JS通过端能力从Server异步请求正文数据,得到数据后,还需要解析JSON,构造DOM,应用CSS样式等一系列耗时的步骤,最终才能由内核进行渲染上屏;
b. 动态信息回填
为了保证首屏渲染结果的准确性,除了在server侧对正文内容和前端代码进行整合外,还需要一些影响页面渲染的客户端状态信息,例如首图地址、字体大小、夜间模式等
这里我们采用动态回填的方式,前端会在直出的html中定义一系列特殊字符,用来占位;客户端在loadUrl之前,会利用正则匹配的方式,查找这些占位字符,并按照协议映射成端信息;经过客户端回填处理后的html内容,已经具备了展现首屏的所有条件
c. 动画间渲染
先看下优化前后效果(上图:优化前;下图:优化后):
我们通过研究源码发现,系统处理view绘制的时候,有一个属性setDrawDuringWindowsAnimating,从命名可以看出来,这个属性是用来控制window做动画的过程中是否可以正常绘制,而恰好在android 4.2到Android N之间,系统为了组件切换的流程性考虑,该字段为false,我们可以利用反射的方式去手动修改这个属性,改进后的效果见上面下图
private void setDrawDuringWindowsAnimating(View view) {
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.M
|| Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) {
return;
}
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) {
handleDispatchDoneAnimating(view);
return;
}
try {
ViewParent rootParent = view.getRootView().getParent();
Method method = rootParent.getClass()
.getDeclaredMethod("setDrawDuringWindowsAnimating", boolean.class);
method.setAccessible(true);
method.invoke(rootParent, true);
} catch (Exception e) {
e.printStackTrace();
}
}
private void handleDispatchDoneAnimating(View paramView) {
try {
ViewParent localViewParent = paramView.getRootView().getParent();
Class localClass = localViewParent.getClass();
Method localMethod = localClass.getDeclaredMethod("handleDispatchDoneAnimating");
localMethod.setAccessible(true);
localMethod.invoke(localViewParent);
} catch (Exception localException) {
localException.printStackTrace();
}
}
经过直出的改造之后,为了更快的渲染首屏,减少过程中涉及到的网络请求耗时,我们可以按照一定的策略和时机,提前从CDN中请求部分落地页html,缓存到本地,这样当用户点击查看新闻时,只需从缓存中加载即可
在落地页中,除了文本外,图片也是重要的组成部分。 直出解决了文字展现的速度问题,但图片的加载渲染速度仍不理想,尤其是首屏中带有图片的文章,其首图的渲染速度才是真正的首屏时间点
传统Hybrid方案,前端页面通过端能力调用NA图片下载能力来缓存和渲染图片,虽然实现了客户端和前端图片缓存的共享,但由于JS执行时机较晚,且多次端能力调用存在效率问题,导致图片渲染延后
终极方案: 借由内核的shouldInterceptRequest回调,拦截落地页图片请求,由客户端调用NA图片下载框架进行下载,并以管道方式填充到内核的WebResourceResponse中
效果如下(上图: 优化前Hybrid方案; 下图: 优化后通用拦截方案):
1.WebView预创建
为了节省WebView的性能损耗,我们可以在合适时机提前创建好WebView,并存入缓存池,当页面需要显示内容时,直接从缓存池获取创建好的WebView,根据性能数据显示,WebView预创建可以提升首屏渲染时间200ms+
由于WebView的初始化需要和context进行绑定,若想实现预创建的逻辑,需要保证context的一致性,常规做法我们考虑可以用fragment来实现承载H5页面的容器,这样context可以用外层的activity实例,但Fragment本身的切换流畅度存在一定问题,并且这样做限定了WebView预创建适用的场景。
简单来说,就是一种新的context包装类,允许外部修改它的baseContext,并且所有ContextWrapper调用的方法都会代理到baseContext来执行
@DebugTrace
public void prepareNewWebView() {
if (mCachedWebViewStack.size() < CACHED_WEBVIEW_MAX_NUM) {
mCachedWebViewStack.push(new WebView(new MutableContextWrapper(getAppContext())));
}
}
private WebView acquireWebViewInternal(Context context) {
if (mCachedWebViewStack == null || mCachedWebViewStack.isEmpty()) {
return new WebView(context);
}
WebView webView = mCachedWebViewStack.pop();
MutableContextWrapper contextWrapper = (MutableContextWrapper) webView.getContext();
contextWrapper.setBaseContext(context);
return webView;
}
a. WebView初始化完成,立刻loadUrl,无需等待框架onCreate或者OnResume结束
b. WebView初始完成后到页面首屏绘制完成之间,尽量减少UI线程的其他操作,繁忙的UI线程会拖慢WebView.loadUrl的速度
a. 内核渲染优化:
内核中主要分为三个线程(IOThread、MainThread、ParserThread),首先IOThread会从网络端或者本地获取html数据,并把数据交给MainThread(渲染线程,十分繁忙,用于JS执行,页面布局等),为了保证MainThread不被阻塞,需要额外起一个后台线程(ParserThread)用来做html的解析工作。
预创建好WebView后,通过预加载JS(与内核约定好的JS内容,内核侧执行该JS时,只做初始化操作),触发WebView初始化逻辑,缩短后续加载url耗时; 80分位性能提升80ms左右
五、新的问题-流量和速度的平衡
解决思路:
  1.压缩预取的包大小,减少下行流量
图文: 优化直出html中内联的css、icon等数据,数据大小减少约40%
(二)后端智能预取:
1) 图文: 通过对图文资源进行评分,来决定4G是否需要预取,多组AB试验最优效果劣化9.5ms
2)视频: 为了平衡性能和流量,在性能劣化可接受的范围内(视频起播时间劣化100ms),针对视频部分采用流量高峰期不预取的策略,减少视频总流量约7%,整体带宽峰值下降3%
通用用户操作行为,对Feed预取进行AI预测,减少无效预取的数量。
六、总结&展望
在总结之前,先来看下整体优化的前后效果对比(上图: 优化前; 下图: 优化后):
-
提前做: 包括预创建WebView和预取数据 -
并行做: 包括图片直出&拦截加载,框架初始化阶段开启异步线程准备数据等 -
轻量化: 对于前端来说,要尽量减少页面大小,删减不必要的JS和CSS,不仅可以缩短网络请求时间,还能提升内核解析时间 -
简单化: 对于简单的信息展示页面,对内容动态性要求不高的场景,可以考虑使用直出替代hybrid,展示内容直接可渲染,无需JS异步加载
-
页面的更新机制,目前方案仅适用于偏静态页面,对于动态性要求较高的业务,需要提供页面更新机制,保证每次显示的正确性 -
开源之路: 后续计划将我们总结下来的这套方案打包开源,前行之路必定坎坷,希望大家多多支持
推荐↓↓↓
长
按
关
注
以上是关于百度APP-Android H5首屏优化实践 - 百度App技术的主要内容,如果未能解决你的问题,请参考以下文章