http://www.ox-holdings.com

在移动终端初始化的同时并行请求页面主资源并做到流式拦截,完美支持静态直出页面和动态直出页面

摘要VasSonic取名于索尼动画形象音速小子,是腾讯QQ会员 VAS团队研发的一个轻量级的高性能的Hybrid框架,专注于提升页面首屏加载速度,完美支持静态直出页面和动态直出页面,兼容离线包等方案。2017年8月8日正式开源!基本介绍VasSonic取名于索尼动画形象音速小子,是腾讯QQ会员 VAS团队研发的一个轻量级的高性能的Hybrid框架,专注于提升页面首屏加载速度,完美支持静态直出页面和动态直出页面,兼容离线包等方案。目前QQ会员、QQ购物、QQ钱包、企鹅电竞等业务已经在使用,平均日均PV在1.2亿以上,并且这个数字还在快速增长。接入VasSonic后首次打开可以在初始化APP的时候并行请求页面资源,并且具备边加载边渲染的能力。非首次打开时,APP可以快速加载上次打开动态缓存在本地的页面资源,然后动态刷新页面。腾讯手机QQ通过VasSonic框架使得页面首屏耗时平均低于1S以下。使用前后对比(OPPO R9机器,3G环境)使用Sonic模式前:使用Sonic模式后:VasSonic功能特性目前VasSonic框架是市面上支持最为完善的Hybrid框架,完美适用于静态直出页面和动态直出页面。具有以下几大特性:快速:VasSonic通过中间层启动子线程并发拉取页面主资源和流式拦截的方式,支持内核边加载边渲染,弱化终端初始化过程耗时的影响,同时对页面进行动态缓存和增量更新,减少页面对网络数据传输的依赖,极速提升H5页面的加载速度。省流量:VasSonic支持动态缓存页面内容,通过客户端和服务端遵守一定的格式规范,每次请求仅需要返回变动的数据块数据,大大减少响应数据传输。良好的用户体验:通过预推送以及动态缓存页面,VasSonic先加载本地缓存页面,用户可以快速看到内容,即使在无网络场景下,依然能看到首屏内容,让H5页面的体验更加接近原生。易用:VasSonic框架来自腾讯VAS团队超过一年的优化提速总结,它是一整套解决方案,可以快速在Android和iOS平台上接入使用,并且后台支持Node.js和PHP平台一键部署,无须繁琐配置流程。源码托管地址 started with AndroidGetting started with iOSGetting started with Node.jsGetting started with PHPDemo下载Hereis the Android sample demo.Hereis the iOS sample demo.

基本介绍

VasSonic取名于索尼动画形象音速小子,是腾讯QQ会员 VAS团队研发的一个轻量级的高性能的Hybrid框架,专注于提升页面首屏加载速度,完美支持静态直出页面和动态直出页面,兼容离线包等方案。目前QQ会员、QQ购物、QQ钱包、企鹅电竞等业务已经在使用,平均日均PV在1.2亿以上,并且这个数字还在快速增长。

接入VasSonic后首次打开可以在初始化APP的时候并行请求页面资源,并且具备边加载边渲染的能力。非首次打开时,APP可以快速加载上次打开动态缓存在本地的页面资源,然后动态刷新页面。腾讯手机QQ通过VasSonic框架使得页面首屏耗时平均低于1S以下。

2、怎么引入到项目中

https://github.com/Tencent/VasSonic/wiki 这里有官方的Android及IOS引入说明

SessionID

每个sessionID唯一指定了一个SonicSession,sessionID的生成规则如下:

NSString *sonicSessionID(NSString *url)
{
    if ([[SonicClient sharedClient].currentUserUniq length] > 0) {
        return stringFromMD5([NSString stringWithFormat:@"%@_%@",[SonicClient sharedClient].currentUserUniq,sonicUrl(url)]);
    }else{
        return stringFromMD5([NSString stringWithFormat:@"%@",sonicUrl(url)]);
    }
}

每个url都能唯一的确定一个sessionID,需要注意的是,算md5的时候并不是直接拿请求的url来算的,而是先经过了sonicUrl的函数的处理。理解sonicUrl对url的处理有助于我们了解VasSonic的session管理机制。

其实sonicUrl做的事情比较简单。

  • 对于一般的url来说,sonicUrl会只保留scheme、host和path,url其他部分的改变不会创建新的session
  • 新增了sonic_remain_params参数,sonic_remain_params里面指定的query参数不同会创建新的session。

举栗说明:

// output: @"https://www.example.com"
sonicUrl(@"https://www.example.com")

// output: @"https://www.example.com"
sonicUrl(@"https://www.example.com:8080") 

// output: @"https://www.example.com"
sonicUrl(@"https://www.example.com/?foo=foo")  

// output: @"https://www.example.com/path"
sonicUrl(@"https://www.example.com/path?foo=foo")

// output @"https://www.example.com/path/foo=foo&"
sonicUrl(@"https://www.example.com/path?foo=foo&bar=bar&sonic_remain_params=foo")

sonicUrl的代码也比较简单,这里就不贴了,有兴趣的同学可以参考这里sonicUrl实现。

