魔都美少年
读完需要
15
分钟
速读仅需10分钟
作者:魔都美少年 链接:https://juejin.im/post/5d2605f8f265da1bc23fa07c
1
痛点是什么?
网页加载缓慢,白屏,使用卡顿。
2
为何有这种问题?
- 调用loadUrl()方法的时候,才会开始网页加载流程
- js臃肿问题
- 加载图片太多
- webview本身问题
3
webiew是怎么加载网页的呢?
webview初始化->DOM下载→DOM解析→CSS请求 下载→CSS解析→渲染→绘制→合成
4
优化方向是?
4.1
webview本身优化
- 提前内核初始化
代码语言:javascript复制1public class App extends Application {
2
3 private WebView mWebView ;
4 @Override
5 public void onCreate() {
6 super.onCreate();
7 mWebView = new WebView(new MutableContextWrapper(this));
8 }
9}
效果:初次内核初始化大概2000ms,第二次50ms以内
- webview复用池
代码语言:javascript复制 1public class WebPools {
2 private final Queue<WebView> mWebViews;
3 private Object lock = new Object();
4 private static WebPools mWebPools = null;
5 private static final AtomicReference<WebPools> mAtomicReference = new AtomicReference<>();
6 private static final String TAG=WebPools.class.getSimpleName();
7
8 private WebPools() {
9 mWebViews = new LinkedBlockingQueue<>();
10 }
11 public static WebPools getInstance() {
12 for (; ; ) {
13 if (mWebPools != null)
14 return mWebPools;
15 if (mAtomicReference.compareAndSet(null, new WebPools()))
16 return mWebPools=mAtomicReference.get();
17 }
18 }
19 public void recycle(WebView webView) {
20 recycleInternal(webView);
21 }
22 public WebView acquireWebView(Activity activity) {
23 return acquireWebViewInternal(activity);
24 }
25 private WebView acquireWebViewInternal(Activity activity) {
26 WebView mWebView = mWebViews.poll();
27 LogUtils.i(TAG,"acquireWebViewInternal webview:" mWebView);
28 if (mWebView == null) {
29 synchronized (lock) {
30 return new WebView(new MutableContextWrapper(activity));
31 }
32 } else {
33 MutableContextWrapper mMutableContextWrapper = (MutableContextWrapper) mWebView.getContext();
34 mMutableContextWrapper.setBaseContext(activity);
35 return mWebView;
36 }
37 }
38 private void recycleInternal(WebView webView) {
39 try {
40 if (webView.getContext() instanceof MutableContextWrapper) {
41 MutableContextWrapper mContext = (MutableContextWrapper) webView.getContext();
42 mContext.setBaseContext(mContext.getApplicationContext());
43 LogUtils.i(TAG,"enqueue webview:" webView);
44 mWebViews.offer(webView);
45 }
46 if(webView.getContext() instanceof Activity){
47 //throw new RuntimeException("leaked");
48 LogUtils.i(TAG,"Abandon this webview , It will cause leak if enqueue !");
49 }
50 }catch (Exception e){
51 e.printStackTrace();
52 }
53 }
54}
带来的问题:内存泄漏
- 独立进程,进程预加载
代码语言:javascript复制1 <service
2 android:name=".PreWebService"
3 android:process=":web"/>
4 <activity
5 android:name=".WebActivity"
6 android:process=":web"/>
启动webview页面前,先启动PreWebService把[web]进程创建了,当启动WebActivity时,系统发发现[web]进程已经存在了,就不需要花费时间Fork出新的[web]进程了。
- 使用x5内核 直接使用腾讯的x5内核,替换原生的浏览器内核
- 其他的解决方案:
- 设置webview缓存
- 加载动画/最后让图片下载
- 渲染时关掉图片加载
- 设置超时时间
- 开启软硬件加速
4.2
加载资源时的优化
这种优化多使用第三方,下面有介绍
4.3
网页端的优化
由网页的前端工程师优化网页,或者说是和移动端一起,将网页实现增量更新,动态更新。app内置css,js文件并控制版本
注意:如果你寄希望于只通过webview的setting来加速网页的加载速度,那你就要失望了。只修改设置,能做的提升非常少。所以本文就着重分析比较下,现在可以使用的第三方webview框架的优缺点。
现在大厂的方法有以下几种:
VasSonic:
https://github.com/Tencent/VasSonic
TBS腾讯浏览服务:
https://x5.tencent.com/
百度app方案:
https://mp.weixin.qq.com/s/AqQgDB-0dUp2ScLkqxbLZg
今日头条方案:
https://mp.weixin.qq.com/s/KwvWURD5WKgLKCetwsH0EQ
5
VasSonic
参考文章:
https://blog.csdn.net/tencent__open/article/details/77324952
5.1
STEP1:
代码语言:javascript复制
代码语言:javascript复制1 //导入 Tencent/VasSonic
2 implementation 'com.tencent.sonic:sdk:3.1.0'
代码语言:javascript复制
5.2
STEP2:
代码语言:javascript复制
代码语言:javascript复制 1//创建一个类继承SonicRuntime
2//SonicRuntime类主要提供sonic运行时环境,包括Context、用户UA、ID(用户唯一标识,存放数据时唯一标识对应用户)等等信息。以下代码展示了SonicRuntime的几个方法。
3public class TTPRuntime extends SonicRuntime
4{
5 //初始化
6 public TTPRuntime( Context context )
7 {
8 super(context);
9 }
10
11 @Override
12 public void log(
13 String tag ,
14 int level ,
15 String message )
16 {
17 //log设置
18 }
19
20 //获取cookie
21 @Override
22 public String getCookie( String url )
23 {
24 return null;
25 }
26
27 //设置cookid
28 @Override
29 public boolean setCookie(
30 String url ,
31 List<String> cookies )
32 {
33 return false;
34 }
35
36 //获取用户UA信息
37 @Override
38 public String getUserAgent()
39 {
40 return "Mozilla/5.0 (Linux; Android 5.1.1; Nexus 6 Build/LYZ28E) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Mobile Safari/537.36";
41 }
42
43 //获取用户ID信息
44 @Override
45 public String getCurrentUserAccount()
46 {
47 return "ttpp";
48 }
49
50 //是否使用Sonic加速
51 @Override
52 public boolean isSonicUrl( String url )
53 {
54 return true;
55 }
56
57 //创建web资源请求
58 @Override
59 public Object createWebResourceResponse(
60 String mimeType ,
61 String encoding ,
62 InputStream data ,
63 Map<String, String> headers )
64 {
65 return null;
66 }
67
68 //网络属否允许
69 @Override
70 public boolean isNetworkValid()
71 {
72 return true;
73 }
74
75 @Override
76 public void showToast(
77 CharSequence text ,
78 int duration )
79 { }
80
81 @Override
82 public void postTaskToThread(
83 Runnable task ,
84 long delayMillis )
85 { }
86
87 @Override
88 public void notifyError(
89 SonicSessionClient client ,
90 String url ,
91 int errorCode )
92 { }
93
94 //设置Sonic缓存地址
95 @Override
96 public File getSonicCacheDir()
97 {
98 return super.getSonicCacheDir();
99 }
100}
代码语言:javascript复制
5.3
STEP3:
代码语言:javascript复制
代码语言:javascript复制 1//创建一个类继承SonicSessionClien
2//SonicSessionClient主要负责跟webView的通信,比如调用webView的loadUrl、loadDataWithBaseUrl等方法。
3public class WebSessionClientImpl extends SonicSessionClient
4{
5 private WebView webView;
6
7 //绑定webview
8 public void bindWebView(WebView webView) {
9 this.webView = webView;
10 }
11
12 //加载网页
13 @Override
14 public void loadUrl(String url, Bundle extraData) {
15 webView.loadUrl(url);
16 }
17
18 //加载网页
19 @Override
20 public void loadDataWithBaseUrl(String baseUrl, String data, String mimeType, String encoding,
21 String historyUrl) {
22 webView.loadDataWithBaseURL(baseUrl, data, mimeType, encoding, historyUrl);
23 }
24
25 //加载网页
26 @Override
27 public void loadDataWithBaseUrlAndHeader(
28 String baseUrl ,
29 String data ,
30 String mimeType ,
31 String encoding ,
32 String historyUrl ,
33 HashMap<String, String> headers )
34 {
35 if( headers.isEmpty() )
36 {
37 webView.loadDataWithBaseURL( baseUrl, data, mimeType, encoding, historyUrl );
38 }
39 else
40 {
41 webView.loadUrl( baseUrl,headers );
42 }
43 }
44}
代码语言:javascript复制
5.4
STEP4:
代码语言:javascript复制
代码语言:javascript复制 1//创建activity
2public class WebActivity extends AppCompatActivity
3{
4 private String url = "http://www.baidu.com";
5 private SonicSession sonicSession;
6
7 @Override
8 protected void onCreate( @Nullable Bundle savedInstanceState )
9 {
10 super.onCreate( savedInstanceState );
11 setContentView( R.layout.activity_web);
12 initView();
13 }
14
15 private void initView()
16 {
17 getWindow().addFlags( WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED);
18
19 //初始化 可放在Activity或者Application的onCreate方法中
20 if( !SonicEngine.isGetInstanceAllowed() )
21 {
22 SonicEngine.createInstance( new TTPRuntime( getApplication() ),new SonicConfig.Builder().build() );
23 }
24 //设置预加载
25 SonicSessionConfig config = new SonicSessionConfig.Builder().build();
26 SonicEngine.getInstance().preCreateSession( url,config );
27
28 WebSessionClientImpl client = null;
29 //SonicSessionConfig 设置超时时间、缓存大小等相关参数。
30 //创建一个SonicSession对象,同时为session绑定client。session创建之后sonic就会异步加载数据了
31 sonicSession = SonicEngine.getInstance().createSession( url,config );
32 if( null!= sonicSession )
33 {
34 sonicSession.bindClient( client = new WebSessionClientImpl() );
35 }
36 //获取webview
37 WebView webView = (WebView)findViewById( R.id.webview_act );
38 webView.setWebViewClient( new WebViewClient()
39 {
40 @Override
41 public void onPageFinished(
42 WebView view ,
43 String url )
44 {
45 super.onPageFinished( view , url );
46 if( sonicSession != null )
47 {
48 sonicSession.getSessionClient().pageFinish( url );
49 }
50 }
51
52 @Nullable
53 @Override
54 public WebResourceResponse shouldInterceptRequest(
55 WebView view ,
56 WebResourceRequest request )
57 {
58 return shouldInterceptRequest( view, request.getUrl().toString() );
59 }
60 //为clinet绑定webview,在webView准备发起loadUrl的时候通过SonicSession的onClientReady方法通知sonicSession:webView ready可以开始loadUrl了。这时sonic内部就会根据本地的数据情况执行webView相应的逻辑(执行loadUrl或者loadData等)
61 @Nullable
62 @Override
63 public WebResourceResponse shouldInterceptRequest(
64 WebView view ,
65 String url )
66 {
67 if( sonicSession != null )
68 {
69 return (WebResourceResponse)sonicSession.getSessionClient().requestResource( url );
70 }
71 return null;
72 }
73 });
74 //webview设置
75 WebSettings webSettings = webView.getSettings();
76 webSettings.setJavaScriptEnabled(true);
77 webView.removeJavascriptInterface("searchBoxJavaBridge_");
78 //webView.addJavascriptInterface(new SonicJavaScriptInterface(sonicSessionClient, intent), "sonic");
79 webSettings.setAllowContentAccess(true);
80 webSettings.setDatabaseEnabled(true);
81 webSettings.setDomStorageEnabled(true);
82 webSettings.setAppCacheEnabled(true);
83 webSettings.setSavePassword(false);
84 webSettings.setSaveFormData(false);
85 webSettings.setUseWideViewPort(true);
86 webSettings.setLoadWithOverviewMode(true);
87
88 //为clinet绑定webview,在webView准备发起loadUrl的时候通过SonicSession的onClientReady方法通知sonicSession:webView ready可以开始loadUrl了。这时sonic内部就会根据本地的数据情况执行webView相应的逻辑(执行loadUrl或者loadData等)。
89 if( client != null )
90 {
91 client.bindWebView( webView );
92 client.clientReady();
93 }
94 else
95 {
96 webView.loadUrl( url );
97 }
98 }
99
100 @Override
101 public void onBackPressed()
102 {
103 super.onBackPressed();
104 }
105
106 @Override
107 protected void onDestroy()
108 {
109 if( null != sonicSession )
110 {
111 sonicSession.destroy();
112 sonicSession = null;
113 }
114 super.onDestroy();
115 }
116}
简单分析下它的核心思想:
并行,充分利用webview初始化的时间进行一些数据的处理。在包含webview的activity启动时会一边进行webview的初始化逻辑,一边并行的执行sonic的逻辑。这个sonic逻辑就是网页的预加载原理:
无缓存模式流程:
左边的webview流程:webview初始化后调用SonicSession的onClientReady方法,告知webview已经初始化完毕。
代码语言:javascript复制
代码语言:javascript复制1client.clientReady();
右边的sonic流程:
- 创建SonicEngine对象
- 通过SonicCacheInterceptor获取本地缓存的url数据
- 数据为空就发送一个CLIENT_CORE_MSG_PRE_LOAD的消息到主线程
- 通过SonicSessionConnection建立一个URLConnection
- 连接获取服务器返回的数据,并在读取网络数据的时候不断判断webview是否发起资源拦截请求。如果发了,就中断网络数据的读取,把已经读取的和未读取的数据拼接成桥接流SonicSessionStream并赋值给SonicSession的pendingWebResourceStream,如果网络读取完成后webview还没有初始化完成,就会cancel掉CLIENT_CORE_MSG_PRE_LOAD消息,同时发送CLIENT_CORE_MSG_FIRST_LOAD消息
- 之后再对html内容进行模版分割及数据保存
- 如果webview处理了CLIENT_CORE_MSG_PRE_LOAD这个消息,它就会调用webview的loadUrl,之后webview会调用自身的资源拦截方法,在这个方法中,会将之前保存的pendingWebResourceStream返回给webview让其解析渲染,
- 如果webview处理的是CLIENT_CORE_MSG_FIRST_LOAD消息,webview如果没有loadUrl过就会调用loadDataWithBaseUrl方法加载之前读取的网络数据,这样webview就可以直接做解析渲染了。
有缓存模式
完全缓存流程:
左边webview的流程跟无缓存一致,右边sonic的流程会通过SonicCacheInterceptor获取本地数据是否为空,不为空就会发生CLIENT_CORE_MSG_PRE_LOAD消息,之后webview就会使用loadDataWithBaseUrl加载网页进行渲染了
6
TBS腾讯浏览服务
https://x5.tencent.com/
集成方法,请按照官网的来操作即可
7
百度app方案
来看下百度app对webview处理的方案
7.1
后端直出-页面静态直出
后端服务器获取html所有首屏内容,包含首屏展现所需的内容和样式。这样客户端获取整个网页并加载时,内核可以直接进行渲染。这里服务端要提供一个接口给客户端取获取网页的全部内容。而且获取的网页中一些需要使用客户端的变量的使用宏替换,在客户端加载网页的时候替换成特定的内容,已适应不同用户的设置,例如字体大小、页面颜色等等。
但是这个方案还有些问题就是网络图片没有处理,还是要花费时间起获取图片。
7.2
智能预取-提前化网络请求
提前从网络中获取部分落地页html,缓存到本地,当用户点击查看时,只需要从缓存中加载即可。
7.3
通用拦截-缓存共享、请求并行
直出解决了文字展现的速度问题,但是图片加载渲染速度还不理想。借由内核的shouldInterceptRequest回调,拦截落地页图片请求,由客户端调用图片下载框架进行下载,并以管道方式填充到内核的WebResourceResponse中。就是说在shouldInterceptRequest拦截所有URL,之后只针对后缀是.PNG/.JPG等图片资源,使用第三方图片下载工具类似于Fresco进行下载并返回一个InputStream。
7.4
总结:
- 提前做:包括预创建WebView和预取数据
- 并行做:包括图片直出&拦截加载,框架初始化阶段开启异步线程准备数据等
- 轻量化:对于前端来说,要尽量减少页面大小,删减不必要的JS和CSS,不仅可以缩短网络请求时间,还能提升内核解析时间
- 简单化:对于简单的信息展示页面,对内容动态性要求不高的场景,可以考虑使用直出替代hybrid,展示内容直接可渲染,无需JS异步加载
8
今日头条方案
那今日头条是怎么处理的呢?
- assets文件夹内预置了文章详情页面的css/js等文件,并且能进行版本控制
- webview预创建的同时,预先加载一个使用JAVA代码拼接的html,提前对js/css资源进行解析。
- 文章详情页面使用预创建的webview,这个webview已经预加载了html,之后就调用js来设置页面内容
- 对于图片资源,使用ContentProvider来获取,而图片则是使用Fresco来下载的
代码语言:javascript复制1content://com.xposed.toutiao.provider.ImageProvider/getimage/origin/eJy1ku0KwiAUhm8l_F3qvuduJSJ0mRO2JtupiNi9Z4MoWiOa65cinMeX57xXVDda6QPKFld0bLQ9UckbJYlR-UpX3N5Smfi5x3JJ934YxWlKWZhEgbeLhBB-QNFyYUfL1s6uUQFgMkKMtwLA4gJSVwrndUWmUP8CC5xhm87izlKY7VDeTgLXZUtOlJzjkP6AxXfiR5eMYdMCB9PHneGHBzh-VzEje7AzV3ZvHYpjJV599w-uZWXvWadQR_vlAhtY_Bn2LKuzu_GGOscc1MfZ4veyTyNuuu4G1giVqQ==/6694469396007485965/3
代码语言:javascript复制
9
整理下这几个大厂的思路
9.1
针对客户端
- 预创建(application onCreate 时)webview
- 预创建的同时加载带有css/js的html文本
- webview复用池
- webview setting的设置
- 预取网页并缓存,预先获取html并缓存本地,需要是从缓存中加载即可
- 资源拦截并行加载,内核初始化和资源加载同时进行。
9.2
针对服务端
- 直出网页的拼装,服务端时获取网页的全部内容,客户端获取后直接加载
- 客户端本地html资源的版本控制
9.3
针对网页前端
- 删减不必要的js/css
- 配合客户端使用VasSonic,只对特定的内容进行页面更新与下载。
10
自己的想法:
- 网页秒开的这个需求,如果如果只是客户端来做,感觉只是做了一半,最好还是前后端一起努力来优化。
- 但是只做客户端方面的优化也是可以的,笔者实际测试了下,通过预取的方式,的确能做到秒开网页。
- 今年就上5G了,有可能在5G的网络下,网页加载根本就不是问题了呢。
11
小技巧
修复白屏现象:系统处理view绘制的时候,有一个属性setDrawDuringWindowsAnimating,这个属性是用来控制window做动画的过程中是否可以正常绘制,而恰好在Android 4.2到Android N之间,系统为了组件切换的流程性考虑,该字段为false,我们可以利用反射的方式去手动修改这个属性
代码语言:javascript复制
代码语言:javascript复制 1/**
2 * 让 activity transition 动画过程中可以正常渲染页面
3 */
4 private void setDrawDuringWindowsAnimating(View view) {
5 if (Build.VERSION.SDK_INT > Build.VERSION_CODES.M
6 || Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) {
7 // 1 android n以上 & android 4.1以下不存在此问题,无须处理
8 return;
9 }
10 // 4.2不存在setDrawDuringWindowsAnimating,需要特殊处理
11 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) {
12 handleDispatchDoneAnimating(view);
13 return;
14 }
15 try {
16 // 4.3及以上,反射setDrawDuringWindowsAnimating来实现动画过程中渲染
17 ViewParent rootParent = view.getRootView().getParent();
18 Method method = rootParent.getClass()
19 .getDeclaredMethod("setDrawDuringWindowsAnimating", boolean.class);
20 method.setAccessible(true);
21 method.invoke(rootParent, true);
22 } catch (Exception e) {
23 e.printStackTrace();
24 }
25 }
26 /**
27 * android4.2可以反射handleDispatchDoneAnimating来解决
28 */
29 private void handleDispatchDoneAnimating(View paramView) {
30 try {
31 ViewParent localViewParent = paramView.getRootView().getParent();
32 Class localClass = localViewParent.getClass();
33 Method localMethod = localClass.getDeclaredMethod("handleDispatchDoneAnimating");
34 localMethod.setAccessible(true);
35 localMethod.invoke(localViewParent);
36 } catch (Exception localException) {
37 localException.printStackTrace();
38 }
39 }