iOS開發(fā)中經(jīng)常會(huì)用到文件的下載與上傳功能,今天咱們來(lái)分享一下文件下載的思路玻墅。文件上傳下篇再說(shuō)介牙。
文件下載分為:小文件下載、大文件下載澳厢。
小文件下載
小文件可以是一張圖片环础,或者一個(gè)文件,這里指在現(xiàn)行的網(wǎng)絡(luò)狀況下基本上不需要等待很久就能下載好的文件剩拢。這里以picjumbo里的一張圖片為例子线得。
NSData方式
其實(shí)我們經(jīng)常用的[NSData dataWithContentsOfURL] 就是一種文件下載方式,猜測(cè)這里面應(yīng)該是發(fā)送了Get請(qǐng)求徐伐。
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請(qǐng)求贯钩,回調(diào)的data就是我們下載到的圖片。
大文件下載
NSURLConnection下載
通過(guò)上面的兩個(gè)方法去下載大文件是不合理的办素,因?yàn)檫@兩個(gè)方法都是一次性返回整個(gè)下載到的文件角雷,返回的data在內(nèi)存中,如果下載一個(gè)幾百兆的東西摸屠,內(nèi)存肯定會(huì)爆的谓罗。
其實(shí)NSURLConnection還提供了另外一種發(fā)送請(qǐng)求的方式
// 發(fā)送請(qǐng)求去下載 (創(chuàng)建完conn對(duì)象后,會(huì)自動(dòng)發(fā)起一個(gè)異步請(qǐng)求)
[NSURLConnection connectionWithRequest:request delegate:self];
這里用到了代理季二,那肯定要遵守協(xié)議了.遵守NSURLConnectionDataDelegate 協(xié)議.
進(jìn)去看看有幾個(gè)代理方法檩咱,其實(shí)我們能用到的也就三個(gè)揭措。
/**
* 請(qǐng)求失敗時(shí)調(diào)用(請(qǐng)求超時(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
{
}
通過(guò)執(zhí)行下載操作绊含,分別log上面三個(gè)方法,會(huì)發(fā)現(xiàn)didReceiveData這個(gè)方法會(huì)被頻繁的調(diào)用炊汹,每次都會(huì)傳回來(lái)一部分data躬充,下面是官方api對(duì)這個(gè)方法的說(shuō)明
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.
由此我們可以知道,這種下載方式是通過(guò)這個(gè)代理方法每次傳回來(lái)一部分文件讨便,最終我們把每次傳回來(lái)的數(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;
/**
* 文件的總長(zhǎng)度
*/
@property (nonatomic, assign) long long totalLength;
/**
* 1.接收到服務(wù)器的響應(yīng)就會(huì)調(diào)用
*
* @param response 響應(yīng)
*/
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
![20150908141106978.png](http://upload-images.jianshu.io/upload_images/5145760-5af285edcb25ceb7.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
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];
不得不說(shuō)蘋果太為開發(fā)者考慮了殊轴,我們不必這么麻煩的去獲取文件總大小了衰倦,
response.expectedContentLength 這句代碼就搞定了。
response.suggestedFilename 這句代表獲取下載的文件名
題外話扯的有點(diǎn)多旁理,言歸正傳樊零,這樣我們確實(shí)可以下載文件,最后拿到的文件也能正常運(yùn)行
但是有個(gè)致命的問(wèn)題韧拒,內(nèi)存淹接!用來(lái)接受文件的NSMutableData一直都在內(nèi)存中,會(huì)隨著文件的下載一直變大叛溢,
所有這種處理方式絕對(duì)是不合理的塑悼。
合理的方式在我們獲取一部分data的時(shí)候就寫入沙盒中,然后釋放內(nèi)存中的data楷掉。
這里要用到NSFilehandle這個(gè)類厢蒜,這個(gè)類可以實(shí)現(xiàn)對(duì)文件的讀取、寫入烹植、更新斑鸦。
下面總結(jié)了一些常用的NSFileHandle的方法,在這個(gè)表中草雕,fh是一個(gè)NSFileHandle對(duì)象巷屿,data是一個(gè)NSData對(duì)象,path是一個(gè)NSString 對(duì)象墩虹,offset是易額Unsigned long long變量嘱巾。
具體關(guān)于NSFileHandle的用法各位自行搜索憨琳。
在接受到響應(yīng)的時(shí)候就在沙盒中創(chuàng)建一個(gè)空的文件,然后每次接收到數(shù)據(jù)的時(shí)候就拼接到這個(gè)文件的最后面旬昭,通過(guò)- (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è)用來(lái)寫數(shù)據(jù)的文件句柄對(duì)象
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ì)寫入文件的長(zhǎng)度
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;
}
這樣在下載過(guò)程中內(nèi)存就會(huì)一直很穩(wěn)定了,并且下載的文件也是沒(méi)問(wèn)題的问拘。
斷點(diǎn)下載
暫停/繼續(xù)下載也是現(xiàn)在下載中必備的功能了遍略,如果沒(méi)有暫停功能,用戶體驗(yàn)相比會(huì)很差骤坐,而且如果突然網(wǎng)絡(luò)不好中斷了绪杏,沒(méi)有實(shí)現(xiàn)斷點(diǎn)下載的話只有重新下了。或油。寞忿。
下面讓我們來(lái)加入斷點(diǎn)下載功能吧。
NSURLConnection 只提供了一個(gè)cancel方法顶岸,這并不是暫停,而是取消下載任務(wù)叫编。如果要實(shí)現(xiàn)斷點(diǎn)下載必須要了解HTTP協(xié)議中請(qǐng)求頭的Range辖佣。
不難看出,通過(guò)設(shè)置請(qǐng)求頭的Range我們可以指定下載的位置搓逾、大小卷谈。
那么我們這樣設(shè)置bytes=500- 從500字節(jié)以后的所有字節(jié),
只需要在didReceiveData中記錄已經(jīng)寫入沙盒中文件的大小(self.currentLength)霞篡,
把這個(gè)大小設(shè)置到請(qǐng)求頭中世蔗,因?yàn)榈谝淮蜗螺d肯定是沒(méi)有執(zhí)行過(guò)didReceive方法,self.currentLength也就為0朗兵,也就是從頭開始下污淋。
上代碼:
#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.請(qǐng)求
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
// 設(shè)置請(qǐng)求頭
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;
}
}
在下載過(guò)程中,為了提高效率余掖,充分利用cpu性能寸爆,通常會(huì)執(zhí)行多線程下載,代碼就不貼了盐欺,分析一下思路:
下載開始赁豆,創(chuàng)建一個(gè)和要下載的文件大小相同的文件(如果要下載的文件為100M,那么就在沙盒中創(chuàng)建一個(gè)100M的文件冗美,然后計(jì)算每一段的下載量魔种,開啟多條線程下載各段的數(shù)據(jù),分別寫入對(duì)應(yīng)的文件部分)粉洼。
NSURLSession下載方式
上面這種下載文件的方式確實(shí)比較復(fù)雜节预,要自己去控制內(nèi)存寫入相應(yīng)的位置甲抖,不過(guò)在蘋果在iOS7推出了一個(gè)新的類NSURLSession,它具備了NSURLConnection所具備的方法,同時(shí)也比它更強(qiáng)大心铃。蘋果推出它的目的大有取代NSURLConnection的趨勢(shì)或者目的准谚。
NSURLSession 也可以發(fā)送Get/Post請(qǐng)求,實(shí)現(xiàn)文件的下載和上傳去扣。
在NSURLSesiion中柱衔,任何請(qǐng)求都可以被看做是一個(gè)任務(wù)。其中有三種任務(wù)類型
// NSURLSessionDataTask : 普通的GET\POST請(qǐng)求
// NSURLSessionDownloadTask : 文件下載
// NSURLSessionUploadTask : 文件上傳(很少用愉棱,一般服務(wù)器不支持)
NSURLSession 簡(jiǎn)單使用
NSURLSession發(fā)送請(qǐng)求非常簡(jiǎn)單唆铐,與connection不同的是,任務(wù)創(chuàng)建后不會(huì)自動(dòng)發(fā)送請(qǐng)求奔滑,需要手動(dòng)開始執(zhí)行任務(wù)艾岂。
// 1.得到session對(duì)象
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請(qǐng)求 自定義請(qǐng)求頭
[session dataTaskWithRequest:<#(NSURLRequest *)#> completionHandler:<#^(NSData *data, NSURLResponse *response, NSError *error)completionHandler#>]
NSURLSession 下載
使用NSURLSession就非常簡(jiǎn)單了朋其,不需要去考慮什么邊下載邊寫入沙盒的問(wèn)題王浴,蘋果都幫我們做好了。代碼如下
NSURL* url = [NSURL URLWithString:@"http://dlsw.baidu.com/sw-search-sp/soft/9d/25765/sogou_mac_32c_V3.2.0.1437101586.dmg"];
// 得到session對(duì)象
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)的方法里面并沒(méi)用NSData傳回來(lái)氓辣,多了一個(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
不過(guò)在下載完成之后會(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];
不過(guò)通過(guò)這種方式下載有個(gè)缺點(diǎn)就是無(wú)法監(jiān)聽下載進(jìn)度絮吵,要監(jiān)聽下載進(jìn)度,蘋果通常的作法是通過(guò)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對(duì)象
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方式簡(jiǎn)單很多吧脐彩,用NSURLSessionDownloadTask做斷點(diǎn)下載也很簡(jiǎn)單碎乃,我們先了解一下任務(wù)的取消方法
- (void)cancelByProducingResumeData:(void (^)(NSData *resumeData))completionHandler;
取消操作以后會(huì)調(diào)用一個(gè)Block,并傳入一個(gè)resumeData惠奸,該參數(shù)包含了繼續(xù)下載文件的位置信息梅誓。也就是說(shuō),當(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)存問(wèn)題蓬蝶。
另外,session還提供了通過(guò)resumeData來(lái)創(chuàng)建任務(wù)的方法
- (NSURLSessionDownloadTask *)downloadTaskWithResumeData:(NSData *)resumeData;
我們只需要在取消操作的回調(diào)中記錄好resumeData尘分,然后在恢復(fù)下載的適合通過(guò)上面的方法創(chuàng)建任務(wù)就好了,相比NSURLconnection簡(jiǎn)單太多了丸氛。
需要注意的是Block中循環(huán)引用的問(wèn)題
__weak typeof(self) selfVc = self;
[self.downloadTask cancelByProducingResumeData:^(NSData *resumeData) {
selfVc.resumeData = resumeData;
selfVc.downloadTask = nil;
}];