2016-07-17 14:46:30 ws1352864983 阅读数 1271

一般来说,如果不进行后台申请,在iOS系统上,当应用退到后台后,只有5s的时间去执行代码,之后将进入挂起状态。只有像音频播放、定位、newsstand、VoIP等功能才能持续在后台运行。但是开发其它应用是我们可以通过申请后台,来获得3分钟的后台执行代码时间(iOS7以前是10分钟)。

最近,我搜集了一些关于iOS程序后台运行的方法,在此整理一下。本篇文章,我会贴出,后台运行3分钟和无限后台的方法。

在此之前,你得了解iOS应用程序的生命周期:

  • Not running 未运行 程序没启动

  • Inactive 未激活 程序在前台运行,不过没有接收到事件。在没有事件处理情况下程序通常停留在这个状态

  • Active 激活 程序在前台运行而且接收到了事件。这也是前台的一个正常的模式

  • Backgroud 后台 程序在后台而且能执行代码,大多数程序进入这个状态后会在在这个状态上停留一会。时间到之后会进入挂起状态

  • (Suspended)。有的程序经过特殊的请求后可以长期处于Backgroud状态

  • Suspended 挂起 程序在后台不能执行代码。系统会自动把程序变成这个状态而且不会发出通知。当挂起时,程序还是停留在内存中的,当系统内存低时,系统就把挂起的程序清除掉,为前台程序提供更多的内存。

iOS应用程序生命周期(前后台切换,应用的各种状态)详解

一般App进入后台之后,超过了后台运行时间,便进入了挂起状态,无法执行代码,但是内存并没有清除。

  • 主要用到2个方法:

    • beginBackgroundTaskWithExpirationHandler://申请后台,该方法只有在App处于激活时调用才有效。

    • endBackgroundTask://注销后台

后台执行3分钟方法:

#import "AppDelegate.h"

@interface AppDelegate (){
    NSInteger count;
}
@property(strong, nonatomic)NSTimer *mTimer;
@property(assign, nonatomic)UIBackgroundTaskIdentifier backIden;

@end

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // Override point for customization after application launch.

    count=0;
    return YES;
}

- (void)applicationDidEnterBackground:(UIApplication *)application {
    _mTimer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(countAction) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:_mTimer forMode:NSRunLoopCommonModes];
    [self beginTask];
}

- (void)applicationWillEnterForeground:(UIApplication *)application {
    NSLog(@"进入前台");
    [self endBack];
}

//计时
-(void)countAction{
    NSLog(@"%li",count++);
}

//申请后台
-(void)beginTask
{
    NSLog(@"begin=============");
    _backIden = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
        //在时间到之前会进入这个block,一般是iOS7及以上是3分钟。按照规范,在这里要手动结束后台,你不写也是会结束的(据说会crash)
        NSLog(@"将要挂起=============");
        [self endBack];
    }];
}

//注销后台
-(void)endBack
{
    NSLog(@"end=============");
    [[UIApplication sharedApplication] endBackgroundTask:_backIden];
    _backIden = UIBackgroundTaskInvalid;
}

@end

无限后台的方法,慎用!因为这个需要申请后台播放音频的权限。如果你的应用不是相关应用,AppStore审核可能不会通过。

好了,看方法!

  • 先在info.plist文件里添加如此一条记录:

  • AppDelegate.m文件

#import "AppDelegate.h"

@interface AppDelegate (){
    NSInteger count;
}
@property(strong, nonatomic)NSTimer *mTimer;
@property(assign, nonatomic)UIBackgroundTaskIdentifier backIden;

@end

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // Override point for customization after application launch.

    count=0;
    return YES;
}

- (void)applicationDidEnterBackground:(UIApplication *)application {
    _mTimer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(countAction) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:_mTimer forMode:NSRunLoopCommonModes];
    [self beginTask];
}

- (void)applicationWillEnterForeground:(UIApplication *)application {
    NSLog(@"进入前台");
    [self endBack];
}

//计时
-(void)countAction{
    NSLog(@"%li",count++);
}

//申请后台
-(void)beginTask
{
    NSLog(@"begin=============");
    _backIden = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{

        NSLog(@"将要挂起=============");
        [self endBack];
    }];
}

//注销后台
-(void)endBack
{
    NSLog(@"end=============");
    [[UIApplication sharedApplication] endBackgroundTask:_backIden];
    _backIden = UIBackgroundTaskInvalid;
}

@end
ViewController.m文件

