VasSonic源碼解析
H5很重要,H5很重要,H5很重要,重要的事情要說三遍。VasSonic是騰訊開源的解決H5首屏渲染痛點的開源項目,本文通過解讀代碼來學習WebView的優化思路。
H5的優劣
H5的優勢很明顯,跨平台、迭代快、開發體驗好。H5的劣勢同樣明顯,加載慢,用戶體驗差。業內大牛想盡各種方法來彌補H5的劣勢,初級使用緩存、預加載等常用方案,高級如Hybrid、ReactNative、Weex等H5的進階解決方案。VasSonic專注於H5的秒開,使用的也是我們常見的性能優化方案。本文嚐試了解VasSonic是如何用常見的手段將性能優化做到極致的。
VasSonic解決什麼問題
關於WebView為什麼打開慢、加載慢,業界已經有很多分析了,結論也是比較一致的,推薦美團點評技術團隊的WebView性能、體驗分析與優化,騰訊關於VasSonic的官方文章也有相關說明。
WebView加載慢的問題主要集中在如下三個階段:
- WebView打開
- 頁麵資源加載
- 數據更新導致頁麵刷新
VasSonic的優化都是為了加速上述三個階段,其經驗可以總結為六個方麵。
- WebView池:預先初始化WebView
- 靜態直出:服務端拉取數據渲染完畢後,通過CDN加速訪問
- 離線預推:離線包方案
- 並行加速:WebView的打開和資源的請求並行
- 動態緩存:動態頁麵緩存在客戶端,用戶下次打開的時候先打開緩存頁麵,然後再刷新
- 動靜分離:為了提升體驗,將頁麵分為靜態模板和動態數據,實現局部刷新
- 預加載:在打開頁麵之前將資源數據都準備好,提升頁麵打開的速度
可以說是非常全麵了,具體細節可以參考騰訊祭出大招VasSonic,讓你的H5頁麵首屏秒開!。
上述優化的核心技術主要涉及幾個方麵:
- WebView池
- 緩存設計
- 資源請求和WebView分離設計
- 動靜分離設計
下麵結合代碼來看看VasSonic是如何實現這些優化點的。
準備工作:
從github VasSonic clone最新代碼,打開sonic-iOS目錄下的SonicSample。
WebView池
UIWebView並不是開源的,想要通過修改源碼來提升打開速度是不太現實的。VasSonic采用的方案是預先創建WebView池。在應用啟動或者空閑的時候預先創建空的WebView,等真正要用的時候直接從池中獲取WebView。
Demo中隻是簡單的預加載了一次WebView,通過創建空的WebView,可以預先啟動Web線程,完成WebView的一些全局性的初始化工作,對二次創建WebView能有數百毫秒的提升。在實際應用中,我們可以采用WebView池的方式來進一步提升打開速度。
//start web thread
UIWebView *webPool = [[UIWebView alloc]initWithFrame:CGRectZero];
[webPool loadHTMLString:@"" baseURL:nil]; // 注意loadHTMLString是必須的
緩存設計
緩存類型
VasSonic將緩存的類型分成了四種,他們分別是模板、頁麵、數據和配置。
/*
* template
*/
SonicCacheTypeTemplate,
/*
* html
*/
SonicCacheTypeHtml,
/*
* dynamic data
*/
SonicCacheTypeData,
/*
* config
*/
SonicCacheTypeConfig,
將模板和數據分離是實現動靜分離的核心技術,模板和數據是從頁麵數據中自動分離出來的,緩存頁麵數據的時候,SonicCache會調用splitTemplateAndDataFromHtmlData:
分割模板和數據,代碼實現如下:
- (NSDictionary *)splitTemplateAndDataFromHtmlData:(NSString *)html
{
// 使用sonicdiff這個tag來將HTML分割成模板和數據
NSError *error = nil;
NSRegularExpression *reg = [NSRegularExpression regularExpressionWithPattern:@"<!--sonicdiff-?(\\w*)-->([\\s\\S]+?)<!--sonicdiff-?(\\w*)-end-->" options:NSRegularExpressionCaseInsensitive error:&error];
if (error) {
return nil;
}
// 分割出來的數據,以sonicdiff指定的名字key保存到數據字典中
NSArray *metchs = [reg matchesInString:html options:NSMatchingReportCompletion range:NSMakeRange(0, html.length)];
NSMutableDictionary *dataDict = [NSMutableDictionary dictionary];
[metchs enumerateObjectsUsingBlock:^(NSTextCheckingResult *obj, NSUInteger idx, BOOL * _Nonnull stop) {
NSString *matchStr = [html substringWithRange:obj.range];
NSArray *seprateArr = [matchStr componentsSeparatedByString:@"<!--sonicdiff-"];
NSString *itemName = [[[seprateArr lastObject]componentsSeparatedByString:@"-end-->"]firstObject];
NSString *formatKey = [NSString stringWithFormat:@"{%@}",itemName];
[dataDict setObject:matchStr forKey:formatKey];
}];
// 分割出來的模板,用key來替換動態數據的位置
NSMutableString *mResult = [NSMutableString stringWithString:html];
[dataDict enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSString *value, BOOL * _Nonnull stop) {
[mResult replaceOccurrencesOfString:value withString:key options:NSCaseInsensitiveSearch range:NSMakeRange(0, mResult.length)];
}];
//if split HTML faild , we can return nothing ,it is not a validat sonic request.
if (dataDict.count == 0 || mResult.length == 0) {
return nil;
}
return @{@"data":dataDict,@"temp":mResult};
}
還是以Demo為例看split的結果。
// 原始頁麵數據
<span >
<!--sonicdiff-data1-->
<p>示例:</p>
<img src="//mc.vip.qq.com/img/img-1.png?max_age=2592000" alt="">
<!--sonicdiff-data1-end-->
</span>
// 分離之後的結果
// --模板
<span >
{data1}
</span>
// --數據
{
"{data1}" = "<!--sonicdiff-data1-->
\n <p>\U793a\U4f8b\Uff1a</p>
\n <img src=\"//mc.vip.qq.com/img/img-1.png?max_age=2592000\" alt=\"\">
\n <!--sonicdiff-data1-end-->";
}
除了頁麵、模板、數據類型的緩存外,還有一個非常重要的緩存是config。先看下config的生成。
- (NSDictionary *)createConfigFromResponseHeaders:(NSDictionary *)headers
{
//Etag,template-tag
NSString *eTag = headers[@"Etag"];
NSString *templateTag = headers[@"template-tag"];
NSString *csp = headers[SonicHeaderKeyCSPHeader];
NSTimeInterval timeNow = (long)[[NSDate date ]timeIntervalSince1970]*1000;
NSString *localRefresh = [@(timeNow) stringValue];
//save configs
eTag = eTag.length > 0? eTag:@"";
templateTag = templateTag.length > 0? templateTag:@"";
eTag = eTag.length > 0? eTag:@"";
csp = csp.length > 0? csp:@"";
NSDictionary *cfgDict = @{
SonicHeaderKeyETag:eTag,
SonicHeaderKeyTemplate:templateTag,
kSonicLocalRefreshTime:localRefresh,
kSonicCSP:csp
};
return cfgDict;
}
ETag大家應該是比較清楚的,在HTTP的緩存設計中有重要作用,當服務端發現客戶端請求帶的資源的ETag和服務端一樣的話,就不會返回完整的資源內容了,節省時間和帶寬,templateTag也是類似的,當templateTag不一樣的時候,服務端才會更新模板。
簡而言之,Config就是保存了這次請求頭中的一些重要信息,留待下次請求的時候發還給服務端做優化。
緩存Key
說完緩存類型,必須要說一下緩存的key,這個非常重要。首次請求會調用saveFirstWithHtmlData:withResponseHeaders:withUrl
緩存數據。入參有htmlData、header和url,前麵已經分析htmlData是需要緩存的頁麵數據,htmlData會被存成html、template和dynamicData三種類型,headers前麵也提到了是緩存成config,那這個url的作用就是生成緩存的key。
- (SonicCacheItem *)saveFirstWithHtmlData:(NSData *)htmlData
withResponseHeaders:(NSDictionary *)headers
withUrl:(NSString *)url
{
NSString *sessionID = sonicSessionID(url);
if (!htmlData || headers.count == 0 || sessionID.length == 0) {
return nil;
}
SonicCacheItem *cacheItem = [self cacheForSession:sessionID];
......
}
首先根據url生成sessionID,然後再將sessionID和特定的SonicCacheItem
實例綁定。這裏我們先說明每個固定url生成的sessionID是一樣的,這才能讓我們在相同的url請求的情況下使用緩存,具體的url生成sessionID的規則在SonicSession
章節詳細說明。
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
SonicSession
講緩存的時候我們提到過作為緩存Key的sessionID,每個sessionID關聯了一個緩存對象SonicCacheItem,同時也關聯了一次URL請求,VasSonic將這個請求抽象為SonicSession。SonicSession在VasSonic的設計裏麵非常關鍵。其將資源的請求和WebView脫離開來,有了SonicSession,結合SonicCache,我們就可以不依賴WebView去做資源的請求,這樣就可以實現WebView打開和資源加載並行、資源預加載等加速方案。
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實現。
自定義請求頭
之前提到過SonicCache的一種緩存類型是Config,SonicSession在初始化時候會根據緩存的Config更新請求頭,以便服務端根據這些信息做相應的優化。
- (void)setupData
{
// 根據sessionID獲取緩存內容
SonicCacheItem *cacheItem = [[SonicCache shareCache] cacheForSession:_sessionID];
self.isFirstLoad = cacheItem.hasLocalCache;
if (!cacheItem.hasLocalCache) {
self.cacheFileData = cacheItem.htmlData;
self.cacheConfigHeaders = cacheItem.config;
self.cacheResponseHeaders = cacheItem.cacheResponseHeaders;
self.localRefreshTime = cacheItem.lastRefreshTime;
}
[self setupConfigRequestHeaders];
}
- (void)setupConfigRequestHeaders
{
NSMutableDictionary *mCfgDict = [NSMutableDictionary dictionaryWithDictionary:self.request.allHTTPHeaderFields];
// 根據緩存設置Etag、templateTag等
NSDictionary *cfgDict = [self getRequestParamsFromConfigHeaders];
if (cfgDict) {
[mCfgDict addEntriesFromDictionary:cfgDict];
}
// 添加一些自定義的緩存頭
[mCfgDict setObject:@"true" forKey:@"accept-diff"];
[mCfgDict setObject:@"true" forKey:@"no-Chunked"];
[mCfgDict setObject:@"GET" forKey:@"method"];
[mCfgDict setObject:@"utf-8" forKey:@"accept-Encoding"];
[mCfgDict setObject:@"zh-CN,zh;" forKey:@"accept-Language"];
[mCfgDict setObject:@"gzip" forKey:@"accept-Encoding"];
[mCfgDict setObject:SonicHeaderValueSDKVersion forKey:SonicHeaderKeySDKVersion];
[mCfgDict setObject:SonicHeaderValueSonicLoad forKey:SonicHeaderKeyLoadType];
// 可以自定義UA,方便app判斷
NSString *userAgent = [SonicClient sharedClient].userAgent.length > 0? [SonicClient sharedClient].userAgent:[[SonicClient sharedClient] sonicDefaultUserAgent];
[mCfgDict setObject:userAgent forKey:@"User-Agent"];
NSURL *cUrl = [NSURL URLWithString:self.url];
// 替換域名為ip,免去dns解析的耗時
if (self.serverIP.length > 0) {
NSString *host = [cUrl.scheme isEqualToString:@"https"]? [NSString stringWithFormat:@"%@:443",self.serverIP]:[NSString stringWithFormat:@"%@:80",self.serverIP];
NSString *newUrl = [self.url stringByReplacingOccurrencesOfString:cUrl.host withString:host];
cUrl = [NSURL URLWithString:newUrl];
[mCfgDict setObject:cUrl.host forKey:@"Host"];
}
[self.request setAllHTTPHeaderFields:mCfgDict];
}
- (NSDictionary *)getRequestParamsFromConfigHeaders
{
NSDictionary *cfgDict = self.cacheConfigHeaders;
NSMutableDictionary *mCfgDict = [NSMutableDictionary dictionary];
if (cfgDict) {
// 設置eTag信息
NSString *eTag = cfgDict[SonicHeaderKeyETag];
if (eTag.length > 0) {
[mCfgDict setObject:eTag forKey:@"If-None-Match"];
}
// 設置templateTag信息
NSString *tempTag = cfgDict[SonicHeaderKeyTemplate];
if (tempTag.length > 0 ) {
[mCfgDict setObject:tempTag forKey:@"template-tag"];
}
}else{
[mCfgDict setObject:@"" forKey:@"If-None-Match"];
[mCfgDict setObject:@"" forKey:@"template-tag"];
}
return mCfgDict;
}
除了會添加自定義的請求頭參數,以及將緩存的config加到請求頭裏麵外,在每次發起請求之前,都會同步cookies,這樣就可以保持狀態了,比如登陸狀態等等。
- (void)start
{
dispatchToMain(^{
if (self.delegate && [self.delegate respondsToSelector:@selector(sessionWillRequest:)]) {
[self.delegate sessionWillRequest:self];
}
[self syncCookies];
});
[self requestStartInOperation];
}
- (void)syncCookies
{
NSURL *cUrl = [NSURL URLWithString:self.url];
// 從係統cookies中讀取cookies信息,並添加到自定義請求頭
NSHTTPCookieStorage *sharedHTTPCookieStorage = [NSHTTPCookieStorage sharedHTTPCookieStorage];
NSArray *cookies = [sharedHTTPCookieStorage cookiesForURL:cUrl];
NSDictionary *cookieHeader = [NSHTTPCookie requestHeaderFieldsWithCookies:cookies];
[self addCustomRequestHeaders:cookieHeader];
}
做了上麵這些工作,我們可以抓包看最終一個請求會長成什麼樣子。通過對Demo中LOAD WITH SONIC抓包發現請求頭中帶了sonic-load-type、template-tag、sonic-sdk-version等等,服務端正是基於這些參數做了優化。
GET /demo/indexv3 HTTP/1.1
Host: mc.vip.qq.com
accept-diff: true
Accept: */*
sonic-load-type: __SONIC_HEADER_VALUE_SONIC_LOAD__
template-tag: 37141a61d0497851179bc4f27867290921e1367e
Accept-Encoding: gzip
If-None-Match: 9a498fe9148d127c8ebd970ebac425ba6e6532b3
Accept-Language: zh-CN,zh;
no-Chunked: true
User-Agent: Mozilla/5.0 (iPhone; U; CPU iPhone OS 2_2 like Mac OS X;en-us) AppleWebKit/525.181 (KHTML, like Gecko) Version/3.1.1 Mobile/5H11 Safari/525.20
sonic-sdk-version: Sonic/1.0
Connection: keep-alive
Cookie: dataImg=1; templateFlag=1
method: GET
網絡連接
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請求攔截部分會有提到。
緩存處理
SonicSession根據請求響應頭中cache-offline
返回的存儲策略的不一樣會有不同的處理,Sonic定義了如下幾種離線存儲的策略。
/**
* 存儲但不刷新頁麵
*/
#define SonicHeaderValueCacheOfflineStore @"store"
/**
* 存儲而且刷新頁麵
*/
#define SonicHeaderValueCacheOfflineStoreRefresh @"true"
/**
* 不存儲但刷新頁麵
*/
#define SonicHeaderValueCacheOfflineRefresh @"false"
/**
* Sonic模式關閉,並在接下來6個小時內不再使用
*/
#define SonicHeaderValueCacheOfflineDisable @"http"
當SonicSession在發起請求之後需要處理本地有緩存和沒有緩存兩種情況。
沒有緩存的情況
沒有緩存,首次加載的情況下根據策略的處理方式也比較簡單,沒啥好說的,直接上代碼。
- (void)firstLoadDidFinish
{
......
if ([policy isEqualToString:SonicHeaderValueCacheOfflineDisable]) {
[[SonicCache shareCache] saveServerDisableSonicTimeNow:self.sessionID];
self.isDataUpdated = YES;
break;
}
if ([policy isEqualToString:SonicHeaderValueCacheOfflineStoreRefresh] || [policy isEqualToString:SonicHeaderValueCacheOfflineStore] || [policy isEqualToString:SonicHeaderValueCacheOfflineRefresh]) {
SonicCacheItem *cacheItem = [[SonicCache shareCache] saveFirstWithHtmlData:self.responseData withResponseHeaders:self.response.allHeaderFields withUrl:self.url];
if (cacheItem) {
self.localRefreshTime = cacheItem.lastRefreshTime;
self.sonicStatusCode = SonicStatusCodeFirstLoad;
self.sonicStatusFinalCode = SonicStatusCodeFirstLoad;
}
if ([policy isEqualToString:SonicHeaderValueCacheOfflineRefresh]) {
[[SonicCache shareCache] removeCacheBySessionID:self.sessionID];
}
[[SonicCache shareCache] removeServerDisableSonic:self.sessionID];
}
......
}
有緩存的情況
有緩存的情況相對來說要複雜一些,需要處理模板更新和數據更新兩種不同的情況。
- (void)updateDidSuccess
{
......
// 處理模板更新的情況,模板更新是大動作,跟首次加載已經區別不大,模板更新一定會導致數據更新
if ([self isTemplateChange]) {
self.cacheFileData = self.responseData;
[self dealWithTemplateChange];
// 模板不變,數據更新
}else{
[self dealWithDataUpdate];
}
// 處理其他離線緩存策略
NSString *policy = [self responseHeaderValueByIgnoreCaseKey: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];
}
}
......
}
模板變化是直接調用了saveFirstWithHtmlData:withResponseHeaders:withUrl:
來更新緩存,可見模板變化會導致之前的緩存都失效。
- (void)dealWithTemplateChange
{
SonicCacheItem *cacheItem = [[SonicCache shareCache] saveFirstWithHtmlData:self.responseData withResponseHeaders:self.response.allHeaderFields withUrl:self.url];
......
}
數據變化則是調用updateWithJsonData:withResponseHeaders:withUrl:
來更新緩存,該函數會將本地的緩存和服務端返回的數據做個diff,然後返回給前端更新界麵。
- (void)dealWithDataUpdate
{
SonicCacheItem *cacheItem = [[SonicCache shareCache] updateWithJsonData:self.responseData withResponseHeaders:self.response.allHeaderFields withUrl:self.url];
......
}
攔截WebView請求
現在SonicSession結合SonicCache能獨立高效處理URL請求,那麼如何使用SonicSession來接管WebView的請求呢?iOS下所有的URL請求都是走URL Loading System的,攔截WebView的請求隻需要自定義實現NSURLProtocol
協議就可以了。
因為NSURLProtocol會攔截所有的請求,那如何隻針對Sonic WebView發起的請求實現攔截呢?可以通過canInitWithRequest:
來實現,隻有請求頭中帶SonicHeaderValueWebviewLoad
的才會被攔截。
+ (BOOL)canInitWithRequest:(NSURLRequest *)request
{
NSString *value = [request.allHTTPHeaderFields objectForKey:SonicHeaderKeyLoadType];
if (value.length == 0) {
return NO;
}
if ([value isEqualToString:SonicHeaderValueSonicLoad]) {
return NO;
}else if([value isEqualToString:SonicHeaderValueWebviewLoad]) {
return YES;
}
return NO;
}
當係統發起請求的時候,Sonic並沒有真正的發起請求,而是用SessionID注冊了回調,讓SonicSession在恰當的時候調動回調。
- (void)startLoading
{
NSThread *currentThread = [NSThread currentThread];
NSString *sessionID = [self.request valueForHTTPHeaderField:SonicHeaderKeySessionID];
__weak typeof(self) weakSelf = self;
// 在SonicSession中注冊回調函數
[[SonicClient sharedClient] registerURLProtocolCallBackWithSessionID:sessionID completion:^(NSDictionary *param) {
[weakSelf performSelector:@selector(callClientActionWithParams:) onThread:currentThread withObject:param waitUntilDone:NO];
}];
}
接下來我們看看SonicSession都是在什麼時機調用回調函數的,首次加載、預加載和完全緩存狀態是不一樣的。
首次加載的時候,根據網絡的實際回調時機調用即可,代碼如下:
- (void)firstLoadRecieveResponse:(NSHTTPURLResponse *)response
{
[self dispatchProtocolAction:SonicURLProtocolActionRecvResponse param:response];
}
- (void)firstLoadDidLoadData:(NSData *)data
{
[self dispatchProtocolAction:SonicURLProtocolActionLoadData param:data];
}
- (void)firstLoadDidFaild:(NSError *)error
{
[self dispatchProtocolAction:SonicURLProtocolActionDidFaild param:error];
......
}
- (void)firstLoadDidFinish
{
[self dispatchProtocolAction:SonicURLProtocolActionDidFinish param:nil];
......
}
有預加載的情況下,根據預加載的情況構造需要回調的動作,代碼如下:
- (NSArray *)preloadRequestActions
{
NSMutableArray *actionItems = [NSMutableArray array];
if (self.response) {
NSDictionary *respItem = [self protocolActionItem:SonicURLProtocolActionRecvResponse param:self.response];
[actionItems addObject:respItem];
}
if (self.isCompletion) {
if (self.error) {
NSDictionary *failItem = [self protocolActionItem:SonicURLProtocolActionDidFaild param:self.error];
[actionItems addObject:failItem];
}else{
if (self.responseData.length > 0) {
NSData *recvCopyData = [[self.responseData copy]autorelease];
NSDictionary *recvItem = [self protocolActionItem:SonicURLProtocolActionLoadData param:recvCopyData];
[actionItems addObject:recvItem];
}
NSDictionary *finishItem = [self protocolActionItem:SonicURLProtocolActionDidFinish param:nil];
[actionItems addObject:finishItem];
}
}else{
if (self.responseData.length > 0) {
NSData *recvCopyData = [[self.responseData copy]autorelease];
NSDictionary *recvItem = [self protocolActionItem:SonicURLProtocolActionLoadData param:recvCopyData];
[actionItems addObject:recvItem];
}
}
return actionItems;
}
完全緩存的情況下,構造完整的回調動作,代碼如下:
- (NSArray *)cacheFileActions
{
NSMutableArray *actionItems = [NSMutableArray array];
NSHTTPURLResponse *response = nil;
if (self.response && [self isCompletionWithOutError] && self.isDataUpdated) {
response = self.response;
}else{
NSDictionary *respHeader = self.cacheResponseHeaders;
response = [[[NSHTTPURLResponse alloc] initWithURL:[NSURL URLWithString:self.url] statusCode:200 HTTPVersion:@"1.1" headerFields:respHeader]autorelease];
}
NSMutableData *cacheData = [[self.cacheFileData mutableCopy] autorelease];
NSDictionary *respItem = [self protocolActionItem:SonicURLProtocolActionRecvResponse param:response];
NSDictionary *dataItem = [self protocolActionItem:SonicURLProtocolActionLoadData param:cacheData];
NSDictionary *finishItem = [self protocolActionItem:SonicURLProtocolActionDidFinish param:nil];
[actionItems addObject:respItem];
[actionItems addObject:dataItem];
[actionItems addObject:finishItem];
self.didFinishCacheRead = YES;
return actionItems;
}
這樣業務使用者隻需要正常的實現UIWebViewDelegate
的協議就可以了,不需要關心回調是來自真正的網絡連接、還是來自預加載,或者是完全的緩存,所有的緩存優化就都能被封裝在SonicSession裏麵了。
這裏有一點需要說明的是SonicURLProtocol和SonicConnection是不一樣的,雖然SonicConnection模仿了NSURLProtocol的接口,但是其父類是NSObject。SonicURLProtocol最大的功能是實現WebView的請求攔截,而SonicConnection則是SonicSession的網絡請求處理類。
頁麵刷新
經過上麵的描述,我們基本已經將整個流程都串起來了。
WebView發起請求 -> SonicURLProtocol實現請求攔截,將控製權交給SonicSession
-> SonicSession根據SessionID獲取請求結果,回調請求過程,請求結果可能來自緩存(SonicCache),也可能來自網絡請求(SonicConnection)
-> WebView根據結果展示頁麵
整個流程最後的WebView頁麵展示,也是非常重要的一塊優化。
- (void)sessionDidFinish:(SonicSession *)session
{
dispatch_block_t opBlock = ^{
self.isCompletion = YES;
if (self.isFirstLoad) {
[self firstLoadDidFinish];
}else{
[self updateDidSuccess];
}
};
dispatchToSonicSessionQueue(opBlock);
}
當請求結束的時候,SonicSession會根據是否是首次加載分別調用firstLoadDidFinish
和updateDidSuccess
,這兩個函數除了對緩存的不同處理外,還有一個非常重要的區別:前者調用了[self dispatchProtocolAction:SonicURLProtocolActionDidFinish param:nil];
,後者則不會。也就是說前者會將請求結束的結果告訴WebView,而後者不會,導致的結果就是**前者會刷新頁麵,而後者不會**。但是updateDidSuccess
中有這麼一段代碼。
- (void)updateDidSuccess
{
......
// 如果js注冊了數據刷新的回調,就調用該回調
if (self.webviewCallBack) {
NSDictionary *resultDict = [self sonicDiffResult];
if (resultDict) {
self.webviewCallBack(resultDict);
}
}
......
}
如果有webviewCallBack
,那麼這個回調是會被調用的,參數是經過diff之後的數據,看到這裏應該同學都明白了,這就是局部刷新的實現機製。
Sonic給JS暴露一個方法叫getDiffDataCallback
,JS隻要設置該回調,最終就是設置了self.webViewCallBack
。
JSExportAs(getDiffData,
- (void)getDiffData:(NSDictionary *)option withCallBack:(JSValue *)jscallback
);
- (void)getDiffData:(NSDictionary *)option withCallBack:(JSValue *)jscallback
{
JSValue *callback = self.owner.jscontext.globalObject;
[[SonicClient sharedClient] sonicUpdateDiffDataByWebDelegate:self.owner completion:^(NSDictionary *result) {
if (result) {
NSData *json = [NSJSONSerialization dataWithJSONObject:result options:NSJSONWritingPrettyPrinted error:nil];
NSString *jsonStr = [[NSString alloc]initWithData:json encoding:NSUTF8StringEncoding];
[callback invokeMethod:@"getDiffDataCallback" withArguments:@[jsonStr]];
}
}];
}
這部分的js相關實現在sonic.js中,有興趣的同學可以自行翻看js源碼。Demo中的更新邏輯如下:
//0-狀態獲取失敗 1-sonic首次 2-頁麵刷新 3-局部刷新 4-完全cache
sonic.getSonicData(function(sonicStatus, reportSonicStatus, sonicUpdateData){
if(sonicStatus == 1){
//首次沒有特殊的邏輯處理,直接執行sonic完成後的邏輯,比如上報等
}else if(sonicStatus == 2){
}else if(sonicStatus == 3){
//局部刷新的時候需要更新頁麵的數據塊和一些JS操作
var html = '';
var id = '';
var elementObj = '';
for(var key in sonicUpdateData){
id = key.substring(1,key.length-1);
html = sonicUpdateData[key];
elementObj = document.getElementById(id+'Content');
elementObj.innerHTML = html;
}
}else if(sonicStatus == 4){
}
afterInit(reportSonicStatus);
});
結論
總結來看VasSonic並不是與眾不同的新技術,但是其對HTML、客戶端WebView有著深入的了解,通過司空見慣的一些技術的極致搭配和使用,極大的提升了WebView的性能。仔細研究SonicSession和SonicCache的實現對於了解VasSonic的設計思想非常重要。最後感謝騰訊團隊給開源界帶來這麼優秀的WebView框架。
參考文獻
最後更新:2017-09-19 10:32:56
上一篇:
中國裝備製造業尋找新路徑:不數字化無未來
下一篇:
阿裏雲前端周刊 - 第 25 期
雲計算的未來暢想(五) -- matrix(終篇)
找不到請求的 .Net Framework Data Provider。可能沒有安裝.
連載:麵向對象葵花寶典:思想、技巧與實踐(17) - 需求分析518方法
"undefined reference to" 問題解決方法
如何在 vmware esxi 5.5 中開放 VNC 端口
5月26日雲棲精選夜讀:權威詳解 | 阿裏新一代實時計算引擎 Blink,每秒支持數十億次計算
一些零碎的思想需要找個地方記錄起來
周鴻禕,你怕啥?
《vSphere性能設計:性能密集場景下CPU、內存、存儲及網絡的最佳設計實踐》一3.2.2 建立實驗室
阿裏新生的麵試經,與老人分享的職業進階攻略及規劃