NSURLConnection 網絡請求

前言

  • DEPRECATED: The NSURLConnection class should no longer be used.
  • NSURLSession is the replacement for NSURLConnection
  • 從 iOS 9 開始 NSURLConnection 的大部分方法被廢棄。

一、NSURLConnection

  • NSURLConnection 提供了兩種方式來實現鏈接,一種是同步的另外一種是異步的,異步的鏈接將會建立一個新的線程,
  • 這個線程將會來負責下載的動做。而對於同步鏈接,在下載鏈接和處理通信時,則會阻塞當前調用線程。
  • 許多開發者都會認爲同步的鏈接將會堵塞主線程,其實這種觀點是錯誤的。一個同步的鏈接是會阻塞調用它的線程。
  • 若是你在主線程中建立一個同步鏈接,沒錯,主線程會阻塞。可是若是你並非從主線程開啓的一個同步的鏈接,它將會相似異步的鏈接同樣。
  • 所以這種狀況並不會堵塞你的主線程。事實上,同步和異步的主要區別就是運行 runtime 爲會異步鏈接建立一個線程,而同步鏈接則不會。
  • 1.1 NSURLConnection 的經常使用類

    • 一、NSURL:請求地址;
    • 二、NSURLRequest:封裝一個請求,保存發給服務器的所有數據,包括一個 NSURL 對象,請求方法、請求頭、請求體 ....;
    • 三、NSMutableURLRequest:NSURLRequest 的子類
    • 四、NSURLConnection:負責發送請求,創建客戶端和服務器的鏈接。發送 NSURLRequest 的數據給服務器,並收集來自服務器的響應數據。
  • 1.2 使用 NSURLConnection 發送請求的步驟

    • 一、建立一個 NSURL 對象,設置請求路徑(設置請求路徑);
    • 二、傳入 NSURL 建立一個 NSURLRequest 對象,設置請求頭和請求體(建立請求對象);任何 NSURLRequest 默認都是 GET 請求。
    • 三、使用 NSURLConnection 發送 NSURLRequest(發送請求)。
      • 發送同步請求:有返回值。
      • 發送異步請求:沒有返回值。
  • 1.3 發送同步請求

    • 使用 NSURLConnection 的 sendSynchronousRequest:returningResponse:error: 類方法,咱們能夠進行同步請求。
    • 在建立一個同步的網絡鏈接的時候咱們須要明白一點,並非是咱們的這個同步鏈接必定會堵塞咱們的主線程,若是這個同步的鏈接是建立在主線程上的,
    • 那麼這種狀況下是會堵塞咱們的主線程的,其餘的狀況下是不必定會堵塞咱們的主線程的。
    • 例如若是在 GCD 的全局併發隊列上初始化了一個同步的鏈接,其實並不會堵塞咱們的主線程的。
  • 1.4 發送異步請求

  • 發送異步請求有兩種方式:
    • 1)使用 block 回調:

      NS_AVAILABLE(10_7, 5_0)
      + (void)sendAsynchronousRequest:(NSURLRequest*) request
      queue:(NSOperationQueue*) queue
      completionHandler:(void (^)(NSURLResponse* response, NSData* data, NSError* connectionError)) handler;
      • 建立一個操做,放在 NSOperation 隊列中執行,默認是異步執行的。當服務器有返回數據的時候調用會開一條新的線程去發送請求,主線程繼續往下走,
      • 當拿到服務器的返回數據的數據的時候再回調 block,執行 block 代碼段。這種狀況不會卡住主線程。
      • queue 隊列的做用是決定這個 block 操做放在哪一個線程執行?刷新 UI 界面的操做應該放在主線程執行,不能放在子線程,在子線程處理 UI 相關操做
      • 會出現一些莫名的問題。使用 [NSOperationQueue mainQueue] 返回一個和主線程相關的隊列,即主隊列,這個 block 操做會被放在主線程中執行。使用
      • [[NSOperationQueue alloc] init] 返回的不是主隊列,這個 block 操做不會被放在主線程中執行。
    • 2)代理:

      - (instancetype)initWithRequest:(NSURLRequest *)request delegate:(id)delegate;
      + (NSURLConnection*)connectionWithRequest:(NSURLRequest *)request delegate:(id)delegate;
      • 要監聽服務器返回的 data,因此使用 協議。
      • 當接收到服務器的響應(連通了服務器)時會調用:
      - (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
      • 當接收到服務器的數據時會調用(可能會被調用屢次,每次只傳遞部分數據,須要拼接接收到的全部數):
      - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
      • 當服務器的數據加載完畢時就會調用:
      - (void)connectionDidFinishLoading:(NSURLConnection *)connection
      • 請求錯誤(失敗)的時候調用(請求超時\斷網\沒有網,通常指客戶端錯誤):
      - (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
      • NSURLConnection 的代理方法默認都是在主線程上執行的,會對界面產生卡頓。
      • For the connection to work correctly, the calling thread’s run loop must be operating in the default run loop mode.
      • 爲了讓鏈接工做正常,調用線程的運行循環必須在默認的運行循環模式下。
      • 若是要讓 NSURLConnection 實如今後臺線程回調代理方法,須要在後臺線程啓動 NSURLConnection,並啓動後臺線程的運行循環,NSURLConnection
      • 執行完畢後,會自動中止後臺線程的運行循環。
      • 啓動子線程的運行循環方法:
      CFRunLoopRun();
      [[NSRunLoop currentRunLoop] run];        // NSRunLoop 只能啓動,沒有提供中止的接口
  • 1.5 開始/中止網絡鏈接

    - (void)start;
    - (void)cancel;
    • 建立網絡鏈接後能夠不使用 start,系統會自動開始網絡鏈接。
    • 取消一個請求後,鏈接不在調用代理方法,若是但願再此鏈接,須要再次建立一個新的網絡鏈接。
  • 1.6 NSURLConnectionDownloadDelegate

    • NSURLConnectionDownloadDelegate 代理方法是爲 Newsstand Kit’s(雜誌包) 建立的下載服務的,Newsstand 主要在國外使用比較普遍,國內極少。
    • 若是使用 NSURLConnectionDownloadDelegate 代理方法監聽下載進度,可以監聽到進度,可是找不到下載的文件。

二、NSURLConnection 同步 GET 請求

// 設置網絡接口
NSString *urlStr = @"http://192.168.88.200:8080/MJServer/video?type=JSON";

// 設置請求路徑
NSURL *url = [NSURL URLWithString:urlStr];

NSURLRequest *urlRequest = [NSURLRequest requestWithURL:url];

// 建立同步網絡請求
NSData *syncNetData = [NSURLConnection sendSynchronousRequest:urlRequest returningResponse:NULL error:NULL];

三、NSURLConnection 同步 POST 請求

// 設置網絡接口
NSURL *url = [NSURL URLWithString:@"http://192.168.88.200:8080/MJServer/video"];

// 建立請求對象
NSMutableURLRequest *urlRequest = [NSMutableURLRequest requestWithURL:url];

// 設置請求方式,默認爲 GET 請求
urlRequest.HTTPMethod = @"POST";

// 設置請求體(請求參數)
urlRequest.HTTPBody = [@"type=JSON" dataUsingEncoding:NSUTF8StringEncoding];

// 建立同步網絡請求
NSData *syncNetData = [NSURLConnection sendSynchronousRequest:urlRequest returningResponse:NULL error:NULL];

四、NSURLConnection 異步 GET 請求

  • 4.1 使用 block 回調方式

    // 設置網絡接口
    NSURL *url = [NSURL URLWithString:@"http://192.168.88.200:8080/MJServer/video?type=XML"];
    
    // 建立請求對象
    NSURLRequest *urlRequest = [NSURLRequest requestWithURL:url];
    
    // 建立異步網絡請求
    [NSURLConnection sendAsynchronousRequest:urlRequest 
    queue:[NSOperationQueue mainQueue] 
    completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
    
        if (connectionError == nil && data != nil) {
    
        }
    }];
  • 4.2 使用 協議 方式

    // 遵照協議 <NSURLConnectionDataDelegate>
    
    @property(nonatomic, retain)NSMutableData *asyncNetData;
    
    // 設置網絡接口
    NSURL *url = [NSURL URLWithString:@"http://192.168.88.200:8080/MJServer/video?type=XML"];
    
    // 建立請求對象
    NSURLRequest *urlRequest = [NSURLRequest requestWithURL:url];
    
    // 建立異步網絡請求
    [NSURLConnection connectionWithRequest:urlRequest delegate:self];
    
    // 協議方法
    
    // 接收到服務器的響應
    - (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
    
        // 異步下載數據源初始化
        self.asyncNetData = [[NSMutableData alloc] init];
    }
    
    // 接收到服務器數據
    - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
    
        // 拼接從服務器下載的數據
        [self.asyncNetData appendData:data];
    }
    
    // 服務器的數據加載完畢
    - (void)connectionDidFinishLoading:(NSURLConnection *)connection {
    
        // 處理從服務器下載的數據
        self.textView.text = [[NSString alloc] initWithData:self.asyncNetData encoding:NSUTF8StringEncoding];
    }
    
    // 請求錯誤
    - (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
    
    }