H5以其开发和维护的成本较低,开发周期较短的天然优势满足了APP快速迭代的需求。目前很多APP或多或少接入了H5页面,但H5存在的缺点是加载速度慢,造成不好的用户体验。因此,如何优化H5的加载速度可以有效提升用户的满意度。

源码托管地址

https://github.com/Tencent/vassonic

(3)SonicSessionClient的实现

    public class SonicSessionClientImpl extends SonicSessionClient {

        private WebView webView;

        public void bindWebView(WebView webView) {
            this.webView = webView;
        }

        public WebView getWebView() {
            return webView;
        }

        @Override
        public void loadUrl(String url, Bundle extraData) {
            webView.loadUrl(url);
        }

        @Override
        public void loadDataWithBaseUrl(String baseUrl, String data, String mimeType, String encoding, String historyUrl) {
            webView.loadDataWithBaseURL(baseUrl, data, mimeType, encoding, historyUrl);
        }


        @Override
        public void loadDataWithBaseUrlAndHeader(String baseUrl, String data, String mimeType, String encoding, String historyUrl, HashMap<String, String> headers) {
            loadDataWithBaseUrl(baseUrl, data, mimeType, encoding, historyUrl);
        }

        public void destroy() {
            if (null != webView) {
                webView.destroy();
                webView = null;
            }
        }

    }

SonicSession

讲缓存的时候我们提到过作为缓存Key的sessionID,每个sessionID关联了一个缓存对象SonicCacheItem,同时也关联了一次URL请求,VasSonic将这个请求抽象为SonicSession。SonicSession在VasSonic的设计里面非常关键。其将资源的请求和WebView脱离开来,有了SonicSession,结合SonicCache,我们就可以不依赖WebView去做资源的请求,这样就可以实现WebView打开和资源加载并行、资源预加载等加速方案。

3. SonicSession

SonicSession由请求的url和delegate(WebView的持有者)唯一确定。首先我们看SonicSession何时后会被初始化。

在Sonic iOS提供的官方例子中可以看到,他们推荐在WebView初始化的时候,创建的SonicSession。Session的创建由SonicEngine来完成,代码如下:

- (void)createSessionWithUrl:(NSString *)url withWebDelegate:(id<SonicSessionDelegate>)aWebDelegate withConfiguration:(SonicSessionConfiguration *)configuration
{
...    

 [self.lock lock];
 SonicSession *existSession = self.tasks[sonicSessionID(url)];
 if (existSession && existSession.delegate != nil) {
     //session can only owned by one delegate
     [self.lock unlock];
     return;
 }

 if (!existSession) {
     //创建Session
     existSession = [[SonicSession alloc] initWithUrl:url withWebDelegate:aWebDelegate Configuration:configuration];

     NSURL *cUrl = [NSURL URLWithString:url];
     existSession.serverIP = [self.ipDomains objectForKey:cUrl.host];

     __weak typeof(self) weakSelf = self;
     __weak typeof(existSession)weakSession = existSession;
     [existSession setCompletionCallback:^(NSString *sessionID){
         [weakSession cancel];
         [weakSelf.tasks removeObjectForKey:sessionID];
     }];

     [self.tasks setObject:existSession forKey:existSession.sessionID];
     [existSession start]; //启动Session
     [existSession release];

 } else {

     if (existSession.delegate == nil) {
         existSession.delegate = aWebDelegate;
     }
 }

 [self.lock unlock];
}

可以看到SonicSession是由url唯一确定,并一次只能绑定到WebView上。

Sonic在初始化的时候会尝试从本地缓存中读取数据,如果数据存在,则直接将数据返回给请求着,否则,会等待请求结束后,将数据返回过去,并且将这次请求数据缓存下来。如果服务器最新的数据到达后,会根据返回码来选择性对浏览器已经渲染的视图进行修正。

这里我们需要介绍下Sonic的缓存和更新思路,Sonic将Html代码人为分为模板(Template)和数据(Data)。通过代码注释的方式,增加了“sonicdiff-xxx”来标注一个数据块的开始与结束。模板就是将数据块抠掉之后的Html,然后通过{albums}来表示这个是一个数据块占位。数据就是JSON格式,直接Key-Value。如图是官方一张图

图片 1

由于我们HTML页面模板更新的频率比较低,而HTML需要展示的数据则更新频繁。通过这个思路,就可以实现HTML页面的增量更新。具体思想可以前往VasSonic:手Q开源Hybrid框架介绍。

这样客户端就可以根据服务器返回的请求头来增量更新HTML页面。具体代码如下:

- (void)updateDidSuccess
{
 switch (self.sonicServer.response.statusCode) {//获得Response响应头
     case 304: //完全使用缓存
     {
         self.sonicStatusCode = SonicStatusCodeAllCached;
         self.sonicStatusFinalCode = SonicStatusCodeAllCached;
         //update headers
         [[SonicCache shareCache] saveResponseHeaders:self.sonicServer.response.allHeaderFields withSessionID:self.sessionID];
     }
         break;
     case 200: // Only need to request dynamic data.
     {
         if (![self.sonicServer isSonicResponse]) {
             [[SonicCache shareCache] removeCacheBySessionID:self.sessionID];
             NSLog(@"Clear cache because while not sonic repsonse!");
             break;
         }

         if ([self isTemplateChange]) {

             [self dealWithTemplateChange];

         }else{

             [self dealWithDataUpdate];
         }

         NSString *policy = [self.sonicServer responseHeaderForKey:SonicHeaderKeyCacheOffline];
         if ([policy isEqualToString:SonicHeaderValueCacheOfflineStore] || [policy isEqualToString:SonicHeaderValueCacheOfflineStoreRefresh] || [policy isEqualToString:SonicHeaderValueCacheOfflineRefresh]) {
             [[SonicCache shareCache] removeServerDisableSonic:self.sessionID];
         }

         if ([policy isEqualToString:SonicHeaderValueCacheOfflineRefresh] || [policy isEqualToString:SonicHeaderValueCacheOfflineDisable]) {

             if ([policy isEqualToString:SonicHeaderValueCacheOfflineRefresh]) {

                 [[SonicCache shareCache]removeCacheBySessionID:self.sessionID];
             }

             if ([policy isEqualToString:SonicHeaderValueCacheOfflineDisable]) {
                 [[SonicCache shareCache] saveServerDisableSonicTimeNow:self.sessionID];
             }
         }

     }
         break;
     default:
     {

     }
         break;
 }

 //use the call back to tell web page which mode used
 if (self.webviewCallBack) {
     NSDictionary *resultDict = [self sonicDiffResult];
     if (resultDict) {
         self.webviewCallBack(resultDict);
     }
 }
 ...
}

由以上代码可以看到,首页会判断服务器返回response的状态码,304表示HTML的模板和数据均没有更新,直接使用缓存的数据。这个时候客服端不需要进行任何操作。

如果是200,则需要判断是模板更新还是数据更新。接下来我们具体看下数据更新和模板更新会做什么操作:

  • 数据更新函数代码如下:
   - (void)dealWithDataUpdate
{
    NSString *htmlString = nil;
    if (self.sonicServer.isInLocalServerMode) {
        NSDictionary *serverResult = [self.sonicServer sonicItemForCache];
        htmlString = serverResult[kSonicHtmlFieldName];
    }

    SonicCacheItem *cacheItem = [[SonicCache shareCache] updateWithJsonData:self.sonicServer.responseData withHtmlString:htmlString withResponseHeaders:self.sonicServer.response.allHeaderFields withUrl:self.url];

    if (cacheItem) {

        self.sonicStatusCode = SonicStatusCodeDataUpdate;
        self.sonicStatusFinalCode = SonicStatusCodeDataUpdate;
        self.localRefreshTime = cacheItem.lastRefreshTime;
        self.cacheFileData = cacheItem.htmlData;
        self.cacheResponseHeaders = cacheItem.cacheResponseHeaders;

        if (_diffData) {
            [_diffData release];
            _diffData = nil;
        }
        _diffData = [cacheItem.diffData copy];

        self.isDataFetchFinished = YES;
    }
}

这里主要工作是取出缓存的html模板,然后将更新后的数据和html模板进行合并。生成新的cacheItem,随后更新本地数据。这里更新数据的格式是JSON格式,key为elementTag Id,value为tag显示的数据。可能有人会有疑惑,最新的数据怎么更新浏览器的显示。我们看到上面updateDidSuccess函数末尾有一段代码。调用了webViewCallback回调,这个操作就完成了将更新的数据通过native调用js的方式来更新数据。

  • Html模板更新:
  - (void)dealWithTemplateChange
{
   NSDictionary *serverResult = [self.sonicServer sonicItemForCache];
   SonicCacheItem *cacheItem = [[SonicCache shareCache] saveHtmlString:serverResult[kSonicHtmlFieldName] templateString:serverResult[kSonicTemplateFieldName] dynamicData:serverResult[kSonicDataFieldName] responseHeaders:self.sonicServer.response.allHeaderFields withUrl:self.url];//更新缓存

   if (cacheItem) {

       self.sonicStatusCode = SonicStatusCodeTemplateUpdate;
       self.sonicStatusFinalCode = SonicStatusCodeTemplateUpdate;
       self.localRefreshTime = cacheItem.lastRefreshTime;
       self.cacheFileData = self.sonicServer.responseData;
       self.cacheResponseHeaders = cacheItem.cacheResponseHeaders;

       self.isDataFetchFinished = YES;

       if (!self.didFinishCacheRead) {
           return;
       }

       NSString *opIdentifier  =  dispatchToMain(^{
           NSString *policy = [self.sonicServer responseHeaderForKey:SonicHeaderKeyCacheOffline];
           if ([policy isEqualToString:SonicHeaderValueCacheOfflineStoreRefresh] || [policy isEqualToString:SonicHeaderValueCacheOfflineRefresh]) {
               if (self.delegate && [self.delegate respondsToSelector:@selector(session:requireWebViewReload:)]) { //通知浏览器重新加载页面
                   NSURLRequest *sonicRequest = [NSURLRequest requestWithURL:[NSURL URLWithString:self.url]];
                   [self.delegate session:self requireWebViewReload:[SonicUtil sonicWebRequestWithSession:self withOrigin:sonicRequest]];
               }
           }
       });
       [self.mainQueueOperationIdentifiers addObject:opIdentifier];
   }
}

