使用NSURLConnection下載文件并使用NSOutputStream保存在本地

NSURLConnection 是iOS 2.0開始
異步加載--是iOS 5.0才有的,在5.0之前是通過代理來實現(xiàn)網(wǎng)絡(luò)開發(fā)
---在開發(fā)簡單的網(wǎng)絡(luò)請求還是挺方便的,直接使用異步方法
---但是在開發(fā)復(fù)雜的網(wǎng)絡(luò)請求,步驟非常繁瑣

方式一.直接使用NSURLConnectionsendAsynchronousRequest方法發(fā)起異步請求:

代碼如下:
NSString *urlStr = @"http://localhost/001--等一分鐘.mp4"; NSString * url = [urlStr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:url]]; `` [NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse * _Nullable response, NSData * _Nullable data, NSError * _Nullable connectionError) {
//這一步是把數(shù)據(jù)寫入磁盤,data首先是保存在內(nèi)存中然后再一起寫入磁盤 [data writeToFile:@"/Users/xxxxx/Desktop/保存數(shù)據(jù)/123.wmv" atomically:YES]; }]

這種方式下載視頻有兩個問題
1.沒有下載進度,會影響用戶體驗
2.內(nèi)存偏高,有一個最大的峰值


一次性寫入磁盤會出現(xiàn)一次峰值.png

我測試的時候的峰值非??植?,達(dá)到了1.96G

解決思路:
1.通過代理方式來解決
進度:首先在響應(yīng)方法中獲得文件總大?。?br> 其次每次接收數(shù)據(jù),計算數(shù)據(jù)的總比例: 每次接收的數(shù)據(jù)拼接/文件總大小
2.保存文件:
a.保存完,寫入磁盤
b.邊下載邊保存

1.使用NSURLConnectionDelegate來解決上面的“沒有下載進度”的問題:代碼如下:
注意: 這個NSURLConnectionDownloadDelegate代理方法千萬別亂用,專用于雜志的下載提供接口!能夠監(jiān)聽下載進度,但是無法拿到下載的內(nèi)容;目前國內(nèi)雜志的app還是比較少,國外比較流行

/** 要下載文件總大小 /
@property(nonatomic ,assign)long long expectedContentLength;
/
* 當(dāng)前下載的大小 */
@property(nonatomic ,assign)long long currentLength;
@end

@implementation ViewController

  • (void)viewDidLoad {
    [super viewDidLoad];
    NSString *urlStr = @"http://localhost/001--消息發(fā)送機制.mp4"; NSString * url = [urlStr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:url]]; NSLog(@"開始"); NSURLConnection *connection = [[NSURLConnection alloc]initWithRequest:request delegate:self ]; //啟動連接 [connection start];

}
//1.接收服務(wù)器響應(yīng)-狀態(tài)行&響應(yīng)頭

  • (void)connection:(NSURLConnection )connection didReceiveResponse:(NSURLResponse )response
    {
    /

    響應(yīng)頭返回的數(shù)據(jù)一般有:建議的下載視頻名稱:textEncodingName
    下載視頻總大小:expectedContentLength
    */
    self.expectedContentLength = response.expectedContentLength;
    self.currentLength = 0;
    }
    //接收到服務(wù)器的數(shù)據(jù),此方法可能會執(zhí)行很多次

  • (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
    {

    //當(dāng)前下載視頻的大小
    self.currentLength = data.length;

    float progress = (float)self.currentLength / self.expectedContentLength;

}

//數(shù)據(jù)接收完成時調(diào)用此代理

  • (void)connectionDidFinishLoading:(NSURLConnection *)connection
    {
    NSLog(@"完成");
    }
    `
    流程是什么樣的?如下圖
請求及數(shù)據(jù)發(fā)送過程.png
  1. 調(diào)用 [connection start];方法發(fā)起請求連接
  2. 調(diào)用代理方法connection: didReceiveResponse:得到響應(yīng)頭(包含文件的大小和文件名稱(suggestedFilename))和狀態(tài)行:
    響應(yīng)頭.png
  3. 調(diào)用connection:didReceiveData:方法來接收數(shù)據(jù),因為數(shù)據(jù)在網(wǎng)絡(luò)中傳輸是以二進制數(shù)據(jù)的形式進行傳輸?shù)模@里的data是發(fā)送的很多一段的二進制數(shù)據(jù),下載完后拼接在一起,然后寫入磁盤,所有這個代理方法會被多次調(diào)用;用 當(dāng)前接收的數(shù)據(jù)大小/總的數(shù)據(jù)大小 = 進度,
    那么進度的問題解決了,接下來是解決寫入磁盤時出現(xiàn)峰值的問題

我在這使用了NSOutputStream輸出流以文件的方式追加到輸出流中,很簡單只需三步即可完成:
1.創(chuàng)建文件流&打開文件流
//創(chuàng)建輸出流, 以文件追加的方式寫入流中
self.outStream = [[NSOutputStream alloc]initToFileAtPath:self.targetFilePath append:YES ];
[self.outStream open];
2.寫入文件流
[self.outStream write:data.bytes maxLength:data.length];
3.關(guān)閉文件流
[self.outStream close];

使用輸入流寫入數(shù)據(jù).png

到這又出現(xiàn)新的問題:默認(rèn)NSURLConnection是在主線程工作,指定了代理的工作隊列之后,
[connection setDelegateQueue:[[NSOperationQueue alloc]init]];
整個下載仍然是在主線程?。。?!UI事件會卡住文件下載

注意:在看到 NSURLConnection中的描述“ For the connection to work correctly, the calling thread’s run loop must be operating in the default run loop mode.”,這句話的意思是: 為了保證連接的正常工作,調(diào)用線程的RunLoop 必須運行在默認(rèn)的運行循環(huán)模式下!!--- 這也是iOS9之后丟棄NSURLConnection的原因

那么接下來如何解決呢?
使用GCD來創(chuàng)建dispatch_async(dispatch_get_global_queue(0, 0), ^{},把請求設(shè)置放block中:
·
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSString *urlStr = @"http://localhost/001--消息發(fā)送機制.mp4";
NSString * url = [urlStr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:url]];
NSLog(@"開始");
NSURLConnection *connection = [[NSURLConnection alloc]initWithRequest:request delegate:self ];

    [connection setDelegateQueue:[[NSOperationQueue alloc]init]];
    //啟動連接
    [connection start];
});·

但是這有個問題會出現(xiàn),代理NSURLConnectionDelegate的方法不會走了??!這是為什么呢,因為這個線程 dispatch_async(dispatch_get_global_queue(0, 0), ^{})出了括號(線程的作用域)后就銷毀了。
那么我們?nèi)绾谓鉀Q這個問題呢?
其實很簡單---手動啟動runloop運行循環(huán)就可以解決了

/** 下載線程的運行循環(huán) */
@property(assign,nonatomic)CFRunLoopRef downloadRunloop;

dispatch_async(dispatch_get_global_queue(0, 0), ^{})的block中啟動拿到runloop
self.downloadRunloop = CFRunLoopGetCurrent();
啟動runloop
CFRunLoopRun();
connectionDidFinishLoading代理方法中,停止下載線程所在的runloop
CFRunLoopStop(self.downloadRunloop);

這樣就解決了卡主線程的問題了

Demo:
https://github.com/wasterd/FileDownloadDemo.git

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容