五、NSURLConnection 異步 POST 請求

  • 5.1 使用 block 回調方式

    // 設置網絡接口
    NSURL *url = [NSURL URLWithString:@"http://192.168.88.200:8080/MJServer/video"];
    
    // 建立請求對象
    NSMutableURLRequest *urlRequest = [NSMutableURLRequest requestWithURL:url];
    
    // 設置請求方式,默認爲 GET 請求
    urlRequest.HTTPMethod = @"POST";
    
    // 設置請求體(請求參數)
    urlRequest.HTTPBody = [@"type=XML" dataUsingEncoding:NSUTF8StringEncoding];
    
    // 建立異步網絡請求
    [NSURLConnection sendAsynchronousRequest:urlRequest queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
    
        if (connectionError == nil && data != nil) {
    
        }
    }];
  • 5.2 使用 協議 方式

    // 遵照協議 <NSURLConnectionDataDelegate>
    
    // 設置網絡接口
    NSURL *url = [NSURL URLWithString:@"http://192.168.88.200:8080/MJServer/video"];
    
    // 建立請求對象
    NSMutableURLRequest *urlRequest = [NSMutableURLRequest requestWithURL:url];
    
    // 設置請求方式,默認爲 GET 請求
    urlRequest.HTTPMethod = @"POST";
    
    // 設置請求體(請求參數)
    urlRequest.HTTPBody = [@"type=XML" dataUsingEncoding:NSUTF8StringEncoding];
    
    // 建立異步網絡請求
    [NSURLConnection connectionWithRequest:urlRequest delegate:self];
    
    // 協議方法
    
    // 已經發送請求體
    - (void)connection:(NSURLConnection *)connection didSendBodyData:(NSInteger)bytesWritten totalBytesWritten:(NSInteger)totalBytesWritten totalBytesExpectedToWrite:(NSInteger)totalBytesExpectedToWrite {
    
    }
    
    // 接收到服務器的響應
    - (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
    
    }
    
    // 接收到服務器數據
    - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
    
    }
    
    // 服務器的數據加載完畢
    - (void)connectionDidFinishLoading:(NSURLConnection *)connection {
    
    }
    
    // 請求錯誤
    - (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
    
    }

