NSURLConnection下載
課程目標
- NSURLConnection下載是一個網(wǎng)絡(luò)多線程的綜合性演練項目
- 充分體會 NSURLConnection 開發(fā)中的細節(jié)
- 雖然 NSURLConnection 在 iOS 9.0 中已經(jīng)被廢棄,但是作為資深的 iOS 程序員,必須要了解 NSURLConnection 的細節(jié)
- 利用 HTTP 請求頭的 Range 實現(xiàn)斷點續(xù)傳
- 利用 NSOutputStream 實現(xiàn)文件流拼接
- 自定義 NSOperation及操作緩存管理
- Block 的綜合演練
- 利用 IB_DESIGNABLE 和 IBInspectable 實現(xiàn)在 Stroybaord 中自定義視圖的實時渲染
-
NSURLSession 從 Xcode 6.0 到 Xcode 6.3.1 都存在內(nèi)存問題,歷時7個月,如下圖所示
session下載QQ內(nèi)存.png
NSURLConnection 的歷史
- iOS 2.0 推出的,至今有10多年的歷史
- 蘋果幾乎沒有對 NSURLConnection 做太大的改動
- sendAsynchronousRequest 方法是 iOS 5.0 之后,蘋果推出的
- 在 iOS 5.0 之前,蘋果的網(wǎng)絡(luò)開發(fā)是處于黑暗時代
- 需要使用代理方法,還需要使用運行循環(huán),才能夠處理復(fù)雜的網(wǎng)絡(luò)請求!
- 只提供了 啟動 和 取消 兩個方法,沒有中間狀態(tài)
使用異步方法下載
- (void)downloadWithURL:(NSURL *)url {
// 請求
NSURLRequest *request = [NSURLRequest requestWithURL:url cachePolicy:1 timeoutInterval:kTimeout];
NSLog(@"start");
// 下載
[NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
// 將文件寫入磁盤
[data writeToFile:@"/Users/liufan/Desktop/123" atomically:YES];
NSLog(@"下載完成");
}];
}
問題:
- 沒有進度跟進,用戶體驗不好
- 會出現(xiàn)內(nèi)存峰值,如果文件太大,在真機上會閃退
解決辦法
- 使用代理方法來解決下載進度跟進的問題
HEAD方法
HEAD 方法通常是用來在下載文件之前,獲取遠程服務(wù)器上的文件信息
- 與 GET 方法相比,同樣能夠拿到響應(yīng)頭,但是不返回數(shù)據(jù)實體
- 用戶可以根據(jù)響應(yīng)頭信息,確定下一步操作
NSURL *url = [NSURL URLWithString:@"http://localhost/demo.json"];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url cachePolicy:1 timeoutInterval:10.0];
request.HTTPMethod = @"HEAD";
NSURLResponse *response = nil;
[NSURLConnection sendSynchronousRequest:request returningResponse:&response error:NULL];
NSLog(@"要下載文件的長度 %tu", response.expectedContentLength);
同步方法
- 同步方法是阻塞式的,通常只有
HEAD方法才會使用同步方法 - 如果在開發(fā)中,看到參數(shù)的類型是
**,就傳入對象的地址
注意
-
NSURLConnectionDownloadDelegate代理方法是為 Newsstand Kit’s(雜志包) 創(chuàng)建的下載服務(wù)的 - Newsstand 主要在國外使用比較廣泛,國內(nèi)極少
- 如果使用
NSURLConnectionDownloadDelegate代理方法監(jiān)聽下載進度,能夠監(jiān)聽到進度,但是:找不到下載的文件
示例代碼如下:
- (void)downloadWithURL:(NSURL *)url {
// 請求
NSURLRequest *request = [NSURLRequest requestWithURL:url cachePolicy:1 timeoutInterval:kTimeout];
[NSURLConnection connectionWithRequest:request delegate:self];
}
#pragma mark - NSURLConnectionDownloadDelegate
- (void)connection:(NSURLConnection *)connection didWriteData:(long long)bytesWritten totalBytesWritten:(long long)totalBytesWritten expectedTotalBytes:(long long) expectedTotalBytes {
NSLog(@"%f", (float)totalBytesWritten / expectedTotalBytes);
}
- (void)connectionDidFinishDownloading:(NSURLConnection *)connection destinationURL:(NSURL *) destinationURL {
NSLog(@"%@", destinationURL);
}
跟蹤下載進度
#pragma mark - NSURLConnectionDataDelegate
// 1. 接收到服務(wù)器響應(yīng)
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
NSLog(@"%@", response);
self.expectedContentLength = response.expectedContentLength;
self.fileSize = 0;
}
// 2. 接收到數(shù)據(jù)
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
self.fileSize += data.length;
float progress = (float)self.fileSize / self.expectedContentLength;
NSLog(@"%f", progress);
}
// 3. 接收完成
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
NSLog(@"下載完成");
}
// 4. 網(wǎng)絡(luò)錯誤
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
NSLog(@"%@", error);
}
拼接數(shù)據(jù)
- (NSMutableData *)fileData {
if (_fileData == nil) {
_fileData = [NSMutableData data];
}
return _fileData;
}
// 2. 接收到數(shù)據(jù)
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
self.fileSize += data.length;
float progress = (float)self.fileSize / self.expectedContentLength;
NSLog(@"%f", progress);
// 拼接數(shù)據(jù)
[self.fileData appendData:data];
}
// 3. 接收完成
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
NSLog(@"下載完成");
[self.fileData writeToFile:@"/Users/liufan/Desktop/321" atomically:YES];
self.fileData = nil;
}
存在的問題
- 內(nèi)存峰值依舊
意外發(fā)現(xiàn):運行結(jié)果和 NSURLConnection 的異步方法的效果幾乎一樣!
NSFileHandle 拼接文件
-
NSFileManager: 主要是做文件的刪除,移動,復(fù)制,檢查文件是否存在等操作,類似于 Finder -
NSFileHandle: 文件句柄(指針),操縱,提示:凡是看到 Handle 這個單詞,就表示對前面一個單詞(File)的獨立操作
- (void)writeData:(NSData *)data {
NSFileHandle *fp = [NSFileHandle fileHandleForWritingAtPath:self.targetPath];
if (fp == nil) {
[data writeToFile:self.targetPath atomically:YES];
} else {
[fp seekToEndOfFile];
[fp writeData:data];
[fp closeFile];
}
}
問題:文件會被重復(fù)追加
// 下載前刪除文件
[[NSFileManager defaultManager] removeItemAtPath:self.targetPath error:NULL];
NSOutputStream 拼接文件
定義屬性
// 1. 接收到服務(wù)器響應(yīng)
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
NSLog(@"%@", response);
self.expectedContentLength = response.expectedContentLength;
self.fileSize = 0;
self.targetPath = [NSTemporaryDirectory() stringByAppendingPathComponent:response.suggestedFilename];
NSLog(@"%@", self.targetPath);
// 刪除文件
[[NSFileManager defaultManager] removeItemAtPath:self.targetPath error:NULL];
// 打開文件流
self.fileStream = [[NSOutputStream alloc] initToFileAtPath:self.targetPath append:YES];
[self.fileStream open];
}
// 2. 接收到數(shù)據(jù)
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
self.fileSize += data.length;
float progress = (float)self.fileSize / self.expectedContentLength;
NSLog(@"%f", progress);
// 拼接數(shù)據(jù)
[self.fileStream write:data.bytes maxLength:data.length];
}
// 3. 接收完成
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
NSLog(@"下載完成");
// 關(guān)閉流
[self.fileStream close];
}
// 4. 網(wǎng)絡(luò)錯誤
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
NSLog(@"%@", error);
[self.fileStream close];
}
文件流操作方法
打開流 - 要對文件讀寫之前,首先需要打開流
- (void)open;
關(guān)閉流 - 對文件讀寫操作完成之后,需要關(guān)閉流
- (void)close;
將數(shù)據(jù)寫入到流
- (NSInteger)write:(const uint8_t *)buffer maxLength:(NSUInteger)len;
斷點續(xù)傳
確認思路
- 檢查服務(wù)器文件信息
- 檢查本地文件
- 如果比服務(wù)器文件小,續(xù)傳
- 如果比服務(wù)器文件大,重新下載
- 如果和服務(wù)器文件一樣,下載完成
- 斷點續(xù)傳
代碼實現(xiàn)
檢查服務(wù)器文件信息
/// 檢查服務(wù)器文件信息
- (void)remoteInfoWithURL:(NSURL *)url {
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
request.HTTPMethod = @"HEAD";
NSURLResponse *response = nil;
[NSURLConnection sendSynchronousRequest:request returningResponse:&response error:NULL];
self.expectedContentLength = response.expectedContentLength;
self.targetPath = [NSTemporaryDirectory() stringByAppendingPathComponent:response.suggestedFilename];
}
檢查本地文件
/// 檢查本地文件大小
- (long long)localFileSize {
NSFileManager *manager = [NSFileManager defaultManager];
long long fileSize = 0;
// 1. 文件是否存在
if ([manager fileExistsAtPath:self.targetPath]) {
fileSize = [[manager attributesOfItemAtPath:self.targetPath error:NULL] fileSize];
}
// 2. 判斷是否大于服務(wù)器大小
if (fileSize > self.expectedContentLength) {
[manager removeItemAtPath:self.targetPath error:NULL];
fileSize = 0;
}
return fileSize;
}
斷點續(xù)傳
/// 從偏移位置下載文件
- (void)downloadWithURL:(NSURL *)url offset:(long long)offset {
self.fileSize = offset;
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url cachePolicy:1 timeoutInterval:kTimeout];
NSString *rangeStr = [NSString stringWithFormat:@"bytes=%lld-", offset];
[request setValue:rangeStr forHTTPHeaderField:@"Range"];
NSURLConnection *conn = [NSURLConnection connectionWithRequest:request delegate:self];
[conn start];
}
修改代理方法
// 1. 接收到服務(wù)器響應(yīng)
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
// 打開文件流
self.fileStream = [[NSOutputStream alloc] initToFileAtPath:self.targetPath append:YES];
[self.fileStream open];
}
下載主方法
- (void)downloadWithURL:(NSURL *)url {
// 1. 檢查服務(wù)器文件信息
[self remoteInfoWithURL:url];
// 2. 檢查本地文件大小
long long fileSize = [self localFileSize];
if (fileSize == self.expectedContentLength) {
NSLog(@"下載完成");
return;
}
// 3. 從偏移位置下載文件
[self downloadWithURL:url offset:fileSize];
}
多線程
- 異步下載
- (void)downloadWithURL:(NSURL *)url {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
// 1. 檢查服務(wù)器文件信息
[self remoteInfoWithURL:url];
// 2. 檢查本地文件大小
long long fileSize = [self localFileSize];
if (fileSize == self.expectedContentLength) {
NSLog(@"下載完成");
return;
}
// 3. 從偏移位置下載文件
[self downloadWithURL:url offset:fileSize];
});
}
- 啟動運行循環(huán)
NSURLConnection *conn = [NSURLConnection connectionWithRequest:request delegate:self];
[conn start];
// NSURLConnection 會在網(wǎng)絡(luò)請求結(jié)束后,自動停止運行循環(huán)
[[NSRunLoop currentRunLoop] run];
NSLog(@"come here %@", [NSThread currentThread]);
完成回調(diào)
回調(diào)細節(jié)
-
進度回調(diào),通常在異步執(zhí)行
- 通常進度回調(diào)的頻率非常高!如果界面上有很多文件,同時下載,又要更新 UI,可能會造成界面的卡頓
- 讓進度回調(diào),在異步執(zhí)行,可以有選擇的處理進度的顯示,例如:只顯示一個指示器!
- 有些時候,如果文件很小,調(diào)用方通常不關(guān)心下載進度!(SDWebImage)
- 異步回調(diào),可以降低對主線程的壓力
-
完成回調(diào),通常在主線程執(zhí)行
- 調(diào)用方不用考慮線程間通訊,直接更新UI即可
- 完成只有一次
增加類方法
/// 實例化下載操作
///
/// @param url 下載文件的URL
/// @param progress 進度回調(diào)
/// @param finised 完成回調(diào)
///
/// @return 下載操作
+ (instancetype)downloadWithURL:(NSURL *)url progress:(void (^)(float progress))progress finised:(void (^)(NSString *filePath, NSError *error))finised;
/// 開始下載
- (void)download;
利用屬性記錄block
- 如果本方法可以直接調(diào)用,就不需要使用屬性記錄
- 如果本方法不能直接調(diào)用,就需要使用屬性記錄,然后在需要的時候執(zhí)行
定義 block 屬性
/// 下載文件 URL
@property (nonatomic, strong) NSURL *url;
/// 進度回調(diào)
@property (nonatomic, copy) void (^progressBlock)(float);
/// 完成回調(diào)
@property (nonatomic, copy) void (^finishedBlock)(NSString *, NSError *);
類方法實現(xiàn)
+ (instancetype)downloadWithURL:(NSURL *)url progress:(void (^)(float))progress finised:(void (^)(NSString *, NSError *))finised {
HMDownloadOperation *d = [[HMDownloadOperation alloc] init];
// 記錄屬性
d.url = url;
d.progressBlock = progress;
d.finishedBlock = finised;
return d;
}
在視圖控制器中準備塊代碼
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
NSURL *url = [NSURL URLWithString:@"http://dldir1.qq.com/qqfile/QQforMac/QQ_V4.0.2.dmg"];
HMDownloadOperation *down = [HMDownloadOperation downloadWithURL:url progress:^(float progress) {
NSLog(@"%f %@", progress, [NSThread currentThread]);
} finised:^(NSString *filePath, NSError *error) {
NSLog(@"%@ %@ %@", filePath, error, [NSThread currentThread]);
}];
[down download];
}
進度回調(diào)
if (self.progressBlock) {
self.progressBlock(progress);
}
完成回調(diào)
dispatch_async(dispatch_get_main_queue(), ^{
self.finishedBlock(self.filePath, nil);
});
失敗回調(diào)
dispatch_async(dispatch_get_main_queue(), ^{
self.finishedBlock(nil, error);
});
暫停下載
暫停下載
- (void)pause {
[self.conn cancel];
}
Cancels an asynchronous load of a request.
After this method is called, the connection makes no further delegate method calls. If you want to reattempt the connection, you should create a new connection object.取消一個異步請求,調(diào)用此方法后,
connection不會再調(diào)用代理方法。如果要再次嘗試連接,需要建立一個新的連接對象
下載進度視圖
屬性
IB_DESIGNABLE
@interface ProgressButton : UIButton
@property (nonatomic, assign) IBInspectable float progress;
@property (nonatomic, strong) IBInspectable UIColor *lineColor;
@property (nonatomic, assign) IBInspectable CGFloat lineWidth;
@end
代碼實現(xiàn)
@implementation ProgressButton
- (void)setProgress:(float)progress {
_progress = progress;
[self setTitle:[NSString stringWithFormat:@"%.02f%%", progress * 100] forState:UIControlStateNormal];
[self setNeedsDisplay];
}
- (void)drawRect:(CGRect)rect {
CGPoint center = CGPointMake(rect.size.width * 0.5, rect.size.height * 0.5);
CGFloat r = (MIN(rect.size.width, rect.size.height) - self.lineWidth) * 0.5;
CGFloat startAngle = - M_PI_2;
CGFloat endAngle = self.progress * 2 * M_PI + startAngle;
UIBezierPath *path = [UIBezierPath bezierPathWithArcCenter:center radius:r startAngle:startAngle endAngle:endAngle clockwise:YES];
path.lineWidth = self.lineWidth;
path.lineCapStyle = kCGLineCapRound;
[self.lineColor setStroke];
[path stroke];
}
@end
Storyboard 技巧
在 SB 中直接設(shè)置自定義視圖屬性
下載管理器
單例
+ (instancetype)sharedDownloadManager {
static id instance;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[self alloc] init];
});
return instance;
}
移植下載方法
- (void)downloadWithURL:(NSURL *)url progress:(void (^)(float))progress finised:(void (^)(NSString *, NSError *))finised {
HMDownloadOperation *downloader = [HMDownloadOperation downloadWithURL:url progress:progress finised:finised];
[downloader download];
}
下載緩沖池
緩沖池屬性
/// 下載緩沖池
@property (nonatomic, strong) NSMutableDictionary *downloaderCache;
// MARK: - 懶加載
- (NSMutableDictionary *)downloaderCache {
if (_downloaderCache == nil) {
_downloaderCache = [[NSMutableDictionary alloc] init];
}
return _downloaderCache;
}
修改下載方法
- (void)downloadWithURL:(NSURL *)url progress:(void (^)(float))progress finised:(void (^)(NSString *, NSError *))finised {
// 1. 判斷下載操作緩沖池中是否存在下載操作
if (self.downloaderCache[url]) {
NSLog(@"正在玩命下載中...");
return;
}
// 2. 實例化下載操作
HMDownloadOperation *downloader = [HMDownloadOperation downloadWithURL:url progress:progress finised:finised];
// 3. 添加到下載操作緩沖池
[self.downloaderCache setObject:downloader forKey:url];
// 4. 開始下載
[downloader download];
}
下載完成后,將操作從緩沖池中刪除
// 2. 實例化下載操作
HMDownloadOperation *downloader = [HMDownloadOperation downloadWithURL:url progress:progress finised:^(NSString *filePath, NSError *error) {
// 將操作從緩沖池中刪除
[self.downloaderCache removeObjectForKey:url];
// 執(zhí)行調(diào)用方準備的 finished
finised(filePath, error);
}];
NSOperation
使用 NSOperation 改造 HMDownloader
修改父類
@interface HMDownloadOperation : NSOperation
重寫 main 方法
- 自定義操作,重寫了main方法,在當操作被添加到隊列的時候,會自動被執(zhí)行
- 不要忘記自動釋放池
- (void)main {
// 自定義操作千萬不要忘記自動釋放池
@autoreleasepool {
// 執(zhí)行下載
[self download];
}
}
修改管理器代碼
操作隊列
@property (nonatomic, strong) NSOperationQueue *downloaderQueue;
- (NSOperationQueue *)downloaderQueue {
if (_downloaderQueue == nil) {
_downloaderQueue = [[NSOperationQueue alloc] init];
}
return _downloaderQueue;
}
修改開始下載代碼
// 4. 開始下載
[self.downloaderQueue addOperation:downloader];
取消下載操作
- (void)pauserWithURL:(NSURL *)url {
// 1. 在緩沖池中查找下載操作
HMDownloadOperation *downloader = self.downloaderCache[url];
// 2. 判斷是否存在下載操作
if (downloader == nil) {
NSLog(@"%@", self.downloaderQueue.operations);
return;
}
// 3. 暫停操作,操作隊列會認為操作已經(jīng)完成,會自動將操作從操作隊列中刪除
[downloader pause];
// 4. 將下載操作從緩沖池中刪除
[self.downloaderCache removeObjectForKey:url];
}
重構(gòu)步驟筆記
重構(gòu)的目的
- 相同的代碼不要出現(xiàn)兩次
- 相同功能的代碼可以及時抽取,以備日后復(fù)用,不要
重復(fù)創(chuàng)建輪子
重構(gòu)的原則
- 明確
每一步的目標 - 小步走
- 測試(每一個改動都有可能出現(xiàn)錯誤)
抽取代碼的步驟
- 新建方法
- 復(fù)制代碼
- 根據(jù)代碼調(diào)整參數(shù)和返回值
- 調(diào)整調(diào)用位置代碼
- 測試
抽取類的步驟
- 示意圖

