VasSonic 3.0.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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343