六、NSURLConnection 文件下載

  • 6.1 獲取文件信息

    NSURL *url = [NSURL URLWithString:@"http://192.168.88.200/download/file/minion_02.mp4"];
    
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
    
    // 使用 HEAD 請求方式
    request.HTTPMethod = @"HEAD";
    
    NSURLResponse *response = nil;
    NSError *error = nil;
    
    // 使用同步請求方式,後續的下載會依賴於此
    [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&error];
    
    if (error == nil && response != nil) {
    
        // 獲取文件大小或名稱
        NSLog(@"要下載文件的長度 %tu", response.expectedContentLength);
    }
  • 6.2 使用 GET 數據請求方式下載,文件句柄存儲

    // 下載文件的總長度
    @property (nonatomic, assign) long long expectedContentLength;
    
    // 當前文件大小
    @property (nonatomic, assign) long long recvedfileLength;
    
    NSURL *url = [NSURL URLWithString:@"http://192.168.88.200/download/file/minion_02.mp4"];
    NSURLRequest *urlRequest = [NSURLRequest requestWithURL:url];
    
    // 遵照協議 <NSURLConnectionDataDelegate>
    [NSURLConnection connectionWithRequest:urlRequest delegate:self];
    
    // 協議方法
    
    - (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
    
        NSString *documentsPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0] stringByAppendingPathComponent:@"321.mp4"];
    
        // 若是文件不存在,方法不會出錯
        [[NSFileManager defaultManager] removeItemAtPath:documentsPath error:NULL];
    
        // 獲取數據總大小
        self.expectedContentLength = response.expectedContentLength;
    
        self.recvedfileLength = 0;
    }
    
    - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
    
        // 將從服務器下載的數據直接寫入文件
        [self writeToFile:data];
    
        // 計算當前數據下載完成的大小
        self.recvedfileLength += data.length;
    
        // 計算下載進度   
        float progress = (float)self.recvedfileLength / self.expectedContentLength;
    }
    
    - (void)connectionDidFinishLoading:(NSURLConnection *)connection {
    
    }
    
    - (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
    
    }
    
    // 將數據寫入文件
    - (void)writeToFile:(NSData *)data {
    
        /*
        NSFileManager:文件管理器,文件複製,刪除,是否存在...操做,相似於在 Finder 中進行的操做
        NSFileHandle :文件 "句柄(指針)" Handle 操縱桿,凡事看到 Handle 單詞,表示對前面一個名詞(File)的操做,
        對一個文件進行獨立的操做。
        */
    
        NSString *documentsPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0] stringByAppendingPathComponent:@"321.mp4"];
    
        // 打開文件,若是文件不存在,fp == nil
        NSFileHandle *fp = [NSFileHandle fileHandleForWritingAtPath:documentsPath];
    
        // 若是文件不存在
        if (fp == nil) {
    
            // 將數據直接寫入文件
            [data writeToFile:documentsPath atomically:YES];
        } else {
    
            // 將文件指針移動到文件末尾
            [fp seekToEndOfFile];
    
            // 寫入數據
            [fp writeData:data];
    
            // 關閉文件,C 語言中有一個默認的約定,凡事打開文件,都必須關閉
            [fp closeFile];
        }
    }
  • 6.3 使用 GET 數據請求方式下載,文件輸出流存儲

    // 下載文件的總長度
    @property (nonatomic, assign) long long expectedContentLength;
    
    // 當前文件大小
    @property (nonatomic, assign) long long recvedfileLength;
    
    // 輸出數據流
    @property (nonatomic, strong) NSOutputStream *fileStream;
    
    NSURL *url = [NSURL URLWithString:@"http://192.168.88.200/download/file/minion_02.mp4"];
    NSURLRequest *urlRequest = [NSURLRequest requestWithURL:url];
    
    // 遵照協議 <NSURLConnectionDataDelegate>
    [NSURLConnection connectionWithRequest:urlRequest delegate:self];
    
    // 協議方法
    
    - (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
    
        NSString *documentsPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0] stringByAppendingPathComponent:@"312.mp4"];
    
        // 若是文件不存在,方法不會出錯
        [[NSFileManager defaultManager] removeItemAtPath:documentsPath error:NULL];
    
        // 以拼接的方式實例化文件流
        self.fileStream = [[NSOutputStream alloc] initToFileAtPath:documentsPath append:YES];
    
        // 打開文件流
        [self.fileStream open];
    
        // 獲取數據總大小
        self.expectedContentLength = response.expectedContentLength;
    
        self.recvedfileLength = 0;
    }
    
    - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
    
        // 將數據的 "字節"一次性寫入文件流,而且指定數據長度
        [self.fileStream write:data.bytes maxLength:data.length];
    
        // 計算當前數據下載完成的大小
        self.recvedfileLength += data.length;
    
        // 計算下載進度   
        float progress = (float)self.recvedfileLength / self.expectedContentLength;
    }
    
    - (void)connectionDidFinishLoading:(NSURLConnection *)connection {
    
        // 關閉文件流
        [self.fileStream close];
    }
    
    - (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
    
        // 關閉文件流
        [self.fileStream close];
    }
  • 6.4 使用 專用下載 方式

    NSURL *url = [NSURL URLWithString:@"http://192.168.88.200/download/file/minion_01.mp4"];
    NSURLRequest *urlRequest = [NSURLRequest requestWithURL:url];
    
    // 遵照協議 <NSURLConnectionDownloadDelegate>
    [NSURLConnection connectionWithRequest:urlRequest delegate:self];
    
    // 協議方法
    
    - (void)connection:(NSURLConnection *)connection didWriteData:(long long)bytesWritten totalBytesWritten:(long long)totalBytesWritten expectedTotalBytes:(long long)expectedTotalBytes {
    
        /*
        下載進度:
    
        bytesWritten            本次下載子節數
        totalBytesWritten       已經下載字節數
        expectedTotalBytes      總下載字節數(文件總大小)
        */
    
        float progress = (float)totalBytesWritten / expectedTotalBytes;
        NSLog(@"%f", progress);
    }
    
    - (void)connectionDidFinishDownloading:(NSURLConnection *)connection destinationURL:(NSURL *) destinationURL {
    
        /*
        destinationURL          下載保存的路徑,下載完成以後,找不到下載的文件。
        */
    
        NSLog(@"%@", destinationURL);
    }
  • 6.5 斷點續傳下載

    // 下載文件的總長度
    @property (nonatomic, assign) long long expectedContentLength;
    
    // 當前文件大小
    @property (nonatomic, assign) long long recvedfileLength;
    
    // 輸出數據流
    @property (nonatomic, strong) NSOutputStream *fileStream;
    
    // 目標目錄
    @property (nonatomic, copy) NSString *targetPath;
    
    @property (nonatomic, strong) NSURLConnection *conn;
    
    NSURL *url = [NSURL URLWithString:@"http://192.168.88.200/download/file/minion_02.mp4"];
    
    // 檢查服務器上的文件信息
    [self checkServerFileInfoWithURL:url];
    
    // 檢查本地文件信息
    long long fileSize = [self checkLocalFileInfo];
    
    // 文件已經下載到本地
    if (fileSize == self.expectedContentLength) {
    
        return;
    }
    
    // 根據本地文件的長度,從對應 "偏移" 位置開始下載
    [self downloadWithURL:url offset:fileSize];
    
    // 檢查服務器上的文件信息
    - (void)checkServerFileInfoWithURL:(NSURL *)url {
    
        NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
        request.HTTPMethod = @"HEAD";
    
        NSURLResponse *response = nil;
        NSError *error = nil;
    
        // 發送同步方法
        [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&error];
    
        if (error == nil && response != nil) {
    
            // 文件大小
            self.expectedContentLength = response.expectedContentLength;
    
            // 文件名,保存到臨時文件夾                                          
            self.targetPath = [NSTemporaryDirectory() stringByAppendingPathComponent:response.suggestedFilename];
        }
    }
    
    // 檢查本地文件信息
    - (long long)checkLocalFileInfo {
    
        long long fileSize = 0;
    
        // 檢查本地是否存在文件
        if ([[NSFileManager defaultManager] fileExistsAtPath:self.targetPath]) {
    
            NSDictionary *dict = [[NSFileManager defaultManager] attributesOfItemAtPath:self.targetPath error:NULL];
    
            // 獲取文件大小
            fileSize = [dict fileSize];
        }
    
        // 判斷是否比服務器的文件大
        if (fileSize > self.expectedContentLength) {
    
            // 刪除文件                                                                 
            [[NSFileManager defaultManager] removeItemAtPath:self.targetPath error:NULL];
            fileSize = 0;
        }
    
        return fileSize;
    }
    
    // 從斷點處開始下載
    - (void)downloadWithURL:(NSURL *)url offset:(long long)offset {
    
        // 記錄本地文件大小
        self.recvedfileLength = offset;
    
        NSMutableURLRequest *urlRequest = [NSMutableURLRequest requestWithURL:url cachePolicy:1 timeoutInterval:15];
    
        // 一旦設置了 Range,response 的狀態碼會變成 206
        [urlRequest setValue:[NSString stringWithFormat:@"bytes=%lld-", offset] forHTTPHeaderField:@"Range"];
    
        // 遵照協議 <NSURLConnectionDataDelegate>
        self.conn = [NSURLConnection connectionWithRequest:urlRequest delegate:self];
    
        [self.conn start];
    }
    
    // 暫停下載
    - (void)pause1 {
    
        [self.conn cancel];
    }
    
    // 協議方法
    - (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
    
        // 以拼接的方式實例化文件流
        self.fileStream = [[NSOutputStream alloc] initToFileAtPath:self.targetPath append:YES];
        [self.fileStream open];
    }
    
    - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
    
        // 將數據的 "字節"一次性寫入文件流,而且指定數據長度
        [self.fileStream write:data.bytes maxLength:data.length];
    
        // 計算當前數據下載完成的大小
        self.recvedfileLength += data.length;
    
        // 計算下載進度                                                                 
        float progress = (float)self.recvedfileLength / self.expectedContentLength;
    }
    
    - (void)connectionDidFinishLoading:(NSURLConnection *)connection {
    
        [self.fileStream close];
    }
    
    - (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
    
        // 關閉文件流
        [self.fileStream close];
    }
  • 6.6 異步下載

    // 下載文件的總長度
    @property (nonatomic, assign) long long expectedContentLength;
    
    // 當前文件大小
    @property (nonatomic, assign) long long recvedfileLength;
    
    // 輸出數據流
    @property (nonatomic, strong) NSOutputStream *fileStream;
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
    
        NSURL *url = [NSURL URLWithString:@"http://192.168.88.200/download/file/minion_02.mp4"];
        NSURLRequest *urlRequest = [NSURLRequest requestWithURL:url];
    
        // 遵照協議 <NSURLConnectionDataDelegate>
        [NSURLConnection connectionWithRequest:urlRequest delegate:self];
    
        // 啓動運行循環
        CFRunLoopRun();
    });
    
    // 協議方法
    - (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
    
        NSString *documentsPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0] stringByAppendingPathComponent:@"312.mp4"];
    
        // 若是文件不存在,方法不會出錯
        [[NSFileManager defaultManager] removeItemAtPath:documentsPath error:NULL];
    
        // 以拼接的方式實例化文件流
        self.fileStream = [[NSOutputStream alloc] initToFileAtPath:documentsPath append:YES];
    
        // 打開文件流
        [self.fileStream open];
    
        // 獲取數據總大小
        self.expectedContentLength = response.expectedContentLength;
    
        self.recvedfileLength = 0;
    }
    
    - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
    
        // 將數據的 "字節"一次性寫入文件流,而且指定數據長度
        [self.fileStream write:data.bytes maxLength:data.length];
    
        // 計算當前數據下載完成的大小
        self.recvedfileLength += data.length;
    
        // 計算下載進度   
        float progress = (float)self.recvedfileLength / self.expectedContentLength;
    }   
    
    - (void)connectionDidFinishLoading:(NSURLConnection *)connection {
    
        // 關閉文件流
        [self.fileStream close];
    }
    
    - (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
    
        // 關閉文件流
        [self.fileStream close];
    }

