Webview秒开框架VasSonic源码分析(一)

转载请注明出处:
Webview秒开框架VasSonic源码分析(一)

Webview秒开框架VasSonic源码分析(二)

地址:http://www.jianshu.com/p/802129f77c20

目录

Webview秒开框架VasSonic源码分析(一)_第1张图片
image

起初想把代码贴上来,一点点分析。但是鉴于VasSoinc中Webview和SonicSession并行执行协同交流时,各状态是不确定的,各因素排列组合下来有n多种情况。一个个分析读者会被绕晕,结果反而不好。于是便基于源码,总结性分析,这样更好一些。
特别感谢一下腾讯VasSonic框架负责人之一的陈同学@lovekidchen,感谢他的帮助,帮我理解了不少问题。

1 先说几个概念

说正文之前,先说下webview的几个概念。方便下面的讲解。

1.1 webviewClient#shouldInterceptRequest

public WebResourceResponse shouldInterceptRequest(WebView view,  
String url) {  
     return null;  
}  

webview加载页面整个过程中(包括点击webview中的超链接),当需要加载任何资源请求时,可以通过这个方法拦截。通过返回WebResourceResponse,让webview可以加载本地提供的资源,而不再需要webview自己加载url对应的资源。里面的逻辑需要在非ui线程进行。
这个方法在21之后被废弃了,改为

public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) 

不过使用前面那个也没有问题。

1.2 webviewClient#onPageFinished

public void onPageFinished(WebView view, String url) {
''               
''  }

这个方法在webview成功加载完成html后会被回调。

1.3 JS调用android代码

todo

1.4 webview的初始化包括哪些

webview 的初始化不只是findViewById来获取webview的实例。webview 的初始化包括webview实例化、设置webSettings、设置webViewClient等,如果是第一次初始化webview,那么时间消耗大约是大几百毫秒。webview初始化示例:

// 实例化webview
WebView webView = (WebView) findViewById(R.id.webview);
// 设置webViewClient
webView.setWebViewClient(new WebViewClient() {
    @Override
    public void onPageFinished(WebView view, String url) {
                //
    }
    @TargetApi(21)
    @Override
    public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
        return shouldInterceptRequest(view, request.getUrl().toString());
    }
    @Override
    public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
        if (sonicSession != null) {
        //step 6: Call sessionClient.requestResource when host allow the application
         // to return the local data .
        return (WebResourceResponse) sonicSession.getSessionClient().requestResource(url);
        }
        return null;
        }
    });
//设置webSettings
WebSettings webSettings = webView.getSettings();
webSettings.setJavaScriptEnabled(true);
    webView.removeJavascriptInterface("searchBoxJavaBridge_");  intent.putExtra(SonicJavaScriptInterface.PARAM_LOAD_URL_TIME, System.currentTimeMillis());
        webView.addJavascriptInterface(new SonicJavaScriptInterface(sonicSessionClient, intent), "sonic");
        webSettings.setAllowContentAccess(true);
        webSettings.setDatabaseEnabled(true);
        webSettings.setDomStorageEnabled(true);
        webSettings.setAppCacheEnabled(true);
        webSettings.setSavePassword(false);
        webSettings.setSaveFormData(false);
        webSettings.setUseWideViewPort(true);
        webSettings.setLoadWithOverviewMode(true);

2 VasSonic特点

VasSonic是腾讯开源的、解决webview首屏提速的框架。这里先说下VasSonic的主要特点:

  • 分离webview初始化和web页面主url(html)的请求。
  • 主url(html)请求的数据进行了本地缓存。除了缓存整个html,还对缓存进行了更精细化的分割,分为:
    模板(html string中不经常变动的字符串)和html string中经常变动的字符串。
    首先这么分割的对象是主url对应的html字符串,js/css/图片资源还未请求加载,都以string的形式存在。分割时注意,不一定模板就是css,js,非模板就是数据。有些css和js也经常变动,有些页面数据也不会变。根据业务场景分割html 字符串。分割标记需要前端来支持。如果请求时发现模板没变,只是非模板的数据变了,那么可以实现页面的局部刷新。局部刷新时,需要js调android代码。

3 web界面的两种加载方式 :预加载和常规加载

说web界面进行常规加载之前,先说下web界面的预加载模式。

3.1 web界面预加载方式

3.1.1 预加载整个html

