iOS 9.0之前 ,使用NSURLConnection比較多
iOS 7.0之后,蘋果官方推出NSURLSession,并在9.0之后推薦使用且丟棄了NSURLConnection
因?yàn)镹SURLSession在異步的處理上比NSURLConnection好很多
本文作為學(xué)習(xí)和了解,詳解iOS9.0之前的NSURLConnection
NSURLConnection:
- 從 iOS 2.0 開始,已經(jīng)有10多年的歷史了
異步方法在 iOS 5.0 之后才有 ,
在 iOS 5.0 之后,是通過代理的方式,來實(shí)現(xiàn)網(wǎng)絡(luò)開發(fā)
本例使用NSURLConnection、NSFileHandle、NSOutputStream
實(shí)現(xiàn)了從本地服務(wù)器下載一個700M視頻的demo

NSOutputStream示意圖
對內(nèi)存進(jìn)行了優(yōu)化,內(nèi)存一直保持在30M之內(nèi)
并用GCD+NSRunloop進(jìn)行異步下載
不影響UI主線程

內(nèi)存占用控制
并在后臺打印出下載完成進(jìn)度百分比

后臺下載完成百分比
Demo詳解:
#import "ViewController.h"
// Newsstand Kit‘s 雜志包,是專門做雜志的,主要在國外使用,因?yàn)閲鴥?nèi)的書籍盜版太嚴(yán)重
// ISBN書號,電子書也必須是唯一的
// NSURLConnectionDownloadDelegate 方法主要針對雜志的下載提供接口
// 如果在開發(fā)中,使用 NSURLConnectionDownloadDelegate 代理方法下載,能夠監(jiān)聽下載進(jìn)度,但是無法找到下載的文件
@interface ViewController () <NSURLConnectionDataDelegate>
@property (weak, nonatomic) IBOutlet UIProgressView *Progress;
@property(nonatomic,assign)long long expectedContentLength; //要下載文件的總長度
@property(nonatomic,assign)long long currentLength; //當(dāng)前下載的長度
//@property(nonatomic,strong) NSMutableData *fileData; //用來每次接受到數(shù)據(jù),拼接數(shù)據(jù)使用
@property(nonatomic,copy) NSString *tartgetFilePath; //保存的目標(biāo)路徑
//@property(nonatomic,assign,getter=isFinished)BOOL finished;
@property(nonatomic,assign)CFRunLoopRef downloadRunloop; //下載線程的運(yùn)行循環(huán)
/*
保存文件的輸出流
- (void)open; 寫入之前,打開流
- (void)close; 完成之后,關(guān)閉流
*/
@property(nonatomic,strong)NSOutputStream *fileStrem;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
}
/**
NSURLConnection - 從 iOS 2.0 開始,已經(jīng)有10多年的歷史了
異步方法在 iOS 5.0 之后才有 , 在 iOS 5.0 之后,是通過代理的方式,來實(shí)現(xiàn)網(wǎng)絡(luò)開發(fā)
那時是iOS網(wǎng)絡(luò)開發(fā)的黑暗史,網(wǎng)絡(luò)開發(fā)很難
就因?yàn)檫@個黑暗史,才成就了一些第三方框架: AFN,ASI這些第三方框架
NS - NextStep 公司的縮寫
- 開發(fā)簡單的網(wǎng)絡(luò)請求比較方便,可以采用異步方法
- 開發(fā)發(fā)雜的網(wǎng)絡(luò)請求,例如:大文件下載,仍然需要使用代理來開發(fā),非常繁瑣!
**對NSURLConnection 的一些細(xì)節(jié)有所了解
問題:1.沒有下載進(jìn)度,會影響用戶體驗(yàn)
2.有內(nèi)存峰值,下載的文件有多大,NSData就會占用多大內(nèi)存
解決方法:
- 通過代理的方式解決
1.進(jìn)度跟進(jìn),解決思路:
1>在響應(yīng)方法中獲得文件總大小
2>每次接受到數(shù)據(jù),計(jì)算獲得數(shù)據(jù)的總長度,和總大小相比,計(jì)算出百分比
2.保存文件的思路
1>保存完成寫入磁盤
測試結(jié)果:和異步方法執(zhí)行一樣,仍然存在內(nèi)存峰值。
推測:蘋果的異步方法的實(shí)現(xiàn)思路,和剛才的delegate實(shí)現(xiàn)思路一樣
問題:下載的內(nèi)存峰值依舊存在
2>下載一個寫一個
1)NSFileHandle 徹底解決了內(nèi)存峰值問題
2)NSOutputStream 輸出流
- Socket 網(wǎng)絡(luò)本質(zhì)上,在客戶端和服務(wù)器之間,數(shù)據(jù)的傳遞,都是以二進(jìn)制流的方式傳遞的。
- 對數(shù)據(jù)流的操作,一定要有一些網(wǎng)絡(luò)底層的思想之后,才容易理解
- 操作方式,比 FindHandle 更簡潔
*/
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
dispatch_async(dispatch_get_global_queue(0, 0), ^{
//1.url
NSString *urlString = @"http://127.0.0.1/1.mp4";
urlString = [urlString stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
NSURL *url = [NSURL URLWithString:urlString];
//2.request
NSURLRequest *request = [NSURLRequest requestWithURL:url];
//3.connection
//開始下載時的線程,是用 dispatch_async 創(chuàng)建的
NSLog(@"開始 %@",[NSThread currentThread]);
// For the connection to work correctly, the calling thread’s run loop must be operating in the default run loop mode.
//為了保證連接的工作正常,調(diào)用線程的run loop 必須運(yùn)行在默認(rèn)的運(yùn)行循環(huán)模式下
NSURLConnection *conn = [NSURLConnection connectionWithRequest:request delegate:self];//使用代理
//設(shè)置代理工作的操作隊(duì)列
[conn setDelegateQueue:[[NSOperationQueue alloc]init]];//新的問題 默認(rèn)還在主線程工作,干擾主線程UI更新
//4.啟動連接
[conn start];
// self.finished = NO;
//5.啟動運(yùn)行循環(huán)
// while (!self.isFinished) {
// //啟動一個死循環(huán),每次監(jiān)聽0.1秒
// [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
// }
//CoreFoundation框架(純C語言,是Foundation的基礎(chǔ))中,提供了對運(yùn)行循環(huán)更底層的操作
/*
CFRunLoopStop(rl) 停止當(dāng)前的runloop
CFRunLoopGetCurrent()當(dāng)前線程的runloop
CFRunLoopRun(); 直接運(yùn)行當(dāng)前線程的運(yùn)行循環(huán)
以下代碼,是最標(biāo)準(zhǔn)的 runloop 的操作方法
*/
//1.拿到當(dāng)前線程的運(yùn)行循環(huán)
self.downloadRunloop = CFRunLoopGetCurrent();
//2.啟動運(yùn)行循環(huán)
CFRunLoopRun();
NSLog(@"到我了嗎?");
});
//直接下載視頻
// [NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse * _Nullable response, NSData * _Nullable data, NSError * _Nullable connectionErro){
// [data writeToFile:@"/Users/Liu/Desktop/123.mp4" atomically:YES];
//
// NSLog(@"完成");
// }];
}
#pragma mark - NSURLConnectionDataDelegate
//1.接收到服務(wù)器的響應(yīng) - 狀態(tài)行&響應(yīng)頭 - 做一些準(zhǔn)備工作
/*
expectedContentLength 要下載文件的總大小 long long
suggestedFilename 服務(wù)器建議保存的文件名
*/
-(void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response{
NSLog(@"%@",response);
//記錄文件總大小
self.expectedContentLength = response.expectedContentLength;
self.currentLength = 0;
//生成目標(biāo)文件路徑
self.tartgetFilePath = [@"/Users/Liu/Desktop" stringByAppendingPathComponent:response.suggestedFilename];
//刪除文件,removeItemAtPath 如果文件存在直接刪除,如果文件不存在,什么也不做
[[NSFileManager defaultManager]removeItemAtPath:self.tartgetFilePath error:NULL];
//以追加的方式打開文件流
self.fileStrem = [[NSOutputStream alloc]initToFileAtPath:self.tartgetFilePath append:YES];
[self.fileStrem open];
}
//-(NSMutableData *)fileData{
// if(_fileData == nil){
// _fileData = [[NSMutableData alloc] init];
// }
// return _fileData;
//}
//2.接收到服務(wù)器的數(shù)據(jù) - 此代理方法可能會執(zhí)行多次 因?yàn)闀盏蕉鄠€data
-(void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data{
NSLog(@"接收到數(shù)據(jù)長度 %tu",data.length);
self.currentLength +=data.length;
//計(jì)算百分比
//progress = 小 long long / 大 long long
float progress = (float)self.currentLength/self.expectedContentLength;
NSLog(@"%f %@",progress,[NSThread currentThread]);
//在主線程更新進(jìn)度條
dispatch_async(dispatch_get_main_queue(), ^{
self.Progress.progress = progress;
});
//將數(shù)據(jù)追加到文件流中
[self.fileStrem write:data.bytes maxLength:data.length];
//拼接數(shù)據(jù)
//
// [self.fileData appendData:data];
}
//-(void)writeToFileWithData:(NSData *)data{
// // 文件操作
// /*
// NSFileManager :主要功能:創(chuàng)建目錄,檢查目錄是否存在,遍歷目錄,刪除文件。。。主要針對文件集的操作,類似于Finder
// NSFileHandle: 文件”句柄“, 如果在開發(fā)中,看到 Handle 這個單詞,就意味著是對前面的單詞“File”進(jìn)行操作的對象
// 主要功能:對同一個文件進(jìn)行二進(jìn)制的讀寫操作的對象
// */
//
// //注意:如果文件不存在,fp 在實(shí)例化的結(jié)果是空
// NSFileHandle *fp = [NSFileHandle fileHandleForWritingAtPath:self.tartgetFilePath];
//
// //判斷文件是否存在 - 如果存在,則追加數(shù)據(jù);如果不存在,直接將數(shù)據(jù)寫入磁盤
// if(fp == nil){
// [data writeToFile:self.tartgetFilePath atomically:YES];
// }else{
// //如果存在,將data“追加”到現(xiàn)有文件
// //1.將文件指針移動到文件的末尾
// [fp seekToEndOfFile];
//
// //2.寫入文件
// [fp writeData:data];
//
// //3.關(guān)閉文件,在C語言的開發(fā)中,凡涉及到文件讀寫,打開和關(guān)閉通常是成對實(shí)現(xiàn)的
// [fp closeFile];
// }
//}
//3.所有數(shù)據(jù)加載完成 - 所有數(shù)據(jù)都傳輸完畢后調(diào)用,只是最后一個通知
-(void)connectionDidFinishLoading:(NSURLConnection *)connection{
//結(jié)束時代理工作的線程,是指定 NSOperationQueue 調(diào)度的
NSLog(@"完成 %@",[NSThread currentThread]);
//把數(shù)據(jù)寫入磁盤
// [self.fileData writeToFile:self.tartgetFilePath atomically:YES];
// //釋放 fileData
// self.fileData = nil;
[self.fileStrem close];
// //設(shè)置結(jié)束標(biāo)記
// self.finished = YES;
//停止下載線程所在的運(yùn)行循環(huán)
CFRunLoopStop(self.downloadRunloop);
}
//4.下載失敗或者錯誤,提示:在正常商業(yè)應(yīng)用開發(fā)中一定要做出錯誤處理
-(void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error{
NSLog(@"失敗 %@",error);
}
@end
PS:本文使用的下載源在本地服務(wù)器127.0.0.1,根據(jù)需要可以修改成其他下載源。