#import "ViewController.h"
#import <AVFoundation/AVFoundation.h>

@interface ViewController ()

@property(strong, nonatomic)AVAudioPlayer *mPlayer;

@property(assign, nonatomic)CGFloat mCount;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.

    _mCount = 0;

    NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:10 target:self selector:@selector(countTime) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
}

-(void)countTime{
    _mCount+=10;
    NSLog(@"%f",_mCount);

    if ([[UIApplication sharedApplication] backgroundTimeRemaining] < 60.) {//当剩余时间小于60时,开如播放音乐,并用这个假前台状态再次申请后台
        NSLog(@"播放%@",[NSThread currentThread]);
        [self playMusic];
        //申请后台
        [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
            NSLog(@"我要挂起了");
        }];
    }
}

-(void)playMusic{
    //1.音频文件的url路径,实际开发中,用无声音乐
    NSURL *url=[[NSBundle mainBundle]URLForResource:@"欢沁.mp3" withExtension:Nil];

    //2.创建播放器(注意:一个AVAudioPlayer只能播放一个url)
    _mPlayer=[[AVAudioPlayer alloc]initWithContentsOfURL:url error:Nil];

    //3.缓冲
    [_mPlayer prepareToPlay];

    //4.播放
    [_mPlayer play];
}

@end

转自:文/此心不改(简书作者)
原文链接:坐飞机

2016-02-01 11:36:30 u010475222 阅读数 157

iOS后台任务

ios后台运行分方式有三种

  • 1 永远运行
  • 2 短暂运行
  • 3 立即挂起

后台永远运行分为这几种情况

  • 播放音频文件(playing audio
  • 获取定位更新(getting location updates)

  • 杂志app中下载新的期刊(downloading new issues for newsstand apps)

  • VoIP 呼叫(handing VoIP calls)

实现方式

  • 1 适配Info.plist 文件

    • info.plist中的 Required background modes key 类型为 NSArray
    • 在其中添加Value 为App registers for location updates 设定后台定位
    • 在其中添加Valur 为 App plays audio or streams audio/video using AirPlay 设定后台音乐播放
  • 2 如果是定位,申请定位权限。 播放音乐申请后台播放权限。具体看具体代码


后台短暂运行

App 进入后台默认情况会立即挂起,不会再运行情况。 后台短暂运行App 进入后台,系统会让其在运行一段时间在挂起。

实现方式

// 接收进入后台通知
        [[NSNotificationCenter defaultCenter] addObserver:self
                                                 selector:@selector(backgroundCleanDisk)
                                                     name:UIApplicationDidEnterBackgroundNotification
                                                   object:nil];
// 后台处理
- (void)backgroundCleanDisk {

      UIApplication*  app = [UIApplication sharedApplication];
    __block UIBackgroundTaskIdentifier bgTask;

     bgTask = [app beginBackgroundTaskWithExpirationHandler:^{

        [app endBackgroundTask:bgTask];s
        bgTask = UIBackgroundTaskInvalid;

    }];   
}

2016-03-02 16:24:38 hherima 阅读数 2373

iOS 通常是不能在后台运行的,尤其是用户点击锁屏键,APP进入后台,网络立马断开等。如何解决这个问题呢?在APP进入后台,APP怎么争取一些时间来“善后”。代码如下:注:需要定义一个属性 UIBackgroundTaskIdentifier _bgTask;该代码可以自定义后台多长时间自动结束任务。

- (void) timerMethod:(NSTimer *)paramSender
{
    /*这里处理后台需要的逻辑,不可太长*/
    
}
- (void)applicationDidEnterBackground:(UIApplication *)application
{
    UIDevice * device = [UIDevice currentDevice];
    if([device respondsToSelector:@selector(isMultitaskingSupported)] && [device isMultitaskingSupported])
    {
        self.pushTimer =  [NSTimer scheduledTimerWithTimeInterval:30.0f target:self selector:@selector(timerMethod:) userInfo:nil repeats:YES];
        //向iOS系统,借用10分钟(默认就是10分钟)时间。当调用beginBackgroundTaskWithExpirationHandler: 记得必须调用endBackgroundTask:方法,否则iOS会终止你的程序.
        _bgTask = [application beginBackgroundTaskWithExpirationHandler:^
                   {
                       NSLog(@"后台10分钟运行完成,APP进程即将被挂起");
                       if(_pushTimer!=nil)
                       {
                           [_pushTimer invalidate];
                       }
                       [application endBackgroundTask:_bgTask];
                       _bgTask = UIBackgroundTaskInvalid;
                   }];
        //如果想提前结束10分钟的后台运行,可在下面加逻辑,目前是空转.
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            NSInteger remaining = [application backgroundTimeRemaining];
            NSLog(@"remain %d S", remaining);
            while (remaining > 30 && _bgTask != UIBackgroundTaskInvalid) {
                sleep(15);
                remaining = [application backgroundTimeRemaining];
                NSLog(@"remain %d S", remaining);//iOS 7就只有180秒,但是超过这个时间程序依然可以运行
                //                if (remaining<=180) {//如果想提前结束10分钟的后台运行,打开这个if
                //                [application endBackgroundTask:_bgTask];
                //                _bgTask = UIBackgroundTaskInvalid;
                //                }
            }
            NSLog(@"background thread finished");
        });
    }
}
2017-09-05 17:00:44 dashenid 阅读数 1987

