iOS 視頻緩存KTVHTTPCache原理和實現(xiàn)

目前iOS端比較常見的視頻緩存的實現(xiàn)方式主要有兩種:
1英融、使用iOS自帶的AVURLAsset的AVAssetResourceLoader來實現(xiàn)但校。
2光稼、在客戶端搭建local服務器坤次,local服務器作為中間者步鉴,代替客戶端請求服務器數(shù)據(jù)揪胃,并將獲取到的數(shù)據(jù)緩存,再提供給客戶端唠叛。
我們項目里使用的是KTVHTTPCache來實現(xiàn)視頻緩存只嚣,KTVHTTPCache的實現(xiàn)方式就是第二種,項目地址:(https://github.com/ChangbaDevs/KTVHTTPCache)艺沼。

具體實現(xiàn):

KTVHTTPCache的使用比較簡單:

NSURL *proxyURL = [KTVHTTPCache proxyURLWithOriginalURL:originalURL];
AVPlayer *player = [AVPlayer playerWithURL:proxyURL];

可以看出册舞,它是將源視頻的URL替換成了自己定義格式的URL,這時我們其實請求的就是local服務器了障般。
核心的流程大概是這樣:


image.png

幾個核心類實現(xiàn):

1调鲸、KTVHCHTTPServer:
用來搭建local server的,內(nèi)部使用第三方庫HTTPServer實現(xiàn):
創(chuàng)建自己的Connection類繼承自HTTPConnection

@interface KTVHCHTTPConnection : HTTPConnection

重寫子類方法挽荡,返回相應的response類

- (NSObject<HTTPResponse> *)httpResponseForMethod:(NSString *)method URI:(NSString *)path
{
    KTVHCLogHTTPConnection(@"%p, Receive request\nmethod : %@\npath : %@\nURL : %@", self, method, path, request.url);
    NSDictionary<NSString *,NSString *> *parameters = [[KTVHCURLTool tool] parseQuery:request.url.query];
    NSURL *URL = [NSURL URLWithString:[parameters objectForKey:@"url"]];
    KTVHCDataRequest *dataRequest = [[KTVHCDataRequest alloc] initWithURL:URL headers:request.allHeaderFields];
    KTVHCHTTPResponse *response = [[KTVHCHTTPResponse alloc] initWithConnection:self dataRequest:dataRequest];
    return response;
}

創(chuàng)建response作為Local Server數(shù)據(jù)返回體藐石,遵循HTTPResponse協(xié)議,實現(xiàn)協(xié)議方法

@interface KTVHCHTTPResponse : NSObject <HTTPResponse>

實現(xiàn)協(xié)議方法

#pragma mark - HTTPResponse
- (NSData *)readDataOfLength:(NSUInteger)length
{
  "讀取數(shù)據(jù)最開始的入口"
   NSData *data = [self.reader readDataOfLength:length];
   KTVHCLogHTTPResponse(@"%p, Read data : %lld", self, (long long)data.length);
   if (self.reader.isFinished) {
       KTVHCLogHTTPResponse(@"%p, Read data did finished", self);
       [self.reader close];
       [self.connection responseDidAbort:self];
   }
   return data;
}
………………(省略定拟,節(jié)省篇幅)

這樣于微,當本地發(fā)生請求時,就會獲取KTVHCHTTPResponse內(nèi)部方法返回的數(shù)據(jù)青自。
2株依、KTVHCDataReader和KTVHCDataSourceManager
從服務器返回類可以看到,數(shù)據(jù)的入口是從KTVHCDataReader的readDataOfLength獲取的延窜。

#pragma mark - KTVHCDataReader
- (NSData *)readDataOfLength:(NSUInteger)length
{
    [self lock];
    if (self.isClosed) {
        [self unlock];
        return nil;
    }
    if (self.isFinished) {
        [self unlock];
        return nil;
    }
    if (self.error) {
        [self unlock];
        return nil;
    }
    NSData *data = [self.sourceManager readDataOfLength:length];
    if (data.length > 0) {
        self->_readedLength += data.length;
        if (self.response.contentLength > 0) {
            self->_progress = (double)self.readedLength / (double)self.response.contentLength;
        }
    }
    KTVHCLogDataReader(@"%p, Read data : %lld", self, (long long)data.length);
    if (self.sourceManager.isFinished) {
        KTVHCLogDataReader(@"%p, Read data did finished", self);
        self->_finished = YES;
        [self close];
    }
    [self unlock];
    return data;
}

從這個方法里我們可以看到恋腕,讀取數(shù)據(jù)又走到了KTVHCDataSourceManager中去。

#pragma mark - KTVHCDataReader
- (void)prepareSourceManager
{
   "兩個數(shù)組保存兩種數(shù)據(jù)來源"
    NSMutableArray<KTVHCDataFileSource *> *fileSources = [NSMutableArray array];
    NSMutableArray<KTVHCDataNetworkSource *> *networkSources = [NSMutableArray array];
    long long min = self.request.range.start;
    long long max = self.request.range.end;
    NSArray *unitItems = self.unit.unitItems;
    for (KTVHCDataUnitItem *item in unitItems) {
        long long itemMin = item.offset;
        long long itemMax = item.offset + item.length - 1;
        if (itemMax < min || itemMin > max) {
            continue;
        }
        if (min > itemMin) {
            itemMin = min;
        }
        if (max < itemMax) {
            itemMax = max;
        }
        min = itemMax + 1;
        KTVHCRange range = KTVHCMakeRange(item.offset, item.offset + item.length - 1);
        KTVHCRange readRange = KTVHCMakeRange(itemMin - item.offset, itemMax - item.offset);
        KTVHCDataFileSource *source = [[KTVHCDataFileSource alloc] initWithPath:item.absolutePath range:range readRange:readRange];
        [fileSources addObject:source];
    }
    [fileSources sortUsingComparator:^NSComparisonResult(KTVHCDataFileSource *obj1, KTVHCDataFileSource *obj2) {
        if (obj1.range.start < obj2.range.start) {
            return NSOrderedAscending;
        }
        return NSOrderedDescending;
    }];
    "對比本地已緩存的數(shù)據(jù)和視頻數(shù)據(jù)量"
   "除了本地的如果還有未獲取的數(shù)據(jù)逆瑞,就需要網(wǎng)絡請求獲取了"
    long long offset = self.request.range.start;
    long long length = KTVHCRangeIsFull(self.request.range) ? KTVHCRangeGetLength(self.request.range) : (self.request.range.end - offset + 1);
    for (KTVHCDataFileSource *obj in fileSources) {
        long long delta = obj.range.start + obj.readRange.start - offset;
        if (delta > 0) {
            KTVHCRange range = KTVHCMakeRange(offset, offset + delta - 1);
            KTVHCDataRequest *request = [self.request newRequestWithRange:range];
            KTVHCDataNetworkSource *source = [[KTVHCDataNetworkSource alloc] initWithRequest:request];
            [networkSources addObject:source];
            offset += delta;
            length -= delta;
        }
        offset += KTVHCRangeGetLength(obj.readRange);
        length -= KTVHCRangeGetLength(obj.readRange);
    } 
 
    if (length > 0) {
        KTVHCRange range = KTVHCMakeRange(offset, self.request.range.end);
        KTVHCDataRequest *request = [self.request newRequestWithRange:range];
        KTVHCDataNetworkSource *source = [[KTVHCDataNetworkSource alloc] initWithRequest:request];
        [networkSources addObject:source];
    }
    NSMutableArray<id<KTVHCDataSource>> *sources = [NSMutableArray array];
    [sources addObjectsFromArray:fileSources];
    [sources addObjectsFromArray:networkSources];
    self.sourceManager = [[KTVHCDataSourceManager alloc] initWithSources:sources delegate:self delegateQueue:self.internalDelegateQueue];
    [self.sourceManager prepare];
}

看到KTVHCDataSourceManager的初始化過程荠藤, 可以看出其實正常獲取數(shù)據(jù)的是KTVHCDataFileSource和KTVHCDataNetworkSource兩個類伙单。
再看一下KTVHCDataSourceManager的readDataOfLength方法:

#pragma mark - KTVHCDataSourceManager
- (NSData *)readDataOfLength:(NSUInteger)length
{
    [self lock];
    if (self.isClosed) {
        [self unlock];
        return nil;
    }
    if (self.isFinished) {
        [self unlock];
        return nil;
    }
    if (self.error) {
        [self unlock];
        return nil;
    }
    "從Source里讀取數(shù)據(jù)"
    NSData *data = [self.currentSource readDataOfLength:length];

    self->_readedLength += data.length;
    KTVHCLogDataSourceManager(@"%p, Read data : %lld", self, (long long)data.length);
    if (self.currentSource.isFinished) {
        "一個source讀完,切換到下一個Source"
        self.currentSource = [self nextSource];
        if (self.currentSource) {
            KTVHCLogDataSourceManager(@"%p, Switch to next source, %@", self, self.currentSource);
            if ([self.currentSource isKindOfClass:[KTVHCDataFileSource class]]) {
                [self.currentSource prepare];
            }
        } else {
            KTVHCLogDataSourceManager(@"%p, Read data did finished", self);
            self->_finished = YES;
        }
    }
    [self unlock];
    return data;
}

**KTVHCDataNetworkSource和KTVHCDataFileSource
從名字就可以看出:這兩個類哈肖,一個是負責從直接從本地文件提供數(shù)據(jù)吻育,一個是負責從網(wǎng)絡讀取之后提供數(shù)據(jù)
KTVHCDataFileSource的readDataOfLength實現(xiàn)比較明顯,就是單純從文件里讀取數(shù)據(jù)牡彻。
看下KTVHCDataNetworkSource:

- (void)ktv_download:(KTVHCDownload *)download didReceiveResponse:(KTVHCDataResponse *)response
{
    [self lock];
    if (self.isClosed || self.error) {
        [self unlock];
        return;
    }
    self->_response = response;
    NSString *path = [KTVHCPathTool filePathWithURL:self.request.URL offset:self.request.range.start];
    self.unitItem = [[KTVHCDataUnitItem alloc] initWithPath:path offset:self.request.range.start];
    KTVHCDataUnit *unit = [[KTVHCDataUnitPool pool] unitWithURL:self.request.URL];
    [unit insertUnitItem:self.unitItem];
    KTVHCLogDataNetworkSource(@"%p, Receive response\nResponse : %@\nUnit : %@\nUnitItem : %@", self, response, unit, self.unitItem);
    [unit workingRelease];
    "創(chuàng)建了兩個文件句柄扫沼,讀和寫。"
    self.writingHandle = [NSFileHandle fileHandleForWritingAtPath:self.unitItem.absolutePath];
    self.readingHandle = [NSFileHandle fileHandleForReadingAtPath:self.unitItem.absolutePath];
    [self callbackForPrepared];
    [self unlock];
}

- (void)ktv_download:(KTVHCDownload *)download didReceiveData:(NSData *)data
{
    [self lock];
    if (self.isClosed || self.error) {
        [self unlock];
        return;
    }
    @try {
        "接收到數(shù)據(jù)之后庄吼,寫入文件缎除。"
        [self.writingHandle writeData:data];
        self.downloadLength += data.length;
        [self.unitItem updateLength:self.downloadLength];
        KTVHCLogDataNetworkSource(@"%p, Receive data : %lld, %lld, %lld", self, (long long)data.length, self.downloadLength, self.unitItem.length);
       "有可用數(shù)據(jù)了,需要回調(diào)通知总寻。"
        [self callbackForHasAvailableData];
    } @catch (NSException *exception) {
        NSError *error = [KTVHCError errorForException:exception];
        KTVHCLogDataNetworkSource(@"%p, write exception\nError : %@", self, error);
        [self callbackForFailed:error];
        if (!self.downloadCalledComplete) {
            KTVHCLogDataNetworkSource(@"%p, Cancel download task when write exception", self);
            [self.downlaodTask cancel];
            self.downlaodTask = nil;
        }
    }
    [self unlock];
}

可以看出器罐,兩個source的實現(xiàn)比較類似,只不過KTVHCDataNetworkSource多了一個從網(wǎng)絡獲取數(shù)據(jù)寫入文件的步驟渐行,其實最終提供數(shù)據(jù)還是通過文件讀取的方式轰坊。
一旦有可用數(shù)據(jù),就通過delegate的方式一直回調(diào),通知response類有可用數(shù)據(jù)。

#pragma mark -  KTVHCHTTPResponse
- (void)ktv_readerDidPrepare:(KTVHCDataReader *)reader
{
    KTVHCLogHTTPResponse(@"%p, Prepared", self);
    if (self.reader.isPrepared && self.waitingResponse == YES) {
        KTVHCLogHTTPResponse(@"%p, Call connection did prepared", self);
        [self.connection responseHasAvailableData:self];
    }
}
"這個回調(diào)獲取有可用的數(shù)據(jù)的通知钙畔。"

- (void)ktv_readerHasAvailableData:(KTVHCDataReader *)reader
{
    KTVHCLogHTTPResponse(@"%p, Has available data", self);
    "這個方法就會觸發(fā)response的readDataOfLength"
    [self.connection responseHasAvailableData:self];
}

- (void)ktv_reader:(KTVHCDataReader *)reader didFailWithError:(NSError *)error
{
    KTVHCLogHTTPResponse(@"%p, Failed\nError : %@", self, error);
    [self.reader close];
    [self.connection responseDidAbort:self];
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市颤芬,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌套鹅,老刑警劉巖站蝠,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異卓鹿,居然都是意外死亡菱魔,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進店門吟孙,熙熙樓的掌柜王于貴愁眉苦臉地迎上來澜倦,“玉大人,你說我怎么就攤上這事杰妓》事。” “怎么了?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵稚失,是天一觀的道長。 經(jīng)常有香客問我恰聘,道長句各,這世上最難降的妖魔是什么吸占? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮凿宾,結(jié)果婚禮上矾屯,老公的妹妹穿的比我還像新娘。我一直安慰自己初厚,他們只是感情好件蚕,可當我...
    茶點故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著产禾,像睡著了一般排作。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上亚情,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天妄痪,我揣著相機與錄音,去河邊找鬼楞件。 笑死衫生,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的土浸。 我是一名探鬼主播罪针,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼黄伊!你這毒婦竟也來了泪酱?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤毅舆,失蹤者是張志新(化名)和其女友劉穎西篓,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體憋活,經(jīng)...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡岂津,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了悦即。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片吮成。...
    茶點故事閱讀 39,690評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖辜梳,靈堂內(nèi)的尸體忽然破棺而出粱甫,到底是詐尸還是另有隱情,我是刑警寧澤作瞄,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布茶宵,位于F島的核電站,受9級特大地震影響宗挥,放射性物質(zhì)發(fā)生泄漏乌庶。R本人自食惡果不足惜种蝶,卻給世界環(huán)境...
    茶點故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望瞒大。 院中可真熱鬧螃征,春花似錦、人聲如沸透敌。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽酗电。三九已至魄藕,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間顾瞻,已是汗流浹背泼疑。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留荷荤,地道東北人退渗。 一個月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像蕴纳,于是被迫代替她去往敵國和親会油。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,577評論 2 353