iOS使用NSURLSession实现后台上传

NSURLSession后台上传的基本逻辑是:首先创建一个后台模式的NSURLSessionConfiguration,然后通过这个configuration创建一个NSURLSession,接着是创建相关的NSURLSessionTask,最后就是处理相关的代理事件。

1、创建NSURLSession

objectivec 复制代码
- (NSURLSession *)backgroundURLSession {
    static NSURLSession *session = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        NSURLSessionConfiguration* sessionConfig = nil;
        NSString *identifier = [NSString stringWithFormat:@"%@.%@", [NSBundle mainBundle].bundleIdentifier, @"HttpUrlManager"];
        sessionConfig = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:identifier];
        //请求的缓存策略
        sessionConfig.requestCachePolicy = NSURLRequestUseProtocolCachePolicy;
        //数据传输超时,当恢复传输时会清零
        sessionConfig.timeoutIntervalForRequest = 60;
        //单条请求超时,决定一条请求的最长生命周期
        sessionConfig.timeoutIntervalForResource = 60;
        //请求的服务类型
        sessionConfig.networkServiceType = NSURLNetworkServiceTypeDefault;
        //是否允许使用移动网络(电话网络)default is YES
        sessionConfig.allowsCellularAccess = YES;
        //后台模式生效,YES允许自适应系统性能调节
        sessionConfig.discretionary = YES;

        session = [NSURLSession sessionWithConfiguration:sessionConfig delegate:self delegateQueue:nil];
    });
    
    return session;
}

NSURLSessionConfiguration配置有三种模式:

objectivec 复制代码
//默认模式类似于原来的NSURLConnection,可以使用缓存的Cache,Cookie,鉴权
+ (NSURLSessionConfiguration *)defaultSessionConfiguration;

//及时模式不使用缓存的Cache,Cookie,鉴权
+ (NSURLSessionConfiguration *)ephemeralSessionConfiguration;

//后台模式在后台完成上传下载,创建Configuration对象的时候需要给一个NSString的ID用于追踪完成工作的Session是哪一个
+ (NSURLSessionConfiguration *)backgroundSessionConfigurationWithIdentifier:(NSString *)identifier

2、后台上传

objectivec 复制代码
- (void)upload:(NSString *)urlStr data:(NSData *)data headers:(NSDictionary *)headers parameters:(NSDictionary *)parameters name:(NSString *)name filename:(NSString *)filename mimeType:(NSString *)mimeType success:(void (^)(id responseObject))success failure:(void (^)(int code, NSString *message))failure {
    NSURL *url = [NSURL URLWithString:urlStr];
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
    request.HTTPMethod = @"POST";
    NSString *string = [NSString stringWithFormat:@"multipart/form-data; charset=utf-8; boundary=%@", kBoundary];
    [request setValue:string forHTTPHeaderField:@"Content-Type"];
    if (headers != nil) {
        for (NSString *key in headers.allKeys) {
            [request setValue:headers[key] forHTTPHeaderField:key];
        }
    }

    NSData *bodyData = [self bodyFormData:data parameters:parameters name:name filename:filename mimeType:mimeType];
    NSString *tempPath = NSTemporaryDirectory();
    NSTimeInterval interval = [NSDate.now timeIntervalSince1970];
    NSString *tempName = [NSString stringWithFormat:@"temp%.0f_%@", interval, filename];
    NSString *tempPath = [tempPath stringByAppendingPathComponent:tempName];
    [bodyData writeToFile:tempPath atomically:YES];

    NSURLSession *session = self.backgroundURLSession;
    NSURLSessionUploadTask *uploadTask = [session uploadTaskWithRequest:request fromFile:[NSURL fileURLWithPath:tempPath]];
    [uploadTask resume];
}

