關(guān)于邊下邊播功能目前流傳的版本大體相同,本篇文章主要介紹另一種可行的實(shí)現(xiàn)方式佳鳖。
關(guān)于AVPlayer在這里我們不做詳細(xì)解釋霍殴,如果你是剛剛開始接觸AVPlayer,不妨先看看下面兩篇文章:
iOS視頻播放詳解1-基本使用
iOS視頻播放詳解2-封裝邊下邊播的播放器
“你這篇都這么長(zhǎng)系吩,還讓我看另外兩篇繁成,太TM蛋疼了,我能不能不看淑玫?”
嗯...頭皮硬的話...你隨意巾腕。??
如果上面兩篇還沒有研究懂的話,出于人道主義關(guān)懷絮蒿,建議還是不要繼續(xù)往下看了尊搬,頭皮真的會(huì)變硬。
講真土涝,從開始了解AVPlayer到寫出這篇文章前前后后歷時(shí)近兩個(gè)月(還不是因?yàn)閼校┓鹗伲韵胍私釧VPlayer的兄dei姐妹別急,慢慢來但壮。
一冀泻、流程
下面這張圖是iOS視頻播放詳解2-封裝邊下邊播的播放器中提供的流程(下文稱流程A),至于詳細(xì)解釋文中也已經(jīng)寫的非常清晰蜡饵,這里之所以要貼出來就是為了讓大家回憶一下弹渔,因?yàn)楸疚囊榻B的流程正是在這個(gè)流程的基礎(chǔ)上變種而來。(代碼基因突變你見過嗎溯祸?)
接下來肢专,我想確認(rèn)一下舞肆,應(yīng)該沒有人叫黑板的吧。
我要敲黑板了博杖,咳咳...
從流程A來看椿胯,我們所需要解決的問題只有兩個(gè)
如何提供數(shù)據(jù)給播放器?
如果緩存中有數(shù)據(jù)使用緩存播放剃根,如果沒有則開啟下載使用網(wǎng)絡(luò)數(shù)據(jù)哩盲。何時(shí)從網(wǎng)絡(luò)下載數(shù)據(jù),怎么下載以及數(shù)據(jù)緩存策略狈醉?
當(dāng)本地沒有緩存文件或者當(dāng)前請(qǐng)求offset大于或者小于已經(jīng)下載的offset時(shí)從offset開始到文件尾向服務(wù)器發(fā)起請(qǐng)求种冬,只要代理重新向服務(wù)器發(fā)起請(qǐng)求,就會(huì)導(dǎo)致緩存的數(shù)據(jù)不連續(xù)舔糖,則加載結(jié)束后不用將緩存的數(shù)據(jù)放入本地cache
從這兩個(gè)問題出發(fā)娱两,我們可以將為播放器提供數(shù)據(jù)與下載數(shù)據(jù)拆分為兩個(gè)單獨(dú)的部分。而播放的過程中一定是有緩存文件的金吗,只不過文件不一定完整十兢,如果無論本地是否有完整的緩存文件存在,播放器的數(shù)據(jù)全部來自緩存而不直接來自網(wǎng)絡(luò)會(huì)怎么樣呢摇庙?且看下圖:
resourceLoaderDelegate:向dataManager請(qǐng)求數(shù)據(jù)旱物,并且使用dataManager返回的數(shù)據(jù)填充loadingRequest。
dataManager:從本地緩存中提供數(shù)據(jù)卫袒,如果遇到緩存中沒有的數(shù)據(jù)則計(jì)算緩存中從指定offset開始缺失的數(shù)據(jù)范圍并通知downloader下載對(duì)應(yīng)范圍數(shù)據(jù)宵呛,并且將downloader下載的數(shù)據(jù)存入緩存等待推送。(注意夕凝,dataManager并不會(huì)直接使用從downloader返回的數(shù)據(jù)宝穗,downloader下載的數(shù)據(jù)只會(huì)被存入緩存文件。)
downloader:下載指定范圍數(shù)據(jù)码秉。
基于上面的流程逮矛,我們?cè)賮砘卮鹨幌逻@兩個(gè)問題:
如何提供數(shù)據(jù)給播放器?
resourceLoaderDelegate接收到loadingRequest之后通過計(jì)算得出所需數(shù)據(jù)起始位置转砖,并且將起始位置偏移量傳遞給dataManager须鼎。dataManager接收到偏移量之后查找到對(duì)應(yīng)的緩存文件,并且開始定速定量推送數(shù)據(jù)給resourceLoaderDelegate府蔗,循環(huán)往復(fù)晋控,直至數(shù)據(jù)末尾蜗顽。(注:一段完整的視頻可能分為多個(gè)文件存儲(chǔ)亥曹。)
注: 在此過程中沒有直接使用任何網(wǎng)絡(luò)數(shù)據(jù)礁遣,全部數(shù)據(jù)從緩存中獲得屋摔。
何時(shí)從網(wǎng)絡(luò)下載數(shù)據(jù),怎么下載以及數(shù)據(jù)緩存策略仲墨?
dataManager在推送數(shù)據(jù)的過程中勋磕,如果發(fā)現(xiàn)緩存中沒有需要的數(shù)據(jù)片段則計(jì)算出從offset開始緩存中缺失的最短數(shù)據(jù)范圍并且通知downloader下載悼枢,downloader下載好指定范圍數(shù)據(jù)返回給dataManager狂男,dataManager接收到下載的數(shù)據(jù)存入緩存等待下一次數(shù)據(jù)推送讀取综看。取決于下載起始偏移量的值downloader下載的數(shù)據(jù)可能有兩種存儲(chǔ)方式即拼接在已有文件之后(下載起始偏移量與已有文件結(jié)束位置相同)或者新建文件存儲(chǔ)(下載起始偏移量與已有文件結(jié)束位置不同)。
看著很簡(jiǎn)單是不是岖食,簡(jiǎn)單不簡(jiǎn)單代碼說了算hh...
其實(shí)在碼代碼的整個(gè)過程中I have questioned the feasibility of this method more than once. 坑太TM多了:毂!泡垃!
- 在整個(gè)播放過程中析珊,可能不止一個(gè)loadingRequest,resourceLoaderDelegate如何得出下一次所需的數(shù)據(jù)起始位置蔑穴?
- 如何解決讀取大文件時(shí)的內(nèi)存暴漲問題忠寻?
- dataManager既然是推送數(shù)據(jù)給resourceLoaderDelegate,以什么樣的方式推送合適存和?
- 如何避免阻塞主線程奕剃?
- 如何避免downloader重復(fù)下載?
- 由于數(shù)據(jù)可能是分段下載捐腿,如何保證數(shù)據(jù)完整性以及正確性?
...
在開始之前首先展示一下成果纵朋,堅(jiān)定一下各位繼續(xù)往下讀的信心(信心爆棚的同學(xué)請(qǐng)看屏幕右側(cè)滾動(dòng)條)。
請(qǐng)忽略上圖中的正在等待下載茄袖,說多了都是淚操软。
What are you 弄啥嘞 (╯‵□′)╯︵┻━┻
這樣做有什么好處呢?
首先類的分工更加明確宪祥,有利于寫代碼聂薪。
其次由于我們是分多個(gè)文件存儲(chǔ)視頻,所以在播放時(shí)只需要下載緩存中沒有的數(shù)據(jù)即可蝗羊,避免了數(shù)據(jù)的重復(fù)下載胆建,節(jié)省流量。
這兩點(diǎn)應(yīng)該是能看得到的最大的好處了吧肘交。
啰啰嗦嗦這么多笆载,接下來我們就開始愉快的擼代碼吧。
二涯呻、實(shí)現(xiàn)
創(chuàng)建assets以及resourceLoaderDelegate
我們要通過AVURLAssets創(chuàng)建AVPlayer凉驻,這樣開始播放視頻時(shí)AVPlayer會(huì)向AVURLAssets請(qǐng)求數(shù)據(jù),而AVURLAssets就會(huì)根據(jù)創(chuàng)建時(shí)我們提供的URL請(qǐng)求數(shù)據(jù)并提供給AVPlayer播放复罐。
到這里為止涝登,好像我們并沒有我們操作的空間,而我們需要能夠在AVPlayer播放的過程中操縱視頻數(shù)據(jù)效诅,這時(shí)我們就需要借助AVURLAssets的resourceLoaderDelegate幫忙胀滚。
resourceLoaderDelegate對(duì)于AVURLAssets來說有什么用呢趟济?
如果我們提供給AVURLAssets的URL是它能夠識(shí)別的URL,那么AVURLAssets就會(huì)自己開始播放咽笼,而當(dāng)我們提供一個(gè)AVURLAssets不能識(shí)別的URL時(shí)顷编,為了能夠正常播放,AVURLAssets就會(huì)詢問resourceLoaderDelegate:
AVURLAssets:兄弟剑刑,這個(gè)地址我播不了媳纬,你能不能播?
resourceLoaderDelegate:哥施掏,你這話問的钮惠,那必須能啊七芭!
AVURLAssets:好的素挽,我需要xxx字節(jié)-xxx字節(jié)的數(shù)據(jù),謝謝狸驳!
resourceLoaderDelegate:ojbk;倭狻(說出來有點(diǎn)傷感,害你播不了的地址就是我偷偷換的)
...
為了我們的resourceLoaderDelegate能夠成功上位锌历,我們需要對(duì)URL做一些手腳贮庞,把原始地址的scheme進(jìn)行更改,至于改什么那就看你的個(gè)人喜好了究西,比如gblw什么的窗慎,隨心所欲,你開心就好卤材。
首先遮斥,我們需要?jiǎng)?chuàng)建SEEResourceLoaderDelegate,基于高內(nèi)聚低耦合原則(我瞎說的)我們?yōu)镾EEResourceLoaderDelegate添加一個(gè)對(duì)象方法扇丛,用于返回一個(gè)綁定當(dāng)前對(duì)象為resourceLoaderDelegate的AVURLAssets對(duì)象术吗。
//SEEResourceLoaderDelegate.h
#import <Foundation/Foundation.h>
@class AVURLAsset;
NS_ASSUME_NONNULL_BEGIN
@interface SEEResourceLoaderDelegate : NSObject
- (AVURLAsset *)assetWithURL:(NSURL *)url;
@end
NS_ASSUME_NONNULL_END
//SEEResourceLoaderDelegate.m
- (AVURLAsset *)assetWithURL:(NSURL *)url {
[self.loadingRequests removeAllObjects];
_url = url;
NSURLComponents * components = [NSURLComponents componentsWithURL:url resolvingAgainstBaseURL:YES];
//替換scheme
components.scheme = @"seeplayer";
NSURL * target = [components URL];
//使用替換后的url創(chuàng)建AVURLAssets
AVURLAsset * asset = [AVURLAsset assetWithURL:target];
//為了防止阻塞主線程,我們新建一個(gè)串行隊(duì)列來接收代理回調(diào)
[asset.resourceLoader setDelegate:self queue:self.queue];
return asset;
}
/**
這個(gè)隊(duì)列用于執(zhí)行接收播放器發(fā)出的resourceLoader帆精、下載器接收回調(diào)较屿、文件管理器推送數(shù)據(jù)
@return 串行隊(duì)列
*/
- (dispatch_queue_t)queue {
if (_queue) {
return _queue;
}
_queue = dispatch_queue_create("player", DISPATCH_QUEUE_SERIAL);
return _queue;
}
根據(jù)我多年的經(jīng)驗(yàn),很少有人能夠把文章里面貼的代碼完整讀完的卓练,別問我為什么知道隘蝎,我就是大部分人里面光榮的一員。
幸會(huì)幸會(huì)...
所以每當(dāng)貼一段代碼我都會(huì)緊跟著對(duì)貼的代碼做一個(gè)簡(jiǎn)單的解釋襟企,簡(jiǎn)直不要太貼心嘱么。
上面的代碼中我們將原始URL進(jìn)行了記錄,并且使用處理后的URL創(chuàng)建了AVURLAssets對(duì)象并且將自己作為AVURLAssets對(duì)象的resourceLoaderDelegate顽悼,為了防止阻塞主隊(duì)列曼振,我們創(chuàng)建了一個(gè)新的串行隊(duì)列來作為回調(diào)隊(duì)列几迄。
創(chuàng)建player
//2 重新設(shè)置item
AVPlayerItem * newItem = [AVPlayerItem playerItemWithAsset:[_resourceLoaderDelegate assetWithURL:url] automaticallyLoadedAssetKeys:@[@"duration",@"preferredRate",@"preferredVolume",@"preferredTransform"]];
//3 播放器播放新url之前清除上一次的監(jiān)聽等
[self see_clearPlayer];
//4 設(shè)置新的item
[_player replaceCurrentItemWithPlayerItem:newItem];
//5 添加對(duì)播放器的監(jiān)聽
[self see_preparePlayer];
以上代碼我們通過AVURLAssets創(chuàng)建AVPlayerItem,并且將player的item進(jìn)行替換冰评。
接下來映胁,我們需要實(shí)現(xiàn)AVAssetResourceLoaderDelegate中定義的代理方法來接收loadingRequest。
實(shí)現(xiàn)AVAssetResourceLoaderDelegate方法
每當(dāng)assets發(fā)出一個(gè)loadingRequest我們就會(huì)在
- (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest *)loadingRequest
中接收到集索,如果我們可以加載loadingRequest中指定的數(shù)據(jù)片段返回YES屿愚,不能則返回NO汇跨。
這還用說务荆?當(dāng)然返回YES,否則我們?cè)谧鍪裁础?/p>
- (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest *)loadingRequest {
//將request添加進(jìn)數(shù)組記錄穷遂,得到數(shù)據(jù)后進(jìn)行填充
[self.loadingRequests addObject:loadingRequest];
return YES;
}
每當(dāng)?shù)玫揭粋€(gè)loadingRequest之后函匕,我們需要對(duì)其中需要的數(shù)據(jù)進(jìn)行填充,而我們得到這個(gè)loadingRequest中所需數(shù)據(jù)的時(shí)間不確定蚪黑,因此我們需要將其存儲(chǔ)起來盅惜,等待得到數(shù)據(jù)之后再向其中填充數(shù)據(jù)。
- (void)resourceLoader:(AVAssetResourceLoader *)resourceLoader didCancelLoadingRequest:(AVAssetResourceLoadingRequest *)loadingRequest {
//移除被取消的loadingRequest
[self.loadingRequests removeObject:loadingRequest];
}
當(dāng)一個(gè)loadingRequest被取消后忌穿,我們?cè)賹?duì)其進(jìn)行數(shù)據(jù)填充是無意義的抒寂,所以我們需要將存儲(chǔ)的loadingRequest從數(shù)組中移除不再進(jìn)行數(shù)據(jù)填充。
通過上面一波騷氣的操作掠剑,我們已經(jīng)成功的得到了播放數(shù)據(jù)的掌控權(quán)屈芜,接下來我們需要考慮的就是如何得從我們得到的一堆loadingRequest中找到播放需要的數(shù)據(jù)的起始位置。
數(shù)據(jù)起始位置查找
正常播放狀態(tài)下朴译,當(dāng)我們每完成一個(gè)loadingRequest井佑,播放器會(huì)重新發(fā)出一個(gè)或者多個(gè)loadingRequest來請(qǐng)求后續(xù)的數(shù)據(jù)。
如果用戶拖動(dòng)進(jìn)度條前進(jìn)或者后退眠寿,播放器同樣會(huì)發(fā)出新的loadingRequest躬翁,而此時(shí)我們之前的loadingRequest并沒有填充完成。
基于上述兩點(diǎn)盯拱,我們發(fā)現(xiàn)播放器最新發(fā)出的loadingRequest請(qǐng)求的一定是接下來播放所需要的數(shù)據(jù)片段盒发。
所以我們只需要找到loadingRequest數(shù)組中最后一個(gè)元素也就是最新發(fā)出的loadingRequest,并且以它的currentOffset為基準(zhǔn)通知dataManager狡逢,當(dāng)dataManager返回?cái)?shù)據(jù)之后進(jìn)行填充即可迹辐。
- (long long)see_expectOffset {
//獲取最新的loadingRequest的currentOffset即可
if (self.loadingRequests.count != 0) {
return ((AVAssetResourceLoadingRequest *)self.loadingRequests.lastObject).dataRequest.currentOffset;
}
return 0;
}
說實(shí)話,為了這幾行代碼嘔心瀝血好幾天...
需要的數(shù)據(jù)的起始位置我們已經(jīng)得到了甚侣,接下來就是dataManager獲取數(shù)據(jù)明吩、推送數(shù)據(jù)。
創(chuàng)建dataManager
既然我們會(huì)將視頻緩存到本地殷费,那么我們用什么來判斷當(dāng)前正在播放的視頻有沒有緩存呢印荔?
在播放一個(gè)視頻之前低葫,我們唯一知道的信息就是播放地址,而視頻的播放地址又具有唯一性仍律,這樣看來URL是我們用來判斷是否擁有緩存的不二選擇嘿悬。因此我們需要利用URL來初始化dataManager,而dataManager會(huì)將數(shù)據(jù)推送出去水泉,因此我們還需要一個(gè)代理作為數(shù)據(jù)的接收者善涨。
- (instancetype)initWithURL:(NSURL *)url delegate:(id<SEEDataManagerDelegate>)delegate {
if (self = [super init]) {
_prepareData = [SEEData mutableData];
_url = url;
_cacheBasePath = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).lastObject;
_fileInfo = [[SEEFileInfo alloc]initWithURL:url];
_totalBytes = _fileInfo.fileAttribute.totalBytes;
_MIMEType = _fileInfo.fileAttribute.MIMEType;
_inputStream = [[SEEInputStream alloc]init];
_inputFile = [_fileInfo fileForOffset:0];
if (_inputFile)[_inputStream setFileAtPath:[_cacheBasePath stringByAppendingPathComponent:_inputFile.path]];
_downloader = [[SEEDownloader alloc]initWithURL:url delegate:self];
_cacheRanges = [NSMutableArray array];
if (_fileInfo.files.count) {
[_fileInfo.files enumerateObjectsUsingBlock:^(SEEFile * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
NSRange range = NSMakeRange((NSUInteger)obj.startOffset,(NSUInteger)obj.endOffset);
[self->_cacheRanges addObject:[NSValue valueWithRange:range]];
}];
}
[self see_postRanges];
[self setDelegate:delegate];
}
return self;
}
ヾ(?`Д′?)我擦,毛啊草则,怎么一下就初始化了這么多東西钢拧,這TM什么鬼?
別急炕横,我慢慢解釋給大家聽源内。
_url : 播放地址,用來查找本地緩存以及下載數(shù)據(jù)份殿。
_cacheBasePath: 緩存目錄膜钓,所有的緩存文件全部放在這個(gè)目錄下。
_fileInfo: 緩存文件信息卿嘲,包括當(dāng)前視頻信息以及每一個(gè)緩存文件信息颂斜。
_inputStream: 輸入流,從緩存文件讀取數(shù)據(jù)拾枣,防止大文件讀取時(shí)的內(nèi)存保障沃疮。
_inputFile: 當(dāng)前正在輸入的文件信息。
_downloader: 下載器放前。
_cacheRanges: 已經(jīng)緩存的數(shù)據(jù)的范圍忿磅。
_prepareData: 推送過程中的數(shù)據(jù)緩存,用于存放正在推送的數(shù)據(jù)凭语。
以上的所有成員變量的作用在后文中對(duì)應(yīng)部分會(huì)進(jìn)行詳細(xì)講解葱她,這里我們先簡(jiǎn)單了解一下即可。
這樣我們?cè)趓esourceLoaderDelegate的- (AVURLAsset *)assetWithURL:(NSURL *)url方法中將dataManager一并創(chuàng)建即可似扔。
讀取大文件
在我們的設(shè)計(jì)中吨些,dataManager只能從本地緩存中讀取數(shù)據(jù),那么問題來了炒辉,假設(shè)本地已經(jīng)有了數(shù)據(jù)豪墅,我們?cè)撛趺醋x取才能既保證效率又避免內(nèi)存暴漲呢?
NSData:如果直接使用NSData進(jìn)行數(shù)據(jù)讀取的話黔寇,在整個(gè)播放過程中偶器,每次獲取數(shù)據(jù)都要將全部文件讀入內(nèi)存,然后找到對(duì)應(yīng)的數(shù)據(jù)返回∑梁洌總感覺這種代碼下不了手颊郎。pass!
NSFileHandle:傳說中能夠解決讀入大文件時(shí)的內(nèi)存暴漲問題(在Stack Overflow上看到有人說并不能)霎苗,但是姆吭,對(duì)不起,每次都要自己算偏移量有點(diǎn)惡心唁盏。pass!
NSInputStream: 單向流内狸,不用操心偏移量,可以控制每次讀取數(shù)據(jù)量大小厘擂,但是讀取過的數(shù)據(jù)不能再次讀取昆淡。嗯...最后一點(diǎn)好像有點(diǎn)惡心,用戶拖拽播放器進(jìn)度條之后好像需要能夠從指定offset讀取數(shù)據(jù)驴党,不過我們可是程序員啊怕個(gè)錘子瘪撇,改获茬!
哥港庄,你眼角怎么濕了...
你懂個(gè)錘子,這是姓 福 的眼淚恕曲!
以下解決方案僅供參考鹏氧,智商壓制,實(shí)在想不出什么更好的辦法佩谣,如果你有把还,請(qǐng)告訴我,作為回報(bào)茸俭,我可以把我潛心研究多年的葵花寶典心得傳授給你吊履。
哎,憋走啊调鬓。雖然欲練此功必先自宮艇炎,但是如不自宮亦可成功的啊親!
思路是這樣的腾窝,首先每次讀取數(shù)據(jù)時(shí)我們通過計(jì)算得出讀取之后的offsetA缀踪,下一次讀取數(shù)據(jù)時(shí)首先判斷要讀取的數(shù)據(jù)起始位置offsetB和當(dāng)前stream讀取到的位置offsetA是否一致,如果一致則繼續(xù)讀取指定長(zhǎng)度返回虹脯,如果不一致關(guān)閉當(dāng)前stream重新創(chuàng)建相同path的stream驴娃,然后一直讀取數(shù)據(jù)到offsetB位置之后再讀取指定長(zhǎng)度數(shù)據(jù)返回。
又見代碼:
@interface SEEInputStream: NSObject
- (void)setFileAtPath:(NSString *)path;
- (NSInteger)read:(uint8_t *)buffer maxLength:(NSUInteger)len startOffset:(long long)startOffset;
@end
@implementation SEEInputStream {
long long _currentOffset;
NSString * _path;
NSInputStream * _stream;
}
- (void)setFileAtPath:(NSString *)path {
//關(guān)閉之前打開的文件
[self close];
_currentOffset = 0;
_path = path;
_stream = [[NSInputStream alloc]initWithFileAtPath:path];
[self open];
}
//重新打開當(dāng)前文件
- (void)resetStream {
[self close];
_stream = [NSInputStream inputStreamWithFileAtPath:_path];
[self open];
_currentOffset = 0;
}
- (void)open {
if (!_stream)return;
[_stream open];
}
- (void)close {
if (!_stream)return;
[_stream close];
_stream = nil;
}
//從stream中讀取數(shù)據(jù)
- (NSInteger)read:(uint8_t *)buffer maxLength:(NSUInteger)len {
NSInteger readBytes = [_stream read:buffer maxLength:len];
if (readBytes == -1 || readBytes == 0) {
NSLog(@"%@",_stream.streamError);
}
_currentOffset += readBytes;
return readBytes;
}
//讀取指定位置數(shù)據(jù)
- (NSInteger)read:(uint8_t *)buffer maxLength:(NSUInteger)len startOffset:(long long)startOffset {
if (startOffset < _currentOffset) {
//如果起始位置小于當(dāng)前讀取到的位置則重新打開stream
[self resetStream];
return [self read:buffer maxLength:len startOffset:startOffset];
}
else if (startOffset > _currentOffset) {
//如果讀取到的位置小于讀取的起始位置則一直讀取數(shù)據(jù)直到startOffset == _currentOffset
int loopCount = (int)((startOffset - _currentOffset) / len);
for (int i = 0; i <= loopCount; i++) {
if (i == loopCount) {
NSUInteger bufferLength = (startOffset - _currentOffset) % len;
if (bufferLength == 0) break;
uint8_t buffer[bufferLength];
[self read:buffer maxLength:bufferLength];
}
else {
uint8_t buffer[len];
[self read:buffer maxLength:len];
}
}
return [self read:buffer maxLength:len startOffset:startOffset];
}
else {
//讀取數(shù)據(jù)返回
return [self read:buffer maxLength:len];
}
}
@end
以上代碼循集,我們將對(duì)于InputStream的管理放在SEEInputStream類中唇敞,外界只需要設(shè)置需要讀取的文件目錄,然后只管盡情的讀取數(shù)據(jù)即可。
那讀取大文件的問題解決了疆柔,問題是我們的文件是分段存儲(chǔ)的蕉世,我怎么知道我該從哪個(gè)文件里面讀取數(shù)據(jù)。
別急婆硬,_fileInfo告訴你狠轻。
文件信息管理
每當(dāng)我們準(zhǔn)備讀取數(shù)據(jù)時(shí)只有一個(gè)offset告訴我們?cè)撟x取的數(shù)據(jù)的起始位置,但是究竟這段數(shù)據(jù)有沒有彬犯,有的話存儲(chǔ)在哪里向楼。這些我們目前還是一臉懵逼的。所以我們需要借助一個(gè)類來幫助我們管理這些瑣事谐区。這樣每當(dāng)dataManager得到一個(gè)offset就問這個(gè)類:“兄弟湖蜕,快告訴我這段數(shù)據(jù)在哪個(gè)文件里面”。然后就去對(duì)應(yīng)的文件讀取數(shù)據(jù)宋列。這也是這個(gè)類的設(shè)計(jì)初衷昭抒。
既然我們?cè)诒镜鼐彺媪藬?shù)據(jù),那總不能每次要播放時(shí)都請(qǐng)求網(wǎng)絡(luò)獲取MIME類型炼杖,數(shù)據(jù)大小等信息吧灭返,否則無網(wǎng)絡(luò)情況下就會(huì)出現(xiàn)本地有緩存而因?yàn)槿鄙龠@些數(shù)據(jù)導(dǎo)致不能播放的問題,所以我們需要存儲(chǔ)這些數(shù)據(jù)坤邪。
每當(dāng)dataManager得到一個(gè)offset時(shí)都會(huì)來詢問它這些數(shù)據(jù)在那個(gè)文件里面熙含,那首先它得知道目前的緩存文件有多少,每個(gè)文件存儲(chǔ)的數(shù)據(jù)是哪一段的數(shù)據(jù)艇纺,多以還需要存儲(chǔ)每個(gè)文件的文件名怎静,數(shù)據(jù)起始位置,數(shù)據(jù)結(jié)束位置黔衡,文件長(zhǎng)度等信息蚓聘。
由此我們的類被設(shè)計(jì)成了這樣
@interface SEEFileAttribute: NSObject
//MIME類型
@property (nonatomic, copy) NSString * MIMEType;
//總數(shù)據(jù)量
@property (nonatomic, assign) long long totalBytes;
//已經(jīng)下載的數(shù)據(jù)總量
@property (nonatomic, assign) long long cacheBytes;
//緩存是否完成 isComplete = totalBytes == cacheBytes
@property (nonatomic, assign) BOOL isComplete;
//文件名
@property (nonatomic, copy) NSString * exceptFileName;
@end
@interface SEEFile: NSObject
//緩存文件名
@property (nonatomic, copy) NSString * path;
//緩存文件起始位置
@property (nonatomic, assign) long long startOffset;
//緩存文件結(jié)束位置
@property (nonatomic, assign) long long endOffset;
//緩存文件長(zhǎng)度
@property (nonatomic, assign) NSUInteger length;
@end
@interface SEEFileInfo: NSObject
@property (nonatomic, strong) SEEFileAttribute * fileAttribute;
@property (nonatomic, strong) NSMutableArray <SEEFile *> * files;
- (instancetype)initWithURL:(NSURL *)url;
@end
這樣,這個(gè)類就掌握了本地緩存文件的所有信息盟劫,接下來我們需要他可以根據(jù)offset提供給我們對(duì)應(yīng)的文件信息:
- (SEEFile *)fileForOffset:(long long)offset {
__block SEEFile * file = nil;
self.missingEndOffset = self.fileAttribute.totalBytes;
[self.files enumerateObjectsUsingBlock:^(SEEFile * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
if (obj.startOffset <= offset && obj.endOffset > offset) {
file = obj;
*stop = YES;
}
}];
return file;
}
通過遍歷緩存文件夜牡,并且使用offset與每個(gè)文件進(jìn)行比較得到包含offset的文件并返回。
讀取并持有數(shù)據(jù)等待推送
dataManager在接到offset之后就可以通過以下代碼來讀取對(duì)應(yīng)的數(shù)據(jù)了捞高。
//如果當(dāng)前訪問的文件中包含后續(xù)數(shù)據(jù)則在當(dāng)前文件中讀取
if (_inputFile && _inputFile.endOffset > startOffset && _inputFile.startOffset <= startOffset) {
[self see_prepareDataFormCache:startOffset];
return;
}
//如果當(dāng)前訪問的文件中不包含后續(xù)數(shù)據(jù)則重新查找對(duì)應(yīng)文件
_inputFile = [_fileInfo fileForOffset:startOffset];
if (_inputFile){
[_inputStream setFileAtPath:[_cacheBasePath stringByAppendingPathComponent:_inputFile.path]];
[self see_prepareDataFormCache:startOffset];
return;
}
由于NSInputStream在讀取數(shù)據(jù)時(shí)必須指定長(zhǎng)度氯材,因此,我們讀取到的數(shù)據(jù)不一定會(huì)在一次推送中被全部接收硝岗,有可能只是接收了其中的一部分氢哮,而另一部分會(huì)在接下來的一次或多次推送中才被接收(取決于loadingRequest),因此需要將讀取到的數(shù)據(jù)保存在內(nèi)存中持續(xù)推送型檀,直到數(shù)據(jù)全部被接收(通過每次推送完成resourceLoaderDelegate返回的offset來判斷數(shù)據(jù)是否被接收)為止冗尤。
在此過程中,我們不僅要保證存儲(chǔ)的數(shù)據(jù)在使用之前被釋放,而且還要保證數(shù)據(jù)的正確性裂七,并且為數(shù)據(jù)的接收者提供本段數(shù)據(jù)的起始位置皆看、結(jié)束位置等信息,以保證接受者可以根據(jù)這些信息正確的使用這些數(shù)據(jù)背零。
為了滿足這些需求腰吟,我們選擇使用一個(gè)專門的類來管理帶推送的數(shù)據(jù)。
@interface SEEData: NSObject
//數(shù)據(jù)起始位置
@property (nonatomic, assign, readonly) long long location;
//數(shù)據(jù)長(zhǎng)度
@property (nonatomic, assign, readonly) NSUInteger length;
//數(shù)據(jù)結(jié)束位置
@property (nonatomic, assign, readonly) long long end;
//數(shù)據(jù)
@property (nonatomic, strong, readonly) __kindof NSData * data;
+ (instancetype)dataWithLocation:(long long)location lenght:(NSUInteger)length data:(__kindof NSData *)data;
/**
拼接數(shù)據(jù)
只有通過mutableData創(chuàng)建的對(duì)象可以使用該方法
@param data 數(shù)據(jù)
@param length 長(zhǎng)度
*/
- (void)appendData:(const void *)data length:(NSUInteger)length;
/**
初始化指定offset之前的數(shù)據(jù)
只有通過mutableData創(chuàng)建的對(duì)象可以使用該方法
@param offset offset
*/
- (void)initOffset:(long long)offset;
//創(chuàng)建的對(duì)象data不可變
+ (instancetype)data;
//創(chuàng)建的對(duì)象data為可變
+ (instancetype)mutableData;
- (id)copy;
- (id)mutableCopy;
- (BOOL)isEqual:(id)object;
@end
@interface SEEData()
@property (nonatomic, assign) long long location;
@property (nonatomic, assign) NSUInteger length;
@property (nonatomic, assign) long long end;
@property (nonatomic, strong) __kindof NSData * data;
@end
@implementation SEEData
+ (instancetype)dataWithLocation:(long long)location lenght:(NSUInteger)length data:(__kindof NSData *)data {
SEEData * instance = [[SEEData alloc]init];
instance.location = location;
instance.length = length;
instance.data = data;
return instance;
}
+ (instancetype)data {
return [self dataWithLocation:0 lenght:0 data:[NSData data]];
}
+ (instancetype)mutableData {
return [self dataWithLocation:0 lenght:0 data:[NSMutableData data]];
}
- (void)appendData:(const void *)buffer length:(NSUInteger)length {
[((NSMutableData *)_data) appendBytes:buffer length:length];
self.length += length;
}
- (void)initOffset:(long long)offset {
if (offset == _location) return;
long long initLength = offset - _location;
_location = offset;
if (initLength < 0 || initLength > _length) {
//如果初始化的位置不在當(dāng)前數(shù)據(jù)片段內(nèi)則清除當(dāng)前全部數(shù)據(jù)
[((NSMutableData *)_data) replaceBytesInRange:NSMakeRange(0, _length) withBytes:NULL length:0];
self.length = 0;
return;
}
//清除指定offset之前的數(shù)據(jù)
[((NSMutableData *)_data) replaceBytesInRange:NSMakeRange(0, initLength) withBytes:NULL length:0];
self.length -= initLength;
}
- (id)copy {
if ([self.data isMemberOfClass:[NSData class]]) {
return [SEEData dataWithLocation:self.location lenght:self.length data:self.data];
}
else {
return [SEEData dataWithLocation:self.location lenght:self.length data:[NSData dataWithData:self.data]];
}
}
- (id)mutableCopy {
if ([self.data isMemberOfClass:[NSData class]]) {
return [SEEData dataWithLocation:self.location lenght:self.length data:[NSMutableData dataWithData:self.data]];
}
else {
return [SEEData dataWithLocation:self.location lenght:self.length data:self.data];
}
}
- (BOOL)isEqual:(id)object {
if ([object isMemberOfClass:[self class]]) {
SEEData * target = object;
return self.location == target.location && self.length == target.length && [self.data isEqualToData:target.data];
}
else {
return NO;
}
}
- (void)setLength:(NSUInteger)length {
_length = length;
_end = _location + _length;
}
- (NSString *)description {
return [NSString stringWithFormat:@"location %lld length %lu end %lld dataLength %lu",_location,_length,_end,_data.length];
}
@end
需要使用-initOffset:清除數(shù)據(jù)的情況:
- 當(dāng)用戶拖動(dòng)進(jìn)度條時(shí)徙瓶,可能導(dǎo)致我們目前存儲(chǔ)的數(shù)據(jù)不是播放器所需的數(shù)據(jù)毛雇。
- 當(dāng)一段數(shù)據(jù)已經(jīng)被接收之后,將接收過的數(shù)據(jù)清除減少內(nèi)存占用侦镇。
這樣我們就可以將準(zhǔn)備好等待推送的數(shù)據(jù)先存儲(chǔ)起來灵疮。
- (void)see_prepareDataFormCache:(long long)startOffset {
uint8_t buffer[262144] = {};
NSInteger readByte = [_inputStream read:buffer maxLength:262144 startOffset:startOffset - _inputFile.startOffset];
[((NSMutableData *)_prepareData.data) appendBytes:buffer length:readByte];
_prepareData.length += readByte;
}
數(shù)據(jù)有了,怎么推送給resourceLoaderDelegate呢壳繁?
數(shù)據(jù)推送方案
既然我們的dataManager是通過推送的方式來向其代理輸出數(shù)據(jù)的震捣,我們需要一個(gè)能夠穩(wěn)定的循環(huán)執(zhí)行指定代碼的方式并且要避免阻塞線程。
Excuse me闹炉? 你說什么蒿赢?for循環(huán)?
while剩胁?
while (alive) {
spittingBlood(3);
}
咳咳... 你不是不忘了NSTimer诉植,時(shí)間可是一種神奇的東西祥国。(嚴(yán)肅臉)
如何使用Timer推送
在擼代碼之前首先要想清楚昵观,什么時(shí)候開始推送?什么時(shí)候結(jié)束推送舌稀?
如果我們?cè)诔跏蓟痙ataManager的時(shí)候就開始推送數(shù)據(jù)可以嗎啊犬?
可以是可以,但是這樣就比較low了壁查,dataManager初始化的時(shí)候可能播放器還沒有開始播放觉至,resourceLoaderDelegate還沒有接收到任何loadingRequest,然后dataManager就像個(gè)二傻子一樣瘋狂推送數(shù)據(jù)睡腿,空氣瞬間凝固语御,氣氛好像有那么一丟丟尷尬。
為了避免上述情況出現(xiàn)席怪,我們選擇在接收到loadingRequest之后再開始數(shù)據(jù)推送应闯,這樣即使播放完成之后我們終止了推送,用戶拖動(dòng)進(jìn)度條或者重新播放時(shí)播放器會(huì)再次發(fā)出loadingRequest挂捻,此時(shí)我們的推送會(huì)被再次開啟碉纺。
- (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest *)loadingRequest {
//將request添加進(jìn)數(shù)組記錄,得到數(shù)據(jù)后進(jìn)行填充
[self.loadingRequests addObject:loadingRequest];
//dataManager開始準(zhǔn)備推送數(shù)據(jù)
[_dataManager begin];
return YES;
}
當(dāng)全部數(shù)據(jù)推送完成、播放另一個(gè)url或者播放器銷毀時(shí)則需要將推送終止骨田,也就是將timer移除耿导。
既然這樣,那我們就開始吧:
- (void)begin {
_timer = [NSTimer timerWithTimeInterval:0.1 target:self selector:@selector(see_pushData:) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:_timer forMode:NSRunLoopCommonModes];
[[NSRunLoop currentRunLoop] run];
}
你以為這樣就可以了态贤?
too young too simple.
運(yùn)行代碼就會(huì)發(fā)現(xiàn)舱呻,當(dāng)我們的執(zhí)行[[NSRunLoop currentRunLoop] run]之后,后面的代碼并沒有執(zhí)行悠汽。導(dǎo)致resourceLoaderDelegate的代理方法沒有返回狮荔,而由于代理方法沒有返回所以播放器一直處在等待狀態(tài),不會(huì)發(fā)出新的loadingRequest無法播放介粘。
所以為什么會(huì)出現(xiàn)這中情況呢殖氏?
仔細(xì)閱讀官方文檔中給出的 run 方法的解釋
If no input sources or timers are attached to the run loop, this method exits immediately; otherwise, it runs the receiver in the NSDefaultRunLoopMode by repeatedly invoking runMode:beforeDate:. In other words, this method effectively begins an infinite loop that processes data from the run loop’s input sources and timers.
大意說run方法會(huì)重復(fù)調(diào)用runMode:beforeDate:方法,也就是啟動(dòng)了一個(gè)無限循環(huán)姻采,來處理Timer和source雅采。
另外官方文檔中還很貼心的給出了一個(gè)例子:
BOOL shouldKeepRunning = YES; // global
NSRunLoop *theRL = [NSRunLoop currentRunLoop];
while (shouldKeepRunning && [theRL runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]);
雖然這個(gè)例子并不是run方法的實(shí)現(xiàn),但是通過這個(gè)例子慨亲,我們大致可以猜測(cè)到run方法的實(shí)現(xiàn)應(yīng)該與其相似婚瓜。
所以我們的代碼變成了這樣:
_timer = [NSTimer timerWithTimeInterval:0.1 target:self selector:@selector(see_pushData:) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:_timer forMode:NSDefaultRunLoopMode];
//[[NSRunLoop currentRunLoop] run];
while ([[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]);
那我們應(yīng)該怎么辦呢?
當(dāng)然是不要在當(dāng)前正在執(zhí)行的子線程添加Timer了刑棵。
可以選擇將timer添加進(jìn)主線程里面巴刻,但是如果我們的播放器是添加在TableViewCell上,這樣做還是有點(diǎn)心虛的蛉签,畢竟好像Timer回調(diào)執(zhí)行的代碼有點(diǎn)多...
所以我們選擇指定一個(gè)子線程來執(zhí)行Timer事件胡陪。
- (void)begin {
if (self.state == SEEDataManagerStateInit) {
self.state = SEEDataManagerStateBegin;
_timerThread = [[NSThread alloc]initWithTarget:self selector:@selector(see_timerWithThread) object:nil];
[_timerThread start];
}
}
- (void)see_timerWithThread {
@autoreleasepool {
[[NSThread currentThread] setName:@"player_timer"];
_timer = [NSTimer timerWithTimeInterval:0.1 target:self selector:@selector(see_pushData:) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:_timer forMode:NSRunLoopCommonModes];
[[NSRunLoop currentRunLoop] run];
}
}
怎么樣,這個(gè)代碼是不是有點(diǎn)眼熟碍舍?
什么柠座?你說聽不懂我在說什么?
相信我片橡,你一定有機(jī)會(huì)和面試官尬聊妈经。??
你還是趕緊看看iOS開發(fā)之線程永駐吧。(沒有喂自己袋鹽捧书,請(qǐng)放心食用)
現(xiàn)在我們的dataManager已經(jīng)成功覺醒吹泡,擁有了能夠穩(wěn)定的循環(huán)執(zhí)行指定代碼的能力。but...
- (void)see_pushData:(NSTimer *)sender {
}
代碼呢经瓷?
數(shù)據(jù)推送邏輯
在考慮推送邏輯之前爆哑,先看看我們現(xiàn)在已經(jīng)完成的工作:
- 接收loadingRequest并記錄。
- 從所有l(wèi)oadingRequest中查找期望數(shù)據(jù)位置了嚎。
- 讀取大文件泪漂。
- 將所有緩存文件使用文件信息管理類進(jìn)行統(tǒng)一管理廊营。
- 內(nèi)存緩存數(shù)據(jù)的管理。
- 推送方案的確定以及實(shí)現(xiàn)萝勤。
有沒有看到我們的推送邏輯露筒?
其實(shí)很簡(jiǎn)單 loop ( 6 => (1,2) => (3,4) => 5 ) 這就是我們的推送邏輯。
dataManager在推送開始時(shí)將當(dāng)前內(nèi)存緩存數(shù)據(jù)推送給resourceLoaderDelegate敌卓,resourceLoaderDelegate接收到數(shù)據(jù)進(jìn)行填充慎式,之后遍歷所有l(wèi)oadingRequest查找到期望數(shù)據(jù)起始位置offset回傳給dataManager(哥,說好了趟径,下一次你給我數(shù)據(jù)的時(shí)候一定要有這個(gè)位置的數(shù)據(jù)瘪吏。),dataManager接收到offset將填充完成的數(shù)據(jù)進(jìn)行清除蜗巧,然后使用offset通過文件信息管理類查找到對(duì)應(yīng)緩存文件讀取數(shù)據(jù)掌眠,將讀取的數(shù)據(jù)存儲(chǔ)在內(nèi)存緩存中等待下一次推送。如此循環(huán)幕屹,直到(地球爆炸蓝丙,宇宙消亡,那是不可能的)播放器銷毀或者全部數(shù)據(jù)推送完成望拖。
如果我們的推送邏輯修改為 loop ((1,2) => (3,4) => 5 => 6)也是可以的渺尘,但是這樣我們?cè)诘谝淮瓮扑蜁r(shí)并不知道代理需要的數(shù)據(jù)的起始位置,這樣我們只能盲目讀取一段數(shù)據(jù)说敏,如果我們讀的這段數(shù)據(jù)不是代理所需要的那TM就尷尬了鸥跟。如果第一次推送不讀取數(shù)據(jù)那就需要跳過 (1,2) => (3,4) => 5 這三步,這又和我們之前討論的邏輯有什么不同呢盔沫?還多了一個(gè)判斷医咨。
所以這里我選擇使用 6 => (1,2) => (3,4) => 5 這種方式,由dataManager主動(dòng)詢問需要的數(shù)據(jù)迅诬,而不是二話不說就準(zhǔn)備數(shù)據(jù)然后代理回應(yīng)之后再去調(diào)整數(shù)據(jù)腋逆。畢竟男孩子還是要主動(dòng)一些的。(滑稽)
等等侈贷,你本地緩存文件從哪里來?
別急等脂,還沒到俏蛮,這是兩碼事憋打岔。
既然這樣也就沒什么好說的了上遥,開始著手把上面的邏輯翻譯成代碼吧:(我們不生產(chǎn)代碼搏屑,我們是代碼的搬運(yùn)工。)
- (void)see_pushData:(NSTimer *)sender {
if (!self->_responder.didReceiveData) return;
long long finishOffset = 0;
//將數(shù)據(jù)推送給接收者得到填充完成位置
finishOffset = [self->_delegate didReceiveData:self->_prepareData];
//清除填充完成的數(shù)據(jù)
[self see_clearFinishData:finishOffset];
//檢查推送是否完成
if (finishOffset && finishOffset == self->_fileInfo.fileAttribute.totalBytes) {
[self stop];
return;
}
//查找數(shù)據(jù)
if (finishOffset < self->_prepareData.location || finishOffset >= self->_prepareData.end) {
//如果請(qǐng)求的數(shù)據(jù)不連續(xù)粉楚,將內(nèi)存緩存清空
if (finishOffset != _prepareData.end)[_prepareData initOffset:finishOffset];
[self see_prepareData:finishOffset];
}
}
- (void)see_clearFinishData:(long long)finishOffset {
//每完成1M數(shù)據(jù)推送清除一次內(nèi)存緩存
if (finishOffset >= _prepareData.location + 1048576) {
[_prepareData initOffset:_prepareData.location + 1048576];
}
}
關(guān)于上述代碼中部分可能存在的疑問:
為什么不填充一次數(shù)據(jù)就立即將數(shù)據(jù)清除而要等到數(shù)據(jù)量達(dá)到某一數(shù)量才進(jìn)行清除辣恋?
這樣設(shè)計(jì)出發(fā)點(diǎn)在于當(dāng)播放器開始播放時(shí)會(huì)先發(fā)出一段2字節(jié)的數(shù)據(jù)請(qǐng)求來確認(rèn)數(shù)據(jù)亮垫,之后會(huì)發(fā)出從0開始到文件末尾的數(shù)據(jù)請(qǐng)求,如果一開始就立即將這兩字節(jié)的數(shù)據(jù)清除當(dāng)再一次發(fā)出請(qǐng)求的時(shí)候?yàn)榱吮WC數(shù)據(jù)正確性伟骨,我們需要將當(dāng)前內(nèi)存緩存中的數(shù)據(jù)全部清除重新從0開始讀取饮潦。
另外這樣做也會(huì)減少在整個(gè)播放過程中清除數(shù)據(jù)的次數(shù),保證性能携狭。畢竟整個(gè)播放過程中進(jìn)行的計(jì)算并不少继蜡,能省一點(diǎn)是一點(diǎn)。
為什么在查找數(shù)據(jù)之前有時(shí)需要清除內(nèi)存緩存逛腿?
首先我們看看清除緩存的方法實(shí)現(xiàn):
- (void)initOffset:(long long)offset {
if (offset == _location) return;
long long initLength = offset - _location;
_location = offset;
if (initLength < 0 || initLength > _length) {
//如果初始化的位置不在當(dāng)前數(shù)據(jù)片段內(nèi)則清除當(dāng)前全部數(shù)據(jù)
[((NSMutableData *)_data) replaceBytesInRange:NSMakeRange(0, _length) withBytes:NULL length:0];
self.length = 0;
return;
}
//清除指定offset之前的數(shù)據(jù)
[((NSMutableData *)_data) replaceBytesInRange:NSMakeRange(0, initLength) withBytes:NULL length:0];
self.length -= initLength;
}
這里我們所說的清除緩存并不是將所有數(shù)據(jù)清除并且將數(shù)據(jù)片段指向0稀并,而是通過修改數(shù)據(jù)將offset之前的數(shù)據(jù)全部清除,只保留內(nèi)存緩存中offset之后的連續(xù)數(shù)據(jù)单默。
當(dāng)finishOffset比當(dāng)前內(nèi)存中的數(shù)據(jù)起始位置小或者大于結(jié)束位置時(shí)碘举,如果我們不清除內(nèi)存緩存直接將數(shù)據(jù)拼接在末尾,此時(shí)內(nèi)存中的數(shù)據(jù)就發(fā)生了錯(cuò)誤搁廓,舉個(gè)例子:
比如說我們的數(shù)據(jù)為 “hello word” (c字符串) 一共是11個(gè)字節(jié)殴俱。
當(dāng)前內(nèi)存緩存如下:
location: 3
length: 1
end: 4
data: "l"
如果此時(shí)代理返回finishOffset為8。
我們不清除內(nèi)存緩存直接拼接:
location: 3
length: 2
end: 5
data: "lo"
你覺得對(duì)嗎枚抵?看起來好像是沒毛病的是吧线欲。其實(shí)是錯(cuò)覺。
那下一次推送的時(shí)候汽摹,由于我們提供的數(shù)據(jù)對(duì)象中當(dāng)前數(shù)據(jù)片段為3-5李丰,并不包含代理所需要的數(shù)據(jù)(其實(shí)是包含的,只不過我們的數(shù)據(jù)出現(xiàn)了錯(cuò)誤)逼泣,因此代理再次返回8:
location: 3
length: 3
end: 6
data: "loo"
...
如此循環(huán)幾次趴泌,直到我們的end等于9為止,此時(shí)我們的內(nèi)存緩存變成了這樣:
location: 3
length: 6
end: 9
data: "looooo"
Excuse me拉庶? 我們的原始數(shù)據(jù)中好像并沒有 "looooo"嗜憔。
所以我們需要對(duì)緩存進(jìn)行清除。
清除之前:
location: 3
length: 1
end: 4
data: "l"
清除之后會(huì)變成這樣:
location: 8
length: 0
end: 8
data: ""
此時(shí)我們將讀取到的數(shù)據(jù)放入緩存:
location: 8
length: 1
end: 9
data: "o"
Are you ok?
ok ok !
到這里我們的主要流程就已經(jīng)完成了氏仗,先按耐住你躁動(dòng)的內(nèi)心吉捶,千萬不要command+R,你會(huì)在手機(jī)屏幕里看到你最愛的人...??
以上我們已經(jīng)完成了使用本地緩存對(duì)播放器進(jìn)行數(shù)據(jù)填充呐舔,接下來我們著手進(jìn)行網(wǎng)絡(luò)數(shù)據(jù)下載并緩存。
數(shù)據(jù)下載邏輯
什么情況下需要下載數(shù)據(jù)呢慷蠕?
根據(jù)我們的設(shè)計(jì)珊拼,只有當(dāng)本地沒有所需的數(shù)據(jù)時(shí)才需要開啟下載,并且將下載的數(shù)據(jù)緩存至本地流炕。
也就是說當(dāng)我們的dataManager執(zhí)行 - (void)see_prepareData:(long long)startOffset 時(shí)如果
- (void)see_prepareData:(long long)startOffset {
//如果當(dāng)前訪問的文件中包含后續(xù)數(shù)據(jù)則在當(dāng)前文件中讀取
if (_inputFile && _inputFile.endOffset > startOffset && _inputFile.startOffset <= startOffset) {
[self see_prepareDataFormCache:startOffset];
return;
}
//如果當(dāng)前訪問的文件中不包含后續(xù)數(shù)據(jù)則重新查找對(duì)應(yīng)文件
_inputFile = [_fileInfo fileForOffset:startOffset];
if (_inputFile){
//打開對(duì)應(yīng)的流
[_inputStream setFileAtPath:[_cacheBasePath stringByAppendingPathComponent:_inputFile.path]];
[self see_prepareDataFormCache:startOffset];
return;
}
//如果代碼執(zhí)行到這里說明緩存中不包含所需數(shù)據(jù)則開啟下載任務(wù)
[self see_prepareDataFromNetwork:startOffset];
}
代碼執(zhí)行到了最后一行澎现,說明本地沒有所需的數(shù)據(jù)仅胞,這時(shí)我們就需要發(fā)出從startOffset開始的網(wǎng)絡(luò)請(qǐng)求來下載對(duì)應(yīng)的數(shù)據(jù)。
既然要下載數(shù)據(jù)剑辫,那么Range要設(shè)置為多少才能保證某一數(shù)據(jù)片段不會(huì)被重復(fù)下載呢干旧?
"bytes = startOffset - " 這樣是顯然不行的,如果在startOffset之后有已經(jīng)緩存過的數(shù)據(jù)片段揭斧,那么這部分?jǐn)?shù)據(jù)將會(huì)被重復(fù)下載莱革。
我們可能需要確定一個(gè)準(zhǔn)確的范圍來進(jìn)行數(shù)據(jù)下載,以保證既能夠得到想要的數(shù)據(jù)而又不會(huì)對(duì)數(shù)據(jù)進(jìn)行重復(fù)下載讹开。
下面的例子就是我們的Range確定方案:
每一次我們都只下載從startOffset開始盅视,尋找本地緩存文件中數(shù)據(jù)起始位置值比startOffset大的最小值作為下載結(jié)束位置進(jìn)行下載,即下載所需最小數(shù)據(jù)片段旦万。這樣既保證了數(shù)據(jù)的正確性闹击,并且防止了數(shù)據(jù)重復(fù)下載。
下載范圍結(jié)束值確定
既然我們的結(jié)束只是基于已緩存的數(shù)據(jù)范圍進(jìn)行查找的成艘,那么毫無疑問這里我們需要借助_fileInfo的幫忙才能完成赏半。
為什么?
你難道忘記了,_fileInfo管理著我們的緩存文件信息淆两,不問他問誰断箫。
仔細(xì)觀察 - (void)see_prepareData:(long long)startOffset 這個(gè)方法的實(shí)現(xiàn),我們會(huì)發(fā)現(xiàn)每當(dāng)下載數(shù)據(jù)之前_fileInfo一定會(huì)使用startOffset查找一次緩存文件秋冰,我們可以在查找過程中順便將這個(gè)值也一并查找出來救欧。所以_fileInfo查找文件的方法被修改成了這樣:
- (SEEFile *)fileForOffset:(long long)offset {
__block SEEFile * file = nil;
self.missingEndOffset = self.fileAttribute.totalBytes;
[self.files enumerateObjectsUsingBlock:^(SEEFile * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
if (obj.startOffset <= offset && obj.endOffset > offset) {
file = obj;
*stop = YES;
}
//查找下載結(jié)束位置
if (obj.startOffset > offset && obj.startOffset < self.missingEndOffset) {
self.missingEndOffset = obj.startOffset - 1;
}
}];
return file;
}
這樣每當(dāng)我們需要下載數(shù)據(jù)的時(shí)候_fileInfo就已經(jīng)為我們準(zhǔn)備好了下載的結(jié)束位置值睁壁。
downloader
說到網(wǎng)絡(luò)請(qǐng)求派歌,可供我們選擇的方案就很多了雇寇,除了直接使用NSURLConnection或者NSURLSession之外還可以使用很多已經(jīng)封裝好的第三方網(wǎng)絡(luò)框架。請(qǐng)隨意選擇虽另。
downloader需要提供以下功能:
- 下載指定范圍數(shù)據(jù)暂刘。
- 請(qǐng)求狀態(tài)回調(diào)
- 數(shù)據(jù)回調(diào)。
- 沒了...
本文中使用的是NSURLSession捂刺,由于沒有什么太大的技術(shù)含量谣拣,我就直接貼代碼了。
//SEEDownloader.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@protocol SEEDownloaderDelegate <NSObject>
@optional
/**
接收到響應(yīng)
@param response 響應(yīng)體對(duì)象
*/
- (void)didReceiveResponse:(NSURLResponse *)response;
/**
傳輸完成
@param error 錯(cuò)誤信息叠萍,如果傳輸正常完成該項(xiàng)為nil
*/
- (void)didCompleteWithError:(NSError * _Nullable)error;
/**
接收數(shù)據(jù)
@param data 數(shù)據(jù)
*/
- (void)didreceiveData:(NSData *)data;
@end
@interface SEEDownloader : NSObject
@property (nonatomic, assign) long long startOffset;
@property (nonatomic, assign) long long currentOffset;
@property (nonatomic, assign) long long endOffset;
//初始化
- (instancetype)initWithURL:(NSURL *)url delegate:(id<SEEDownloaderDelegate>)delegate;
/**
從 offset 位置開始重新請(qǐng)求文件
@param offset 起始 offset
*/
- (void)resetWithStartOffset:(long long)offset endOffset:(long long)endOffset;
//取消下載芝发,清除資源
- (void)invalidateAndCancel;
@end
NS_ASSUME_NONNULL_END
//SEEDownloader.m
#import "SEEDownloader.h"
#import "SEEPlayerMacro.h"
@interface SEEDownloader () <NSURLSessionDataDelegate>
@end
@implementation SEEDownloader {
NSURLSession * _session;
NSURL * _url;
__weak id <SEEDownloaderDelegate> _delegate;
struct {
char didReceiveResponse;
char didCompleteWithError;
char didReceiveData;
}_responder;
NSString * _headerRange;
}
- (instancetype)initWithURL:(NSURL *)url delegate:(nonnull id<SEEDownloaderDelegate>)delegate {
if (self = [super init]) {
_startOffset = -1;
_endOffset = -1;
_currentOffset = -1;
_url = url;
[self setDelegate:delegate];
}
return self;
}
#pragma mark public method
/**
從指定offset開始下載
@param startOffset offset
*/
- (void)resetWithStartOffset:(long long)startOffset endOffset:(long long)endOffset {
/* 需要開啟下載的情況
1. 請(qǐng)求起始位置不再當(dāng)前下載范圍之內(nèi)
2. 請(qǐng)求起始位置在當(dāng)前下載的范圍內(nèi)并且比當(dāng)前下載到的位置大300K 以上
*/
if (startOffset >= _startOffset && startOffset <= _endOffset){
if (startOffset <= _currentOffset + 307200) {
return;
}
}
self.startOffset = startOffset;
self.currentOffset = startOffset;
self.endOffset = endOffset;
[self see_downloadStart];
}
- (void)invalidateAndCancel {
if (_session) {
[_session invalidateAndCancel];
_session = nil;
}
}
#pragma mark private method
- (void)see_downloadStart {
NSMutableURLRequest * request = [NSMutableURLRequest requestWithURL:_url];
if (self.endOffset == 0) _headerRange = [NSString stringWithFormat:@"bytes=%lld-",self.startOffset];
else _headerRange = [NSString stringWithFormat:@"bytes=%lld-%lld",self.startOffset,self.endOffset];
[request setValue:_headerRange forHTTPHeaderField:@"Range"];
[self invalidateAndCancel];
NSURLSessionConfiguration * configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
_session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:[NSOperationQueue currentQueue]];
NSURLSessionDataTask * task = [_session dataTaskWithRequest:request];
[task resume];
}
#pragma mark delegate
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
didReceiveResponse:(NSURLResponse *)response
completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler {
if (self.endOffset == 0) {
self.endOffset = response.expectedContentLength - 1;
}
if (_responder.didReceiveResponse) {
[_delegate didReceiveResponse:response];
}
completionHandler(NSURLSessionResponseAllow);
}
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
didReceiveData:(NSData *)data {
_currentOffset += data.length;
if (_responder.didReceiveData) {
[_delegate didreceiveData:data];
}
}
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(nullable NSError *)error {
if (_responder.didCompleteWithError) {
[_delegate didCompleteWithError:error];
}
//每次下載完成重新調(diào)整結(jié)束位置,防止由于網(wǎng)絡(luò)問題導(dǎo)致下載結(jié)束后重新請(qǐng)求同段數(shù)據(jù)時(shí)通不過- (void)resetWithStartOffset:(long long)startOffset endOffset:(long long)endOffset 方法檢測(cè)苛谷。
if ([task.originalRequest.allHTTPHeaderFields[@"Range"] isEqualToString:_headerRange]) {
_endOffset = _currentOffset - 1;
}
}
#pragma mark getter & setter
- (void)setDelegate:(id <SEEDownloaderDelegate>)delegate {
_delegate = delegate;
//_responder
_responder.didReceiveResponse = [delegate respondsToSelector:@selector(didReceiveResponse:)];
_responder.didCompleteWithError = [delegate respondsToSelector:@selector(didCompleteWithError:)];
_responder.didReceiveData = [delegate respondsToSelector:@selector(didreceiveData:)];
}
@end
這里我們唯一需要注意的就是當(dāng)我們的下載由于網(wǎng)絡(luò)異常而終止時(shí)我們需要將_endOffset修改為當(dāng)前已經(jīng)下載完成的數(shù)據(jù)的末尾,使其處于假完成狀態(tài)格郁,這樣當(dāng)網(wǎng)絡(luò)恢復(fù)之后腹殿,dataManager檢測(cè)到磁盤沒有數(shù)據(jù)就會(huì)再次發(fā)出從中斷位置開始的下載請(qǐng)求独悴,而此時(shí),我們已經(jīng)將當(dāng)前下載器手動(dòng)結(jié)束锣尉,- (void)resetWithStartOffset:(long long)startOffset endOffset:(long long)endOffset 可以正常執(zhí)行刻炒。
當(dāng)然如果大家有什么更好的方式,請(qǐng)通過各種渠道告訴我自沧,謝過坟奥!
本地緩存文件創(chuàng)建以及數(shù)據(jù)寫入
我們已經(jīng)成功的將數(shù)據(jù)進(jìn)行了下載,并且通過代理回調(diào)給了dataManager拇厢,接下來dataManager接收到數(shù)據(jù)之后就需要對(duì)數(shù)據(jù)進(jìn)行存儲(chǔ)爱谁。
我們的數(shù)據(jù)是存儲(chǔ)在多個(gè)文件中的,因此我們需要在接收到數(shù)據(jù)之前將輸出流創(chuàng)建完成孝偎,所以在dataManager接收到由downloader回調(diào)回來的響應(yīng)時(shí)進(jìn)行輸出流的創(chuàng)建访敌。
- (void)didReceiveResponse:(NSURLResponse *)response {
if (self.MIMEType == nil) {
self.MIMEType = response.MIMEType;
}
if (self.totalBytes == 0) {
self.totalBytes = response.expectedContentLength;
}
long long startOffset = _downloader.startOffset;
_fileInfo.fileAttribute.exceptFileName = response.suggestedFilename;
//關(guān)閉當(dāng)前輸出文件
[self see_closeCurrentOutputFile];
//初始化輸出流
[self see_initOutputFile:startOffset];
}
首先我們將當(dāng)前視頻的基本信息存儲(chǔ)起來,之后如果當(dāng)前有正在寫入的文件將其關(guān)閉并且創(chuàng)建一個(gè)新的輸出流衣盾。
為了使緩存文件數(shù)量盡可能的少寺旺,我們選擇在創(chuàng)建新的輸出流之前首先檢查新的數(shù)據(jù)是否可以拼接在某一個(gè)現(xiàn)有的緩存文件之后,如果沒有再創(chuàng)建新的文件:
/**
1. 當(dāng)目前文件中有在offset處結(jié)尾的文件势决,則將下載的文件拼接在該文件之后
2. 如果當(dāng)前文件中沒有在offset處結(jié)尾的文件阻塑,則新建文件存儲(chǔ)
@param offset 數(shù)據(jù)起始位置
*/
- (void)see_initOutputFile:(long long)offset {
_outputFile = [_fileInfo acceptableFileForDownloadOffset:offset];
if (_outputFile == nil){
_outputFile = [[SEEFile alloc]init];
_outputFile.startOffset = offset;
NSString * path = [self see_pathForOffset:offset];
_outputFile.path = [path lastPathComponent];
[_fileInfo.files addObject:_outputFile];
[_cacheRanges addObject:[NSValue valueWithRange:NSMakeRange((NSUInteger)offset, (NSUInteger)offset)]];
}
_outputStream = [NSOutputStream outputStreamToFileAtPath:[_cacheBasePath stringByAppendingPathComponent:_outputFile.path] append:YES];
[_outputStream open];
}
之后我們?cè)诮邮盏綌?shù)據(jù)之后只需要通過輸出流寫入對(duì)應(yīng)的文件即可:
- (void)didreceiveData:(NSData *)data {
if (_outputFile) {
_outputFile.length += data.length;
NSInteger index = [_fileInfo.files indexOfObject:_outputFile];
[_cacheRanges replaceObjectAtIndex:index withObject:[NSValue valueWithRange:NSMakeRange((NSUInteger) _outputFile.startOffset, (NSUInteger)_outputFile.endOffset)]];
}
if (_outputStream)[_outputStream write:data.bytes maxLength:data.length];
}
當(dāng)下一次推送數(shù)據(jù)時(shí)就可以從緩存中讀出對(duì)應(yīng)的數(shù)據(jù)了。
三果复、Demo
關(guān)于邊下邊播功能到這里就全部結(jié)束了陈莽,至于播放、暫停据悔、進(jìn)度條等等等等這些有機(jī)會(huì)會(huì)寫在另一篇文章中传透,如果有我會(huì)在下面貼出相關(guān)文章。
Demo中已經(jīng)有部分播放极颓、暫停朱盐、進(jìn)度、緩存進(jìn)度條等功能菠隆,大家可以自行參考兵琳。
另外,雖然可能大概也許文章中有些許漏洞骇径,后續(xù)發(fā)現(xiàn)問題會(huì)及時(shí)更新躯肌,還請(qǐng)大家尊重原創(chuàng)。