前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >安卓Webview网页秒开策略探索

安卓Webview网页秒开策略探索

作者头像
Rouse
发布2019-07-17 17:46:26
3.4K1
发布2019-07-17 17:46:26
举报
文章被收录于专栏:Android补给站Android补给站

魔都美少年

读完需要

15

分钟

速读仅需10分钟

作者:魔都美少年 链接:https://juejin.im/post/5d2605f8f265da1bc23fa07c

1

痛点是什么?

网页加载缓慢,白屏,使用卡顿。

2

为何有这种问题?

  1. 调用loadUrl()方法的时候,才会开始网页加载流程
  2. js臃肿问题
  3. 加载图片太多
  4. webview本身问题

3

webiew是怎么加载网页的呢?

webview初始化->DOM下载→DOM解析→CSS请求+下载→CSS解析→渲染→绘制→合成

4

优化方向是?

4.1

webview本身优化

  • 提前内核初始化
代码语言:javascript
复制
代码语言: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
复制
代码语言: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
复制
代码语言: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内核,替换原生的浏览器内核
  • 其他的解决方案:

  1. 设置webview缓存
  2. 加载动画/最后让图片下载
  3. 渲染时关掉图片加载
  4. 设置超时时间
  5. 开启软硬件加速

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流程:

  1. 创建SonicEngine对象
  2. 通过SonicCacheInterceptor获取本地缓存的url数据
  3. 数据为空就发送一个CLIENT_CORE_MSG_PRE_LOAD的消息到主线程
  4. 通过SonicSessionConnection建立一个URLConnection
  5. 连接获取服务器返回的数据,并在读取网络数据的时候不断判断webview是否发起资源拦截请求。如果发了,就中断网络数据的读取,把已经读取的和未读取的数据拼接成桥接流SonicSessionStream并赋值给SonicSession的pendingWebResourceStream,如果网络读取完成后webview还没有初始化完成,就会cancel掉CLIENT_CORE_MSG_PRE_LOAD消息,同时发送CLIENT_CORE_MSG_FIRST_LOAD消息
  6. 之后再对html内容进行模版分割及数据保存
  7. 如果webview处理了CLIENT_CORE_MSG_PRE_LOAD这个消息,它就会调用webview的loadUrl,之后webview会调用自身的资源拦截方法,在这个方法中,会将之前保存的pendingWebResourceStream返回给webview让其解析渲染,
  8. 如果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

今日头条方案

那今日头条是怎么处理的呢?

  1. assets文件夹内预置了文章详情页面的css/js等文件,并且能进行版本控制
  2. webview预创建的同时,预先加载一个使用JAVA代码拼接的html,提前对js/css资源进行解析。
  3. 文章详情页面使用预创建的webview,这个webview已经预加载了html,之后就调用js来设置页面内容
  4. 对于图片资源,使用ContentProvider来获取,而图片则是使用Fresco来下载的
代码语言:javascript
复制
代码语言:javascript
复制
1content://com.xposed.toutiao.provider.ImageProvider/getimage/origin/eJy1ku0KwiAUhm8l_F3qvuduJSJ0mRO2JtupiNi9Z4MoWiOa65cinMeX57xXVDda6QPKFld0bLQ9UckbJYlR-UpX3N5Smfi5x3JJ934YxWlKWZhEgbeLhBB-QNFyYUfL1s6uUQFgMkKMtwLA4gJSVwrndUWmUP8CC5xhm87izlKY7VDeTgLXZUtOlJzjkP6AxXfiR5eMYdMCB9PHneGHBzh-VzEje7AzV3ZvHYpjJV599w-uZWXvWadQR_vlAhtY_Bn2LKuzu_GGOscc1MfZ4veyTyNuuu4G1giVqQ==/6694469396007485965/3
代码语言:javascript
复制

9

整理下这几个大厂的思路

9.1

针对客户端

  1. 预创建(application onCreate 时)webview
  2. 预创建的同时加载带有css/js的html文本
  3. webview复用池
  4. webview setting的设置
  5. 预取网页并缓存,预先获取html并缓存本地,需要是从缓存中加载即可
  6. 资源拦截并行加载,内核初始化和资源加载同时进行。

9.2

针对服务端

  1. 直出网页的拼装,服务端时获取网页的全部内容,客户端获取后直接加载
  2. 客户端本地html资源的版本控制

9.3

针对网页前端

  1. 删减不必要的js/css
  2. 配合客户端使用VasSonic,只对特定的内容进行页面更新与下载。

10

自己的想法:

  1. 网页秒开的这个需求,如果如果只是客户端来做,感觉只是做了一半,最好还是前后端一起努力来优化。
  2. 但是只做客户端方面的优化也是可以的,笔者实际测试了下,通过预取的方式,的确能做到秒开网页。
  3. 今年就上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    }

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-07-12,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Android补给站 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 无缓存模式流程:
    • 有缓存模式
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档