预加载指开发者预先判断哪些页面可能会被用户打开,预加载这些web页面。比如猫眼资讯列表/头条资讯列表等。预加载时先把html从服务器拉下来(都是文本,所以销毁流量少,用户可以接受)。然后等打开这个页面时,使用webview.loadDataWithBaseURL(html)加载页面。之后进行css、js、图片加载时,可以使用webviewClient#shouldInterceptRequest加载本地资源(apk asset目录下可以预置一些资源)。

3.1.2 apk预置模板

如果业务形式像猫眼的feed流那样,所有的资讯几乎模样都是一样的。那么我们甚至可以把html的模板(概念等同于上面提到的模板)放到apk的asset目录下。这样预加载时只加载非模板部分(文字,图片url,css等字符串),等用户打开那一页的时候,拼接非模板数据和模板为整个的html字符串。之后的流程和上面是一样的,使用webview.loadDataWithBaseURL(html)加载页面。之后进行css、js、图片加载时,可以使用webviewClient#shouldInterceptRequest加载本地资源。

3.1.3 web界面采用预加载时VasSonic的使用方式

因为VasSonic分离了webview初始化和主url(html)的请求。所以VasSonic本身支持预加载(调用sonicSession#preload()方法)。

不过,预加载不一定需要VasSonic的参与。即使你不使用VasSonic也可以进行预加载。使用你app中的网络框架将html加载下拉。然后等打开页面时,使用webview.loadDataWithBaseURL(html)进行加载。

说下VasSonic的预加载过程。在没打开页面时,使用sonnicSession#preload来加载数据。把html数据放到本地的某个位置,当打开页面后,使用webview.loadDataWithBaseURL(html)直接加载。(当然html中的css ,js等url对应的资源还没有加载。VasSonic目前的版本并没有对这些副资源进行预加载或这些副资源的本地存储好消息是下一个版本会支持。你可以自己预置一些css、js、图片的资源到apk中,使用WebviewClient#shouldInterceptRequest自己拦截加载)。

3.2 web页面的常规加载过程:

3.2.1 常规加载过程

当我们不使用预加载时,我们打开一个页面,需要一下流程加载页面:
初始化webview->webview进行主框架url加载请求(html)->成功后,进行css,js,图片的请求(图片可以等界面显示时再加载)->显示界面
(图,这里应该有个 图,todo)

3.2.2 web页面常规加载过程中的时间浪费

我们看下可能存在的时间浪费:

  • 首次初始化webview会销毁几百毫秒时间。webview初始化之前是不能进行主html加载的。这个等待时间浪费掉了。
  • 主url(html)请求成功后,才能进行css,js等资源的请求。因为只有等到html获取到以后,才知道css、js、图片的url是什么。这个问题的优化方案可以采用把资源放到本地。前面说了,目前的VasSonic没有对js/css等副资源的加载、缓存做优化。使用webviewClient#onIntercepterRequest来进行本地资源加载。
    综上,我们接下来就看下VasSonic对第一个点是怎么优化的。

3.2.3 webview的初始化和sonicSession的并发进行

对于上面的第一个问题,VasSnoic是这么解决的:webview的初始化和主url(html)请求(之后统称SonicSession)并发进行。

3.2.3.1 webview初始化流程:

webview的初始化主要包括webview的实例化、webviewSetting的设置、webviewClient的设置。示例代码前面已经给出了。这里要说的是,在我们初始化完成后,要手动调用(代码中sonicSessionClient是webview的代理类。因为有可能不同的项目用的webview不一样)

if (sonicSessionClient != null) {
            sonicSessionClient.bindWebView(webView);
            sonicSessionClient.clientReady();
        } 

通知SonicSession webview已经初始化好了。

3.2.3.2 SonicSession数据请求流程:

如果有缓存,先加载缓存(通知webview加载)。然后(不管有没有缓存),从服务器获取html。发送请求获取服务端的html时,VasSonic的第一个版本需要我们在请求中提供一些自定义的header(缓存html的摘要sha1,缓存html模板的摘要sha1)。这样服务端根据这些摘要来判断决定返回整个html或只返回html中非模板的部分(返回的整个html或部分html通知webview刷新)。第一个版本的VasSonic需要后台的参与。为了让框架对后台透明,降低接入难度。VasSonic的2.0在版本中,不需要后台的参与。自己模拟后台的行为。即,在任何时候,都会从服务端拉取整个html,分割成模板和非模板。然后与本地的html的模板和非模板摘要对比。如果整个html都相同,那么什么都不做;如果模板相同,那么通知webview只刷新页面中的非模板区域;如果都不相同,那么通知webview刷新整个页面。

3.2.4 webview的初始化和sonicSession的协同合作

3.2.4.1 协同合作的通信机制和时机

既然webview的初始化和sonicSession请求数据并发进行,那么肯定需要进行通信交流来进行协同合作。那使用什么机制什么时候来通知对方呢?

3.2.4.1.1 webview向SonicSession发送信号
  1. 前面提到了,当webview初始化好了之后,会手动调用sonicSessionClient#clientReady()来通知sonicSession webview已经初始化好了。
  2. 如果没有缓存时,框架内部调用webview.load(url)时(url为html对应的url),webviewClient#shouldInterceptRequest会被触发,会通知sonicSession把已经加载到内存的资源传递给webview,让webview加载。其实质是”webviewClient#shouldInterceptRequest触发后,中断sonicSession的加载,把已经解析到内存的流和节点流包成一个自定义InputStream流“传递给webview,webview加载这个流时会先加载内存中的流,再加载节点流。
  3. 1.0版本中,如果有缓存时先加载缓存。当缓存页面加载完成后,会触发webviewClient#onPageFinished,继而通知sonicSession中断服务器端数据的加载,把已经解析到内存的流和节点流包成一个自定义InputStream流“传递给webview,webview加载这个流时会先加载内存中的流,再加载节点流。不过,在2.0版本中,如果有缓存时,也是先加载缓存。然后启动服务端的数据加载,一直加载直到html加载完成,不会受缓存页面加载成功信号的中断。多说一句,为什么这里会一直加载html直到结束呢?因为2.0版本进行了后台模拟的工作,拉取服务端完整的html和缓存中的html做etag,templetag对比,自己模拟来使response返回相应的header。这样就不需要后台的参与了。也因为这个,所以需要一直加载服务端html直到html加载完成。
    上面三点讲的是webview向SonicSession发送信号。简单来说就是webview初始化好的时候、webviewClient#shouldInterceptRequest被触发时、webviewClient#onPageFinished被触发时。
3.2.4.1.2 SonicSession向webview发送信号

那么SonicSession在加载过程中,也会发送信号给webview,让其加载数据。通过前面的SonicSession工作过程分析,知道sonicSession会先进行缓存加载,然后进行服务端html加载。服务端html加载时(1.0)/加载后(2.0),分析webview需要首次加载还是非模板数据更新还是模板跟新,数据加载和分析逻辑在VasSonic中对应:

    protected abstract void handleFlow_FirstLoad();
    protected abstract void handleFlow_DataUpdate(String serverRsp);
    protected abstract void handleFlow_TemplateChange(String newHtml);

获取到相应的数据后,就需要发送信号通知webview进行加载。因为SonicSeesion的加载数据过程是在异步线程,webview加载数据在主线程。所以需要handler 发送message来通知。我们看一眼VasSonic 的handler处理逻辑:

public boolean handleMessage(Message msg) {

        // fix issue[https://github.com/Tencent/VasSonic/issues/89]
        if (super.handleMessage(msg)) {
            return true; // handled by super class
        }

        if (CLIENT_CORE_MSG_BEGIN < msg.what && msg.what < CLIENT_CORE_MSG_END && !clientIsReady.get()) {
            pendingClientCoreMessage = Message.obtain(msg);
            SonicUtils.log(TAG, Log.INFO, "session(" + sId + ") handleMessage: client not ready, core msg = " + msg.what + ".");
            return true;
        }

        switch (msg.what) {
            case CLIENT_CORE_MSG_PRE_LOAD:
                handleClientCoreMessage_PreLoad(msg);
                break;
            case CLIENT_CORE_MSG_FIRST_LOAD:
                handleClientCoreMessage_FirstLoad(msg);
                break;
            case CLIENT_CORE_MSG_CONNECTION_ERROR:
                handleClientCoreMessage_ConnectionError(msg);
                break;
            case CLIENT_CORE_MSG_SERVICE_UNAVAILABLE:
                handleClientCoreMessage_ServiceUnavailable(msg);
                break;
            case CLIENT_CORE_MSG_DATA_UPDATE:
                handleClientCoreMessage_DataUpdate(msg);
                break;
            case CLIENT_CORE_MSG_TEMPLATE_CHANGE:
                handleClientCoreMessage_TemplateChange(msg);
                break;
            case CLIENT_MSG_NOTIFY_RESULT:
                setResult(msg.arg1, msg.arg2, true);
                break;
            case CLIENT_MSG_ON_WEB_READY: {
                diffDataCallback = (SonicDiffDataCallback) msg.obj;
                setResult(srcResultCode, finalResultCode, true);
                break;
            }

            default: {
                if (SonicUtils.shouldLog(Log.DEBUG)) {
                    SonicUtils.log(TAG, Log.DEBUG, "session(" + sId + ") can not  recognize refresh type: " + msg.what);
                }
                return false;
            }

        }
        return true;
    }

处理的情况还是不少的。因为不确定因素导致协同合作是一个动态的过程。所以协同起来还是很繁琐的。

3.2.4.2 协同合作的动态性

上面讲的是什么时候发送信号,但发送信号后的处理是不确定的。举个例子,当webview初始化完成时,SonicSession可能已经加载好了缓存数据,但可能没加载好网络数据,也可能是其他情况。反过来sonicSession下载好了数据让webview进行加载缓存/刷新模板/模板更新/第一次加载时,webview初始化有么有完成不确定。或者让webview进行模板更新时,webview这时候初始化完成情况不确定,有没有加载缓存不确定。这些不确定因素导致,整个协同工作是一个排列组合多因素动态的过程。所以
如果在这里如果每一种情况都进行分析,显然是不现实的。但是,通读完他们的代码,我有这么一个感觉:他们把能减少的时间等待都利用了,用来做一些其他的事情,或者想办法减少这种等待时间。所以对于协同合作的动态性,我们也可以有个整体的把握。说到这里,其实我在撸他们代码的时候,就越发的佩服腾讯团队做出的这个框架。配合他们的耐心与做事情追求极致的精神。

3.2.4.3 协同合作情况举例

接下来就分析webview和sonicSession通信协同合作的两种常见情况,其他情况可以直接阅读源码进行分析。
既然是常见情况,那么我们做以下两点假设:

  1. SonicSession缓存加载时间比webview的初始化时间要长。
  2. SonicSession服务器加载html的时间比SonicSession缓存加载时间要长。
3.2.4.3.1 本地有缓存时

当本地有缓存时,那么我们分析一下这种情况。

  1. webview初始化与SonicSession同时进行。
  2. SonicSession进行缓存加载,加载好缓存数据以后,封装成message通过handler传递主线程looper 队列中。即,通知webview加载缓存数据。webview这时候还没初始化好,那么会把之前的message再封装成pendingClientCoreMessage存起来。等到webview初始化好以后,会再把这个message传递到主线程looper队列中等待webview加载。
  3. SonicSession开始从服务端加载html数据。如果是2.0的情况并且我们设置使用纯终端模式,那么SonicSession会把服务端的整个html数据全部加载下来(此过程不会被外界中断)。然后模拟得到etag,templeTag等response 的header值。然后通过handler通知webview进行局部刷新/模板更新。这里的具体情况可以看我写的 Webview秒开框架VasSonic源码分析(二)-1.0与2.0版本的不同 。
3.2.4.3.2 本地没有缓存时-“截流加载”

如果本地没有缓存时,我们分析一下这种情况。

  1. webview初始化与SonicSession同时进行。

  2. SonicSession从服务端加载html。

  3. webview初始化好了,这时候服务端的html数据还没加载完成。如果等到html加载完成以后再让webview进行加载,那么这里就有一个等待情况。为了避免这种等待浪费。VasSonic是这么做的:webview初始化好时,会发送一个信号,让SonicSession中断其网络加载。中断时,会把已经加载的数据放到内存的outputSream流和serverRsp 字符串中,然后把内存outputSream流和未加载的节点流包成SonicSessionStream返回。sonicInputstream#read时会先读取内存中的流,再读取节点流中的数据:

      public synchronized InputStream getResponseStream(AtomicBoolean breakConditions) {
             if (readServerResponse(breakConditions)) {
                 BufferedInputStream netStream = !TextUtils.isEmpty(serverRsp) ? null : connectionImpl.getResponseStream();
                 return new SonicSessionStream(this, outputStream, netStream);
             } else {
                 return null;
             }
         }
    

代码中的breakConditions就是外界的中断信号。当内部有调用webview.load(url)时,webviewClient#shouldInterceptRequest会被触发,进而把sonicInputstream传递给webview进行解析渲染。

4 如果前端不介入,使用VasSonic框架的好处

2.0版本不需要后台介入,但需要前端配合来进行局部刷新。如果前端不介入。那么优点就是

  • 首次加载,利用webview初始化的时间进行服务端html的加载,然后webview初始化后,webview“截流加载”。
  • 如果有缓存,那么webview先加载缓存。再进行模板更新,因为没有前端配合,没有模板概念,也没有局部刷新。这时候加载的是新的完整的html。

5 感谢/参考资料

  • VasSonic github 地址

你可能感兴趣的:(Webview秒开框架VasSonic源码分析(一))