由于iOS系统对于后台执行任务管控比较严格,如果app没有任务继续执行,那么app在进入后台一段时间后会被系统杀死。如果下载大文件的话,需要考虑如何在app进入后台后继续下载。

app保活策略

   app保活是指app进入后台后,通过静默的重复执行某个后台允许的任务,保证App不被系统杀死。例如:后台播放没有声音的音频。app保活应用比较广泛,不仅仅可以实现后台下载大文件。其他的应用场景:比如用户通过一定路径进入某个比较重要的页面,但这个页面操作可能需要切换app等,为了让用户下载进入app的时候仍然停留在这个页面。那么需要通过开启保活策略,同时可以根据业务需求设置,一直进行保活,保活一段时间后,关闭掉保活策略。

使用系统提供的后台下载策略

  使用系统提供了NSURLDownloadTask 可以实现后台下载大文件的需求。原理:app创建允许后台下载的session,并注册到操作系统中。此时下载的操作会交给操作系统处理。当文件下载好后,会返回下载好的默认路径。进入后台后,只要app不被用户主动杀死,那么这个注册的session里的下载任务会继续执行。等到session上的任务下载完成后,会唤醒app执行一些简单的操作。具体大家可以参考网址:《iOS 原生级别后台下载详解》 下载任务的创建需要经历几个阶段。

下载任务第一次创建

  如果一个文件第一次创建下载需求,那么不需要考虑太多,直接创建downloadTask就好了。

离开创建下载页面后,再次进入下载页面(此时app未被系统杀死)

  如果全局持有task,那么可以根据保存的task恢复原来的下载逻辑,以及进度信息,不用重新创建task。最大程度节省恢复下载的成本。由于我参考了YTKNetwork的实现逻辑(全局持有正在执行的request对象),因此恢复起来不需要直接比对task了。具体代码如下:

JKDownloadRequest *request = nil;
    NSArray *allRequests = [[[JKNetworkAgent sharedAgent] allRequests] copy];
    for (JKBaseRequest *baseRequest in allRequests) {
        if ([baseRequest isKindOfClass:[JKDownloadRequest class]]) {
            JKDownloadRequest *downloadRequest = (JKDownloadRequest *)baseRequest;
            if ([downloadRequest.absoluteString isEqualToString:url]
                && downloadRequest.backgroundPolicy == backgroundPolicy
                && [downloadRequest.downloadedFilePath isEqualToString:downloadedPath]) {
                request = downloadRequest;
            }
        }
    }
app被系统杀死后,再次进入下载页面或者再次下载之前已经创建过的下载任务

  当App重新启动时,之前全局保存的task,已经不存在了,需要调用相关方法重新从操作系统中获取上次正在进行的未完成的task,并全局持有。然后根据需要执行相应的恢复逻辑。具体代码如下:

NSURLSessionTask *task = [[JKNetworkAgent sharedAgent] lastExcutingBackgroundTaskOfURLString:url];// 通过和从操作系统获取到的app启动前正在执行的tasks,让后根据url匹配到到对应的task,执行响应的恢复逻辑。
        if (task) {
            request.requestTask = task;
            request = [self initWithUrl:url];
            request.downloadedFilePath = downloadedPath;
            request.backgroundPolicy = backgroundPolicy;
            request.isRecoveredFromSystem = YES;
        }

对于后台任务的session上的task执行完以后,此时操作系统会唤醒app,我这边将逻辑进行了封装,大家可以监听如下的通知,然后在通知响应的方法中执行相应的操作。具体示例如下:

- (instancetype)init
{
    self = [super init];
    if (self) {
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleBackgroundDownloadingTasksComplete:) name:JKBackgroundTaskCompleteAndInvokeAppNotification object:nil];
    }
    return self;
}

- (void)handleBackgroundDownloadingTasksComplete:(NSNotification *)notification
{
    if (![notification.object isKindOfClass:[NSURLSessionDownloadTask class]]) {
        return;
    }
    NSURLSessionDownloadTask *task = (NSURLSessionDownloadTask *)notification.object;
    NSArray<JKDownloadFileModel *>* models = [JKDownloadFileModel allDownloadingFiles];
    for (JKDownloadFileModel *model in models) {
        NSString *urlString = task.originalRequest.URL.absoluteString;
        if ([urlString isEqualToString:model.fileURLString]) {
            [JKDownloadFileModel deleteObjectFromDownloading:model];
            model.downloadedSize = model.fileSize;
            [JKDownloadFileModel insertObjectToDownloaded:model];
        }
    }
    
}

备注:收到这个通知的时候,app并未正常执行didFinishLaunching逻辑,如果这里的逻辑需要用到一些正常启动时创建的对象,那么需要在这里提前创建好单独处理。 另外这个地方由于涉及到反复的启动,函数的执行逻辑判断需要多次的启动才可以判断。我这里通过打印日志的形式来实现的。这里用到了自己写的一个日志打印库JKLogHelper,可以很方便通过打印日志来调试程