读懂「 唱吧KTVHTTPCache 」设计思想

读懂「 唱吧 KTVHTTPCache 」设计思想

经过作者的指点,本文更新了日志原理说明,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,我会不定期的分享一些开发技巧

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 156,069评论 4 358
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,212评论 1 287
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 105,912评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,424评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,741评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,194评论 1 206
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,553评论 2 307
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,289评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,923评论 1 237
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,251评论 2 240
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,775评论 1 255
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,144评论 2 249
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,698评论 3 228
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,936评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,658评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,214评论 2 267
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,159评论 2 258

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,082评论 18 139
  • 深入浅出HTTP协议(WEB开发和面试必备) 1.基础概念篇 a.简介 HTTP是Hyper Text Trans...
    半世韶华忆阑珊阅读 1,191评论 0 7
  • 本文整理自MIN飞翔博客 [1] 1. 概念 协议是指计算机通信网络中两台计算机之间进行通信所必须共同遵守的规定或...
    HoyaWhite阅读 2,572评论 2 20
  • API定义规范 本规范设计基于如下使用场景: 请求频率不是非常高:如果产品的使用周期内请求频率非常高,建议使用双通...
    有涯逐无涯阅读 2,441评论 0 6
  • 点这里,音乐和文章更配哦~ 快乐多少有一点,不过寂寞更强烈。难过时候不流泪,流泪也不算伤悲。 男人必须保持风度,所...
    邢姑娘阅读 642评论 1 0