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 子资源加载
一般的流程是
- 查询一下是否存在缓存,来置位是否 首次加载
- 做host的ip映射(如果有的话)
- 如果是 首次加载 的话,会尝试在缓存的Response里面查询 子资源地址list,如果存在则会直接开始加载子资源(在主文档之前开始,和前面的并行类似)
- 在真正发起请求之前,还会检查一下缓存是否已经过期,以及同步Cookies,保证一些状态的正确
SonicConnection
真正发起请求连接的是 SonicConnection,但是如前面所说它只是一层封装,SonicServer 其实也只做了两件事情
- isFirstLoadRequest,以保证WebView的边加载边解析渲染的特性没有丢掉
- 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的呢?
我们理一下这个流程
- Session发起了主文档的加载,url,requestS
- WebView发起主文档的加载,url, requestW
- 虽然url一样,但是request不一样,Session的 requestS 不会被网络层拦截,WebView的 requestW 会被拦截
- WebView的 requestW 被拦截的时候,实际上是没有发起真实的请求的,而是和Session的 requestS 绑在了一起,绑定的key就是sessionID,存在 requestS 的header里,然后等待 requestS 的返回结果,或者从 CacheModel 返回
- 在Session的 requestS 的 response 回来的时候,就马上发起了子资源的请求,确保了在Webkit的子资源请求之前发起,且实际上它们和主文档是并行加载的
- 因此当Session的主文档加载完了,也就是Webkit主文档加载完了,轮到Webkit发起子资源请求的时候,其实已经有部分已经完成了,相当于通过并行加载,优化了不少加载时间
就此,数据就给到了Webkit进行排版和渲染了
SonicCache
从缓存的数据结构来看,这里会把主文档分割开四个部分来缓存
- html数据
- 分割出来的模板
- 动态更新的局部数据段
- 配置项
实际上就是我们在让WebView加载的过程中,自己去加载一次这份数据并保存起来,等到WebView加载的时候,询问我们自己的CacheModel,有的话直接返回
但是这里很容易发现问题
SonicCache 是自己维护的数据结构以及存储逻辑,和Webkit的 NSURLCache 是分开的,这么说实际上是存了两份资源的数据,因为Webkit自己本身也会存一份,这样就导致了内存和磁盘都浪费了,极端情况下可能会出现峰值过高的情况,有待测试验证
Diff机制
这里有一个Diff的机制,可以加速在同一模板下的页面的加载
- 在主文档加载结束的时候,根据服务器response字段告知是否模板发生变化了
- 如果模板过期了,则更新整份缓存
- 如果模板没变,只是data更新了,服务器只会返回data的部分,减少网络开销和优化加载时间,同时不更新模板部分,并从responseData拿到新的data段,合并到模板上
- 保存缓存数据
其中的templateTag和ETag其实就是一个校验过程
总结
- 不依赖 UIWebView ,对于第三方接入非常方便
- 但同时也就放弃了 WKWebView,因为需要网络层接管
- 需要后台和前端配合,才能发挥出作用
- 否则只会优化了WebView创建的时间,预加载都没有用,对于Webkit原来的流程来说,性价比很低,初次打开速度没有优势,二次打开速度远不如Webkit的PageCache
-
SonicCache 是独立的,Webkit无法使用这部分的缓存
- 包括预加载,和子资源
- 其实这个点是可以优化的,只需同步过去 NSURLCache ,或者子资源直接存到HttpCache?
- 预加载后的加载,还是会有白屏的不好体验,和没有预加载的时候速度感觉没有任何区别,只是断网可以加载而已
- 我看的版本好像ip映射表没有用上,不知道是不是bug,而且服务器response其实也可以返回更新ip映射表,进一步减少子资源或者后续的dns解析时间
- 内存峰值猜测比Webkit高,需要测试有待验证