想了解,多文件下载和管理,看这一篇文章,是在这基础上再次封装的:点击打开链接

使用NSURLSessionDataTask,进行封装下载的,看这篇文章 点击打开链接

使用:

#import "ViewController.h"
#import "BackgroundDownloadTool.h"

@interface ViewController ()<BackgroundDownloadToolDelegate> {
    BackgroundDownloadTool *tool;
}
@property (weak, nonatomic) IBOutlet UILabel *label;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    tool = [BackgroundDownloadTool new];
    tool.delegate = self;
    [tool initTask];
}

-(void)BackgroundDownloadToolCallbackProgress:(double)progress Error:(NSError *)error {
    NSLog(@"%f",progress);
    self.label.text = [NSString stringWithFormat:@"%f",progress];
}

- (IBAction)start:(id)sender {
    [tool startOrContinueDownload];
}

- (IBAction)stop:(id)sender {
    [tool pauseDownload];
}


主要代码:

AppDelegate.m

#import "AppDelegate.h"
#import "BackgroundDownloadTool.h"

@interface AppDelegate ()

@end

@implementation AppDelegate


- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // Override point for customization after application launch.
    return YES;
}

-(void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)())completionHandler {
    [BackgroundDownloadTool sharedInstance].backgroundSessionCompletionHandler = completionHandler;
}

BackgroundDownloadTool.h

#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>

@protocol BackgroundDownloadToolDelegate<NSObject>
@optional
-(void)BackgroundDownloadToolCallbackProgress:(double)progress Error:(NSError *)error identifier:(NSString *)identifier;
-(void)BackgroundDownloadToolDownloadingFinish:(NSString *)path identifier:(NSString *)identifier;
@end

@interface BackgroundDownloadTool : NSObject
@property(nonatomic,strong)NSString *identifier;//唯一SessionConfiguration
@property(nonatomic,strong)NSString *fileName;//保存的文件名
@property(nonatomic,strong)NSString *urlStr;

@property (nonatomic, weak) IBOutlet id<BackgroundDownloadToolDelegate> delegate;
@property (strong, nonatomic)NSData *resumeData;//已下载的数据
@property (nonatomic, copy) void (^backgroundSessionCompletionHandler)();

- (void)startOrContinueDownload;//开始或继续下载
- (void)pauseDownload;//暂停下载
-(void)cancelDownload;//取消下载

-(void)removeDownloadFile;//移除已下载好的文件
-(void)removeCacheDownloadFile;//移除缓存的文件

- (void)downloadProgress:(void (^)(CGFloat progress, NSError *err))downloadProgressBlock complement:(void (^)(NSString *path))completeBlock;

@end


BackgroundDownloadTool.m

#import "BackgroundDownloadTool.h"
#import "NSURLSession+TYCorrectedResumeData.h"

#define IS_IOS10ORLATER ([[[UIDevice currentDevice] systemVersion] floatValue] >= 10)
#define IS_IOS8ORLATER ([[[UIDevice currentDevice] systemVersion] floatValue] >= 8)

@interface BackgroundDownloadTool ()<NSURLSessionDelegate, NSURLSessionTaskDelegate,NSURLSessionDownloadDelegate> {
    NSString *tmpPath;
    BOOL _isDownloadStateCompleted;
    int64_t allSize;
    BOOL _isStart;
}

@property (nonatomic) NSURLSession *session;
@property (nonatomic) NSURLSessionDownloadTask *downloadTask;
@property (nonatomic,strong) NSString *DownloadPath;
@property (nonatomic,strong) NSString *CachePath;
@property (nonatomic,strong) NSString *Location;
@property (nonatomic,strong) NSFileManager *manage;
@property (strong, nonatomic) NSOperationQueue *queue;

