H5很重要疗涉,H5很重要著隆,H5很重要搞乏,重要的事情要說三遍辙售。VasSonic是騰訊開源的解決H5首屏渲染痛點的開源項目杀怠,本文通過解讀代碼來學(xué)習(xí)WebView的優(yōu)化思路袭蝗。
H5的優(yōu)劣
H5的優(yōu)勢很明顯肤视,跨平臺茵汰、迭代快、開發(fā)體驗好绑榴。H5的劣勢同樣明顯哪轿,加載慢,用戶體驗差翔怎。業(yè)內(nèi)大牛想盡各種方法來彌補H5的劣勢窃诉,初級使用緩存、預(yù)加載等常用方案姓惑,高級如Hybrid褐奴、ReactNative、Weex等H5的進階解決方案于毙。VasSonic專注于H5的秒開敦冬,使用的也是我們常見的性能優(yōu)化方案。本文嘗試了解VasSonic是如何用常見的手段將性能優(yōu)化做到極致的唯沮。
VasSonic解決什么問題
關(guān)于WebView為什么打開慢脖旱、加載慢,業(yè)界已經(jīng)有很多分析了介蛉,結(jié)論也是比較一致的萌庆,推薦美團點評技術(shù)團隊的WebView性能、體驗分析與優(yōu)化币旧,騰訊關(guān)于VasSonic的官方文章也有相關(guān)說明践险。
WebView加載慢的問題主要集中在如下三個階段:
- WebView打開
- 頁面資源加載
- 數(shù)據(jù)更新導(dǎo)致頁面刷新
VasSonic的優(yōu)化都是為了加速上述三個階段,其經(jīng)驗可以總結(jié)為六個方面吹菱。
- WebView池:預(yù)先初始化WebView
- 靜態(tài)直出:服務(wù)端拉取數(shù)據(jù)渲染完畢后巍虫,通過CDN加速訪問
- 離線預(yù)推:離線包方案
- 并行加速:WebView的打開和資源的請求并行
- 動態(tài)緩存:動態(tài)頁面緩存在客戶端,用戶下次打開的時候先打開緩存頁面鳍刷,然后再刷新
- 動靜分離:為了提升體驗占遥,將頁面分為靜態(tài)模板和動態(tài)數(shù)據(jù),實現(xiàn)局部刷新
- 預(yù)加載:在打開頁面之前將資源數(shù)據(jù)都準(zhǔn)備好输瓜,提升頁面打開的速度
可以說是非常全面了瓦胎,具體細節(jié)可以參考騰訊祭出大招VasSonic,讓你的H5頁面首屏秒開尤揣!搔啊。
上述優(yōu)化的核心技術(shù)主要涉及幾個方面:
- WebView池
- 緩存設(shè)計
- 資源請求和WebView分離設(shè)計
- 動靜分離設(shè)計
下面結(jié)合代碼來看看VasSonic是如何實現(xiàn)這些優(yōu)化點的。
準(zhǔn)備工作:
從github VasSonic clone最新代碼芹缔,打開sonic-iOS目錄下的SonicSample坯癣。
WebView池
UIWebView并不是開源的,想要通過修改源碼來提升打開速度是不太現(xiàn)實的最欠。VasSonic采用的方案是預(yù)先創(chuàng)建WebView池示罗。在應(yīng)用啟動或者空閑的時候預(yù)先創(chuàng)建空的WebView,等真正要用的時候直接從池中獲取WebView芝硬。
Demo中只是簡單的預(yù)加載了一次WebView蚜点,通過創(chuàng)建空的WebView,可以預(yù)先啟動Web線程拌阴,完成WebView的一些全局性的初始化工作绍绘,對二次創(chuàng)建WebView能有數(shù)百毫秒的提升。在實際應(yīng)用中迟赃,我們可以采用WebView池的方式來進一步提升打開速度陪拘。
//start web thread
UIWebView *webPool = [[UIWebView alloc]initWithFrame:CGRectZero];
[webPool loadHTMLString:@"" baseURL:nil]; // 注意loadHTMLString是必須的
緩存設(shè)計
緩存類型
VasSonic將緩存的類型分成了四種,他們分別是模板纤壁、頁面左刽、數(shù)據(jù)和配置。
/*
* template
*/
SonicCacheTypeTemplate,
/*
* html
*/
SonicCacheTypeHtml,
/*
* dynamic data
*/
SonicCacheTypeData,
/*
* config
*/
SonicCacheTypeConfig,
將模板和數(shù)據(jù)分離是實現(xiàn)動靜分離的核心技術(shù)酌媒,模板和數(shù)據(jù)是從頁面數(shù)據(jù)中自動分離出來的欠痴,緩存頁面數(shù)據(jù)的時候,SonicCache會調(diào)用splitTemplateAndDataFromHtmlData:
分割模板和數(shù)據(jù)秒咨,代碼實現(xiàn)如下:
- (NSDictionary *)splitTemplateAndDataFromHtmlData:(NSString *)html
{
// 使用sonicdiff這個tag來將HTML分割成模板和數(shù)據(jù)
NSError *error = nil;
NSRegularExpression *reg = [NSRegularExpression regularExpressionWithPattern:@"<!--sonicdiff-?(\\w*)-->([\\s\\S]+?)<!--sonicdiff-?(\\w*)-end-->" options:NSRegularExpressionCaseInsensitive error:&error];
if (error) {
return nil;
}
// 分割出來的數(shù)據(jù)喇辽,以sonicdiff指定的名字key保存到數(shù)據(jù)字典中
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來替換動態(tài)數(shù)據(jù)的位置
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的結(jié)果雨席。
// 原始頁面數(shù)據(jù)
<span id="data1Content">
<!--sonicdiff-data1-->
<p>示例:</p>
![](//mc.vip.qq.com/img/img-1.png?max_age=2592000)
<!--sonicdiff-data1-end-->
</span>
// 分離之后的結(jié)果
// --模板
<span id="data1Content">
{data1}
</span>
// --數(shù)據(jù)
{
"{data1}" = "<!--sonicdiff-data1-->
\n <p>\U793a\U4f8b\Uff1a</p>
\n <img src=\"http://mc.vip.qq.com/img/img-1.png?max_age=2592000\" alt=\"\">
\n <!--sonicdiff-data1-end-->";
}
除了頁面菩咨、模板、數(shù)據(jù)類型的緩存外陡厘,還有一個非常重要的緩存是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大家應(yīng)該是比較清楚的雏亚,在HTTP的緩存設(shè)計中有重要作用缨硝,當(dāng)服務(wù)端發(fā)現(xiàn)客戶端請求帶的資源的ETag和服務(wù)端一樣的話,就不會返回完整的資源內(nèi)容了罢低,節(jié)省時間和帶寬查辩,templateTag也是類似的,當(dāng)templateTag不一樣的時候网持,服務(wù)端才會更新模板宜岛。
簡而言之,Config就是保存了這次請求頭中的一些重要信息功舀,留待下次請求的時候發(fā)還給服務(wù)端做優(yōu)化萍倡。
緩存Key
說完緩存類型,必須要說一下緩存的key辟汰,這個非常重要列敲。首次請求會調(diào)用saveFirstWithHtmlData:withResponseHeaders:withUrl
緩存數(shù)據(jù)阱佛。入?yún)⒂衕tmlData、header和url戴而,前面已經(jīng)分析htmlData是需要緩存的頁面數(shù)據(jù)凑术,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];
......
}
首先根據(jù)url生成sessionID扶踊,然后再將sessionID和特定的SonicCacheItem
實例綁定泄鹏。這里我們先說明每個固定url生成的sessionID是一樣的,這才能讓我們在相同的url請求的情況下使用緩存秧耗,具體的url生成sessionID的規(guī)則在SonicSession
章節(jié)詳細說明备籽。
SonicCacheItem
每個緩存Key,也就是根據(jù)url生成的sessionID都會對應(yīng)一個SonicCacheItem的實例绣版,用來緩存所有的數(shù)據(jù)胶台。SonicCacheItem也就是一個緩存的數(shù)據(jù)結(jié)構(gòu),包含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關(guān)聯(lián)了一個緩存對象SonicCacheItem,同時也關(guān)聯(lián)了一次URL請求杭朱,VasSonic將這個請求抽象為SonicSession阅仔。SonicSession在VasSonic的設(shè)計里面非常關(guān)鍵。其將資源的請求和WebView脫離開來弧械,有了SonicSession八酒,結(jié)合SonicCache,我們就可以不依賴WebView去做資源的請求刃唐,這樣就可以實現(xiàn)WebView打開和資源加載并行羞迷、資源預(yù)加載等加速方案。
SessionID
每個sessionID唯一指定了一個SonicSession画饥,sessionID的生成規(guī)則如下:
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來算的抖甘,而是先經(jīng)過了sonicUrl
的函數(shù)的處理热鞍。理解sonicUrl
對url的處理有助于我們了解VasSonic的session管理機制。
其實sonicUrl
做的事情比較簡單。
- 對于一般的url來說薇宠,
sonicUrl
會只保留scheme偷办、host和path,url其他部分的改變不會創(chuàng)建新的session - 新增了
sonic_remain_params
參數(shù)昼接,sonic_remain_params
里面指定的query參數(shù)不同會創(chuàng)建新的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
的代碼也比較簡單悴晰,這里就不貼了慢睡,有興趣的同學(xué)可以參考這里sonicUrl實現(xiàn)。
自定義請求頭
之前提到過SonicCache的一種緩存類型是Config铡溪,SonicSession在初始化時候會根據(jù)緩存的Config更新請求頭漂辐,以便服務(wù)端根據(jù)這些信息做相應(yīng)的優(yōu)化。
- (void)setupData
{
// 根據(jù)sessionID獲取緩存內(nèi)容
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];
// 根據(jù)緩存設(shè)置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) {
// 設(shè)置eTag信息
NSString *eTag = cfgDict[SonicHeaderKeyETag];
if (eTag.length > 0) {
[mCfgDict setObject:eTag forKey:@"If-None-Match"];
}
// 設(shè)置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;
}
除了會添加自定義的請求頭參數(shù)哈扮,以及將緩存的config加到請求頭里面外纬纪,在每次發(fā)起請求之前,都會同步cookies滑肉,這樣就可以保持狀態(tài)了包各,比如登陸狀態(tài)等等。
- (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];
// 從系統(tǒng)cookies中讀取cookies信息靶庙,并添加到自定義請求頭
NSHTTPCookieStorage *sharedHTTPCookieStorage = [NSHTTPCookieStorage sharedHTTPCookieStorage];
NSArray *cookies = [sharedHTTPCookieStorage cookiesForURL:cUrl];
NSDictionary *cookieHeader = [NSHTTPCookie requestHeaderFieldsWithCookies:cookies];
[self addCustomRequestHeaders:cookieHeader];
}
做了上面這些工作问畅,我們可以抓包看最終一個請求會長成什么樣子。通過對Demo中LOAD WITH SONIC抓包發(fā)現(xiàn)請求頭中帶了sonic-load-type六荒、template-tag护姆、sonic-sdk-version等等,服務(wù)端正是基于這些參數(shù)做了優(yōu)化掏击。
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
網(wǎng)絡(luò)連接
VasSonic默認提供了基于URLSession的SonicConnection
來發(fā)起請求和處理響應(yīng)卵皂。SonicConnection
做的事情并不多,主要實現(xiàn)了兩個接口砚亭,并提供SonicSessionProtocol
定義的網(wǎng)絡(luò)回調(diào)接口供session處理灯变。
- (void)startLoading; // 開始請求
- (void)stopLoading; // 取消請求
// SonicSessionProtocol
// 收到響應(yīng)的時候回調(diào)
- (void)session:(SonicSession *)session didRecieveResponse:(NSHTTPURLResponse *)response;
// 加載數(shù)據(jù)之后回調(diào)
- (void)session:(SonicSession *)session didLoadData:(NSData *)data;
// 連接錯誤的時候回調(diào)
- (void)session:(SonicSession *)session didFaild:(NSError *)error;
// 結(jié)束加載的時候回調(diào)
- (void)sessionDidFinish:(SonicSession *)session;
如果需要在發(fā)起請求和處理響應(yīng)階段做一些自定義的動作的話,比如實現(xiàn)離線包方案等等钠惩,就可以自定義繼承于SonicConnection的Connection對象柒凉,在回調(diào)SonicSessionProtocol
方法之前做些處理。
注冊自定義的Connection對象使用如下的方法篓跛,可以同時注冊多個膝捞,通過實現(xiàn)canInitWithRequest:
來決定使用哪個Connection。
+ (BOOL)registerSonicConnection:(Class)connectionClass;
+ (void)unregisterSonicConnection:(Class)connectionClass;
值得注意的是,SonicConnection的所有接口設(shè)計都類似NSURLProtocol協(xié)議蔬咬,但他并不繼承自NSURLProtocol
鲤遥,原因在本文最后WebView請求攔截部分會有提到。
緩存處理
SonicSession根據(jù)請求響應(yīng)頭中cache-offline
返回的存儲策略的不一樣會有不同的處理林艘,Sonic定義了如下幾種離線存儲的策略盖奈。
/**
* 存儲但不刷新頁面
*/
#define SonicHeaderValueCacheOfflineStore @"store"
/**
* 存儲而且刷新頁面
*/
#define SonicHeaderValueCacheOfflineStoreRefresh @"true"
/**
* 不存儲但刷新頁面
*/
#define SonicHeaderValueCacheOfflineRefresh @"false"
/**
* Sonic模式關(guān)閉,并在接下來6個小時內(nèi)不再使用
*/
#define SonicHeaderValueCacheOfflineDisable @"http"
當(dāng)SonicSession在發(fā)起請求之后需要處理本地有緩存和沒有緩存兩種情況狐援。
沒有緩存的情況
沒有緩存钢坦,首次加載的情況下根據(jù)策略的處理方式也比較簡單,沒啥好說的啥酱,直接上代碼爹凹。
- (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];
}
......
}
有緩存的情況
有緩存的情況相對來說要復(fù)雜一些,需要處理模板更新和數(shù)據(jù)更新兩種不同的情況镶殷。
- (void)updateDidSuccess
{
......
// 處理模板更新的情況禾酱,模板更新是大動作,跟首次加載已經(jīng)區(qū)別不大绘趋,模板更新一定會導(dǎo)致數(shù)據(jù)更新
if ([self isTemplateChange]) {
self.cacheFileData = self.responseData;
[self dealWithTemplateChange];
// 模板不變颤陶,數(shù)據(jù)更新
}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];
}
}
......
}
模板變化是直接調(diào)用了saveFirstWithHtmlData:withResponseHeaders:withUrl:
來更新緩存,可見模板變化會導(dǎo)致之前的緩存都失效陷遮。
- (void)dealWithTemplateChange
{
SonicCacheItem *cacheItem = [[SonicCache shareCache] saveFirstWithHtmlData:self.responseData withResponseHeaders:self.response.allHeaderFields withUrl:self.url];
......
}
數(shù)據(jù)變化則是調(diào)用updateWithJsonData:withResponseHeaders:withUrl:
來更新緩存滓走,該函數(shù)會將本地的緩存和服務(wù)端返回的數(shù)據(jù)做個diff,然后返回給前端更新界面拷呆。
- (void)dealWithDataUpdate
{
SonicCacheItem *cacheItem = [[SonicCache shareCache] updateWithJsonData:self.responseData withResponseHeaders:self.response.allHeaderFields withUrl:self.url];
......
}
攔截WebView請求
現(xiàn)在SonicSession結(jié)合SonicCache能獨立高效處理URL請求闲坎,那么如何使用SonicSession來接管WebView的請求呢?iOS下所有的URL請求都是走URL Loading System的茬斧,攔截WebView的請求只需要自定義實現(xiàn)NSURLProtocol
協(xié)議就可以了腰懂。
因為NSURLProtocol會攔截所有的請求,那如何只針對Sonic WebView發(fā)起的請求實現(xiàn)攔截呢项秉?可以通過canInitWithRequest:
來實現(xiàn)绣溜,只有請求頭中帶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;
}
當(dāng)系統(tǒng)發(fā)起請求的時候娄蔼,Sonic并沒有真正的發(fā)起請求怖喻,而是用SessionID注冊了回調(diào),讓SonicSession在恰當(dāng)?shù)臅r候調(diào)動回調(diào)岁诉。
- (void)startLoading
{
NSThread *currentThread = [NSThread currentThread];
NSString *sessionID = [self.request valueForHTTPHeaderField:SonicHeaderKeySessionID];
__weak typeof(self) weakSelf = self;
// 在SonicSession中注冊回調(diào)函數(shù)
[[SonicClient sharedClient] registerURLProtocolCallBackWithSessionID:sessionID completion:^(NSDictionary *param) {
[weakSelf performSelector:@selector(callClientActionWithParams:) onThread:currentThread withObject:param waitUntilDone:NO];
}];
}
接下來我們看看SonicSession都是在什么時機調(diào)用回調(diào)函數(shù)的锚沸,首次加載、預(yù)加載和完全緩存狀態(tài)是不一樣的涕癣。
首次加載的時候哗蜈,根據(jù)網(wǎng)絡(luò)的實際回調(diào)時機調(diào)用即可,代碼如下:
- (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];
......
}
有預(yù)加載的情況下,根據(jù)預(yù)加載的情況構(gòu)造需要回調(diào)的動作距潘,代碼如下:
- (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;
}
完全緩存的情況下炼列,構(gòu)造完整的回調(diào)動作,代碼如下:
- (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;
}
這樣業(yè)務(wù)使用者只需要正常的實現(xiàn)UIWebViewDelegate
的協(xié)議就可以了音比,不需要關(guān)心回調(diào)是來自真正的網(wǎng)絡(luò)連接俭尖、還是來自預(yù)加載,或者是完全的緩存洞翩,所有的緩存優(yōu)化就都能被封裝在SonicSession里面了稽犁。
這里有一點需要說明的是SonicURLProtocol和SonicConnection是不一樣的,雖然SonicConnection模仿了NSURLProtocol的接口菱农,但是其父類是NSObject缭付。SonicURLProtocol最大的功能是實現(xiàn)WebView的請求攔截,而SonicConnection則是SonicSession的網(wǎng)絡(luò)請求處理類循未。
頁面刷新
經(jīng)過上面的描述,我們基本已經(jīng)將整個流程都串起來了秫舌。
WebView發(fā)起請求 -> SonicURLProtocol實現(xiàn)請求攔截的妖,將控制權(quán)交給SonicSession
-> SonicSession根據(jù)SessionID獲取請求結(jié)果,回調(diào)請求過程足陨,請求結(jié)果可能來自緩存(SonicCache)嫂粟,也可能來自網(wǎng)絡(luò)請求(SonicConnection)
-> WebView根據(jù)結(jié)果展示頁面
整個流程最后的WebView頁面展示,也是非常重要的一塊優(yōu)化墨缘。
- (void)sessionDidFinish:(SonicSession *)session
{
dispatch_block_t opBlock = ^{
self.isCompletion = YES;
if (self.isFirstLoad) {
[self firstLoadDidFinish];
}else{
[self updateDidSuccess];
}
};
dispatchToSonicSessionQueue(opBlock);
}
當(dāng)請求結(jié)束的時候星虹,SonicSession會根據(jù)是否是首次加載分別調(diào)用firstLoadDidFinish
和updateDidSuccess
,這兩個函數(shù)除了對緩存的不同處理外镊讼,還有一個非常重要的區(qū)別:前者調(diào)用了[self dispatchProtocolAction:SonicURLProtocolActionDidFinish param:nil];
宽涌,后者則不會。也就是說前者會將請求結(jié)束的結(jié)果告訴WebView蝶棋,而后者不會卸亮,導(dǎo)致的結(jié)果就是前者會刷新頁面,而后者不會玩裙。但是updateDidSuccess
中有這么一段代碼兼贸。
- (void)updateDidSuccess
{
......
// 如果js注冊了數(shù)據(jù)刷新的回調(diào),就調(diào)用該回調(diào)
if (self.webviewCallBack) {
NSDictionary *resultDict = [self sonicDiffResult];
if (resultDict) {
self.webviewCallBack(resultDict);
}
}
......
}
如果有webviewCallBack
吃溅,那么這個回調(diào)是會被調(diào)用的溶诞,參數(shù)是經(jīng)過diff之后的數(shù)據(jù),看到這里應(yīng)該同學(xué)都明白了决侈,這就是局部刷新的實現(xiàn)機制螺垢。
Sonic給JS暴露一個方法叫getDiffDataCallback
,JS只要設(shè)置該回調(diào),最終就是設(shè)置了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相關(guān)實現(xiàn)在sonic.js中蹂楣,有興趣的同學(xué)可以自行翻看js源碼。Demo中的更新邏輯如下:
//0-狀態(tài)獲取失敗 1-sonic首次 2-頁面刷新 3-局部刷新 4-完全cache
sonic.getSonicData(function(sonicStatus, reportSonicStatus, sonicUpdateData){
if(sonicStatus == 1){
//首次沒有特殊的邏輯處理讯蒲,直接執(zhí)行sonic完成后的邏輯痊土,比如上報等
}else if(sonicStatus == 2){
}else if(sonicStatus == 3){
//局部刷新的時候需要更新頁面的數(shù)據(jù)塊和一些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);
});
結(jié)論
總結(jié)來看VasSonic并不是與眾不同的新技術(shù),但是其對HTML墨林、客戶端WebView有著深入的了解赁酝,通過司空見慣的一些技術(shù)的極致搭配和使用,極大的提升了WebView的性能旭等。仔細研究SonicSession和SonicCache的實現(xiàn)對于了解VasSonic的設(shè)計思想非常重要酌呆。最后感謝騰訊團隊給開源界帶來這么優(yōu)秀的WebView框架。