由于模板更新会更新整个网页结构,因此,避免不了需要重新刷新浏览器。首先会解析出返回htmlString中包含的模板和数据,然后分别进行存储,并更新本地变量。最后利用 [self.delegate session:self requireWebViewReload:[SonicUtil sonicWebRequestWithSession:self withOrigin:sonicRequest]]; 这句代码来通知浏览器刷新。

以上分析的是存在缓存,处理的结果,如果是第一次加载页面,那么就不会走这个策略。直接通知浏览器渲染并存储网页数据。比较简单,这里就不再赘述。

VasSonic功能特性

目前VasSonic框架是市面上支持最为完善的Hybrid框架,完美适用于静态直出页面和动态直出页面。

具有以下几大特性:

快速:VasSonic通过中间层启动子线程并发拉取页面主资源和流式拦截的方式,支持内核边加载边渲染,弱化终端初始化过程耗时的影响,同时对页面进行动态缓存和增量更新,减少页面对网络数据传输的依赖,极速提升H5页面的加载速度。

省流量:VasSonic支持动态缓存页面内容,通过客户端和服务端遵守一定的格式规范,每次请求仅需要返回变动的数据块数据,大大减少响应数据传输。

良好的用户体验:通过预推送以及动态缓存页面,VasSonic先加载本地缓存页面,用户可以快速看到内容,即使在无网络场景下,依然能看到首屏内容,让H5页面的体验更加接近原生。

易用:VasSonic框架来自腾讯VAS团队超过一年的优化提速总结,它是一整套解决方案,可以快速在Android和iOS平台上接入使用,并且后台支持Node.js和PHP平台一键部署,无须繁琐配置流程。

说明:我这里只分析了预加载及首次加载。

首先是

SonicSessionConfig sessionConfig = new SonicSessionConfig.Builder().build();

内部实现其实就是创建了一个默认的SonicSessionConfig对象,这个类的作用是设置超时时间、缓存大小等相关参数。
然后调用SonicEngine.getInstance().preCreateSession(DEMO_URL, sessionConfig);
我们看看SonicEngine的preCreateSession方法的实现:

    public synchronized boolean preCreateSession(@NonNull String url, @NonNull SonicSessionConfig sessionConfig) {
             // 创建sessionId
            String sessionId = makeSessionId(url, sessionConfig.IS_ACCOUNT_RELATED); 
            if (!TextUtils.isEmpty(sessionId)) {
                  // 判断session缓存是否过期,以及sessionConfig是否发生变化 如果满足则从preloadSessionPool中移除
                SonicSession sonicSession = lookupSession(sessionConfig, sessionId, false);
                if (null != sonicSession) {
                    runtime.log(TAG, Log.ERROR, "preCreateSession:sessionId(" + sessionId + ") is already in preload pool.");
                    return false;
                }
                if (preloadSessionPool.size() < config.MAX_PRELOAD_SESSION_COUNT) {
                    if (isSessionAvailable(sessionId) && runtime.isNetworkValid()) {
                            // 创建session
                        sonicSession = internalCreateSession(sessionId, url, sessionConfig);
                        if (null != sonicSession) {
                            preloadSessionPool.put(sessionId, sonicSession);
                            return true;
                        }
                    }
                } else {
                    runtime.log(TAG, Log.ERROR, "create id(" + sessionId + ") fail for preload size is bigger than " + config.MAX_PRELOAD_SESSION_COUNT + ".");
                }
            }
            return false;
        }

如上创建session的过程重要的已经注释了,里面的核心语句是 sonicSession = internalCreateSession(sessionId, url, sessionConfig);接着我们看下相关源码:

        private SonicSession internalCreateSession(String sessionId, String url, SonicSessionConfig sessionConfig) {
            if (!runningSessionHashMap.containsKey(sessionId)) {
                SonicSession sonicSession;
                if (sessionConfig.sessionMode == SonicConstants.SESSION_MODE_QUICK) {
                    sonicSession = new QuickSonicSession(sessionId, url, sessionConfig);
                } else {
                    sonicSession = new StandardSonicSession(sessionId, url, sessionConfig);
                }
                sonicSession.addCallback(sessionCallback);

                if (sessionConfig.AUTO_START_WHEN_CREATE) {
                    sonicSession.start();
                }
                return sonicSession;
            }
            if (runtime.shouldLog(Log.ERROR)) {
                runtime.log(TAG, Log.ERROR, "internalCreateSession error:sessionId(" + sessionId + ") is running now.");
            }
            return null;
        }