@property(nonatomic,copy)void (^downloadProgressBlock)(CGFloat progress, NSError *err);
@property(nonatomic,copy)void (^completeBlock)(NSString *path);

@end

@implementation BackgroundDownloadTool
-(NSString *)Location {
    if (!_Location) {
        _Location =[[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory , NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"BackgroundDownload"];
        [self.manage createDirectoryAtPath:_Location withIntermediateDirectories:YES attributes:nil error:nil];
    }
    return _Location;
}

- (NSString *)DownloadPath {
    if (!_DownloadPath) {
        _DownloadPath =[self.Location stringByAppendingPathComponent:self.fileName];
    }
    return _DownloadPath;
}

- (NSString *)CachePath {
    if (!_CachePath) {
        _CachePath =[self.Location stringByAppendingPathComponent:[NSString stringWithFormat:@"cashe%@",self.fileName]];
    }
    return _CachePath;
}

//初始化session
-(NSURLSession *)session {
    if (!_session) {
        if (_identifier) {
            if (IS_IOS8ORLATER) {
                NSURLSessionConfiguration *configure = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:_identifier];
                _session = [NSURLSession sessionWithConfiguration:configure delegate:self delegateQueue:self.queue];
            }else{
                _session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration backgroundSessionConfiguration:_identifier] delegate:self delegateQueue:self.queue];
            }
        }else {
            _session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:self delegateQueue:self.queue];
        }
    }
    return _session;
}

- (NSOperationQueue *)queue {
    if (!_queue) {
        _queue = [[NSOperationQueue alloc]init];
        _queue.maxConcurrentOperationCount = 1;
    }
    return _queue;
}

- (NSFileManager *)manage{
    if (!_manage){
        _manage = [NSFileManager defaultManager];
    }
    return _manage;
}

-(instancetype)init{
    if (self = [super init]) {
        self.identifier = @"BackgroundSession.tmp";
        self.urlStr = @"http://baobab.wdjcdn.com/1456317490140jiyiyuetai_x264.mp4";
        self.fileName = @"test.mp4";
    }
    return self;
}

+(BackgroundDownloadTool *)sharedInstance {
    static dispatch_once_t pred = 0;
    __strong static id internet = nil;
    dispatch_once(&pred, ^{
        internet = [[self alloc] init];
    });
    return internet;
}

- (void)downloadProgress:(void (^)(CGFloat progress, NSError *err))downloadProgressBlock complement:(void (^)(NSString *path))completeBlock {
    self.downloadProgressBlock = downloadProgressBlock;
    self.completeBlock = completeBlock;
}

-(void)initTask {
    NSURL *downloadURL = [NSURL URLWithString:self.urlStr];
    NSURLRequest *request = [NSURLRequest requestWithURL:downloadURL];
    self.downloadTask = [self.session downloadTaskWithRequest:request];
}

-(NSInteger)AlreadyDownloadLength {
    return [[[NSFileManager defaultManager]attributesOfItemAtPath:self.DownloadPath error:nil][NSFileSize] integerValue];
}

// 是否已经下载
- (BOOL)isDownloadCompletedWithDownload {
    return [self.manage fileExistsAtPath:self.DownloadPath];
}

-(void)startOrContinueDownload {
    if (_isStart) {
        return;
    }
    if (![self isDownloadCompletedWithDownload]) {
        [self initTask];
        self.resumeData = [NSData dataWithContentsOfFile:self.CachePath];
        if (self.resumeData) {
            if (IS_IOS10ORLATER) {
                self.downloadTask = [self.session downloadTaskWithCorrectResumeData:self.resumeData];
            } else {
                self.downloadTask = [self.session downloadTaskWithResumeData:self.resumeData];
            }
        }
        _isStart = YES;
        [self.downloadTask resume];
    }else {
        NSLog(@"已经下载完成");
        !self.completeBlock?:self.completeBlock(self.DownloadPath);

        if ([self.delegate respondsToSelector:@selector(BackgroundDownloadToolDownloadingFinish:identifier:)]) {
            [self.delegate BackgroundDownloadToolDownloadingFinish:self.DownloadPath identifier:self.identifier];
        }
    }
}

-(void)cancelDownload {
    _isStart = NO;
    [self.downloadTask suspend];
    [self.downloadTask cancel];
    self.downloadTask = nil;
    [self removeCacheDownloadFile];
}