- (NSData *)bodyFormData:(NSData *)data parameters:(NSDictionary *)parameters name:(NSString *)name filename:(NSString *)filename mimeType:(NSString *)mimeType {
    if (data == nil || data.length == 0) {
        return nil;
    }
    NSMutableData *formData = [NSMutableData data];
    NSData *lineData = [@"\r\n" dataUsingEncoding:NSUTF8StringEncoding];
    NSData *boundary = [[NSString stringWithFormat:@"--%@", kBoundary] dataUsingEncoding:NSUTF8StringEncoding];
    
    if (parameters != nil) {
        [parameters enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
            [formData appendData:boundary];
            [formData appendData:lineData];
            NSString *thisFieldString = [NSString stringWithFormat:@"Content-Disposition: form-data; name=\"%@\"\r\n\r\n%@", key, obj];
            [formData appendData:[thisFieldString dataUsingEncoding:NSUTF8StringEncoding]];
            [formData appendData:lineData];
        }];
    }
    
    [formData appendData:boundary];
    [formData appendData:lineData];
    NSString *thisFieldString = [NSString stringWithFormat:@"Content-Disposition: form-data; name=\"%@\"; filename=\"%@\"\r\nContent-Type: %@", name, filename, mimeType];
    [formData appendData:[thisFieldString dataUsingEncoding:NSUTF8StringEncoding]];
    [formData appendData:lineData];
    [formData appendData:lineData];
    
    [formData appendData:data];
    [formData appendData:lineData];
    [formData appendData:[[NSString stringWithFormat:@"--%@--\r\n", kBoundary] dataUsingEncoding:NSUTF8StringEncoding]];
    
    return formData;
}

上传有4种方法:

objectivec 复制代码
/* Creates an upload task with the given request.  The body of the request will be created from the file referenced by fileURL */
- (NSURLSessionUploadTask *)uploadTaskWithRequest:(NSURLRequest *)request fromFile:(NSURL *)fileURL;

/* Creates an upload task with the given request.  The body of the request is provided from the bodyData. */
- (NSURLSessionUploadTask *)uploadTaskWithRequest:(NSURLRequest *)request fromData:(NSData *)bodyData;

- (NSURLSessionUploadTask *)uploadTaskWithRequest:(NSURLRequest *)request fromFile:(NSURL *)fileURL completionHandler:(void (^)(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error))completionHandler;

- (NSURLSessionUploadTask *)uploadTaskWithRequest:(NSURLRequest *)request fromData:(nullable NSData *)bodyData completionHandler:(void (^)(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error))completionHandler;

后台模式不支持使用带回调的上传方法,否则会报错:

Completion handler blocks are not supported in background sessions. Use a delegate instead.

后台模式不支持使用NSData的上传方法,否则会报错:

Upload tasks from NSData are not supported in background sessions

所以如果使用后台模式上传,选择uploadTaskWithRequest:(NSURLRequest *)request fromFile:(NSURL *)fileURL方法。

NSURLSessionDataDelegate上传代理事件

objectivec 复制代码
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didSendBodyData:(int64_t)bytesSent totalBytesSent:(int64_t)totalBytesSent totalBytesExpectedToSend:(int64_t)totalBytesExpectedToSend {
    NSLog(@"URLSession didSendBodyData progress: %f" ,totalBytesSent/(float)totalBytesExpectedToSend);
}

- (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session {
    NSLog(@"%s", __func__);
}

- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
    NSMutableData *responseData = self.responsesData[@(dataTask.taskIdentifier)];
    if (!responseData) {
        responseData = [NSMutableData dataWithData:data];
        self.responsesData[@(dataTask.taskIdentifier)] = responseData;
    } else {
        [responseData appendData:data];
    }
}

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
    if (error) {
        NSLog(@"URLSession didCompleteWithError %@ failed: %@", task.originalRequest.URL, error);
    }
    NSMutableData *responseData = self.responsesData[@(task.taskIdentifier)];
    if (responseData) {
        NSDictionary *response = [NSJSONSerialization JSONObjectWithData:responseData options:0 error:nil];
        if (response) {
            NSLog(@"response = %@", response);
        } else {
            NSString *errMsg = [[NSString alloc] initWithData:responseData encoding:NSUTF8StringEncoding];
            NSLog(@"responseData = %@", errMsg);
        }
        [self.responsesData removeObjectForKey:@(task.taskIdentifier)];
    } else {
        NSLog(@"responseData is nil");
    }
}

3、后台请求

objectivec 复制代码
- (void)request:(NSString *)urlStr method:(NSString *)method headers:(NSDictionary *)headers parameters:(NSDictionary *)parameters success:(void (^)(id responseObject))success failure:(void (^)(int code, NSString *message))failure {
    urlStr = [self getFullUrlString:urlStr parameters:parameters];
    NSURL *url = [NSURL URLWithString:urlStr];
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
    request.HTTPMethod = method;
    if (headers != nil) {
        for (NSString *key in headers.allKeys) {
            [request setValue:headers[key] forHTTPHeaderField:key];
        }
    }

    NSURLSession *session = self.backgroundURLSession;
    NSURLSessionDataTask *task = [session dataTaskWithRequest:request];
    [task resume];
}

