iOS 後臺上傳&資料同步

leaveslife發表於2018-02-07

iOS 後臺上傳的處理邏輯,大概和後臺下載的邏輯相差無幾。基本的邏輯是:首先建立一個NSURLSessionConfiguration,然後通過這個configuration,建立一個NSURLSession,接著是建立相關的NSURLSessionTask,最後就是處理相關的回撥方法。有了後臺上傳給資料同步提供方便。

一、建立NSURLSession

建立一個後臺下載的session

- (NSURLSession *)getDownloadURLSession {
    NSString *identifier = [self backgroundSessionIdentifier];
    NSURLSessionConfiguration* sessionConfig = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:identifier];
    NSURLSession *session = [NSURLSession sessionWithConfiguration:sessionConfig
                                            delegate:self
                                       delegateQueue:[NSOperationQueue mainQueue]];
    return session;
}
複製程式碼

獲取後臺下載的標識

- (NSString *)backgroundSessionIdentifier {
    NSString *bundleId = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleIdentifier"];
    NSString *identifier = [NSString stringWithFormat:@"%@.BackgroundSession", bundleId];
    return identifier;
}
複製程式碼

二、建立上傳的NSURLSessionTask

NSURLSessionUploadTask 的父類是NSURLSessionDataTask,它的父類是NSURLSessionTask。在建立上傳的task的時候,有幾個注意點(坑點)。下面介紹總結的三種不同型別的後臺上傳方式。

  1. 直接通過檔案上傳(最簡單) 這種上傳方式是,網路上最容易搜尋到的一種上傳方法,直接拿到檔案的本地路徑,然後進行上傳。其中- (NSURLSessionUploadTask *)uploadTaskWithRequest:(NSURLRequest *)request fromData:(NSData *)bodyData;不支援後臺上傳。
- (void)bgUploadFromFile{
    NSLog(@"%s", __func__);
    NSURL *url = [NSURL URLWithString:kStreamUploadUrl];
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
    [request setHTTPMethod:@"POST"];
    NSString *path = [[NSBundle mainBundle] pathForResource:@"icon.jpg" ofType:nil];
    self.uploadTask = [self.backgroundSession uploadTaskWithRequest:request fromFile:[NSURL fileURLWithPath:path]];
    [self.uploadTask resume];
}
複製程式碼
  1. 通過form檔案流進行上傳 使用這種方式上傳form型別的資料的時候有個坑點,那麼就是必須要給予兩個請求頭:Content-LengthContent-Type,沒有這兩個請求頭,點選是沒有反應的!
- (void)bgUploadStreamForm
{
    NSLog(@"%s", __func__);
    NSURL *url = [NSURL URLWithString:kFormUploadUrl];
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
    [request setHTTPMethod:@"POST"];
    NSString *path = [[NSBundle mainBundle]pathForResource:@"icon" ofType:@"jpg"];
    NSData *bodydata = [self buildBodyDataWithPicPath:path];
    NSString *contentType = [NSString stringWithFormat:@"multipart/form-data; charset=utf-8;boundary=%@",boundary];
    [request setValue:contentType forHTTPHeaderField:@"Content-Type"];
    [request setValue:[NSString stringWithFormat:@"%zd", bodydata.length] forHTTPHeaderField:@"Content-Length"];
    request.HTTPBodyStream = [NSInputStream inputStreamWithData:bodydata];
    self.uploadTask = [self.backgroundSession uploadTaskWithStreamedRequest:request];
    [self.uploadTask resume];
}
複製程式碼

拼接form型別的資料,上傳中需要的額外引數,也可以在form當中拼接,提交給伺服器

-(NSData*)buildBodyDataWithPicPath:(NSString *)path{
    
    NSMutableData *bodyData = [NSMutableData data];
    NSMutableString *bodyStr = [NSMutableString string];
    [bodyStr appendFormat:@"--%@\r\n",boundary];//\n:換行 \n:切換到行首
    [bodyStr appendFormat:@"Content-Disposition: form-data; name=\"sampleFile\"; filename=\"icon.jpg\""];
    [bodyStr appendFormat:@"\r\n\r\n"];
    
    NSData *start = [bodyStr dataUsingEncoding:NSUTF8StringEncoding];
    [bodyData appendData:start];
    NSData *picData = [NSData dataWithContentsOfFile:path];
    [bodyData appendData:picData];

    bodyStr = [NSMutableString string];
    [bodyStr appendFormat:@"\r\n--%@--",boundary];
    
    NSData *endData = [bodyStr dataUsingEncoding:NSUTF8StringEncoding];
    [bodyData appendData:endData];
    return bodyData;
    
}
複製程式碼
  1. 檔案流進行上傳 這種上傳方式對於客戶端來講的話,也是比較方便簡單,效能好的一種方法,但是特殊的地方就是伺服器需要特殊處理,簡單來講就是有點反伺服器的常規。還有請求頭和上面一樣必須要做處理。
