VasSonic 3.0.0 源码解读

2018-04-25  本文已影响0人  _ivanC

VasSonic 其实在我看来比较亮点有两个地方

让我们来一步一步分析它的工作原理

初始化

Demo在 AppDelegate 上提前做了一些初始化

// 注册网络层接管
[NSURLProtocol registerClass:[SonicURLProtocol class]];
   
//start web thread
UIWebView *webPool = [[UIWebView alloc]initWithFrame:CGRectZero];
[webPool loadHTMLString:@"" baseURL:nil];

这里其实也是为了去掉UIWebView的第一次初始化的开销,实际情况上我们可以按需选择时机来做这一步

优化UIWebView的创建开销

VasSonic 的资源加载和UIWebView的生命周期是 分离

从代码上不难发现 SonicWebViewController 就是一层 WebView 的包装,在 init 的时候其实就发起了目标网页的请求

- (instancetype)initWithUrl:(NSString *)aUrl useSonicMode:(BOOL)isSonic unStrictMode:(BOOL)state
{
    ......
    
    [[SonicEngine sharedEngine] createSessionWithUrl:self.url withWebDelegate:self];
    
    ......
}

而在 loadView 的地方才真正创建 UIWebView

- (void)loadView
{
    ......
    
    self.webView = [[UIWebView alloc]initWithFrame:self.view.bounds];
    
    ......
   
    [self.webView loadRequest:[SonicUtil sonicWebRequestWithSession:session withOrigin:request]];
    
    ......
}

换句话说,这两个行为可以算是并行的,这样的话就连 UIWebView重复创建的时间都没有浪费,已经开始请求资源了

从美团的技术文章WebView性能、体验分析与优化,我们可以看到,重复创建WebView是有一定的时间开销的

而且从 UIWebView 的使用层面来看,几乎没有任何改变,只是在要发起的 requestHeader 多加了一些特殊标识(主要为了网络层拦截判断),可以说很方便第三方接入了

SonicEngine

这里出现了一个 SonicEngine ,它其实是一个中央的调度者,既负责一些配置的读取,操作的控制(例如缓存),ip映射表(让使用者自行优化dns的开销),还负责维护 SonicSession 队列,通过delegate和session绑定

SonicSession

对于资源加载的控制,实际上由 SonicSession 完成,一个 Session 对应着一个主文档的加载

先看看它的组成部分

一般的流程是


  1. 查询一下是否存在缓存,来置位是否 首次加载
  2. 做host的ip映射(如果有的话)
  3. 如果是 首次加载 的话,会尝试在缓存的Response里面查询 子资源地址list,如果存在则会直接开始加载子资源(在主文档之前开始,和前面的并行类似)
  4. 在真正发起请求之前,还会检查一下缓存是否已经过期,以及同步Cookies,保证一些状态的正确

SonicConnection

真正发起请求连接的是 SonicConnection,但是如前面所说它只是一层封装,SonicServer 其实也只做了两件事情

  1. isFirstLoadRequest,以保证WebView的边加载边解析渲染的特性没有丢掉
  2. isInLocalServerMode,需要通过参数 enableLocalSever 来激活

回调交互

一些业务逻辑,以及和URLProtocol的交互,都可以在请求回调上找到,我们来看看经典的几个回调,这里简化了代码

- (void)server:(SonicServer *)server didRecieveResponse:(NSHTTPURLResponse *)response
{
    // 从Response头部信息获取子资源列表,进行预加载
    [self preloadSubResourceWithResponseHeaders:response.allHeaderFields];

    // 同步Cookies
    dispatchToMain(^{
            NSArray *cookiesFromResp = [NSHTTPCookie cookiesWithResponseHeaderFields:response.allHeaderFields forURL:response.URL];
            [[NSHTTPCookieStorage sharedHTTPCookieStorage] setCookies:cookiesFromResp forURL:response.URL mainDocumentURL:self.sonicServer.request.mainDocumentURL];
        });

    
    if (self.isFirstLoad) {
        // 如果是首次加载的话,通知protocol处理
      [self firstLoadRecieveResponse:response];
    }else{
        // 只是记录response headers
      if ([self.sonicServer isSonicResponse] && !self.configuration.enableLocalServer) {
            self.cacheResponseHeaders = response.allHeaderFields;
      }
      if (self.configuration.enableLocalServer) {
          self.cacheResponseHeaders = response.allHeaderFields;
      }
    }
}

- (void)server:(SonicServer *)server didReceiveData:(NSData *)data
{
    dispatch_block_t opBlock = ^{
        if (self.isFirstLoad) {
              // 如果是首次加载的话,通知protocol处理
            [self firstLoadDidLoadData:data];
        }
    };
}

- (void)server:(SonicServer *)server didCompleteWithError:(NSError *)error
{
    // 设置完成标识
    self.isCompletion = YES;
   
   if (self.isFirstLoad) {
        // 如果是首次加载的话,通知protocol处理,并通知engine清理
        [self firstLoadDidFaild:error];
    } else {
       // 通知engine进行清理工作
       [self updateDidFaild];
    }
}