- (NSString *)getFullUrlString:(NSString *)urlStr parameters:(NSDictionary *)parameters {
    NSMutableString *newStr = [NSMutableString stringWithString:urlStr];
    if (parameters.allKeys.count > 0) {
        BOOL isFirst = NO;
        for (NSString *key in parameters) {
            isFirst = YES;
            [newStr appendString:isFirst?@"?":@"&"];
            [newStr appendFormat:@"%@=%@", key, parameters[key]];
        }
    }
    return newStr;
}

4、Session和ApplicationDelegate交互

使用BackgroundSession后台模式,在Task执行的时候,当用户切到后台时,Session会和ApplicationDelegate做交互,在BackgroundSession中的Task还会继续下载/上传。

现在分三个场景分析下Session和Application的关系:

(1)当加入了多个Task,程序没有切换到后台。

这种情况Task会按照NSURLSessionConfiguration的设置正常下载,不会和ApplicationDelegate有交互。

(2)当加入了多个Task,程序切到后台,所有Task都完成下载

在切到后台之后,Session的Delegate不会再收到,Task相关的消息,直到所有Task全都完成后,系统会调用ApplicationDelegate的application:handleEventsForBackgroundURLSession:completionHandler:回调,之后"汇报"下载工作,对于每一个后台下载的Task调用Session的Delegate中的URLSession:downloadTask:didFinishDownloadingToURL:(成功的话)和URLSession:task:didCompleteWithError:(成功或者失败都会调用)。

AppDelegate:

objectivec 复制代码
@property (copy, nonatomic) void(^backgroundSessionCompletionHandler)();

- (void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier 
  completionHandler:(void (^)())completionHandler {
    self.backgroundSessionCompletionHandler = completionHandler;
}

Session的Delegate

objectivec 复制代码
@interface MyViewController()<NSURLSessionDelegate>
@end

@implementation MyViewController

- (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session {
    AppDelegate *appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
    if (appDelegate.backgroundSessionCompletionHandler) {
        void (^completionHandler)() = appDelegate.backgroundSessionCompletionHandler;
        appDelegate.backgroundSessionCompletionHandler = nil;
        completionHandler();
    }
    NSLog(@"All tasks are finished");
}

@end

(3)当加入了多个Task,程序切到后台,下载完成了几个Task,然后用户又切换到前台。(程序没有退出)

切到后台之后,Session的Delegate仍然收不到消息。在下载完成几个Task之后再切换到前台,系统会先汇报已经下载完成的Task的情况,然后继续下载没有下载完成的Task,后面的过程同第一种情况。

(4)当加入了多个Task,程序切到后台,几个Task已经完成,但还有Task还没有下载完的时候关掉强制退出程序,然后再进入程序的时候。(程序退出了)

由于程序已经退出了,后面没有下完Session就不在了后面的Task肯定是失败了。但是已经下载成功的那些Task,新启动的程序也没有听"汇报"的机会了。经过实验发现,这个时候之前在NSURLSessionConfiguration设置的NSString类型的ID起作用了,当ID相同的时候,一旦生成Session对象并设置Delegate,马上可以收到上一次关闭程序之前没有汇报工作的Task的结束情况(成功或者失败)。

相关推荐
黑化旺仔15 小时前
【OC】Runtime:类元数据、加载初始化与消息派发
开发语言·macos·ios·objective-c
songgeb15 小时前
iOS App支持设置RTL语言的实践
ios·swift·ui kit
软泡芙16 小时前
【iOS】swift启动屏显示黑屏
ios
用户693717500138418 小时前
当你向领导申请 AI 编程工具预算时,如果领导追问“能提效多少,有量化数据吗”,你该怎么回答?
android·后端·ios
Layer18 小时前
一些脱离 Mac 依赖的 Agent 操作 iPhone 实现
ios·ai编程·测试
用户693717500138419 小时前
当我说 Claude 搭配 GLM-5.3-Flash 太好用了,同事却提醒我:你可能正在失去对代码的掌控
android·后端·ios
茶底世界之下20 小时前
一键裁方会丢掉什么?用 Awaking 给园林照片做两种画幅
ios
Marco_Marco13521 小时前
定位SDK接入实战:Android/iOS/鸿蒙三端怎么选、怎么接
android·ios·harmonyos
老李IT笔记1 天前
关掉设备上的 AI 扩展:两个键各管一层,配错了等于没配
人工智能·ios·智能手机
2501_915921431 天前
iOS应用耗电测试:使用Energy Log、Keymob和Instruments进行性能监控与能源消耗分析
android·ios·小程序·https·uni-app·iphone·webview