- (void)pauseDownload {
    _isStart = NO;
    if (![self isDownloadCompletedWithDownload]) {
        __weak __typeof(self) wSelf = self;
        [self.downloadTask cancelByProducingResumeData:^(NSData * resumeData) {
            __strong __typeof(wSelf) sSelf = wSelf;
            sSelf.resumeData = resumeData;
            [sSelf saveData:resumeData];
        }];
    }else {
        NSLog(@"已经下载完成");
    }
}

-(void)saveData:(NSData *)data {
    [data writeToFile:self.CachePath atomically:YES];
}

//监听进度
-(void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite {
    
    if (downloadTask == self.downloadTask) {
        allSize = totalBytesExpectedToWrite;
        double progress = (double)totalBytesWritten / (double)totalBytesExpectedToWrite;
        dispatch_async(dispatch_get_main_queue(), ^{
            !self.downloadProgressBlock?:self.downloadProgressBlock(progress,nil);
            
            if ([self.delegate respondsToSelector:@selector(BackgroundDownloadToolCallbackProgress:Error:identifier:)]) {
            [self.delegate BackgroundDownloadToolCallbackProgress:progress Error:nil identifier:self.identifier];
            }
        });
    }
}

-(void)removeDownloadFile {
    NSError *error;
    if ([self.manage fileExistsAtPath:self.DownloadPath ] ) {
        [self.manage removeItemAtPath:self.DownloadPath  error:&error];
        if (error) {
            NSLog(@"removeItem error %@",error);
        }
    }
}

-(void)removeCacheDownloadFile {
    NSError *error;
    if ([self.manage fileExistsAtPath:self.CachePath ] ) {
        [self.manage removeItemAtPath:self.CachePath  error:&error];
        if (error) {
            NSLog(@"removeItem error %@",error);
        }
    }
}

//下载成功
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location {
    NSLog(@"downloadTask:%lu didFinishDownloadingToURL:%@", (unsigned long)downloadTask.taskIdentifier, location);
    NSString *locationString = [location path];
    NSError *error;
    [self removeDownloadFile];
    [self removeCacheDownloadFile];
    
    [self.manage moveItemAtPath:locationString toPath:self.DownloadPath error:&error];
    if (error) {
        NSLog(@"moveItemAtPath error %@",error);
    }
    _isStart = NO;
    !self.completeBlock?:self.completeBlock(self.DownloadPath);

    if ([self.delegate respondsToSelector:@selector(BackgroundDownloadToolDownloadingFinish:identifier:)]) {
        [self.delegate BackgroundDownloadToolDownloadingFinish:self.DownloadPath identifier:self.identifier];
    }
}

//下载完成
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
    if (error) {
        _isStart = NO;
        if ([error.userInfo objectForKey:NSURLSessionDownloadTaskResumeData]) {
            NSData *data = [error.userInfo objectForKey:NSURLSessionDownloadTaskResumeData];
            self.resumeData = data;
            [self saveData:data];
        }
    }
    
    double progress = (double)task.countOfBytesReceived / (double)task.countOfBytesExpectedToReceive;
    dispatch_async(dispatch_get_main_queue(), ^{
        !self.downloadProgressBlock?:self.downloadProgressBlock(progress,error);
        if ([self.delegate respondsToSelector:@selector(BackgroundDownloadToolCallbackProgress:Error:identifier:)]) {
            [self.delegate BackgroundDownloadToolCallbackProgress:progress Error:error identifier:self.identifier];
        }
    });
    self.downloadTask = nil;
}