- (void)bgUploadStreamFile {
    
    NSLog(@"%s", __func__);
    NSURL *url = [NSURL URLWithString:kStreamUploadUrl];
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
    [request setHTTPMethod:@"POST"];
    NSString *path = [[NSBundle mainBundle]pathForResource:@"icon" ofType:@"jpg"];
    NSFileManager *fileMgr = [NSFileManager defaultManager];
    NSDictionary *fileAttri = [fileMgr attributesOfItemAtPath:path error:nil];
    NSNumber *fileSize = [fileAttri valueForKey:@"NSFileSize"];
    request.HTTPBodyStream = [NSInputStream inputStreamWithFileAtPath:path];
    [request setValue:[NSString stringWithFormat:@"%zd", fileSize.integerValue] forHTTPHeaderField:@"Content-Length"];
    [request setValue:@"application/octet-stream" forHTTPHeaderField:@"Content-Type"];
    self.uploadTask = [self.backgroundSession uploadTaskWithStreamedRequest:request];
    [self.uploadTask resume];
}
複製程式碼
  1. 這裡簡單聊聊分片上傳
  • 首先分片上傳,在上面三種方式中你認為哪一種方式可以了?做過分片開發的同學,應該會裡面反應過來選擇2。是的,在第二種上傳方式中,給予了我們自定義上傳資料的可能。分片上傳與斷點續傳是有區別的。
  • 在上傳和下載中,續傳的過程中,每次暫停後上傳的過程中,我們會帶一個range請求頭,這個range才是上傳實現續傳的關鍵,檔案伺服器通過range的範圍,來給我們開始的資料流,客戶端根據range來拼接本地的快取檔案。
  • 那我們上傳的過程中,如何實現分片上傳呢?斷點續傳的區別又是什麼?假設我們有200MB的檔案,我們單次上傳的時候,檔案塊有點大,那麼我們分片上傳的時候,會將這個檔案進行分塊,假設我們分成10塊,那麼每塊就是20MB;接著對分塊的資料進行上傳,分塊的資料一定要有個分塊的標號,根據切割的開始為標號0,結束為標號9。分別對這10塊資料進行上傳,上傳的時候將標號帶到請求頭或者form引數中。
  • 最後檔案伺服器接收到資料之後,根據標號進行儲存到快取目錄,假設檔名為fasadas_0 ~ fasadas_9,客戶端上傳10個分塊結束之後,呼叫一個檔案合併的介面,然後伺服器檢測分塊檔案,合併成一個檔案,回撥成功。

到這裡其實還有坑,這時候2,3後臺上傳還是不起作用,還是有坑,下面繼續走

三、上傳的代理

  1. 必須實現這個代理,上面的2,3方式的上傳才能夠進行
/* Sent if a task requires a new, unopened body stream.  This may be
 * necessary when authentication has failed for any request that
 * involves a body stream.
 */
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
 needNewBodyStream:(void (^)(NSInputStream * _Nullable bodyStream))completionHandler {
    
    NSInputStream *inputStream = task.originalRequest.HTTPBodyStream ;;
    if (completionHandler) {
        completionHandler(inputStream);
    }
}
複製程式碼
  1. 上傳的進度回撥方法
-(void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didSendBodyData:(int64_t)bytesSent totalBytesSent:(int64_t)totalBytesSent totalBytesExpectedToSend:(int64_t)totalBytesExpectedToSend{
    
    NSLog(@"progress: %f" ,totalBytesSent/(float)totalBytesExpectedToSend);
}
複製程式碼
  1. 首個上傳任務在後臺上傳成功的回撥
/* If an application has received an
 * -application:handleEventsForBackgroundURLSession:completionHandler:
 * message, the session delegate will receive this message to indicate
 * that all messages previously enqueued for this session have been
 * delivered.  At this time it is safe to invoke the previously stored
 * completion handler, or to begin any internal updates that will
 * result in invoking the completion handler.
 */
- (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session {
    NSLog(@"%s", __func__);
}
複製程式碼
  1. AppDelegate中首個後臺上傳任務在後臺完成是的回撥
- (void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)(void))completionHandler {
    NSLog(@"%s", __func__);
}

複製程式碼

這幾個回撥是比較重要的上傳回撥方法,部分和下面類似,細節處理可檢視上傳的回撥處理

四、有了這些東西沒有上傳測試伺服器請看這裡

上傳測試伺服器:github.com/onezens/fil…

demo:github.com/onezens/bac…

相關文章