iOS 下載管理器

總體內容

一、NSURLConncetion 下載

  • 1.1、我們先使用NSURLConncetion 下載一個視頻試試,完整代碼在demo中的 Test1ViewController

    視頻連接:@"http://images.ciotimes.com/2ittt-zm.mp4"

    • <1>、對視頻鏈接進行編碼
      在iOS程序中,訪問一些http/https的資源服務時,如果url中存在中文或者特殊字符時,會導致無法正常的訪問到資源或服務,想要解決這個問題,需要對url進行編碼。

      NSString *urlStr = @"http://images.ciotimes.com/2ittt-zm.mp4";
      
      // 在 iOS9 之後廢棄了
      // urlStr = [urlStr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
      // iOS9 之後取代上面的 api
      urlStr = [urlStr stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]];
      
    • <2>、string 轉 NSURL

      NSURL *url = [NSURL URLWithString:urlStr];
      
    • <3>、創建 NSURLRequest 對象

      NSURLRequest *request = [NSURLRequest requestWithURL:url];
      
    • <4>、NSURLConnection 下載 視頻

      // iOS9 之後廢棄了
      [NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue]
         completionHandler:^(NSURLResponse * _Nullable response,
         NSData * _Nullable data, NSError * _Nullable connectionError) {
      
            // 下載視頻的名字
            NSString *videoName =  [urlStr lastPathComponent];
            // 下載到桌面的文件夾 JK視頻下載器
            NSString *downPath = [NSString stringWithFormat:@"/Users/wangchong/Desktop/JK視頻下載器/%@",videoName];
            // 將數據寫入到硬盤
            [data writeToFile:downPath atomically:YES];
      
            NSLog(@"下載完成");
      }];
      

      提示:NSURLConnectioniOS 2.0之後就有了,sendAsynchronousRequest這個方法是在 iOS5.0 之後出現的

    • 完整的代碼

      // 1、對視頻鏈接進行編碼
      NSString *urlStr = @"http://images.ciotimes.com/2ittt-zm.mp4";
      // iOS9 之後的 api
      urlStr = [urlStr stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]];
      
      // 2、string 轉 NSURL
      NSURL *url = [NSURL URLWithString:urlStr];
      
      // 3、創建 NSURLRequest 對象
      NSURLRequest *request = [NSURLRequest requestWithURL:url];
      
      // 4、NSURLConnection 下載 視頻
      // iOS9 之後廢棄了
      [NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue]
          completionHandler:^(NSURLResponse * _Nullable response,
          NSData * _Nullable data, NSError * _Nullable connectionError) {
      
            // 下載視頻的名字
            NSString *videoName =  [urlStr lastPathComponent];
            // 下載到桌面的文件夾 JK視頻下載器
            NSString *downPath = [NSString stringWithFormat:@"/Users/wangchong/Desktop/JK視頻下載器/%@",videoName];
            // 將數據寫入到硬盤
            [data writeToFile:downPath atomically:YES];
      
            NSLog(@"下載完成");
      }];
      
    • 上面下載會出現的兩個問題:

      • (1)、沒有下載進度,會影響用戶的體驗
      • (2)、內存偏高,會有最大峯值(一次性把數據寫入),內存隱患



  • 1.2、NSURLConnection 進度監聽,完整代碼在demo中的 Test2ViewController

    • (1)、在 1.1 裏面我們使用的是 NSURLConnectionblock方法進行的下載,會有下載沒有進度和出現峯值的問題,那麼下面我們就使用 NSURLConnection 的代理方法來解決這些問題

      • 下載沒有進度的解決辦法:通過代理來解決
      • 進度跟進:在響應頭中獲取文件的總大小,在每次接收數據的時候計算數據的比例
    • (2)、代理方法選擇 NSURLConnectionDataDelegate,其他兩個的NSURLConnection代理方法都是不對的

    • (3)、定義一個記錄總視頻大小的屬性和接收到的數據包或者下載的數據總大小

      /** 要下載的文件總大小 */
      @property(nonatomic,assign) long long exceptedContentLength;
      
      /** 當前已經下載的文件總大小 */
      @property(nonatomic,assign) long long currentDownContentLength;
      

      提示:類型要選擇 long long,系統使用的就是這個類型

    • (4)、常用的代理方法

      // 1、接收服務器的響應 --- 狀態和響應頭做一些準備工作
      // expectedContentLength : 文件的總大小
      // suggestedFilename : 服務器建議保存的名字
      - (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response{
      
           // 記錄文件的總大小
           self.exceptedContentLength = response.expectedContentLength;
           // 當前下載的文件大小初始化爲 0
           self.currentDownContentLength = 0;
           NSLog(@"\nURL=%@\nMIMEType=%@\ntextEncodingName=%@\nsuggestedFilename=%@",response.URL,response.MIMEType,response.textEncodingName,response.suggestedFilename);
      }
      
      // 2、接收服務器的數據,由於數據是分塊發送的,這個代理方法會被執行多次,因此我們也會拿到多個data
      - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data{
      
           NSLog(@"接收到的數據長度=%tu",data.length);
           // 計算百分比
           // progress = (float)long long / long long
           float progress = (float)self.currentDownContentLength/self.exceptedContentLength;
      
           JKLog(@"下載的進度=%f",progress);
      }
      
      // 3、接收到所有的數據加載完畢後會有一個通知
      - (void)connectionDidFinishLoading:(NSURLConnection *)connection{
      
           NSLog(@"下載完畢");
      }
      
      // 4、下載錯誤的處理
      -(void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error{
      
            NSLog(@"鏈接失敗");
      }
      

      提示:計算百分比 progress = (float)long long / long long; 要記得轉換類型,兩個整數相除的結果是不會有小數的,轉成 float就好

  • 1.3、拼接數據然後寫入磁盤(不可取,比 1.1 更嚴重),完整代碼在demo中的 Test3ViewController

    • 由於在 1.1 中出現的 峯值 問題,在這裏來解決一下,兩種方式嘗試
      第一種: 從服務器獲取完 數據包 data 後一次性寫入磁盤
      第二種:獲取一個數據包就寫入一次磁盤

    • (1)、定義視頻下載到的路徑以及數據的data

      /**
        保存下載視頻的路徑
      */
      @property(nonatomic,strong) NSString *downFilePath;
      /**
        保存視頻數據
      */
      @property(nonatomic,strong) NSMutableData *fileData;
      
      -(NSMutableData *)fileData{
      
           if (!_fileData) {
      
                _fileData = [[NSMutableData alloc]init];
           }
          return _fileData;
      }
      
    • (2)、代理中的方法

      // 1、接收服務器的響應 --- 狀態和響應頭做一些準備工作
      // expectedContentLength : 文件的總大小
      // suggestedFilename : 服務器建議保存的名字
      - (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response{
      
          // 記錄文件的總大小
          self.exceptedContentLength = response.expectedContentLength;
          // 當前下載的文件大小初始化爲 0
          self.currentDownContentLength = 0;
      
          // 創建下載的路徑
          self.downFilePath = [@"/Users/wangchong/Desktop/JK視頻下載器" stringByAppendingPathComponent:response.suggestedFilename];
      
      }
      
      // 2、接收服務器的數據,由於數據是分塊發送的,這個代理方法會被執行多次,因此我們也會拿到多個data
      - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data{
      
          // JKLog(@"接收到的數據長度=%tu",data.length);
          self.currentDownContentLength += data.length;
      
          // 計算百分比
          // progress = (float)long long / long long
          float progress = (float)self.currentDownContentLength/self.exceptedContentLength;
      
          JKLog(@"下載的進度=%f",progress);
      
          self.progressLabel.text = [NSString stringWithFormat:@"下載進度:%f",progress];
      
          // 拼接每次獲取到服務器的數據包 data
          [self.fileData appendData:data];
      
      }
      
      // 3、接收到所有的數據加載完畢後會有一個通知
      - (void)connectionDidFinishLoading:(NSURLConnection *)connection{
      
           JKLog(@"下載完畢");
      
           // 數據獲取完,寫入磁盤
           [self.fileData writeToFile:self.downFilePath atomically:YES];
      }
      
      // 4、下載錯誤的處理
      -(void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error{
      
            JKLog(@"鏈接失敗");
      }
      

    分析:
    第一種: 從服務器獲取完 數據包 data 後一次性寫入磁盤的問題:不僅僅會出現峯值的問題,由於 fileData是強引用無法釋放,會造成內存暴增,由此可以看出和1.1中的異步效果一樣,應該是蘋果底層的實現方式

  • 1.4、NSFileHandle數據包邊下載邊寫入磁盤,完整代碼在demo中的 Test4ViewController

    • 提起NSFileHandle,我們老解釋一下它與NSFileManager的區別

      • NSFileManager:主要的功能是創建目錄、檢查目錄是否存在、遍歷目錄、刪除文件
        拷貝文件、剪切文件等等,主要是針對文件的操作
      • NSFileHandle:文件"句柄",對文件的操作,主要功能是:對同一個文件進行二進制讀寫
    • (1)、我們寫一個寫入數據的方法,如下

      // 把數據寫入到磁盤的方法
      -(void)writeFileData:(NSData *)data{
      
           NSFileHandle *fp = [NSFileHandle fileHandleForWritingAtPath:self.downFilePath];
      
           // 如果文件不存在,直接x將數據寫入磁盤
           if (fp == nil) {
      
                 [data writeToFile:self.downFilePath atomically:YES];
           }else{
      
                 // 如果存在,將data追加到現在文件的末尾
                 [fp seekToEndOfFile];
                 // 寫入文件
                 [fp writeData:data];
                 // 關閉文件
                 [fp closeFile];
            }
      }
      

      提示:通過測試,邊下載邊寫入磁盤解決了峯值的問題

    • (2)、如何判斷文件是否下載完成 ?
      答:判斷進度?判斷完成通知?,判斷時間?判斷大小?這些都不太好,比較好的方式是使用MD5
      MD5:

      • <1>.服務器對你下載的文件計算好一個MD5,將此 MD5 傳給客戶端
      • <2>.開始下載文件......
      • <3>.下載完成時,對下載的文件做一次MD5
      • <4>.比較服務器返回的MD5和我們自己計算的MD5,如果二者相等,就代表下載完成
  • 1.5、NSOutputStream 拼接文件,完整代碼在demo中的 Test5ViewController

    • (1)、創建一個保存文件的輸出流 NSOutputStream 屬性

      /* 保存文件的輸出流
         - (void)open; 寫入之前,打開流
         - (void)close; 寫入完畢之後,關閉流
       */
      @property(nonatomic,strong)NSOutputStream *fileStream;
      
    • (2)、創建輸出流並打開

      // 創建輸出流
      self.fileStream = [[NSOutputStream alloc]initToFileAtPath:self.downFilePath append:YES];
      [self.fileStream open];
      
    • (3)、將數據拼接起來,並判斷是否可寫如,一般情況下可寫入,除非磁盤空間不足

      // 判斷是否有空間可寫
      if ([self.fileStream hasSpaceAvailable]) {
      
           [self.fileStream write:data.bytes maxLength:data.length];
      }
      
    • (4)、關閉文件流

      接收到所有的數據加載完畢後會有一個通知
      - (void)connectionDidFinishLoading:(NSURLConnection *)connection{
      
            NSLog(@"下載完畢");
            [self.fileStream close];
      }
      
    • (5)、把NSURLConncetion放到子線程,但是雖然寫入的操作是在子線程,但是默認的connection 是在主線程工作,指定了代理的工作的隊列之後,整個下載還是在主線程 。UI事件能夠卡住下載

      NSURLConnection *connection = [[NSURLConnection alloc]initWithRequest:request delegate:self];
      
      // 設置代理工作的操作 [[NSOperationQueue alloc]init] 默認創建一個異步併發隊列
      [connection setDelegateQueue:[[NSOperationQueue alloc]init]];
      [connection start];
      
  • 1.6、解決1.5中 NSURLConncetion的下載在主線程的問題,完整代碼在demo中的 Test6ViewController

    • (1)、將網絡操作放在異步線程,異步的運行循環默認不啓動,沒有辦法監聽接下來的網絡事件

      dispatch_async(dispatch_get_global_queue(0, 0), ^{
      
          // 1、對視頻鏈接進行編碼
          // 在iOS程序中,訪問一些HTTP/HTTPS的資源服務時,如果url中存在中文或者特殊字符時,會導致無法正常的訪問到資源或服務,想要解決這個問題,需要對url進行編碼。
          NSString *urlStr = @"http://images.ciotimes.com/2ittt-zm.mp4";
          urlStr = [urlStr stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]];
      
          // 2、string 轉 NSURL
          NSURL *url = [NSURL URLWithString:urlStr];
      
          // 3、創建 NSURLRequest 對象
          NSURLRequest *request = [NSURLRequest requestWithURL:url];
      
          // 4、NSURLConnection 下載 視頻
          /**
             By default, for the connection to work correctly, the calling thread’s run loop must be operating in the default run loop mode.
             爲了保證鏈接工作正常,調用線程的RunLoop,必須在默認的運行循環模式下
           */
          NSURLConnection *connection = [[NSURLConnection alloc]initWithRequest:request delegate:self];
      
          // 設置代理工作的操作 [[NSOperationQueue alloc]init] 默認創建一個異步併發隊列
          [connection setDelegateQueue:[[NSOperationQueue alloc]init]];
      
          [connection start];
      });
      

      分析:上面的代碼是有很大的問題的,子線程執行完後會直接死掉,不會繼續執行 start 後面的操作,也就是說沒有辦法下載;解決辦法是給子線程創建 Runloop

    • (2)、定義一個保存下載線程的運行循環

      @property(nonatomic,assign)CFRunLoopRef downloadRunloop;
      
    • (3)、在 [connection start];之後啓動我們創建的子線程可以活下去的Runloop

      /*
         CoreFoundation 框架 CFRunLoop
         CFRunloopStop() 停止指定的runloop
         CFRunloopGetCurrent() 獲取當前的Runloop
         CFRunloopRun() 直接啓動當前的運行循環
       */
      
      //1、拿到當前的運行循環
      self.downloadRunloop = CFRunLoopGetCurrent();
      
      //2.啓動當前的運行循環
      CFRunLoopRun();
      
    • (4)、在下載完成後停止下載線程所在的runloop

      // 所有數據加載完畢--所有數據加載完畢,會一個通知!
      - (void)connectionDidFinishLoading:(NSURLConnection *)connection
      {
          NSLog(@"完畢!%@",[NSThread currentThread]);
      
          //關閉文件流
          [self.fileStream close];
      
          //停止下載線程所在的runloop
          CFRunLoopStop(self.downloadRunloop);
      }
      

二、NSURLSession 下載大文件,以下測試我們使用Apache 服務器裏面的數據

  • 2.1、NSURLSession 簡介 以及 簡單使用,完整代碼在JKNSURLSession中的 Test1ViewController
    NSURLSession是在iOS 7.0(15年)的時候推出的,在最開始的時候也會出現峯值,後來解決後大家才重新使用NSURLSession,NSURLSession所有的任務都是session發起的,默認所有任務都是“掛起”的,需要resume執行。

    • 簡單的使用:

      -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
      
           // 1.創建url: http://localhost/test.json
           NSURL *url = [NSURL URLWithString:@"http://localhost/test.json"];
           [self taskWithUrl:url];
      }
      
      -(void)taskWithUrl:(NSURL *)url{
      
           [[[NSURLSession sharedSession]dataTaskWithURL:url completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
      
              // 反序列化
              id result = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
      
               NSLog(@"%@",result);
           }] resume];
      }
      

      提示:NSURLSession 是一個單利,目的是使開發更容易,默認是不啓動的,需要開發者調用 resume 啓動 NSURLSession,如上面

  • 2.2、NSURLSession 簡單的下載,完整代碼在JKNSURLSession中的 Test2ViewController

    -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    
         // 1.創建url:http://localhost
         NSURL *url = [NSURL URLWithString:@"http://localhost/2ittt-zm.mp4"];
         [self taskWithUrl:url];
    }
    
    -(void)taskWithUrl:(NSURL *)url{
    
        /*
          如果在回調方法中,不做任何處理,下載的文件會被刪除
          下載是默認下載到tmp文件夾,系統會自動回收這個區域
    
          設計目的:
          1.通常從網絡下載文件,什麼樣的格式文件最多?zip
          2.如果是zip包,下載之後要解壓
          3.解壓之後,原始的zip就不需要了。系統會自動幫你刪除
         */
    
         [[[NSURLSession sharedSession]downloadTaskWithURL:url completionHandler:^(NSURL * _Nullable location, NSURLResponse * _Nullable response, NSError * _Nullable error) {
      
               NSLog(@"%@",location);
      
         }]resume];
    }
    

    提示:location打印是:file:///Users/wangchong/Library/Developer/CoreSimulator/Devices/643379A0-0449-4FE2-AD19-71258BDDBAE6/data/Containers/Data/Application/E6F1AABA-BDBE-4191-A167-02D5DCD19D41/tmp/CFNetworkDownload_OaisFm.tmp,我們可以看到 tmp,臨時存放下載文件的地方

  • 2.3、文件解壓縮,完整代碼在JKNSURLSession中的 Test3ViewController

    • (1)、這裏我們需要使用一個工具SSZipArchive,在demo裏面有


      SSZipArchive的功能壓縮文件解壓文件

    • (2)、解壓我們服務器的一個文件到 Library/Caches裏面

      -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
      
          // 1.創建url:http://localhost
          NSURL *url = [NSURL URLWithString:@"http://localhost/ftp.docx.zip"];
          [self taskWithUrl:url];
      }
      
      -(void)taskWithUrl:(NSURL *)url{
      
          [[[NSURLSession sharedSession]downloadTaskWithURL:url completionHandler:^(NSURL * _Nullable location, NSURLResponse * _Nullable response, NSError * _Nullable error) {
      
               NSLog(@"%@",location);
               // 文件解壓目標路徑,不能指定目標文件。因爲我們解壓文件會產生多個文件
               NSString *cachePath = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES)lastObject];
               [SSZipArchive unzipFileAtPath:location.path toDestination:cachePath];
      
          }]resume];
      }
      
  • 2.4、NSURLSession下載進度監聽,完整代碼在JKNSURLSession中的 Test4ViewController

    • (1)、創建一個 NSURLSession對象

      // 全局的網絡會話,管理所有的網絡任務
      @property(nonatomic,strong) NSURLSession *session;
      
      -(NSURLSession *)session{
      
           if (!_session) {
                /**
                 全局網絡環境的一個配置
                 比如:身份驗證,瀏覽器類型以及緩存,超時,這些都會被記錄在
                */
                NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
                _session = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:nil];
            }
           return _session;
      }
      
      -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
      
             // 1.創建url:http://localhost
             NSURL *url = [NSURL URLWithString:@"http://localhost/2ittt-zm.mp4"];
             //如果你要監聽下載進度,必須使用代理。
             //如果你要更進下載進度,就不能block 。
             [[self.session downloadTaskWithURL:url]resume];
      }
      

      提示:

      • 如果你要監聽下載進度,必須使用代理。
      • [NSURLSession sharedSession] 是全局的單例。整個系統都會用,也就是其他的應用程序也會用
      • 如果你要更進下載進度,就不能block 。
    • (2)、常用的代理方法(其中下載完成的方法是在iOS7.0之後必須要寫的,在iOS7之前,下面的三個方法都必須寫)

      /**
         1、下載完成的方法
       */
      -(void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location{
      
           NSLog(@"下載完成");
      }
      
      /**
        2、下載續傳數據的方法
      */
      -(void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didResumeAtOffset:(int64_t)fileOffset expectedTotalBytes:(int64_t)expectedTotalBytes{
      
      
       }
      
      /**
        3、下載進度的方法
      
        @param session 網絡會話
        @param downloadTask 調用代理方式的下載任務
        @param bytesWritten 本次下載的字節數
        @param totalBytesWritten 已經下載的字節數
        @param totalBytesExpectedToWrite 期望下載的字節數 -- 文件的總大小
       */
      -(void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite{
      
           float progress = (float)totalBytesWritten/totalBytesExpectedToWrite;
           NSLog(@"進度=%f",progress);
      }
      
  • 2.5、自定義progressview,完整代碼在JKNSURLSession中的 Test5ViewController

    • (1)、自定義progressview我們選擇繼承於 UIButton,原因是:button可以設置文字,展示的時候比較方便,當然也可以使用其他的控件,比如lable,那麼我們自定義一個類 JKProgressBtn 繼承於UIButton,代碼如下

      • JKProgressBtn.h裏面的代碼

        #import <UIKit/UIKit.h>
        
        NS_ASSUME_NONNULL_BEGIN
        
        @interface JKProgressBtn : UIButton
        /**
           表示進度的值
        */
        @property(nonatomic,assign) float progress;
        
        @end
        
        NS_ASSUME_NONNULL_END
        
      • JKProgressBtn.m裏面的代碼

        #import "JKProgressBtn.h"
        
        @implementation JKProgressBtn
        
        -(instancetype)initWithFrame:(CGRect)frame{
        
              self = [super initWithFrame:frame];
        
              if (self) {
        
                 [self setTitleColor:[UIColor brownColor] forState:UIControlStateNormal];
              }
        
              return self;
        }
        
        -(void)setProgress:(float)progress{
        
              _progress = progress;
        
              // 進度的Label
              [self setTitle:[NSString stringWithFormat:@"%0.2f%%",_progress*100] forState:UIControlStateNormal];
              // 刷新視圖
              [self setNeedsDisplay];
        }
        
        -(void)drawRect:(CGRect)rect{
        
        
              CGSize s = rect.size;
              // 圓心
              CGPoint center = CGPointMake(s.width*0.5, s.height*0.5);
              // 半徑
              CGFloat r = (s.height > s.width) ? s.width*0.5:s.height*0.5;
              r = r - 5;
              // 其實角度
              CGFloat startAngle = -M_PI_2;
              // 結束角度
              CGFloat endAngle = self.progress*2*M_PI + startAngle;
        
              /**
                 第1個參數:圓心
                 第2個參數:半徑
                 第3個參數:起始角度
                 第4個參數:結束角度
                 第5個參數:YES:順時針 / NO:逆時針
               */
              UIBezierPath *bezierPath = [UIBezierPath bezierPathWithArcCenter:center radius:r startAngle:startAngle endAngle:endAngle clockwise:YES];
              // 圓環的寬度
              bezierPath.lineWidth = 10.0;
              // 設置圓環的樣式(圓形)
              bezierPath.lineCapStyle = kCGLineCapRound;
              // 給圓環添加顏色
              [[UIColor purpleColor]setStroke];
        
              // 繪製路徑
              [bezierPath stroke];
        }
        
        @end
        

        提示:UIBezierPath:貝塞爾曲線的起始角度是時鐘的3點,也就是數學上x的正軸方向,故上面我們把起始角度設置爲-M_PI_2,也就是 時鐘的12點,同理數學上y的正軸方向,其他的參數在上面描述的很清楚

        • 解釋一下: bezierPath.lineWidth = 10.0;,在貝塞爾曲線裏面,半徑決定後,圓環的寬度是以半徑向外擴展的,所以纔有上面的: r = r - 5;
    • (2)、JKProgressBtn 的使用,在NSURLSession下載進度的方法裏面刷新JKProgressBtn的進度,如下:


      • 先在控制器裏面定義一個 JKProgressBtn屬性並初始化添加到控制器

        // 進度的View
        @property(nonatomic,strong) JKProgressBtn *progressView;
        
        -(JKProgressBtn *)progressView{
        
             if (!_progressView) {
        
                  _progressView = [[JKProgressBtn alloc]initWithFrame:CGRectMake([UIScreen mainScreen].bounds.size.width/2-50, [UIScreen mainScreen].bounds.size.height/2-50, 100, 100)];
                  _progressView.backgroundColor = [UIColor yellowColor];
             }
        
             return _progressView;
        }
        
        // 添加進度View
        [self.view addSubview:self.progressView];
        
      • 在下載進度的方法裏面設置主線程刷新 progressView的值

        /**
          3、下載進度的方法
        
          @param session 網絡會話
          @param downloadTask 調用代理方式的下載任務
          @param bytesWritten 本次下載的字節數
          @param totalBytesWritten 已經下載的字節數
          @param totalBytesExpectedToWrite 期望下載的字節數 -- 文件的總大小
        */
        -(void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite{
        
               float progress = (float)totalBytesWritten/totalBytesExpectedToWrite;
               NSLog(@"進度=%f",progress);
               dispatch_async(dispatch_get_main_queue(), ^{
        
                       self.progressView.progress = progress;
               });
        }
        

        提示:NSURLSession創建的下載是在子線程執行的,所以上面纔在主線程刷新UI

  • 2.6、斷點續傳,完整代碼在JKNSURLSession中的 Test6ViewController

    • (1)、創建三個按鈕,開始下載暫停下載繼續下載

    • (2)、創建一個全局的下載任務

      /**
         設置一個全局的下載任務
       */
      @property(nonatomic,strong) NSURLSessionDownloadTask *downloadTask;
      
    • (3)、開始下載、暫停下載,繼續下載 三個方法對應的代碼如下

      #pragma mark 開始下載
      -(void)startLoadTask{
      
             NSLog(@"開始下載");
             // 1.創建url:http://localhost
             NSURL *url = [NSURL URLWithString:@"http://localhost/2ittt-zm.mp4"];
             self.downloadTask = [self.session downloadTaskWithURL:url];
             // 2、執行下載
             [self.downloadTask resume];
      }
      
      #pragma mark 暫停下載
      -(void)pauseLoadTask{
      
             NSLog(@"暫停下載");
      
             [self.downloadTask cancelByProducingResumeData:^(NSData * _Nullable resumeData) {
      
                 self.resumeData = resumeData;
      
                 //釋放任務
                 self.downloadTask = nil;
             }];
      }
      
      #pragma mark 繼續下載
      -(void)resumeLoadTask{
      
             NSLog(@"繼續下載");
      
            // 防止繼續下載被執行兩次,故下面把self.resumeData賦爲nil
            if (self.resumeData == nil) return;
      
            self.downloadTask = [self.session downloadTaskWithResumeData:self.resumeData];
            self.resumeData = nil;
            [self.downloadTask resume];
      }
      

      提示:斷點續傳其實也就是在暫停下載的時候獲取下載的resumeData,再次接着下載的時候,用resumeData再獲取一個NSURLSessionDownloadTask,從而接着下載

  • 2.7、NSURLSession 強引用 問題

    • (1)、NSURLSession是一個強引用,在下載完成的時候要進行釋放,不管是是否支持不在當前界面下載,當所有的下載任務都完成後,需要進行釋放 session,並賦nil,否則會造成內存泄漏

      /**
         1、下載完成的方法
       */
      -(void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location{
      
          NSLog(@"下載完成");
          [self.session finishTasksAndInvalidate];
          self.session = nil;
      }
      
    • (2)、如果只支持當前界面下載的情況,退出當前界面,取消下載,session 賦nil

      [self.session invalidateAndCancel];
      self.session = nil;
      
  • 2.8、完整代碼請看 JKNSURLSession 中的 Test6ViewController

三、下載管理器 demo地址:JKDownloaderKit

  • 3.1、下載思路

    • (1)、創建另兩個文件夾:JKDownloadCompleted(下載完成的文件夾)和JKDownLoading(下載中的文件夾),在下載中的資源會存在JKDownLoading中,下載完成後會移動到JKDownloadCompleted裏面

    • (2)、先查看服務器上的文件大小

    • (3)、查看本地是否存在文件,如果存在如下

      • 如果文件小於服務器文件的大小,從本地文件長度開始下載(斷點續傳)
      • 如果文件等於服務器文件的大小,再把文件生成一個MD5與服務器對文件返回的MD5做對比,如果一樣,代表下載完成
      • 如果文件大於服務器文件的大小,發生錯誤,直接刪除文件,重新下載
    • (4)、如果本地不存在該文件,直接下載

    • 上傳視頻的思路:
      在上傳視頻的時候,如果視頻斷開了(程序退出了),那麼就要去服務器請求看看自己之前上傳了多少,接着上傳就好,和視頻的下載原理是一樣的,對比
    • 總體思路圖


    • (5)、在監聽下載的方法中,當下載完成後做如下的操作

      • 在沒有 error 的情況下,文件下載是完畢了,但是不一定成功,分析如下
        • 判斷, 本地緩存 == 文件總大小 (如果不相等,說明下載有問題,刪除下載路徑下的文件,重新下載;如果相等在驗證文件內容的MD5值是否一樣,一樣的話纔是真正的下載完成,否則下載是有問題的,刪除下載路徑下的文件,重新下載)
      • 下載-文件完整性驗證機制:驗證文件的合法性, 下載數據是否完整可用
        • 服務器返回文件下載信息的同時, 會返回該文件內容的md5值
        • 本地下載完成後, 可以, 在本地已下載的文件的MD5值和服務器返回的進行對比;
        • 爲了簡單, 有的, 服務器返回的下載文件MD5值, 直接就是命名稱爲對應的文件名稱
  • 3.2、創建一個管理下載的類

    • 命名下載的類爲:JKDownLoader,繼承於 NSObject,定義一個下載的方法

      /**
        定義一個下載的方法
        (1)、先查看服務器上的文件大小
        (2)、查看本地是否存在文件,如果存在如下
      
               2.1、如果文件小於服務器文件的大小,從本地文件長度開始下載(斷點續傳)
               2.2、如果文件等於服務器文件的大小,再把文件生成一個MD5與服務器對文件返回的MD5做對比,如果一樣,代表下載完成
               2.3、如果文件大於服務器文件的大小,發生錯誤,直接刪除文件,重新下載
        (3)、如果本地不存在該文件,直接下載
      
        @param url 下載的url
      */
      -(void)downloadWithUrl:(NSURL *)url{
      
        
      }
      
  • 3.3、從 服務器 獲取下載文件的信息

    • 我們需要設置下載的總大小以及下載後存放的位置

      /**
         文件的總大小
       */
      @property(nonatomic,assign) long long expectdContentLength;
      
      /**
        文件的下載路徑
       */
      @property(nonatomic,strong) NSString *downloadPath;
      
    • 獲取文件信息的私有ipa

      #pragma mark 私有方法
      -(void)selectServerFileInfoWithUrl:(NSURL *)url{
      
          // 1.請求信息:我們只需要獲取頭部信息就好
          NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url cachePolicy:1 timeoutInterval:kTimeOut];
          request.HTTPMethod = @"HEAD";
      
          // 2.建立網絡連接
          NSURLResponse *response = nil;
          [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:NULL];
      
          NSLog(@"%@ %@ %lld",data,response,response.expectedContentLength);
          // 3.記錄服務器的文件信息
          // 3.1、文件長度
          self.expectdContentLength = response.expectedContentLength;
      
          // 3.2、建議保存的文件名字,將在文件保存在tmp,系統會自動回收
          self.downloadPath = [NSTemporaryDirectory() stringByAppendingPathComponent:response.suggestedFilename];
      }
      
      • 提示:這裏採用的是同步方法,因爲我們需要根據文件的信息去操作下面的下載操作,不能使用異步
      • NSURLConnection:默認是在 主線程 進行操作,而NSURLSession 是在 子線程 進行操作
  • 3.4、檢查 本地 下載文件的信息

    /**
       2.從本地檢查要下載的文件信息(除了文件下載完,其他的情況都需要下載文件)
    
       @return YES:需要下載,NO:不需要下載
     */
    -(BOOL)checkLocalFileInfo{
    
          long long fileSize = 0;
          // 1.判斷文件是否存在
          if ([[NSFileManager defaultManager]fileExistsAtPath:self.downloadPath]) {
      
               // 獲取本地存在文件大小
               NSDictionary *attributes = [[NSFileManager defaultManager]attributesOfItemAtPath:self.downloadPath error:NULL];
               NSLog(@"%@",attributes);
               fileSize = [attributes[NSFileSize] longLongValue];
           }
    
           // 2.根據文件大小來判斷文件是否存在
           if(fileSize > self.expectdContentLength){
      
                // 文件異常,刪除該文件
                [[NSFileManager defaultManager]removeItemAtPath:self.downloadPath error:NULL];
                fileSize = 0;
           }else if (fileSize == self.expectdContentLength) {
                // 文件已經下載完
                return NO;
           }
    
           return YES;
    }
    
  • 3.5、文件下載實現

    • (1)、定義一個屬性保存下載的地址 URL

      /**
         視頻的下載地址 URL
       */
      @property(nonatomic,strong) NSURL *downloadUrl;
      
    • (2)、視頻下載從當前的字節開始下載,不管字節是不是0,都是檢查過本地路徑的字節,本地有的話,當前字節就不是0,也就是斷點續傳;沒有的話就是0,也就是從頭開始下載

      • 拓展一個 HTTP 屬性 Range,下載會用到

        Bytes = 0-499  : 從 0 到 499 的 500 個字節
        Bytes = 500-999 : 從500-999的第二個500字節
        Bytes = 500-  : 從500開始到以後所有的字節
        Bytes = -500 最後500個字節
        Bytes = 500-699,800-1299,1600-2000 同時指定多個範圍
        
    • (3)、開始下載,這裏先使用上面 中的 NSURLConncetion放在子線程,開啓Runloop的代理方法來下載,把 NSURLConncetion 放在異步併發的隊列,用文件流拼接寫入路徑,下面只展示部分代碼,完整代碼看 demo

      /* 保存文件的輸出流
         - (void)open; 寫入之前,打開流
         - (void)close; 寫入完畢之後,關閉流
       */
      @property(nonatomic,strong)NSOutputStream *fileStream;
      
      /*
         保存下載線程的運行循環,也就是下載任務的 runloop
       */
      @property(nonatomic,assign)CFRunLoopRef downloadRunloop;
      
      #pragma mark 下載文件
      -(void)downloadFile{
      
           dispatch_async(dispatch_get_global_queue(0, 0), ^{
      
                // 1.建立請求
                NSMutableURLRequest *request = [[NSMutableURLRequest alloc]initWithURL:self.downloadUrl cachePolicy:1 timeoutInterval:MAXFLOAT];
                // 2.設置下載的字節範圍,self.currentLength到之後所有的字節
                NSString *downloadRangeStr = [NSString stringWithFormat:@"bytes=%lld-",self.currentContentLength];
                // 3.設置請求頭字段
                [request setValue:downloadRangeStr forHTTPHeaderField:@"Range"];
                // 4.開始網絡連接
                NSURLConnection *connection = [NSURLConnection connectionWithRequest:request delegate:self];
      
                // 5.設置代理工作的操作 [[NSOperationQueue alloc]init] 默認創建一個異步併發隊列
                [connection setDelegateQueue:[[NSOperationQueue alloc]init]];
                // 啓動連接
                [connection start];
      
                //5.啓動運行循環
                /*
                  CoreFoundation 框架 CFRunLoop
                  CFRunloopStop() 停止指定的runloop
                  CFRunloopGetCurrent() 獲取當前的Runloop
                  CFRunloopRun() 直接啓動當前的運行循環
                */
      
                // (1)、拿到當前的運行循環
                self.downloadRunloop = CFRunLoopGetCurrent();
                // (2)、啓動當前的運行循環
               CFRunLoopRun();
      
          });
      
      }
      
      #pragma mark NSURLConnection的代理NSURLConnectionDataDelegate的方法
      // 1、接收服務器的響應 --- 狀態和響應頭做一些準備工作
      - (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response{
      
           self.fileStream = [[NSOutputStream alloc]initToFileAtPath:self.downloadPath append:YES];
           [self.fileStream open];   
      }
      // 2、接收服務器的數據,由於數據是分塊發送的,這個代理方法會被執行多次,因此我們也會拿到多個data
      - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data{
           // 接收數據,用輸出流拼接,計算下載進度
      
          // 將數據拼接起來,並判斷是否可寫如,一般情況下可寫入,除非磁盤空間不足
          if ([self.fileStream hasSpaceAvailable]) {
      
               [self.fileStream write:data.bytes maxLength:data.length];
          }
      
          // 當前長度拼接
          self.currentContentLength += data.length;
      
          // 計算百分比
          // progress = (float)long long / long long
          float progress = (float)self.currentContentLength/self.expectdContentLength;
      
          // 傳送百分比
          if (self.progress) {
                self.progress(progress);
          }
      
          NSLog(@"%f %@",progress,[NSThread currentThread]);
      
      }
      // 3、接收到所有的數據下載完畢後會有一個通知
      - (void)connectionDidFinishLoading:(NSURLConnection *)connection;
      {
            NSLog(@"下載完畢");
      
            [self.fileStream close];
      
            // 下載完成的回調
            if (self.completion) {
                self.completion(self.downloadPath);
            }
      
           // 關閉當前下載完的 RunLoop
           CFRunLoopStop(self.downloadRunloop);
      }
      
      // 4、下載錯誤的處理
      -(void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error{
      
           NSLog(@"連接失敗:%@",error.localizedDescription);
           // 關閉流
           [self.fileStream close];
      
           // 關閉當前的 RunLoop
           CFRunLoopStop(self.downloadRunloop);
      }
      
  • 3.6、暫停下載操作

    暫停下載操作直接調用:NSURLConnectioncancel 就好

  • 3.7、多文件下載管理

    我們創建一個下載管理器JKDownloaderManger,設置成單利,用來下載多個文件,同時創建下載緩存池,避免多次下載同一個文件

    • (1)、單利的實現(一個靜態變量,三個方法,纔是完整的單利)

      static id instance;
      
      +(instancetype)allocWithZone:(struct _NSZone *)zone{
      
            static dispatch_once_t onceToken;
            dispatch_once(&onceToken, ^{
                  instance = [super allocWithZone:zone];
            });
      
            return instance;
      }
      
      -(id)copy{
      
            return instance;
      }
      
      +(instancetype)shareDownloaderManger{
      
            static dispatch_once_t onceToken;
            dispatch_once(&onceToken, ^{
      
                  instance = [[self alloc]init];
            });
      
            return instance;
      }
      
    • (2)、創建字典緩存池

      @property(nonatomic,strong) NSMutableDictionary *downloadCache;
      
    • (3)、下載方法的實現( 以NSURL的url.path 爲鍵 )

      -(void)jk_downloadWithUrl:(NSURL *)url withDownProgress:(downProgress)progress completion:(downCompletion)completion fail:(downFailed)failed
      {
         // 1.判斷緩存池裏面是否有同一個下載任務
         JKDownLoader *downLoader = self.downloadCache[url.path];
      
         if (downLoader != nil) {
              NSLog(@"已經在下載列表中,請不要重複下載");
              return;
         }
      
         // 2.創建新的下載任務
         downLoader = [[JKDownLoader alloc]init];
      
         // 3.將下載任務添加到緩存池
         [self.downloadCache setValue:downLoader forKey:url.path];
      
          __weak typeof(self) weakSelf = self;
          [downLoader jk_downloadWithUrl:url withDownProgress:progress completion:^(NSString * _Nonnull downFilePath) {
                 // 1.將下載從緩存池移除
                 [weakSelf.downloadCache removeObjectForKey:url.path];
                 // 2.執行調用方法的回調
                 if (completion) {
                       completion(downFilePath);
                 }
          } fail:failed];
      
      }
      
    • (4)、下載暫停:暫停下載,從緩存池移除該url的path

      #pragma mark 暫停某個文件下載
      -(void)pauseloadWithUrl:(NSURL *)url{
      
          // 1.通過url獲取下載任務
          JKDownLoader *downLoader = self.downloadCache[url.path];
      
          // 2.暫停下載
          if (downLoader == nil){
      
                if (self.failed) {
                     self.failed(@"已經暫停下載,請不要重複點擊");
                }
                return;
         }
         [downLoader pause];
      
         // 3.從緩存池移除
         [self.downloadCache removeObjectForKey:url.path];
      }
      

      iOS 下載器完整的代碼請查看demo

到此下載完畢,下一篇會闡述 下載中下載完成 文件夾裏面文件的讀取,敬請關注

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章