讀懂「 唱吧 KTVHTTPCache 」設計思想

Lefe_x發表於2019-03-04

經過作者的指點,本文更新了日誌原理說明,HttpServer 作用,圖片快取

最近看到各大V轉發關於 唱吧音視訊框架 KTVHTTPCache 的開源訊息,首先我非常感謝唱吧 iOS 團隊能夠無私地把自己的成果開源。我本人對於快取的設計也比較感興趣,也喜歡寫一些東西,希望能把自己一些小技巧分享給需要的同學,這也是我們 #iOS知識小集# 一直做的事情。抱著好奇的心,想了解一下唱吧是如何設計 KTVHTTPCache 的,沒想到越看越難,最後竟然花了將近2天的時間看完了。

安裝時解讀

在進行安裝的時候,發現 KTVHTTPCache 主要依賴了 CocoaHTTPServer 這個庫,而 CocoaHTTPServer 又依賴了 CocoaAsyncSocketCocoaLumberjack。可以肯定一點 KTVHTTPCache 使用 CocoaHTTPServer 作為 HttpServer。

CocoaHTTPServer is a small, lightweight, embeddable HTTP server for Mac OS X or iOS applications.

Sometimes developers need an embedded HTTP server in their app. Perhaps it's a server application with remote monitoring. Or perhaps it's a desktop application using HTTP for the communication backend. Or perhaps it's an iOS app providing over-the-air access to documents. Whatever your reason, CocoaHTTPServer can get the job done

podinstall.png

Readme 中提到:

KTVHTTPCache 由 HTTP Server 和 Data Storage 兩大模組組成。

另外一個主要模組就是 Data Storage,它主要負責資源載入及快取處理。從這裡可以看出,KTVHTTPCache 主要的工作量是設計 Data Storage 這個模組,也就是它的核心所在。

使用

其本質是對 HTTP 請求進行快取,對傳輸內容並沒有限制,因此應用場景不限於音視訊線上播放,也可以用於檔案下載、圖片載入、普通網路請求等場景。 --- KTVHTTPCache

既然這麼好使,我們可以試試各種情況,demo 中雖然沒有給出其它方式的快取示例,我們可以探索一下。不過我測試了下載圖片的,並沒有成功,其它中情況也就沒有試驗。我猜測,如果想支援這幾種情況,應該需要修改原始碼(如果作者能看到,忘解答一下,不知道我的猜測是否正確)。

視訊快取( Demo 中提供,親測可以)

  • 全域性啟動一次即可,主要用來啟動 HttpServer,不理解的話,你可以把它想成手機端的HTTP伺服器,當你向HTTP伺服器發出 Request 後,伺服器會給你一個 Response,後面我們會特意分析一個 HttpServer。

[KTVHTTPCache proxyStart:&error];

  • 根據原 url 生成一個 proxy url(代理 Url),並使用代理 url 獲取資料,這樣 HttpServer 就會截獲這次請求。比如原 url 為 http://lzaiuw.changba.com/userdata/video/940071102.mp4 它對應的 proxy url 為
http://localhost:53112/request-940071102.mp4?requestType=content&originalURL
=http%3A%2F%2Flzaiuw.changba.com%2Fuserdata%2Fvideo%2F940071102.mp4
複製程式碼

看圖會更好理解:

proxyUrl.png

NSString * proxyURLString = [KTVHTTPCache proxyURLStringWithOriginalURLString:URLString];
複製程式碼
  • 播放,注意這裡使用的是代理url,進行播放,而不是原url。 [AVPlayer playerWithURL:[NSURL URLWithString: proxyURLString]];

圖片快取

這次試驗結果沒能成功,在快取中找不到快取圖片,或許是我少些了什麼。

- (void)testImageCache
{
    NSString *imageUrl = @"http://g.hiphotos.baidu.com/image/pic/item/e824b899a9014c08d8614343007b02087af4f4fa.jpg";
    NSString *proxyStr = [KTVHTTPCache proxyURLStringWithOriginalURLString:imageUrl];
    NSURLSessionTask *task2 = [[NSURLSession sharedSession] downloadTaskWithURL:[NSURL URLWithString:proxyStr]];
    [task2 resume];
}
複製程式碼