- (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session
{
    if (self.backgroundSessionCompletionHandler) {
        self.backgroundSessionCompletionHandler();
    }
}


@end


NSURLSession+TYCorrectedResumeData.h

#import "NSURLSession+TYCorrectedResumeData.h"
#import <UIKit/UIKit.h>

#define IS_IOS10ORLATER ([[[UIDevice currentDevice] systemVersion] floatValue] >= 10)

@implementation NSURLSession (TYCorrectedResumeData)

- (NSURLSessionDownloadTask *)downloadTaskWithCorrectResumeData:(NSData *)resumeData {
    NSString *kResumeCurrentRequest = @"NSURLSessionResumeCurrentRequest";
    NSString *kResumeOriginalRequest = @"NSURLSessionResumeOriginalRequest";
    
    NSData *cData = correctResumeData(resumeData);
    cData = cData?cData:resumeData;
    NSURLSessionDownloadTask *task = [self downloadTaskWithResumeData:cData];
    NSMutableDictionary *resumeDic = getResumeDictionary(cData);
    if (resumeDic) {
        if (task.originalRequest == nil) {
            NSData *originalReqData = resumeDic[kResumeOriginalRequest];
            NSURLRequest *originalRequest = [NSKeyedUnarchiver unarchiveObjectWithData:originalReqData ];
            if (originalRequest) {
                [task setValue:originalRequest forKey:@"originalRequest"];
            }
        }
        if (task.currentRequest == nil) {
            NSData *currentReqData = resumeDic[kResumeCurrentRequest];
            NSURLRequest *currentRequest = [NSKeyedUnarchiver unarchiveObjectWithData:currentReqData];
            if (currentRequest) {
                [task setValue:currentRequest forKey:@"currentRequest"];
            }
        }
        
    }
    return task;
}

#pragma mark- private

NSData * correctRequestData(NSData *data) {
    if (!data) {
        return nil;
    }
    // return the same data if it's correct
    if ([NSKeyedUnarchiver unarchiveObjectWithData:data] != nil) {
        return data;
    }
    NSMutableDictionary *archive = [[NSPropertyListSerialization propertyListWithData:data options:NSPropertyListMutableContainersAndLeaves format:nil error:nil] mutableCopy];
    
    if (!archive) {
        return nil;
    }
    NSInteger k = 0;
    id objectss = archive[@"$objects"];
    while ([objectss[1] objectForKey:[NSString stringWithFormat:@"$%ld",k]] != nil) {
        k += 1;
    }
    NSInteger i = 0;
    while ([archive[@"$objects"][1] objectForKey:[NSString stringWithFormat:@"__nsurlrequest_proto_prop_obj_%ld",i]] != nil) {
        NSMutableArray *arr = archive[@"$objects"];
        NSMutableDictionary *dic = arr[1];
        id obj = [dic objectForKey:[NSString stringWithFormat:@"__nsurlrequest_proto_prop_obj_%ld",i]];
        if (obj) {
            [dic setValue:obj forKey:[NSString stringWithFormat:@"$%ld",i+k]];
            [dic removeObjectForKey:[NSString stringWithFormat:@"__nsurlrequest_proto_prop_obj_%ld",i]];
            [arr replaceObjectAtIndex:1 withObject:dic];
            archive[@"$objects"] = arr;
        }
        i++;
    }
    if ([archive[@"$objects"][1] objectForKey:@"__nsurlrequest_proto_props"] != nil) {
        NSMutableArray *arr = archive[@"$objects"];
        NSMutableDictionary *dic = arr[1];
        id obj = [dic objectForKey:@"__nsurlrequest_proto_props"];
        if (obj) {
            [dic setValue:obj forKey:[NSString stringWithFormat:@"$%ld",i+k]];
            [dic removeObjectForKey:@"__nsurlrequest_proto_props"];
            [arr replaceObjectAtIndex:1 withObject:dic];
            archive[@"$objects"] = arr;
        }
    }
    // Rectify weird "NSKeyedArchiveRootObjectKey" top key to NSKeyedArchiveRootObjectKey = "root"
    if ([archive[@"$top"] objectForKey:@"NSKeyedArchiveRootObjectKey"] != nil) {
        [archive[@"$top"] setObject:archive[@"$top"][@"NSKeyedArchiveRootObjectKey"] forKey: NSKeyedArchiveRootObjectKey];
        [archive[@"$top"] removeObjectForKey:@"NSKeyedArchiveRootObjectKey"];
    }
    // Reencode archived object
    NSData *result = [NSPropertyListSerialization dataWithPropertyList:archive format:NSPropertyListBinaryFormat_v1_0 options:0 error:nil];
    return result;
}

NSMutableDictionary *getResumeDictionary(NSData *data) {
    NSMutableDictionary *iresumeDictionary = nil;
    if (IS_IOS10ORLATER) {
        id root = nil;
        id  keyedUnarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:data];
        @try {
            root = [keyedUnarchiver decodeTopLevelObjectForKey:@"NSKeyedArchiveRootObjectKey" error:nil];
            if (root == nil) {
                root = [keyedUnarchiver decodeTopLevelObjectForKey:NSKeyedArchiveRootObjectKey error:nil];
            }
        } @catch(NSException *exception) {
            
        }
        [keyedUnarchiver finishDecoding];
        iresumeDictionary = [root mutableCopy];
    }
    
    if (iresumeDictionary == nil) {
        iresumeDictionary = [NSPropertyListSerialization propertyListWithData:data options:NSPropertyListMutableContainersAndLeaves format:nil error:nil];
    }
    return iresumeDictionary;
}