- (void)serverDidCompleteWithoutError:(SonicServer *)server
{
     self.isCompletion = YES;
     
     if (self.isFirstLoad) {
          // 如果是首次加载的话,通知protocol处理,并通知engine清理
        [self firstLoadDidSuccess];
    } else {
          // 对处理返回的结果,更新缓存数据,diff通知前端等,并通知engine清理
        [self updateDidSuccess];
      }
        
    //更新缓存时间
    if (self.configuration.supportCacheControl) {
            
            [[SonicCache shareCache] updateCacheExpireTimeWithResponseHeaders:self.sonicServer.response.allHeaderFields withSessionID:self.sessionID];

    }
}

这样一看,好像和WebView没有什么关联,就像是纯粹的下载保存模块而已?

SonicURLProtocol

实际上这里有一个巧妙的绑定,前面提到的Session的主请求,以及resoureLoader的子请求,都会被 SonicURLProtocol 所拦截

- (void)startLoading
{    
    NSThread *currentThread = [NSThread currentThread];
    
    __weak typeof(self) weakSelf = self;
    
    // 通过sessionID,把protocol的请求和session的请求绑起来
    NSString * sessionID = sonicSessionID(self.request.mainDocumentURL.absoluteString);
    SonicSession *session = [[SonicEngine sharedEngine] sessionById:sessionID];
    
    // 先判断是不是子资源的请求,否则就是主文档的
    if ([session.resourceLoader canInterceptResourceWithUrl:self.request.URL.absoluteString]) {
                
        // 用block的方式建立关联
        [session.resourceLoader preloadResourceWithUrl:self.request.URL.absoluteString withProtocolCallBack:^(NSDictionary *param) {
            [weakSelf performSelector:@selector(callClientActionWithParams:) onThread:currentThread withObject:param waitUntilDone:NO];
        }];
        
    }else{
       
        NSString *sessionID = [self.request valueForHTTPHeaderField:SonicHeaderKeySessionID];

          // 用block的方式建立关联,session会保存这个block,在connection的回调里面call回来
        [[SonicEngine sharedEngine] registerURLProtocolCallBackWithSessionID:sessionID completion:^(NSDictionary *param) {
            
            [weakSelf performSelector:@selector(callClientActionWithParams:) onThread:currentThread withObject:param waitUntilDone:NO];
            
        }];
        
    }
}

这里我们说它巧妙的地方在于,这样的话,逻辑就全部保留在Session内部,而无需分散代码了

加载流程

回头看最开始发起请求的时候,不难发现,WebView和Session发起的请求不是同一个request,那么数据到底是怎么给到webkit的呢?

我们理一下这个流程


  1. Session发起了主文档的加载,url,requestS
  2. WebView发起主文档的加载,url, requestW
  3. 虽然url一样,但是request不一样,Session的 requestS 不会被网络层拦截,WebView的 requestW 会被拦截
  4. WebView的 requestW 被拦截的时候,实际上是没有发起真实的请求的,而是和Session的 requestS 绑在了一起,绑定的key就是sessionID,存在 requestS 的header里,然后等待 requestS 的返回结果,或者从 CacheModel 返回
  5. 在Session的 requestSresponse 回来的时候,就马上发起了子资源的请求,确保了在Webkit的子资源请求之前发起,且实际上它们和主文档是并行加载的
  6. 因此当Session的主文档加载完了,也就是Webkit主文档加载完了,轮到Webkit发起子资源请求的时候,其实已经有部分已经完成了,相当于通过并行加载,优化了不少加载时间

就此,数据就给到了Webkit进行排版和渲染了

SonicCache

从缓存的数据结构来看,这里会把主文档分割开四个部分来缓存

实际上就是我们在让WebView加载的过程中,自己去加载一次这份数据并保存起来,等到WebView加载的时候,询问我们自己的CacheModel,有的话直接返回

但是这里很容易发现问题

SonicCache 是自己维护的数据结构以及存储逻辑,和Webkit的 NSURLCache 是分开的,这么说实际上是存了两份资源的数据,因为Webkit自己本身也会存一份,这样就导致了内存和磁盘都浪费了,极端情况下可能会出现峰值过高的情况,有待测试验证

Diff机制

这里有一个Diff的机制,可以加速在同一模板下的页面的加载

  1. 在主文档加载结束的时候,根据服务器response字段告知是否模板发生变化了
  2. 如果模板过期了,则更新整份缓存
  3. 如果模板没变,只是data更新了,服务器只会返回data的部分,减少网络开销和优化加载时间,同时不更新模板部分,并从responseData拿到新的data段,合并到模板上
  4. 保存缓存数据

其中的templateTag和ETag其实就是一个校验过程

总结

参考文章

轻量级高性能Hybrid框架VasSonic秒开实现解析

腾讯祭出大招VasSonic,让你的H5页面首屏秒开!

WebView性能、体验分析与优化

Github: Tencent/VasSonic

上一篇下一篇

猜你喜欢

热点阅读