控制檯列印出的日誌可以發現,圖片沒能快取是因為 content type 錯誤導致的,因為 KTVHTTPCache 目前只支援 video, audioapplication/octet-stream 三種型別的,如果想快取圖片可以新增一種 content type。

KTVHTTPCache[818:16793] KTVHCDataNetworkSource  :   response error
http://g.hiphotos.baidu.com/image/pic/item/e824b899a9014c08d8614343007b02087af4f4fa.jpg
content type error
複製程式碼

KTVHCDataRequest 類中的初始化方法中修改

self.acceptContentTypes = @[KTVHCDataContentTypeVideo,
                            KTVHCDataContentTypeAudio,
                            KTVHCDataContentTypeOctetStream];
複製程式碼

self.acceptContentTypes = @[KTVHCDataContentTypeVideo,
                            KTVHCDataContentTypeAudio,
                            KTVHCDataContentTypeOctetStream,
                            KTVHCDataContentTypeImage];
複製程式碼

這樣既可以支援快取圖片的需求。這裡作者 @程式設計師Single 特別提示,如果做圖片快取,不建議使用 HttpServer 做中轉,也就不需要 Proxy URL,這樣會節省不必要的開銷。HttpServer 的主要目的是為了 Hook 播放器的網路請求,從而可以做到快取資料。可以直接使用 KTVHTTPCache 中的方法來生成 Reader 讀取資料。可以參考 KTVHCHTTPResponse 的實現。

+ (KTVHCDataReader *)cacheConcurrentReaderWithRequest:(KTVHCDataRequest *)request;

+ (KTVHCDataReader *)cacheSerialReaderWithRequest:(KTVHCDataRequest *)request;
複製程式碼

框架設計

KTVHTTPCache 由 HTTP Server 和 Data Storage 兩大模組組成。前者負責與 Client 互動,後者負責資源載入及快取處理。

這句話如果沒有看原始碼,其實很難理解,涉及到如何互動的問題(我是這樣認為的,也許你比我聰明,能理解作者的含義)。通俗地講,HTTP Server 和 Data Storage 是 KTVHTTPCache 兩大重要組成部分, HTTP Server 主要負責與使用者互動,也就是最頂層,最直接與使用者互動(比如下載資料),而 Data Storage 則在後面為 HTTP Server 提供資料,資料主要從 DataSourcer 中獲取,如果本地有資料,它會從 KTVHCDataFileSource 中獲取,反之會從 KTVHCDataNetworkSource 中讀取資料,這裡會走下載邏輯(KTVHCDownload)。

KTVHTTPCache.jpeg

HttpServer

這層設計比較簡單,主要是用了 CocoaHTTPServer 來作為本地的 HttpServer。HttpServer 說白了就是一個手機端的伺服器,用來與使用者(作者說的 client)互動,使用者提出資料載入需求後,它會從不同的地方來獲取資料來源,如果本地沒有會從網路中下載資料。它主要的作用是 hook 播放器的網路請求,進行資料的載入。它主要的類如圖:

aonaotu-download-1.png

  • KTVHCHTTPServer:是一個單例,用來管理 HttpServer 服務,負責開啟或關閉服務;
  • KTVHCHTTPConnection:它繼承於 HTTPConnection,表示一個連線,它主要為 HttpServer 提供 Response。
  • KTVHCHTTPRequest:一個請求,也就是一個資料模型;
  • KTVHCHTTPResponse:一個 Response;
  • KTVHCHTTPResponsePing:主要用來 ping 時的 Response;
  • KTVHCHTTPURL:主要用來處理 URL,比如把原 Url 生成 proxy url;

其實 HttpServer 的關鍵點是在 KTVHCHTTPConnection 中下面這個方法,它是連線快取模組的一個橋樑。使用 KTVHCDataRequestKTVHCHTTPConnection 來生成 KTVHCHTTPResponse關鍵點在於生成這個 Response。 這段程式碼僅僅為了說明問題,有刪減:

- (NSObject<HTTPResponse> *)httpResponseForMethod:(NSString *)method URI:(NSString *)path
{    
    KTVHCHTTPURL * URL = [KTVHCHTTPURL URLWithServerURIString:path];
    
    switch (URL.type)
    {
        case KTVHCHTTPURLTypePing:
        {
            return [KTVHCHTTPResponsePing responseWithConnection:self];
        }
        case KTVHCHTTPURLTypeContent:
        {
            KTVHCHTTPRequest * currentRequest = [KTVHCHTTPRequest requestWithOriginalURLString:URL.originalURLString];
            
            KTVHCDataRequest * dataRequest = [currentRequest dataRequest];
            KTVHCHTTPResponse * currentResponse = [KTVHCHTTPResponse responseWithConnection:self dataRequest:dataRequest];
            
            return currentResponse;
        }
    }
    return nil;
}
複製程式碼

connetction.png

DataStroage

主要用來快取資料,載入資料,也就是提供資料給 HttpServer。上面程式碼中關鍵的一句程式碼 [KTVHCHTTPResponse responseWithConnection:self dataRequest:dataRequest],它會在這個方法的內部使用 KTVHCDataStorage 生成一個 KTVHCDataReader,負責讀取資料。生成 KTVHCDataReader 後通過 [self.reader prepare] 來準備資料來源 KTVHCDataSourcer,這裡主要有兩個資料來源,KTVHCDataFileSourceKTVHCDataNetworkSource,它實現了協議 KTVHCDataSourceProtocolKTVHCDataNetworkSource 會通過 KTVHCDownload 下載資料。

需要說明一點,快取是分片處理的

aonaotu-download.png

  • KTVHCDataStorage: 是一個單例,它負責管理整個快取,比如讀取、儲存和合並快取;
  • KTVHCDataReader:主要用來讀取資料;
  • KTVHCDataRequest:用來請求資料,表示一個請求;
  • KTVHCDataResponse:一個資料響應;
  • KTVHCDataReader:讀取資料;
  • KTVHCDataCacheItem:快取資料模型,表一個快取項;
  • KTVHCDataCacheItemZone:快取區,一個快取項中會有多個快取區,比如0-99,100-299 等;
  • KTVHCDataSourcer:資料來源中心,負責處理不同資料來源,它包含有一個資料佇列 KTVHCDataSourceQueue;
  • KTVHCDataSourceQueue:資料佇列;
  • KTVHCDataSourceProtocol:一個協議,作為資料來源時需要實現這個協議;
  • KTVHCDataFileSource:本地資料來源,實現了 KTVHCDataSourceProtocol 協議;
  • KTVHCDataNetworkSource:網路資料來源,實現了 KTVHCDataSourceProtocol 協議;
  • KTVHCDataUnit:資料單元,相當於一個快取目錄,比如一個視訊的快取;
  • KTVHCDataUnitItem:資料單元項,快取目錄下不同片段的快取;
  • KTVHCDataUnitPool:資料單元池,它是一個單例,含有一個 KTVHCDataUnitQueue;
  • KTVHCDataUnitQueue:資料單元佇列,儲存了多個 KTVHCDataUnit,它會以 archive 的方式快取到本地;

dataUnit.png

快取目錄構成

結構

05f68836443a1535b73bfcf3c2e86d99 這個是由請求的原 url,md5 後生成的字串,其中它的子目錄下會有多個檔案,命名規則為:urlmd5_offset_數字。檔名最後一位數字因為

Data Storeage 同時支援並行和序列。在並行場景中極端情況可能遇到恰好同時存在兩個相同 Offset 的 Network Source,用來保證並行載入的安全性(實際場景中也沒遇到過,但在結構設計時把這部分考慮進去了)-- @程式設計師Single

  • 05f68836443a1535b73bfcf3c2e86d99
  • 05f68836443a1535b73bfcf3c2e86d99_0_0
  • 05f68836443a1535b73bfcf3c2e86d99_196608_0
  • 05f68836443a1535b73bfcf3c2e86d99_738108_0

沙盒目錄:

cache.png

快取策略

例如一次請求的 Range 為 0-999,本地快取中已有 200-499 和 700-799 兩段資料。那麼會對應生成 5 個 Source,分別是:

  • 網路: 0-199
  • 本地: 200-499
  • 網路: 500-699
  • 本地: 700-799
  • 網路: 800-999

