iOS后台上传

iOS 后台上传的处理逻辑,大概和后台下载的逻辑相差无几。基本的逻辑是:首先创建一个NSURLSessionConfiguration,然后通过这个configuration,创建一个NSURLSession,接着是创建相关的NSURLSessionTask,最后就是处理相关的回调方法。

创建NSURLSession

创建一个后台下载的session

1
2
3
4
5
6
7
8
- (NSURLSession *)getDownloadURLSession {
NSString *identifier = [self backgroundSessionIdentifier];
NSURLSessionConfiguration* sessionConfig = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:identifier];
NSURLSession *session = [NSURLSession sessionWithConfiguration:sessionConfig
delegate:self
delegateQueue:[NSOperationQueue mainQueue]];
return session;
}

获取后台下载的标识

1
2
3
4
5
- (NSString *)backgroundSessionIdentifier {
NSString *bundleId = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleIdentifier"];
NSString *identifier = [NSString stringWithFormat:@"%@.BackgroundSession", bundleId];
return identifier;
}

创建上传的NSURLSessionTask

NSURLSessionUploadTask 的父类是NSURLSessionDataTask,它的父类是NSURLSessionTask。在创建上传的task的时候,有几个注意点(坑点)。下面介绍总结的三种不同类型的后台上传方式。

直接通过文件上传(最简单)
这种上传方式是,网络上最容易搜索到的一种上传方法,直接拿到文件的本地路径,然后进行上传。其中- (NSURLSessionUploadTask *)uploadTaskWithRequest:(NSURLRequest *)request fromData:(NSData *)bodyData;不支持后台上传。

1
2
3
4
5
6
7
8
9
- (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];
}

通过form文件流进行上传
使用这种方式上传form类型的数据的时候有个坑点,那么就是必须要给予两个请求头:Content-LengthContent-Type,没有这两个请求头,点击是没有反应的!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- (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当中拼接,提交给服务器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
-(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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- (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];
}

这里简单聊聊分片上传

  • 首先分片上传,在上面三种方式中你认为哪一种方式可以了?做过分片开发的同学,应该会里面反应过来选择2。是的,在第二种上传方式中,给予了我们自定义上传数据的可能。分片上传与断点续传是有区别的。
  • 在上传和下载中,续传的过程中,每次暂停后上传的过程中,我们会带一个range请求头,这个range才是上传实现续传的关键,文件服务器通过range的范围,来给我们开始的数据流,客户端根据range来拼接本地的缓存文件。
  • 那我们上传的过程中,如何实现分片上传呢?断点续传的区别又是什么?假设我们有200MB的文件,我们单次上传的时候,文件块有点大,那么我们分片上传的时候,会将这个文件进行分块,假设我们分成10块,那么每块就是20MB;接着对分块的数据进行上传,分块的数据一定要有个分块的标号,根据切割的开始为标号0,结束为标号9。分别对这10块数据进行上传,上传的时候将标号带到请求头或者form参数中。
  • 最后文件服务器接收到数据之后,根据标号进行存储到缓存目录,假设文件名为fasadas_0 ~ fasadas_9,客户端上传10个分块结束之后,调用一个文件合并的接口,然后服务器检测分块文件,合并成一个文件,回调成功。

到这里其实还有坑,这时候2,3后台上传还是不起作用,还是有坑,下面继续走

上传的代理

必须实现这个代理,上面的2,3方式的上传才能够进行

1
2
3
4
5
6
7
8
9

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
needNewBodyStream:(void (^)(NSInputStream * _Nullable bodyStream))completionHandler {

NSInputStream *inputStream = task.originalRequest.HTTPBodyStream ;;
if (completionHandler) {
completionHandler(inputStream);
}
}

上传的进度回调方法

1
2
3
4
-(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
2
3
- (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session {
NSLog(@"%s", __func__);
}

AppDelegate中首个后台上传任务在后台完成是的回调

1
2
3
- (void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)(void))completionHandler {
NSLog(@"%s", __func__);
}

这几个回调是比较重要的上传回调方法,部分和下面类似,细节处理可查看上传的回调处理

有了这些东西没有上传测试服务器请看这里

node.js简单的上传测试服务器代码:https://github.com/onezens/fileUploadServer
demo:https://github.com/onezens/backgroundUploadDemo