美文网首页
VasSonic 3.0.0 源码解读

VasSonic 3.0.0 源码解读

作者: _ivanC | 来源:发表于2018-04-25 15:00 被阅读0次

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

  • 独立的资源加载
  • 模板diff机制

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

初始化

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 对应着一个主文档的加载

先看看它的组成部分

  • SonicServer 只是逻辑封装,判断是否第一次加载以及本地模式
  • SonicConnection 只是NSURLSession的包装
  • SonicResourceLoader 子资源加载

一般的流程是


  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

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

  • html数据
  • 分割出来的模板
  • 动态更新的局部数据段
  • 配置项

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

但是这里很容易发现问题

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

Diff机制

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

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

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

总结

  • 不依赖 UIWebView ,对于第三方接入非常方便
    • 但同时也就放弃了 WKWebView,因为需要网络层接管
  • 需要后台和前端配合,才能发挥出作用
    • 否则只会优化了WebView创建的时间,预加载都没有用,对于Webkit原来的流程来说,性价比很低,初次打开速度没有优势,二次打开速度远不如Webkit的PageCache
  • SonicCache 是独立的,Webkit无法使用这部分的缓存
    • 包括预加载,和子资源
    • 其实这个点是可以优化的,只需同步过去 NSURLCache ,或者子资源直接存到HttpCache?
    • 预加载后的加载,还是会有白屏的不好体验,和没有预加载的时候速度感觉没有任何区别,只是断网可以加载而已
  • 我看的版本好像ip映射表没有用上,不知道是不是bug,而且服务器response其实也可以返回更新ip映射表,进一步减少子资源或者后续的dns解析时间
  • 内存峰值猜测比Webkit高,需要测试有待验证

参考文章

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

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

WebView性能、体验分析与优化

Github: Tencent/VasSonic

相关文章

网友评论

      本文标题:VasSonic 3.0.0 源码解读

      本文链接:https://www.haomeiwen.com/subject/vpcylftx.html