NSData *correctResumeData(NSData *data) {
    NSString *kResumeCurrentRequest = @"NSURLSessionResumeCurrentRequest";
    NSString *kResumeOriginalRequest = @"NSURLSessionResumeOriginalRequest";
    if (data == nil) {
        return  nil;
    }
    NSMutableDictionary *resumeDictionary = getResumeDictionary(data);
    if (resumeDictionary == nil) {
        return nil;
    }
    resumeDictionary[kResumeCurrentRequest] = correctRequestData(resumeDictionary[kResumeCurrentRequest]);
    resumeDictionary[kResumeOriginalRequest] = correctRequestData(resumeDictionary[kResumeOriginalRequest]);
    NSData *result = [NSPropertyListSerialization dataWithPropertyList:resumeDictionary format:NSPropertyListXMLFormat_v1_0 options:0 error:nil];
    return result;
}

@end



我的业余技术微信公众号:YKJGZH,欢迎大家进入


2018-09-19 10:36:09 Q52077987 阅读数 628

Background Task

因为iOS程序切换到后台之后,很有可能被系统杀掉,因此切换到后台的时候需要保存重要数据。UIApplication的这个方法能让系统给App一段时间,执行重要任务。

beginBackgroundTaskWithName:expirationHandler:

系统给了多少时间呢?可以用backgroundTimeRemaining API查看。如果超时,expirationHandler参数的block将会被调用(不超时就不会调用)。

-(void)applicationDidEnterBackground:(UIApplication *)application{
    __block UIBackgroundTaskIdentifier taskId = [[UIApplication sharedApplication] beginBackgroundTaskWithName:@"fakeTask" expirationHandler:^{
        NSLog(@"end");
    }];
    
    dispatch_async(dispatch_get_main_queue(), ^{
        
        for (int i = 0; i < 10000; i++){
            if(i % 10 == 0){
                UIApplicationState state = [[UIApplication sharedApplication] applicationState];
                NSTimeInterval timeLeft = [[UIApplication sharedApplication] backgroundTimeRemaining];
                NSLog(@"App Status: %ld, task time left: %.2f", (long)state, timeLeft);
            }

            NSLog(@"counting: %d", i);
        }
        
        [[UIApplication sharedApplication] endBackgroundTask:taskId];
        taskId = UIBackgroundTaskInvalid;
    });
}

需要使用endBackgroundTask来显示的结束task,否则任务就一定会超时,当然超时的时候会执行expirationHandler,但是最后程序会被杀死。

如果不在Background的状态下调用beginBackgroundTaskWithName函数呢?会正常执行。

Background Download

使用URLSession可以让应用程序进行后台下载,即使App被系统杀死了,后台下载仍能顺利完成。

首先需要将URLSession的Config配置成Background

-(NSURLSession *)session{
    if(_session == nil){
        NSURLSessionConfiguration *conf = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:@"bk_download"];
        conf.sessionSendsLaunchEvents = YES;
        conf.discretionary = YES;
        conf.networkServiceType = NSURLNetworkServiceTypeBackground;
        
        _session = [NSURLSession sessionWithConfiguration:conf delegate:self delegateQueue:nil];
    }
    
    return _session;
}

Background 类型的Session不支持Block形式的Task,要使用delegate

-(void)downloadFileInBackground{
    NSString *urlStr = @"http://dg.101.hk/1.rar";
    NSURL *url = [NSURL URLWithString:urlStr];
    NSURLSessionTask *backgroundTask = [self.session downloadTaskWithURL:url];
    backgroundTask.earliestBeginDate = [[NSDate date] dateByAddingTimeInterval:10];
    [backgroundTask resume];
}

然后在delegate中写下载完成的回调,这一步和前台的下载一样:

-(void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location{
//...
}

后台下载完成的回调

-(void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)(void))completionHandler{
    NSLog( @"handleEventsForBackgroundURLSession");
    self.downloadHandler = completionHandler;
	self.session; //如果app被系统杀死,要重新创建session。
}

handleEventsForBackgroundURLSession这个回调函数无论App在后台还是被系统杀死,都会调用,如果App被系统杀死了,则要重建URLSession(必须用同一个id)。在这个函数中还要将completionHandler保存起来,在所有任务都完成后,调用这个handler告诉系统,已经处理完成。

