iOS渣逼(2)泥淖蝦渣逼看URLConnection

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(@"下載完成");
    }];
}

問題:

  1. 沒有進度跟進,用戶體驗不好
  2. 會出現(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ù)傳

確認思路

  1. 檢查服務(wù)器文件信息
  2. 檢查本地文件
    • 如果比服務(wù)器文件小,續(xù)傳
    • 如果比服務(wù)器文件大,重新下載
    • 如果和服務(wù)器文件一樣,下載完成
  3. 斷點續(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é)

  1. 進度回調(diào),通常在異步執(zhí)行

    1. 通常進度回調(diào)的頻率非常高!如果界面上有很多文件,同時下載,又要更新 UI,可能會造成界面的卡頓
    2. 讓進度回調(diào),在異步執(zhí)行,可以有選擇的處理進度的顯示,例如:只顯示一個指示器!
    3. 有些時候,如果文件很小,調(diào)用方通常不關(guān)心下載進度!(SDWebImage)
    4. 異步回調(diào),可以降低對主線程的壓力
  2. 完成回調(diào),通常在主線程執(zhí)行

    1. 調(diào)用方不用考慮線程間通訊,直接更新UI即可
    2. 完成只有一次

增加類方法

///  實例化下載操作
///
///  @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)用方傳遞,保證代碼的靈活性
  • 復(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),可以使用 斷言
最后編輯于
?著作權(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)容