下載目標1.png
抽取主方法
- 新建類
- 抽取主方法
- 在
.h中定義方法接口,明確該方法是否適合被外部調(diào)用 - 在
.m中增加方法實現(xiàn)
- 在
- 將主方法復(fù)制到新方法中
- 復(fù)制相關(guān)的
子方法 - 復(fù)制相關(guān)屬性
- 檢查代碼的
有效性- 調(diào)整內(nèi)部變量,讓
NSURL由調(diào)用方傳遞,保證代碼的靈活性
- 調(diào)整內(nèi)部變量,讓
- 復(fù)制代理方法,
- 注釋更新
UI部分的代碼 - 使用
#warning TODO提醒自己此處有未完成的工作 - 這樣做可以不影響重構(gòu)的節(jié)奏
- 注釋更新
- 調(diào)整視圖控制器
測試重構(gòu)方法執(zhí)行 - 調(diào)整視圖控制器代碼,刪除被移走代碼
- 再次測試,確保調(diào)整沒有失誤!
確認接口
- 確認重構(gòu)的接口
- 需要
進度回調(diào) - 需要
完成&錯誤回調(diào)
- 需要
- 定義類方法,傳遞回調(diào)參數(shù)
- 實現(xiàn)類方法,記錄住回調(diào) block
- 調(diào)整調(diào)用方法
- 增加
block實現(xiàn) - 測試
- 增加已經(jīng)下載完成的回調(diào)
- 進度回調(diào)(100%)
- 完成回調(diào)(路徑)
- 斷言
- 暫停操作
- 測試,測試,測試!
新問題:如果連續(xù)點擊,會重復(fù)下載,造成錯亂!<br /><br />解決辦法:建立一個下載管理器的單例,負責所有的文件下載,以及下載操作的緩存!
- 示意圖

下載目標2.png
抽取下載管理器
- 建立單例
-
接管下載操作- 定義接口方法
- 實現(xiàn)方法
- 替換方法
- 測試
- 操作緩存
- 暫停實現(xiàn)
- 最大并發(fā)數(shù),
NSOperationQueue+NSOperation
block 小結(jié)
- block 是
C語言的數(shù)據(jù)結(jié)構(gòu) - 是預(yù)先準備好的代碼,在需要時執(zhí)行,類似于匿名函數(shù)指針
- 可以被當作參數(shù)傳遞
- 在需要時,可以對
block進行擴展 - 如果當前方法不執(zhí)行
block,需要使用屬性記錄 -
block屬性需要使用copy描述符 - 對于必須傳遞的
block回調(diào),可以使用斷言