七、NSURLConnection 下載單例封裝

  • 7.1 QDownloaderOperation.h

    @interface QDownloaderOperation : NSOperation
    
    /// 類方法
    + (instancetype)downloaderWithURL:(NSURL *)url 
    progress:(void (^)(float progress))progress 
    successed:(void (^)(NSString *targetPath))successed 
    failed:(void (^)(NSError *error))failed;
    
    /// 暫停當前下載
    - (void)pauseDownload;
    
    /// 取消當前下載
    - (void)cancelDownload;
    
    @end
  • 7.2 QDownloaderOperation.m

    @interface QDownloaderOperation () <NSURLConnectionDataDelegate>
    
    /// 下載文件總長度
    @property (nonatomic, assign) long long expectedContentLength;
    
    /// 已下載文件大小
    @property (nonatomic, assign) long long recvedfileLength;
    
    /// 下載目標目錄
    @property (nonatomic, copy) NSString *targetPath;
    
    /// 下載文件輸出數據流
    @property (nonatomic, strong) NSOutputStream *fileStream;
    
    /// block 屬性
    @property (nonatomic, copy) void (^progressBlock)(float);
    @property (nonatomic, copy) void (^successedBlock)(NSString *);
    @property (nonatomic, copy) void (^failedBlock)(NSError *);
    
    /// 網絡鏈接屬性
    @property (nonatomic, strong) NSURLConnection *conn;
    @property (nonatomic, strong) NSURL *downloadURL;
    
    @end
    
    + (instancetype)downloaderWithURL:(NSURL *)url progress:(void (^)(float))progress successed:(void (^)(NSString *))successed failed:(void (^)(NSError *))failed {
    
        QDownloaderOperation *downloader = [[self alloc] init];
    
        downloader.progressBlock = progress;
        downloader.successedBlock = successed;
        downloader.failedBlock = failed;
    
        downloader.downloadURL = url;
    
        return downloader;
    }
    
    - (void)main {
        @autoreleasepool {
    
            // 檢查服務器上的文件信息
            [self checkServerFileInfoWithURL:self.downloadURL];
    
            if (self.isCancelled) return;
    
            // 檢查本地文件信息
            long long fileSize = [self checkLocalFileInfo];
    
            if (fileSize == self.expectedContentLength) {
    
                // 下載完成的回調
                if (self.successedBlock) {
                    dispatch_async(dispatch_get_main_queue(), ^{
                        self.successedBlock(self.targetPath);
                    });
    
                    // 下載進度的回調
                    if (self.progressBlock) {
                        self.progressBlock(1.0);
                    }
                }
                return;
            }
    
            // 根據本地文件的長度,從對應 "偏移" 位置開始下載
            [self downloadWithURL:self.downloadURL offset:fileSize];
        }
    }
    
    - (void)pauseDownload {
    
        // 取消一個請求,調用此方法後,鏈接不在調用代理方法
        [self.conn cancel];
    }
    
    - (void)cancelDownload {
    
        [self.conn cancel];
        [[NSFileManager defaultManager] removeItemAtPath:self.targetPath error:NULL];
    }
    
    /// 檢查服務器上的文件信息
    - (void)checkServerFileInfoWithURL:(NSURL *)url {
    
        NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
        request.HTTPMethod = @"HEAD";
    
        NSURLResponse *response = nil;
        NSError *error = nil;
    
        // 發送同步方法
        [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&error];
    
        if (error == nil && response != nil) {
    
            // 文件大小
            self.expectedContentLength = response.expectedContentLength;
    
            // 文件名
            self.targetPath = [NSTemporaryDirectory() stringByAppendingPathComponent:response.suggestedFilename];
        }
    }
    
    /// 檢查本地文件信息
    
    - (long long)checkLocalFileInfo {
    
        long long fileSize = 0;
    
        // 檢查本地是否存在文件
        if ([[NSFileManager defaultManager] fileExistsAtPath:self.targetPath]) {
            NSDictionary *dict = [[NSFileManager defaultManager] attributesOfItemAtPath:self.targetPath error:NULL];
    
            // 獲取文件大小
            fileSize = [dict fileSize];
        }
    
        // 判斷是否比服務器的文件大
        if (fileSize > self.expectedContentLength) {
    
            // 刪除文件
            [[NSFileManager defaultManager] removeItemAtPath:self.targetPath error:NULL];
            fileSize = 0;
        }
    
        return fileSize;
    }
    
    /// 從斷點處開始下載
    - (void)downloadWithURL:(NSURL *)url offset:(long long)offset {
    
        // 記錄本地文件大小
        self.recvedfileLength = offset;
    
        NSMutableURLRequest *urlRequest = [NSMutableURLRequest requestWithURL:url cachePolicy:1 timeoutInterval:15];
    
        // 一旦設置了 Range,response 的狀態碼會變成 206
        [urlRequest setValue:[NSString stringWithFormat:@"bytes=%lld-", offset] forHTTPHeaderField:@"Range"];
    
        // 遵照協議 <NSURLConnectionDataDelegate>
        self.conn = [NSURLConnection connectionWithRequest:urlRequest delegate:self];
    
        [self.conn start];
    
        // 開啓子線程運行循環
        CFRunLoopRun();
    }
    
    /// 協議方法
    
    - (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
    
        // 以拼接的方式實例化文件流
        self.fileStream = [[NSOutputStream alloc] initToFileAtPath:self.targetPath append:YES];
    
        // 打開文件流
        [self.fileStream open];
    }
    
    - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
    
        // 將數據的 "字節" 一次性寫入文件流,而且指定數據長度
        [self.fileStream write:data.bytes maxLength:data.length];
    
        // 計算當前數據下載完成的大小
        self.recvedfileLength += data.length;
    
        // 計算下載進度
        float progress = (float)self.recvedfileLength / self.expectedContentLength;
    
        // 進度的回調
        if (self.progressBlock) {
            self.progressBlock(progress);
        }
    }
    
    - (void)connectionDidFinishLoading:(NSURLConnection *)connection {
    
        // 關閉文件流
        [self.fileStream close];
    
        // 完成的回調
        if (self.successedBlock) {
    
            // 主線程回調
            dispatch_async(dispatch_get_main_queue(), ^{
                self.successedBlock(self.targetPath);
            });
        }
    }
    
    - (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
    
        // 關閉文件流
        [self.fileStream close];
    
        // 失敗的回調
        if (self.failedBlock) {
            self.failedBlock(error);
        }
    }
  • 7.3 QDownloaderManager.h

    @interface QDownloaderManager : NSObject
    
    /// 單例
    + (instancetype)sharedManager;
    
    /// 開始下載
    - (void)downloadWithURL:(NSURL *)url progress:(void (^)(float progress))progress successed:(void (^)(NSString *targetPath))successed failed:(void (^)(NSError *error))failed;
    
    /// 暫停下載
    - (void)pauseWithURL:(NSURL *)url;
    
    /// 取消下載
    - (void)cancelWithURL:(NSURL *)url;
    
    @end
  • 7.4 QDownloaderManager.m

    @interface QDownloaderManager ()
    
    /// 下載操做緩衝池
    @property (nonatomic, strong) NSMutableDictionary *downloadCache;
    
    /// 下載操做隊列
    @property (nonatomic, strong) NSOperationQueue *downloadQueue;
    
    @end
    
    + (instancetype)sharedManager {
        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 successed:(void (^)(NSString *))successed failed:(void (^)(NSError *))failed {
    
        // 檢查緩衝池中是否有下載,若是有,直接返回
        if (self.downloadCache[url.absoluteString] != nil) {
            NSLog(@"正在在玩命下載中...");
            return;
        }
    
        QDownloaderOperation *downloader = [QDownloaderOperation downloaderWithURL:url progress:progress successed:^(NSString *targetPath) {
    
            // 刪除下載操做
            [self.downloadCache removeObjectForKey:url.absoluteString];
    
            if (successed != nil) {
                successed(targetPath);
            }
    
        } failed:^(NSError *error) {
    
            [self.downloadCache removeObjectForKey:url.absoluteString];
    
            if (failed != nil) {
                failed(error);
            }
        }];
    
        // 添加到緩衝池
        [self.downloadCache setObject:downloader forKey:url.absoluteString];
        // 添加到隊列
        [self.downloadQueue addOperation:downloader];
    }
    
    - (void)pauseWithURL:(NSURL *)url {
    
        // 判斷緩衝池中是否有對應的下載操做
        QDownloaderOperation *downloader = self.downloadCache[url.absoluteString];
    
        if (downloader != nil) {
    
            // 暫停 downloader 內部的 NSURLConnection
            [downloader pauseDownload];
    
            // 給操做發送取消消息 NSOperation
            [downloader cancel];
    
            // 從緩衝池中清除
            [self.downloadCache removeObjectForKey:url.absoluteString];
        }
    }
    
    - (void)cancelWithURL:(NSURL *)url {
    
        // 判斷緩衝池中是否有對應的下載操做
        QDownloaderOperation *downloader = self.downloadCache[url.absoluteString];
    
        if (downloader != nil) {
    
            // 取消 downloader 內部的 NSURLConnection
            [downloader cancelDownload];
    
            // 給操做發送取消消息 NSOperation
            [downloader cancel];
    
            // 從緩衝池中清除
            [self.downloadCache removeObjectForKey:url.absoluteString];
        }
    }
    
    /// 懶加載
    
    - (NSMutableDictionary *)downloadCache {
        if (_downloadCache == nil) {
            _downloadCache = [NSMutableDictionary dictionary];
        }
        return _downloadCache;
    }
    
    - (NSOperationQueue *)downloadQueue {
        if (_downloadQueue == nil) {
            _downloadQueue = [[NSOperationQueue alloc] init];
    
            // 設置最大併發數
            _downloadQueue.maxConcurrentOperationCount = 5;
        }
        return _downloadQueue;
    }
  • 7.5 ViewController.m

    // 下載進度
    @property (weak, nonatomic) IBOutlet UIProgressView *progressView;
    
    @property (nonatomic, strong) NSURL *url;
    
    // 開始下載
    
    NSURL *url = [NSURL URLWithString:@"http://120.25.226.186:32812/resources/videos/minion_01.mp4"];
    self.url = url;
    
    [[QDownloaderManager sharedManager] downloadWithURL:url progress:^(float progress) {
    
        dispatch_async(dispatch_get_main_queue(), ^{
    
            self.progressView.progress = progress;
        });
    
        } successed:^(NSString *targetPath) {
    
            NSLog(@"%@", targetPath);
    
        } failed:^(NSError *error) {
    
            NSLog(@"%@", error);
    }];
    
    // 暫停下載
    
    [[QDownloaderManager sharedManager] pauseWithURL:self.url];
    
    // 取消下載
    
    [[QDownloaderManager sharedManager] cancelWithURL:self.url];
相關文章
相關標籤/搜索