日誌系統

做音視訊專案時,一個好的 Log 管理可以提高除錯效率,而 KTVHTTPCache 可以追蹤到每一次異常的請求。而且回記錄到一個 KTVHTTPCache.log 檔案中。

KTVHTTPCache[818:16603] Proxy Start Success
KTVHTTPCache[818:16603] <KTVHCHTTPURL: 0x6040002349e0>  :   alloc
KTVHTTPCache[818:16603] KTVHCHTTPURL            :   Ping, original url, KTVHCHTTPURLPingResponseFile
KTVHTTPCache[818:16603] KTVHCHTTPURL            :   proxy url, http://localhost:49816/request-KTVHCHTTPURLPingResponseFile?requestType=ping&originalURL=KTVHCHTTPURLPingResponseFile
KTVHTTPCache[818:16603] <KTVHCHTTPURL: 0x6040002349e0>  :   dealloc
KTVHTTPCache[818:16795] <KTVHCHTTPConnection: 0x6000001331a0>  :   alloc
KTVHTTPCache[818:16793] KTVHCHTTPConnection     :   receive request, GET, /request-KTVHCHTTPURLPingResponseFile?requestType=ping&originalURL=KTVHCHTTPURLPingResponseFile
KTVHTTPCache[818:16793] <KTVHCHTTPURL: 0x604000238b00>  :   alloc
KTVHTTPCache[818:16793] KTVHCHTTPURL            :   Server URI, /request-KTVHCHTTPURLPingResponseFile?requestType=ping&originalURL=KTVHCHTTPURLPingResponseFile, original url, KTVHCHTTPURLPingResponseFile, type, 0
KTVHTTPCache[818:16793] <KTVHCHTTPResponsePing: 0x604000238b40>  :   alloc
KTVHTTPCache[818:16793] <KTVHCHTTPURL: 0x604000238b00>  :   dealloc
KTVHTTPCache[818:16793] KTVHCHTTPResponsePing   :   conetnt length, 4
KTVHTTPCache[818:16793] KTVHCHTTPResponsePing   :   read data length, 4, offset, 4 pang
KTVHTTPCache[818:16793] KTVHCHTTPResponsePing   :   check done, 1
KTVHTTPCache[818:16793] KTVHCHTTPResponsePing   :   connection did close, 4, 4
KTVHTTPCache[818:16793] <KTVHCHTTPResponsePing: 0x604000238b40>  :   dealloc
KTVHTTPCache[818:16603] KTVHCHTTPServer         :   ping result, 1
複製程式碼

日誌的定義主要在類 KTVHCLog 中定義了一些巨集。可以通過下面的方法開啟日誌:

// 開啟控制檯列印
[KTVHTTPCache logSetConsoleLogEnable:YES];
// 開啟本地日誌記錄
[KTVHTTPCache logSetRecordLogEnable:YES];
複製程式碼

特別說明一點可以控制到每個類是否列印:

KTVHCLogEnable(HTTPServer, YES, YES)
複製程式碼

主要一個日誌的方法:

#define KTVHCLogging(target, console_log_enable, record_log_enable, ...)            \
if ((console_log_enable) || (record_log_enable))       \
{                                                                                   \
NSString * va_args = [NSString stringWithFormat:__VA_ARGS__];                   \
NSString * log = [NSString stringWithFormat:@"%@  :   %@", target, va_args];    \
if (record_log_enable) {                      \
NSLog(@"%@", log);;                                          \
}                                                                               \
if (console_log_enable) {                    \
NSLog(@"%@", log);                                                          \
}                                                                               \
}
複製程式碼

總結

學習這個庫總體來說比較耗時,但是能學到作者的思想,這裡總結一下:

  • 職責明確,每個類的作用定義明確;
  • KTVHCDataFileSourceKTVHCDataNetworkSource,使用協議 KTVHCDataSourceProtocol 的方式實現不同的 Source,而不用繼承,耦合性更低;
  • 使用簡單,內部定義複雜,緩緩相扣;
  • 使用 NSLock 保證執行緒安全;
  • 日誌定義周全,除錯更容易;

歡迎關注我微博 Lefe_x,我會不定期的分享一些開發技巧

相關文章