这个函数中主要是判断seesionConfig的sessionMode参数,从而创建具体的SonicSession对象,并将 sessionId, url, sessionConfig 作为构造参数。之后调用sonicSession.start();。我们接着来看SonicSession的start方法:

    public void start() {
            if (!sessionState.compareAndSet(STATE_NONE, STATE_RUNNING)) {
                SonicUtils.log(TAG, Log.DEBUG, "session(" + sId + ") start error:sessionState=" + sessionState.get() + ".");
                return;
            }

            SonicUtils.log(TAG, Log.INFO, "session(" + sId + ") now post sonic flow task.");

            statistics.sonicStartTime = System.currentTimeMillis();

            isWaitingForSessionThread.set(true);

            SonicEngine.getInstance().getRuntime().postTaskToSessionThread(new Runnable() {
                @Override
                public void run() {
                    runSonicFlow();
                }
            });

            notifyStateChange(STATE_NONE, STATE_RUNNING, null);
        }

这个方法核心是 在SonicRuntime中创建子线程从而执行runSonicFlow();方法。接着我们来看子线程调用runSonicFlow方法:

    private void runSonicFlow() {
            ......  

            String htmlString = SonicCacheInterceptor.getSonicCacheData(this);

            boolean hasHtmlCache = !TextUtils.isEmpty(htmlString);

            ......  
            handleLocalHtml(htmlString);

            final SonicRuntime runtime = SonicEngine.getInstance().getRuntime();
            if (!runtime.isNetworkValid()) {
                //Whether the network is available
                if (hasHtmlCache && !TextUtils.isEmpty(config.USE_SONIC_CACHE_IN_BAD_NETWORK_TOAST)) {
                    runtime.postTaskToMainThread(new Runnable() {
                        @Override
                        public void run() {
                            if (clientIsReady.get() && !isDestroyedOrWaitingForDestroy()) {
                                runtime.showToast(config.USE_SONIC_CACHE_IN_BAD_NETWORK_TOAST, Toast.LENGTH_LONG);
                            }
                        }
                    }, 1500);
                }
                SonicUtils.log(TAG, Log.ERROR, "session(" + sId + ") runSonicFlow error:network is not valid!");
            } else {
                handleFlow_Connection(htmlString);
                statistics.connectionFlowFinishTime = System.currentTimeMillis();
            }

            ......
        }

在这个方法中首先通过调用SonicCacheInterceptor.getSonicCacheData(this);拿到缓存,但是因为是第一次加载所以htmlString为null,然后 调用handleLocalHtml()方法,这个方法是抽象的,因此我们找到QuickSonicSession这个具体类,来看一下实现:

    protected void handleLocalHtml(String localHtml) {
            Message msg = mainHandler.obtainMessage(CLIENT_CORE_MSG_PRE_LOAD);
            if (!TextUtils.isEmpty(localHtml)) {
                msg.arg1 = PRE_LOAD_WITH_CACHE;
                msg.obj = localHtml;
            } else {
                SonicUtils.log(TAG, Log.INFO, "session(" + sId + ") runSonicFlow has no cache, do first load flow.");
                msg.arg1 = PRE_LOAD_NO_CACHE;
            }
            mainHandler.sendMessage(msg);
        }

代码比较少,其实就是发送了一个CLIENT_CORE_MSG_PRE_LOAD + PRE_LOAD_NO_CACHE 消息给主线程。接着我们来看主线程对这个消息的处理,即QuickSonicSession的handleMessage方法:

    public boolean handleMessage(Message msg) {

            super.handleMessage(msg);

            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;
        }

这个方法主要判断当前客户端webview是否已经初始化完毕,即clientIsReady.get()的返回值是否为真,而这个值则是在我们插入代码的最后调用SnoicSession.onClientReady中更新值得。之后会分析,假如此时webview没有初始化完成,则我们将这个消息赋值给pendingClientCoreMessage,如果初始化已经好了,则调用handleClientCoreMessage_PreLoad,这里我们假设还没有初始化完成,即把消息给了pendingClientCoreMessage。到这里主线程接受消息做了介绍,然后回到子线程runSonicFlow方法继续看,已经忘记这个方法的盆友回头再去看一眼。我们说完了handleLocalHtml方法。然后来看runSonicFlow中调用的另一个核心代码,handleFlow_Connection(htmlString);

    protected void handleFlow_Connection(String htmlString) {
            ......

            sessionConnection = SonicSessionConnectionInterceptor.getSonicSessionConnection(this, intent);

            // connect
            long startTime = System.currentTimeMillis();
            int responseCode = sessionConnection.connect();
            if (SonicConstants.ERROR_CODE_SUCCESS == responseCode) {
                statistics.connectionConnectTime = System.currentTimeMillis();
                if (SonicUtils.shouldLog(Log.DEBUG)) {
                    SonicUtils.log(TAG, Log.DEBUG, "session(" + sId + ") connection connect cost = " + (System.currentTimeMillis() - startTime) + " ms.");
                }

                startTime = System.currentTimeMillis();
                responseCode = sessionConnection.getResponseCode();
                statistics.connectionRespondTime = System.currentTimeMillis();
                if (SonicUtils.shouldLog(Log.DEBUG)) {
                    SonicUtils.log(TAG, Log.DEBUG, "session(" + sId + ") connection response cost = " + (System.currentTimeMillis() - startTime) + " ms.");
                }

                // If the page has set cookie, sonic will set the cookie to kernel.
                startTime = System.currentTimeMillis();
                Map<String, List<String>> HeaderFieldsMap = sessionConnection.getResponseHeaderFields();
                if (SonicUtils.shouldLog(Log.DEBUG)) {
                    SonicUtils.log(TAG, Log.DEBUG, "session(" + sId + ") connection get header fields cost = " + (System.currentTimeMillis() - startTime) + " ms.");
                }

                if (null != HeaderFieldsMap) {
                    String keyOfSetCookie = null;
                    if (HeaderFieldsMap.containsKey("Set-Cookie")) {
                        keyOfSetCookie = "Set-Cookie";
                    } else if (HeaderFieldsMap.containsKey("set-cookie")) {
                        keyOfSetCookie = "set-cookie";
                    }
                    if (!TextUtils.isEmpty(keyOfSetCookie)) {
                        List<String> cookieList = HeaderFieldsMap.get(keyOfSetCookie);
                        SonicEngine.getInstance().getRuntime().setCookie(getCurrentUrl(), cookieList);
                    }
                }
            }

            ......

            if (TextUtils.isEmpty(htmlString)) {
                handleFlow_FirstLoad(); // first mode
            } else {
                ......
            }

            saveHeaders(sessionConnection);
        }

