閱讀513 返回首頁    go 阿裏雲 go 技術社區[雲棲]


VasSonic源碼解析

H5很重要,H5很重要,H5很重要,重要的事情要說三遍。VasSonic是騰訊開源的解決H5首屏渲染痛點的開源項目,本文通過解讀代碼來學習WebView的優化思路。

H5的優劣

H5的優勢很明顯,跨平台、迭代快、開發體驗好。H5的劣勢同樣明顯,加載慢,用戶體驗差。業內大牛想盡各種方法來彌補H5的劣勢,初級使用緩存、預加載等常用方案,高級如Hybrid、ReactNative、Weex等H5的進階解決方案。VasSonic專注於H5的秒開,使用的也是我們常見的性能優化方案。本文嚐試了解VasSonic是如何用常見的手段將性能優化做到極致的。

VasSonic解決什麼問題

關於WebView為什麼打開慢、加載慢,業界已經有很多分析了,結論也是比較一致的,推薦美團點評技術團隊的WebView性能、體驗分析與優化,騰訊關於VasSonic的官方文章也有相關說明

WebView加載慢的問題主要集中在如下三個階段:

  1. WebView打開
  2. 頁麵資源加載
  3. 數據更新導致頁麵刷新

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會根據是否是首次加載分別調用firstLoadDidFinishupdateDidSuccess,這兩個函數除了對緩存的不同處理外,還有一個非常重要的區別:前者調用了[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

  上一篇:go  中國裝備製造業尋找新路徑:不數字化無未來
  下一篇:go  阿裏雲前端周刊 - 第 25 期