iOS開發(fā)中經(jīng)常會(huì)用到文件的下載與上傳功能级解,今天咱們來分享一下文件下載的思路冒黑。文件上傳下篇再說。
文件下載分為小文件下載與大文件下載
小文件下載
小文件可以是一張圖片蠕趁,或者一個(gè)文件薛闪,這里指在現(xiàn)行的網(wǎng)絡(luò)狀況下基本上不需要等待很久就能下載好的文件辛馆。這里以picjumbo里的一張圖片為例子俺陋。
NSData方式
其實(shí)我們經(jīng)常用的[NSData dataWithContentsOfURL]
就是一種文件下載方式,猜測這里面應(yīng)該是發(fā)送了Get請求昙篙。
NSURL *url = [NSURL URLWithString:@"https://picjumbo.imgix.net/HNCK8461.jpg?q=40&w=1650&sharp=30"];
NSData *data = [NSData dataWithContentsOfURL:url];
當(dāng)然下載代碼應(yīng)該放到子線程執(zhí)行
NSURLConnection方式下載
NSURL* url = [NSURL URLWithString:@"https://picjumbo.imgix.net/HNCK8461.jpg?q=40&w=1650&sharp=30"];
[NSURLConnection sendAsynchronousRequest:[NSURLRequest requestWithURL:url] queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
self.imageView.image = [UIImage imageWithData:data];
}];
就是發(fā)送一個(gè)異步的Get請求腊状,回調(diào)的data就是我們下載到的圖片。
這些都很簡單苔可,今天主要說的是大文件的下載缴挖。
大文件下載
NSURLConnection下載
通過上面的兩個(gè)方法去下載大文件是不合理的,因?yàn)檫@兩個(gè)方法都是一次性返回整個(gè)下載到的文件焚辅,返回的data在內(nèi)存中映屋,如果下載一個(gè)幾百兆的東西苟鸯,內(nèi)存肯定會(huì)爆的。
其實(shí)NSURLConnection還提供了另外一種發(fā)送請求的方式
// 發(fā)送請求去下載 (創(chuàng)建完conn對象后棚点,會(huì)自動(dòng)發(fā)起一個(gè)異步請求)
[NSURLConnection connectionWithRequest:request delegate:self];
這里用到了代理早处,那肯定要遵守協(xié)議了.遵守NSURLConnectionDataDelegate
協(xié)議.
進(jìn)去看看有幾個(gè)代理方法,其實(shí)我們能用到的也就三個(gè)瘫析。
/**
* 請求失敗時(shí)調(diào)用(請求超時(shí)砌梆、網(wǎng)絡(luò)異常)
*
* @param error 錯(cuò)誤原因
*/
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
{
}
/**
* 1.接收到服務(wù)器的響應(yīng)就會(huì)調(diào)用
*
* @param response 響應(yīng)
*/
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
}
/**
* 2.當(dāng)接收到服務(wù)器返回的實(shí)體數(shù)據(jù)時(shí)調(diào)用(具體內(nèi)容,這個(gè)方法可能會(huì)被調(diào)用多次)
*
* @param data 這次返回的數(shù)據(jù)
*/
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
}
/**
* 3.加載完畢后調(diào)用(服務(wù)器的數(shù)據(jù)已經(jīng)完全返回后)
*/
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
}
通過執(zhí)行下載操作贬循,分別log上面三個(gè)方法咸包,會(huì)發(fā)現(xiàn)didReceiveData這個(gè)方法會(huì)被頻繁的調(diào)用,每次都會(huì)傳回來一部分data杖虾,下面是官方api對這個(gè)方法的說明
is called with a single immutable NSData object to the delegate,
representing the next portion of the data loaded from the connection. This is the only guaranteed for the delegate to receive the data from the resource load.
由此我們可以知道烂瘫,這種下載方式是通過這個(gè)代理方法每次傳回來一部分文件,最終我們把每次傳回來的數(shù)據(jù)合并成一個(gè)我們需要的文件亏掀。
這時(shí)候我們通常想到的方法是定義一個(gè)全局的NSMutableData,接受到響應(yīng)的時(shí)候初始化這個(gè)MutableData忱反,在didReceiveData方法里面去拼接
[self.totalData appendData:data];
最后在完成下載的方法里面吧整個(gè)MutableData寫入沙盒。
代碼如下:
@property (weak, nonatomic) IBOutlet UIProgressView *myPregress;
@property (nonatomic,strong) NSMutableData* fileData;
/**
* 文件的總長度
*/
@property (nonatomic, assign) long long totalLength;
/**
* 1.接收到服務(wù)器的響應(yīng)就會(huì)調(diào)用
*
* @param response 響應(yīng)
*/
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
self.fileData = [NSMutableData data];
// 獲取要下載的文件的大小
self.totalLength = response.expectedContentLength;
}
/**
* 2.當(dāng)接收到服務(wù)器返回的實(shí)體數(shù)據(jù)時(shí)調(diào)用(具體內(nèi)容滤愕,這個(gè)方法可能會(huì)被調(diào)用多次)
*
* @param data 這次返回的數(shù)據(jù)
*/
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
[self.fileData appendData:data];
self.myPregress.progress = (double)self.fileData.length / self.totalLength;
}
/**
* 3.加載完畢后調(diào)用(服務(wù)器的數(shù)據(jù)已經(jīng)完全返回后)
*/
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
// 拼接文件路徑
NSString *cache = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
NSString *file = [cache stringByAppendingPathComponent:response.suggestedFilename];
// 寫到沙盒中
[self.fileData writeToFile:file atomically:YES];
}
我這里下載的是javajdk温算。(度娘的地址)
注意:通常大文件下載是需要給用戶展示下載進(jìn)度的。
這個(gè)數(shù)值是 已經(jīng)下載的數(shù)據(jù)大小/要下載的文件總大小
已經(jīng)下載的數(shù)據(jù)我們可以記錄间影,要下載的文件總大小在服務(wù)器返回的響應(yīng)頭里面可以拿到注竿,在接受到響應(yīng)的方法里執(zhí)行
NSHTTPURLResponse *res = (NSHTTPURLResponse*)response;
NSDictionary *headerDic = res.allHeaderFields;
NSLog(@"%@",headerDic);
self.fileLength = [[headerDic objectForKey:@"Content-Length"] intValue];
不得不說蘋果太為開發(fā)者考慮了,我們不必這么麻煩的去獲取文件總大小了魂贬,
response.expectedContentLength 這句代碼就搞定了巩割。
response.suggestedFilename 這句代表獲取下載的文件名
題外話扯的有點(diǎn)多,言歸正傳付燥,這樣我們確實(shí)可以下載文件宣谈,最后拿到的文件也能正常運(yùn)行
但是有個(gè)致命的問題,內(nèi)存键科!用來接受文件的NSMutableData一直都在內(nèi)存中闻丑,會(huì)隨著文件的下載一直變大,
所有這種處理方式絕對是不合理的勋颖。
合理的方式在我們獲取一部分data的時(shí)候就寫入沙盒中嗦嗡,然后釋放內(nèi)存中的data。
這里要用到NSFilehandle這個(gè)類饭玲,這個(gè)類可以實(shí)現(xiàn)對文件的讀取侥祭、寫入、更新。
下面總結(jié)了一些常用的NSFileHandle的方法矮冬,在這個(gè)表中谈宛,fh是一個(gè)NSFileHandle對象,data是一個(gè)NSData對象胎署,path是一個(gè)NSString 對象入挣,offset是易額Unsigned long long變量。
具體關(guān)于NSFileHandle的用法各位自行搜索硝拧。
在接受到響應(yīng)的時(shí)候就在沙盒中創(chuàng)建一個(gè)空的文件径筏,然后每次接收到數(shù)據(jù)的時(shí)候就拼接到這個(gè)文件的最后面,通過- (unsigned long long)seekToEndOfFile;
這個(gè)方法
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
// 文件路徑
NSString* ceches = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
NSString* filepath = [ceches stringByAppendingPathComponent:response.suggestedFilename];
// 創(chuàng)建一個(gè)空的文件到沙盒中
NSFileManager* mgr = [NSFileManager defaultManager];
[mgr createFileAtPath:filepath contents:nil attributes:nil];
// 創(chuàng)建一個(gè)用來寫數(shù)據(jù)的文件句柄對象
self.writeHandle = [NSFileHandle fileHandleForWritingAtPath:filepath];
// 獲得文件的總大小
self.totalLength = response.expectedContentLength;
}
/**
* 2.當(dāng)接收到服務(wù)器返回的實(shí)體數(shù)據(jù)時(shí)調(diào)用(具體內(nèi)容障陶,這個(gè)方法可能會(huì)被調(diào)用多次)
*
* @param data 這次返回的數(shù)據(jù)
*/
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
// 移動(dòng)到文件的最后面
[self.writeHandle seekToEndOfFile];
// 將數(shù)據(jù)寫入沙盒
[self.writeHandle writeData:data];
// 累計(jì)寫入文件的長度
self.currentLength += data.length;
// 下載進(jìn)度
self.myPregress.progress = (double)self.currentLength / self.totalLength;
}
/**
* 3.加載完畢后調(diào)用(服務(wù)器的數(shù)據(jù)已經(jīng)完全返回后)
*/
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
self.currentLength = 0;
self.totalLength = 0;
// 關(guān)閉文件
[self.writeHandle closeFile];
self.writeHandle = nil;
}
這樣在下載過程中內(nèi)存就會(huì)一直很穩(wěn)定了滋恬,并且下載的文件也是沒問題的。
斷點(diǎn)下載
暫停/繼續(xù)下載也是現(xiàn)在下載中必備的功能了抱究,如果沒有暫停功能恢氯,用戶體驗(yàn)相比會(huì)很差,而且如果突然網(wǎng)絡(luò)不好中斷了鼓寺,沒有實(shí)現(xiàn)斷點(diǎn)下載的話只有重新下了勋拟。。妈候。
下面讓我們來加入斷點(diǎn)下載功能吧敢靡。
NSURLConnection 只提供了一個(gè)cancel方法,這并不是暫停苦银,而是取消下載任務(wù)啸胧。如果要實(shí)現(xiàn)斷點(diǎn)下載必須要了解HTTP協(xié)議中請求頭的Range。
不難看出幔虏,通過設(shè)置請求頭的Range我們可以指定下載的位置纺念、大小。
那么我們這樣設(shè)置bytes=500- 從500字節(jié)以后的所有字節(jié)
,
只需要在didReceiveData中記錄已經(jīng)寫入沙盒中文件的大邢肜ā(self.currentLength)陷谱,
把這個(gè)大小設(shè)置到請求頭中,因?yàn)榈谝淮蜗螺d肯定是沒有執(zhí)行過didReceive方法瑟蜈,self.currentLength也就為0烟逊,也就是從頭開始下。
注意:通過這種方法踪栋,我從我們公司的服務(wù)器上下載東西暫停開始都沒用問題焙格,下載好的視頻也能正常播放图毕,但是百度軟件的下載鏈接不成功(暫停以后在請求服務(wù)器不給響應(yīng))夷都,應(yīng)該是百度服務(wù)器做了限制吧,或者我寫的不完美,有想法歡迎交流囤官。
上代碼:
#pragma mark --按鈕點(diǎn)擊事件
- (IBAction)btnClicked:(UIButton *)sender {
// 狀態(tài)取反
sender.selected = !sender.isSelected;
// 斷點(diǎn)續(xù)傳
// 斷點(diǎn)下載
if (sender.selected) { // 繼續(xù)(開始)下載
// 1.URL
NSURL *url = [NSURL URLWithString:@"http://localhost:8080//term_app/hdgg.zip"];
// 2.請求
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
// 設(shè)置請求頭
NSString *range = [NSString stringWithFormat:@"bytes=%lld-", self.currentLength];
[request setValue:range forHTTPHeaderField:@"Range"];
// 3.下載
self.connection = [NSURLConnection connectionWithRequest:request delegate:self];
} else { // 暫停
[self.connection cancel];
self.connection = nil;
}
}
在下載過程中冬阳,為了提高效率,充分利用cpu性能党饮,通常會(huì)執(zhí)行多線程下載肝陪,代碼就不貼了,分析一下思路:
下載開始刑顺,創(chuàng)建一個(gè)和要下載的文件大小相同的文件(如果要下載的文件為100M氯窍,那么就在沙盒中創(chuàng)建一個(gè)100M的文件,然后計(jì)算每一段的下載量蹲堂,開啟多條線程下載各段的數(shù)據(jù)狼讨,分別寫入對應(yīng)的文件部分)。
NSURLSession下載方式
上面這種下載文件的方式確實(shí)比較復(fù)雜柒竞,要自己去控制內(nèi)存寫入相應(yīng)的位置政供,不過在蘋果在iOS7推出了一個(gè)新的類NSURLSession
,它具備了NSURLConnection所具備的方法,同時(shí)也比它更強(qiáng)大朽基。蘋果推出它的目的大有取代NSURLConnection的趨勢或者目的布隔。
NSURLSession
也可以發(fā)送Get/Post請求,實(shí)現(xiàn)文件的下載和上傳稼虎。
在NSURLSesiion中衅檀,任何請求都可以被看做是一個(gè)任務(wù)。其中有三種任務(wù)類型
// NSURLSessionDataTask : 普通的GET\POST請求
// NSURLSessionDownloadTask : 文件下載
// NSURLSessionUploadTask : 文件上傳(很少用霎俩,一般服務(wù)器不支持)
NSURLSession 簡單使用
NSURLSession發(fā)送請求非常簡單术吝,與connection不同的是,任務(wù)創(chuàng)建后不會(huì)自動(dòng)發(fā)送請求茸苇,需要手動(dòng)開始執(zhí)行任務(wù)排苍。
// 1.得到session對象
NSURLSession* session = [NSURLSession sharedSession];
NSURL* url = [NSURL URLWithString:@""];
// 2.創(chuàng)建一個(gè)task,任務(wù)
NSURLSessionDataTask* dataTask = [session dataTaskWithURL:url completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
// data 為返回?cái)?shù)據(jù)
}];
// 3.開始任務(wù)
[dataTask resume];
// 發(fā)送post請求 自定義請求頭
[session dataTaskWithRequest:<#(NSURLRequest *)#> completionHandler:<#^(NSData *data, NSURLResponse *response, NSError *error)completionHandler#>]
NSURLSession 下載
使用NSURLSession就非常簡單了学密,不需要去考慮什么邊下載邊寫入沙盒的問題淘衙,蘋果都幫我們做好了。代碼如下
NSURL* url = [NSURL URLWithString:@"http://dlsw.baidu.com/sw-search-sp/soft/9d/25765/sogou_mac_32c_V3.2.0.1437101586.dmg"];
// 得到session對象
NSURLSession* session = [NSURLSession sharedSession];
// 創(chuàng)建任務(wù)
NSURLSessionDownloadTask* downloadTask = [session downloadTaskWithURL:url completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error) {
}];
// 開始任務(wù)
[downloadTask resume];
是不是跟NSURLConnection很像腻暮,但仔細(xì)看會(huì)發(fā)現(xiàn)回調(diào)的方法里面并沒用NSData傳回來彤守,多了一個(gè)location,顧名思義哭靖,location就是下載好的文件寫入沙盒的地址具垫,打印一下發(fā)現(xiàn)下載好的文件被自動(dòng)寫入的temp文件夾下面了。
location:file:///Users/yeaodong/Library/Developer/CoreSimulator/Devices/E52B4B95-53E1-46A2-9881-8C969958FBC0/data/Containers/Data/Application/BFB9F0CA-0F50-4682-BBBD-B71B54C39EBE/tmp/CFNetworkDownload_YNnuIS.tmp
不過在下載完成之后會(huì)自動(dòng)刪除temp中的文件试幽,所有我們需要做的只是在回調(diào)中把文件移動(dòng)(或者復(fù)制筝蚕,反正之后會(huì)自動(dòng)刪除)到caches中。
NSString *caches = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
// response.suggestedFilename : 建議使用的文件名,一般跟服務(wù)器端的文件名一致
NSString *file = [caches stringByAppendingPathComponent:response.suggestedFilename];
// 將臨時(shí)文件剪切或者復(fù)制Caches文件夾
NSFileManager *mgr = [NSFileManager defaultManager];
// AtPath : 剪切前的文件路徑
// ToPath : 剪切后的文件路徑
[mgr moveItemAtPath:location.path toPath:file error:nil];
不過通過這種方式下載有個(gè)缺點(diǎn)就是無法監(jiān)聽下載進(jìn)度起宽,要監(jiān)聽下載進(jìn)度洲胖,蘋果通常的作法是通過delegate,這里也一樣坯沪。而且NSURLSession的創(chuàng)建方式也有所不同绿映。
首先遵守協(xié)議<NSURLSessionDownloadDelegate>
注意不要寫錯(cuò)
點(diǎn)進(jìn)去發(fā)現(xiàn)協(xié)議里面有三個(gè)方法。
#pragma mark -- NSURLSessionDownloadDelegate
/**
* 下載完畢會(huì)調(diào)用
*
* @param location 文件臨時(shí)地址
*/
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask
didFinishDownloadingToURL:(NSURL *)location
{
}
/**
* 每次寫入沙盒完畢調(diào)用
* 在這里面監(jiān)聽下載進(jìn)度腐晾,totalBytesWritten/totalBytesExpectedToWrite
*
* @param bytesWritten 這次寫入的大小
* @param totalBytesWritten 已經(jīng)寫入沙盒的大小
* @param totalBytesExpectedToWrite 文件總大小
*/
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask
didWriteData:(int64_t)bytesWritten
totalBytesWritten:(int64_t)totalBytesWritten
totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite
{
self.pgLabel.text = [NSString stringWithFormat:@"下載進(jìn)度:%f",(double)totalBytesWritten/totalBytesExpectedToWrite];
}
/**
* 恢復(fù)下載后調(diào)用叉弦,
*/
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask
didResumeAtOffset:(int64_t)fileOffset
expectedTotalBytes:(int64_t)expectedTotalBytes
{
}
這上面的注釋已經(jīng)很詳細(xì)了,相信大家都能看懂吧藻糖。
NSURLSession創(chuàng)建方式,這里就不能使用Block回調(diào)方式了卸奉,如果給下載任務(wù)設(shè)置了completionHandler這個(gè)block,也實(shí)現(xiàn)了下載的代理方法颖御,優(yōu)先執(zhí)行block榄棵,代理方法也就不會(huì)執(zhí)行了。
// 得到session對象
NSURLSessionConfiguration* cfg = [NSURLSessionConfiguration defaultSessionConfiguration]; // 默認(rèn)配置
NSURLSession* session = [NSURLSession sessionWithConfiguration:cfg delegate:self delegateQueue:[NSOperationQueue mainQueue]];
// 創(chuàng)建任務(wù)
NSURLSessionDownloadTask* downloadTask = [session downloadTaskWithURL:url];
// 開始任務(wù)
[downloadTask resume];
相比之前的NSURLConnection方式簡單很多吧潘拱,用NSURLSessionDownloadTask做斷點(diǎn)下載也很簡單疹鳄,我們先了解一下任務(wù)的取消方法
- (void)cancelByProducingResumeData:(void (^)(NSData *resumeData))completionHandler;
取消操作以后會(huì)調(diào)用一個(gè)Block,并傳入一個(gè)resumeData芦岂,該參數(shù)包含了繼續(xù)下載文件的位置信息瘪弓。也就是說,當(dāng)你下載了10M得文件數(shù)據(jù)禽最,暫停了腺怯。那么你下次繼續(xù)下載的時(shí)候是從第10M這個(gè)位置開始的,而不是從文件最開始的位置開始下載川无。因而為了保存這些信息呛占,所以才定義了這個(gè)NSData類型的這個(gè)屬性:resumeData。這個(gè)data只包含了url跟已經(jīng)下載了多少數(shù)據(jù)懦趋,不會(huì)很大晾虑,不用擔(dān)心內(nèi)存問題。
另外,session還提供了通過resumeData來創(chuàng)建任務(wù)的方法
- (NSURLSessionDownloadTask *)downloadTaskWithResumeData:(NSData *)resumeData;
我們只需要在取消操作的回調(diào)中記錄好resumeData仅叫,然后在恢復(fù)下載的適合通過上面的方法創(chuàng)建任務(wù)就好了帜篇,相比NSURLconnection簡單太多了。
需要注意的是Block中循環(huán)引用的問題
__weak typeof(self) selfVc = self;
[self.downloadTask cancelByProducingResumeData:^(NSData *resumeData) {
selfVc.resumeData = resumeData;
selfVc.downloadTask = nil;
}];
示例程序下載:https://github.com/hongfenglt/HFDownLoad
這篇博客斷斷續(xù)續(xù)寫了兩三天诫咱,可能某些地方思路有些亂笙隙,歡迎大神指正。