这个方法首先调用SonicSessionConnectionInterceptor.getSonicSessionConnection(this, intent);拿到SessionConnection的具体对象,然后调用sessionConnection的connect方法,connect方法只是简单的调用了internalConnect()方法,这个方法是抽象的,因此我们需要到具体的实现类即SessionConnectionDefaultImpl内部类中查看 :

    protected synchronized int internalConnect() {
                URLConnection urlConnection = getConnection();
                if (urlConnection instanceof HttpURLConnection) {
                    HttpURLConnection httpURLConnection = (HttpURLConnection) urlConnection;
                    try {
                        httpURLConnection.connect();
                        return SonicConstants.ERROR_CODE_SUCCESS;
                    } catch (Throwable e) {
                    ......
                    }
                }
                return SonicConstants.ERROR_CODE_UNKNOWN;
            }

这里首先调用了getConnection()方法,我们紧接着看一下这个方法:

    private URLConnection getConnection() {
                if (null == connectionImpl) {
                    connectionImpl = createConnection();
                    if (null != connectionImpl) {
                        String currentUrl = session.srcUrl;
                        SonicSessionConfig config = session.config;
                        connectionImpl.setConnectTimeout(config.CONNECT_TIMEOUT_MILLIS);
                        connectionImpl.setReadTimeout(config.READ_TIMEOUT_MILLIS);
                        /**
                         *  {@link SonicSessionConnection#CUSTOM_HEAD_FILED_ACCEPT_DIFF} is need to be set If client needs incrementally updates. <br>
                         *  <p><b>Note: It doesn't support incrementally updated for template file.</b><p/>
                         */
                        connectionImpl.setRequestProperty(CUSTOM_HEAD_FILED_ACCEPT_DIFF, config.ACCEPT_DIFF_DATA ? "true" : "false");

                        String eTag = intent.getStringExtra(CUSTOM_HEAD_FILED_ETAG);
                        if (null == eTag) eTag = "";
                        connectionImpl.setRequestProperty("If-None-Match", eTag);

                        String templateTag = intent.getStringExtra(CUSTOM_HEAD_FILED_TEMPLATE_TAG);
                        if (null == templateTag) templateTag = "";
                        connectionImpl.setRequestProperty(CUSTOM_HEAD_FILED_TEMPLATE_TAG, templateTag);

                        connectionImpl.setRequestProperty("method", "GET");
                        connectionImpl.setRequestProperty("accept-Charset", "utf-8");
                        connectionImpl.setRequestProperty("accept-Encoding", "gzip");
                        connectionImpl.setRequestProperty("accept-Language", "zh-CN,zh;");
                        connectionImpl.setRequestProperty(CUSTOM_HEAD_FILED_SDK_VERSION, "Sonic/" + SonicConstants.SONIC_VERSION_NUM);

                        SonicRuntime runtime = SonicEngine.getInstance().getRuntime();
                        String cookie = runtime.getCookie(currentUrl);
                        if (!TextUtils.isEmpty(cookie)) {
                            connectionImpl.setRequestProperty("cookie", cookie);
                        } else {
                            SonicUtils.log(TAG, Log.ERROR, "create UrlConnection cookie is empty");
                        }
                        String userAgent = runtime.getUserAgent();
                        if (!TextUtils.isEmpty(userAgent)) {
                            userAgent += " Sonic/" + SonicConstants.SONIC_VERSION_NUM;
                        } else {
                            userAgent = "Sonic/" + SonicConstants.SONIC_VERSION_NUM;
                        }
                        connectionImpl.setRequestProperty("User-Agent", userAgent);
                    }
                }
                return connectionImpl;
            }