-(void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session{
    NSLog(@"URLSessionDidFinishEventsForBackgroundURLSession");
    if(self.downloadHandler){
        self.downloadHandler();
    }
}

当所有任务执行完毕后,会回到URLSessionDidFinishEventsForBackgroundURLSession这个函数,在这里告诉系统执行完毕吗,可以被Suspend了。

如果程序是在前台,则下载完成不会调用handleEventsForBackgroundURLSession和URLSessionDidFinishEventsForBackgroundURLSession。

同样,用户主动杀死的程序不能使用后台下载功能。

测试时,如何让系统杀死应用程序?
https://forums.developer.apple.com/thread/92687#280376

Background Fetch

Background Fetch可以让App没有启动或者在后台的时候,周期性的获取数据。一些内容类型的App,比如新闻,小说等,可以通过使用Background Fetch技术让用户更快的获取信息。一个最合适的场景描述是:一个新闻App,通过Background Fetch技术在夜里获取了用户关注的最新内容并保存到本地,早上用户在地铁等信号不好的地方打开App,可以直接查看本地保存的最新新闻。

使用Background Fetch功能需要申请Capability:
在这里插入图片描述
第二步,在App启动的时候设置请求周期

[[UIApplication sharedApplication] setMinimumBackgroundFetchInterval:UIApplicationBackgroundFetchIntervalMinimum];

第三步,实现AppDelegate的application:performFetchWithCompletionHandler:方法

-(void)application:(UIApplication *)application performFetchWithCompletionHandler:(nonnull void (^)(UIBackgroundFetchResult))completionHandler{
    
    // 请求数据
    completionHandler(UIBackgroundFetchResultNewData);
}

这个方法的执行时间不要超过30秒,实际上越短越好,如果这个方法执行时间过长,iOS系统就会降低Background Fetch的调用频率。

When this method is called, your app has up to 30 seconds of wall-clock time to perform the download operation and call the specified completion handler block.

调试设置
在这里插入图片描述
将launch due to background fetch event勾上,然后点击Run按钮启动应用。

注意,Background Fetch不会工作,如果用户主动杀死了App。

https://stackoverflow.com/questions/35478726/background-fetch-is-not-working-after-killing-the-app

Remote Notification

在iOS7以后推送消息的时候可以唤醒App,执行一段代码,也叫静默推送。静默推送和普通推送的流程有些差别:
在这里插入图片描述
在这里插入图片描述
普通推送,系统收到推送消息之后,应用程序并不做任何事情,等待用户操作。静默推送,在系统收到推送之后,唤起App,App的didReceiveRemoteNotification方法被调用,在这个方法中可以发起一个网络请求,下载数据。

静默推送只需要在普通推送的payload中增加一个content-available的key:

{
"aps":
	{ 
	"content-available":1,
	"alert":"This is some fancy message2.",
	"badge":6,
	"sound": "default"
	}
}

需要注意的是:

- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler {
    NSLog(@"iOS7及以上系统,收到通知:%@", userInfo);
    completionHandler(UIBackgroundFetchResultNewData);
}

静默推送唤起的任务最多执行30s的时间,completionHandler必须被调用,告诉系统任务执行完毕。

As soon as you finish processing the notification, you must call the block in the handlerparameter or your app will be terminated. Your app has up to 30 seconds of wall-clock time to process the notification and call the specified completion handler block.

另外,在用户点击了推动消息启动了应用之后,didReceiveNotificationResponse这个方法会被调用,因此要注意数据的处理是否会和didReceiveRemoteNotification重复。

- (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void (^)())completionHandler{
    completionHandler(); // 系统要求执行这个方法
}

同样,如果用户主动杀死了App,静默推动也不会起作用。

其他

如何禁止应用程序在后台运行?

在Info.pist中添加Key UIApplicationExitsOnSuspend,这样应用程序就不会进入Background状态,切换到后台会调用applicationWillTerminate:然后结束。

应用程序后台任务完成后如何通知用户?
可以使用LocalNotification技术。对于静默推送,本身已经通知了用户,不需要再次使用LocalNotification技术。对于IM类型的应用程序,使用静默推送和LocalNotification组合可以优化设计。

其他后台运行的情况

当应用程序需要如下功能的时候,往往会在后台运行:Location, voip, audio, bluetooth, newsstand-content,对应的都需要在Capabilities中注册服务。

官方文档:Background Execution

iOS后台网络任务

阅读数 782

iOS-APP后台运行代码

阅读数 1276

没有更多推荐了,返回首页