这里主要是创建URLConnection,并设置header,包括UserAgent和其他如cookie等的头部信息。接着我们回到handleFlow_Connection方法中,再接着看另一个核心代码 handleFlow_FirstLoad();

    protected void handleFlow_FirstLoad() {
            SonicSessionConnection.ResponseDataTuple responseDataTuple = sessionConnection.getResponseData(wasInterceptInvoked, null);
            if (null == responseDataTuple) {
                SonicUtils.log(TAG, Log.ERROR, "session(" + sId + ") handleFlow_FirstLoad error:responseDataTuple is null!");
                return;
            }

            pendingWebResourceStream = new SonicSessionStream(this, responseDataTuple.outputStream, responseDataTuple.responseStream);

            String htmlString = null;
            if (responseDataTuple.isComplete) {
                try {
                    htmlString = responseDataTuple.outputStream.toString("UTF-8");
                } catch (Throwable e) {
                    pendingWebResourceStream = null;
                    SonicUtils.log(TAG, Log.ERROR, "session(" + sId + ") handleFlow_FirstLoad error:" + e.getMessage() + ".");
                }
            }

            boolean hasCacheData = !TextUtils.isEmpty(htmlString);
            SonicUtils.log(TAG, Log.INFO, "session(" + sId + ") handleFlow_FirstLoad:hasCacheData=" + hasCacheData + ".");

            mainHandler.removeMessages(CLIENT_CORE_MSG_PRE_LOAD);
            Message msg = mainHandler.obtainMessage(CLIENT_CORE_MSG_FIRST_LOAD);
            msg.obj = htmlString;
            msg.arg1 = hasCacheData ? FIRST_LOAD_WITH_CACHE : FIRST_LOAD_NO_CACHE;
            mainHandler.sendMessage(msg);

            ......
        }

这个方法核心代码是第一句,调用sessionConnection.getResponseData()方法。我们来看一下

    public synchronized ResponseDataTuple getResponseData(AtomicBoolean breakCondition, ByteArrayOutputStream outputStream) {
            BufferedInputStream responseStream = getResponseStream();
            if (null != responseStream) {
                if (null == outputStream) {
                    outputStream = new ByteArrayOutputStream();
                }
                byte[] buffer = new byte[session.config.READ_BUF_SIZE];
                try {
                    int n = 0;
                    while (!breakCondition.get() && -1 != (n = responseStream.read(buffer))) {
                        outputStream.write(buffer, 0, n);
                    }
                    ResponseDataTuple responseDataTuple = new ResponseDataTuple();
                    responseDataTuple.responseStream = responseStream;
                    responseDataTuple.outputStream = outputStream;
                    responseDataTuple.isComplete = -1 == n;
                    return responseDataTuple;
                } catch (Throwable e) {
                    SonicUtils.log(TAG, Log.ERROR, "getResponseData error:" + e.getMessage() + ".");
                }
            }
            return null;
        }

首先我们应该明确这个方法现在还是在子线程中,然后其实主要就是获得outputStream并放在responseDataTuple中返回。但是这里注意breakCondition.get()方法,当breakCondition的值为true时,会终止读取流,brekCondition就是传入的wasInterceptInvoked,而wasInterceptInvoked从何而来呢,它是SonicSession的一个成员,默认值为false。那么这个值什么时候会为true呢,我们插入的代码中对webviewClient.shouldInterceptRequest做了重写,然后调用了sonicSession.getSessionClient().requestResource(url)方法,在这个方法中对wasInterceptInvoked设置为true,为什么要这么做呢,主要是webview如果开始拦截资源的请求,则应该将还未读完的流和已读取的拼接成SonicSessionStream,继而赋给pendingWebResourceStream,这个流在onClientRequestResource中进而封装成webResourceResponse然后返回给shouldInterceptRequest。完成资源的预加载流给webview。

然后我们来看一下当webview初始化结束时,UI线程调用 QuickSonicSession.onClientReady方法:

    public boolean onClientReady() {
        if (clientIsReady.compareAndSet(false, true)) {
            SonicUtils.log(TAG, Log.INFO, "session(" + sId + ") onClientReady: have pending client core message ? -> " + (null != pendingClientCoreMessage) + ".");
            if (null != pendingClientCoreMessage) {
                Message message = pendingClientCoreMessage;
                pendingClientCoreMessage = null;
                handleMessage(message);
            } else if (STATE_NONE == sessionState.get()) {
                start();
            }
            return true;
        }
        return false;
    }

这个方法将clientIsReady的值置为true,表示webview已经初始化完成,然后post之前Message,就是当时handleFlow_FirstLoad方法发出的Message,当时由于webview没有初始化,所以讲Message保存在这个时候再发送出去,接着通过handleMessage调用handleClientCoreMessage_FirstLoad方法:

   private void handleClientCoreMessage_FirstLoad(Message msg) {
        switch (msg.arg1) {
            case FIRST_LOAD_NO_CACHE: {
                if (wasInterceptInvoked.get()) {
                    SonicUtils.log(TAG, Log.INFO, "session(" + sId + ") handleClientCoreMessage_FirstLoad:FIRST_LOAD_NO_CACHE.");
                    setResult(SONIC_RESULT_CODE_FIRST_LOAD, SONIC_RESULT_CODE_FIRST_LOAD, true);
                } else {
                    SonicUtils.log(TAG, Log.ERROR, "session(" + sId + ") handleClientCoreMessage_FirstLoad:url was not invoked.");
                }
            }
            break;
            case FIRST_LOAD_WITH_CACHE: {
                if (wasLoadUrlInvoked.compareAndSet(false, true)) {
                    SonicUtils.log(TAG, Log.INFO, "session(" + sId + ") handleClientCoreMessage_FirstLoad:oh yeah, first load hit 304.");
                    sessionClient.loadDataWithBaseUrlAndHeader(currUrl, (String) msg.obj, "text/html", "utf-8", currUrl, getHeaders());
                    setResult(SONIC_RESULT_CODE_FIRST_LOAD, SONIC_RESULT_CODE_HIT_CACHE, false);
                } else {
                    SonicUtils.log(TAG, Log.INFO, "session(" + sId + ") FIRST_LOAD_WITH_CACHE load url was invoked.");
                    setResult(SONIC_RESULT_CODE_FIRST_LOAD, SONIC_RESULT_CODE_FIRST_LOAD, true);
                }
            }
            break;
        }
    }

这时因为之前已经提前加载过url了,所以FIRST_LOAD_WITH_CACHE为true,接着调用sessionClient.loadDataWithBaseUrlAndHeader(currUrl, (String) msg.obj, "text/html", "utf-8", currUrl, getHeaders());方法实际就是调用webview的loadDataWithBaseURL方法。

网络连接

VasSonic默认提供了基于URLSession的SonicConnection来发起请求和处理响应。SonicConnection做的事情并不多,主要实现了两个接口,并提供SonicSessionProtocol定义的网络回调接口供session处理。

- (void)startLoading; // 开始请求
- (void)stopLoading;  // 取消请求

// SonicSessionProtocol
// 收到响应的时候回调
- (void)session:(SonicSession *)session didRecieveResponse:(NSHTTPURLResponse *)response;
// 加载数据之后回调
- (void)session:(SonicSession *)session didLoadData:(NSData *)data;
// 连接错误的时候回调
- (void)session:(SonicSession *)session didFaild:(NSError *)error;
// 结束加载的时候回调
- (void)sessionDidFinish:(SonicSession *)session;

如果需要在发起请求和处理响应阶段做一些自定义的动作的话,比如实现离线包方案等等,就可以自定义继承于SonicConnection的Connection对象,在回调SonicSessionProtocol方法之前做些处理。

注册自定义的Connection对象使用如下的方法,可以同时注册多个,通过实现canInitWithRequest:来决定使用哪个Connection。

+ (BOOL)registerSonicConnection:(Class)connectionClass;
+ (void)unregisterSonicConnection:(Class)connectionClass;

值得注意的是,SonicConnection的所有接口设计都类似NSURLProtocol协议,但他并不继承自NSURLProtocol,原因在本文最后WebView请求拦截部分会有提到。

首屏就是指用户在没有滚动时候看到的内容渲染完成并且可以交互的时间。至于加载时间,则是整个页面滚动到底部,所有内容加载完毕并可交互的时间。

开发者指南

Getting started with Android

Getting started with iOS

Getting started with Node.js

Getting started with PHP

(4)SonicJavaScriptInterface的实现

这个类根据自己情况,如果用了jsbridge等库,则这个类不用实现。

SonicCacheItem

每个缓存Key,也就是根据url生成的sessionID都会对应一个SonicCacheItem的实例,用来缓存所有的数据。SonicCacheItem也就是一个缓存的数据结构,包含htmlData、templateString、dynamicData、diffData等等。

/**
 * Memory cache item.
 */
@interface SonicCacheItem : NSObject

/** Html. */
@property (nonatomic,retain)NSData         *htmlData;

/** Config. */
@property (nonatomic,retain)NSDictionary   *config;

/** Session. */
@property (nonatomic,readonly)NSString     *sessionID;

/** Template string. */
@property (nonatomic,copy)  NSString       *templateString;

/** Generated by local dynamic data and server dynamic data. */
@property (nonatomic,retain)NSDictionary   *diffData;

/** Sonic divide HTML to tepmlate and dynamic data.  */
@property (nonatomic,retain)NSDictionary   *dynamicData;

/** Is there file cache exist. */
@property (nonatomic,readonly)BOOL         hasLocalCache;

/** Last refresh time.  */
@property (nonatomic,readonly)NSString     *lastRefreshTime;

/** Cache some header fields which will be used later. */
@property (nonatomic,readonly)NSDictionary *cacheResponseHeaders;

/** Initialize an item with session id. */
- (instancetype)initWithSessionID:(NSString *)aSessionID;

@end

看一个开源库,我通常会摸清楚其类层次关系,从整体把握其组件,然后在抽茧剥丝。不然会像走入一个迷宫,有种“只在此山中,云深不知处”的感觉。

郑重声明:本文版权归新匍京a奥门-最全网站手机版app官方下载所有,转载文章仅为传播更多信息之目的,如作者信息标记有误,请第一时间联系我们修改或删除,多谢。