URLSessionDidFinishEventsForBackgroundURLSession N

2019-03-27 05:00发布

问题:

NSURLSession Delegate method
URLSessionDidFinishEventsForBackgroundURLSession is not Calling ?

I already enabled the Background Modes in project capabilities settings.

Here is the code

AppDelegate.h Method

@interface AppDelegate : UIResponder <UIApplicationDelegate>

@property (strong, nonatomic) UIWindow *window;

@property (nonatomic, copy) void(^backgroundTransferCompletionHandler)();

@end

AppDelegate.m Method

-(void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)())completionHandler{

    self.backgroundTransferCompletionHandler = completionHandler;

}

ViewController.m Method

- (void)viewDidLoad
{
    [super viewDidLoad];
    //Urls
    [self initializeFileDownloadDataArray];

    NSArray *URLs = [[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask];
    self.docDirectoryURL = [URLs objectAtIndex:0];

    NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration backgroundSessionConfiguration:@"com.GACDemo"];
    sessionConfiguration.HTTPMaximumConnectionsPerHost = 5;


    self.session = [NSURLSession sessionWithConfiguration:sessionConfiguration
                                                 delegate:self
                                            delegateQueue:nil];
}

NSUrlSession Method

-(void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session{
    AppDelegate *appDelegate = [UIApplication sharedApplication].delegate;

    // Check if all download tasks have been finished.
    [self.session getTasksWithCompletionHandler:^(NSArray *dataTasks, NSArray *uploadTasks, NSArray *downloadTasks) {

        if ([downloadTasks count] == 0) {
            if (appDelegate.backgroundTransferCompletionHandler != nil) {
                // Copy locally the completion handler.
                void(^completionHandler)() = appDelegate.backgroundTransferCompletionHandler;

                // Make nil the backgroundTransferCompletionHandler.
                appDelegate.backgroundTransferCompletionHandler = nil;

                [[NSOperationQueue mainQueue] addOperationWithBlock:^{
                    // Call the completion handler to tell the system that there are no other background transfers.
                    completionHandler();

                    // Show a local notification when all downloads are over.
                    UILocalNotification *localNotification = [[UILocalNotification alloc] init];
                    localNotification.alertBody = @"All files have been downloaded!";
                    [[UIApplication sharedApplication] presentLocalNotificationNow:localNotification];
                }];
            }
        }
    }];
}

I'm able to download all the files one by one but After downloading all the files, URLSessionDidFinishEventsForBackgroundURLSession method is not calling .

I have to perform some action method After Downloading all the files only.

回答1:

These delegate methods won't get called if:

  1. The app is already running when the tasks finish;

  2. The app was terminated by double-tapping on the device's home button and manually killing it; or

  3. If you fail to start a background NSURLSession with the same identifier.

So, the obvious questions are:

  • How was the app terminated? If not terminated, or if terminated incorrectly (e.g. you manually kill it by double-tapping on the home button), that will prevent these delegate methods from getting called.

  • Are you seeing handleEventsForBackgroundURLSession called at all?

  • Are you doing this on a physical device? This behaves differently on the simulator.

Bottom line, there's not enough here to diagnose the precise problem, but these are common reasons why that delegate method might not get called.

You later said:

Actually this is the first time I'm using NSURLSession class. My actual requirement is once the download (all the images) is completed then only I can retrieve the images from document directory and I can show in UICollectionView.

I'm following this tutorial from APPCODA. Here is the link http://appcoda.com/background-transfer-service-ios7

If that's your requirement, then background NSURLSession might be overkill. It's slower than standard NSURLSession, and more complicated. Only use background sessions if you really need large downloads to continue in the background after the app is suspended/terminated.

That tutorial you reference seems like a passable introduction to a pretty complicated topic (though I disagree with the URLSessionDidFinish... implementation, as discussed in comments). I would do something like:

- (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session {
    // log message so we can see completion in device log; remove this once you're done testing the app

    NSLog(@"%s", __FUNCTION__);

    // Since you may be testing whether the terminated app is awaken when the
    // downloads are done, you might want to post local notification. 
    // (Otherwise, since you cannot use the debugger, you're just staring
    // at the device console hoping you see your log messages.) Local notifications
    // are very useful in testing this, so you can see when this method is 
    // called, even if the app wasn't running. Obviously, you have to register
    // for local notifications for this to work.
    //
    // UILocalNotification *notification = [[UILocalNotification alloc] init];
    // notification.fireDate = [NSDate date];
    // notification.alertBody = [NSString stringWithFormat:NSLocalizedString(@"Downloads done", nil. nil)];
    //
    // [[UIApplication sharedApplication] scheduleLocalNotification:notification];

    // finally, in `handleEventsForBackgroundURLSession` you presumably
    // captured the `completionHandler` (but did not call it). So this 
    // is where you'd call it on the main queue. I just have a property 
    // of this class in which I saved the completion handler.

    dispatch_async(dispatch_get_main_queue(), ^{
        if (self.savedCompletionHandler) {
            self.savedCompletionHandler();
            self.savedCompletionHandler = nil;
        }
    });
}

The question in my mind is whether you really want background session at all if you're just downloading images for collection view. I'd only do that if there were so many images (or they were so large) that they couldn't be reasonably downloaded while the app was still running.


For the sake of completeness, I'll share a full demonstration of background downloads below:

//  AppDelegate.m

#import "AppDelegate.h"
#import "SessionManager.h"

@interface AppDelegate ()

@end

@implementation AppDelegate

// other app delegate methods implemented here

// handle background task, starting session and saving 
// completion handler

- (void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)())completionHandler {
    [SessionManager sharedSession].savedCompletionHandler = completionHandler;
}

@end

And

//  SessionManager.h

@import UIKit;

@interface SessionManager : NSObject

@property (nonatomic, copy) void (^savedCompletionHandler)();

+ (instancetype)sharedSession;
- (void)startDownload:(NSURL *)url;

@end

and

//  SessionManager.m

#import "SessionManager.h"

@interface SessionManager () <NSURLSessionDownloadDelegate, NSURLSessionDelegate>
@property (nonatomic, strong) NSURLSession *session;
@end

@implementation SessionManager

+ (instancetype)sharedSession {
    static id sharedMyManager = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        sharedMyManager = [[self alloc] init];
    });
    return sharedMyManager;
}

- (instancetype)init {
    self = [super init];
    if (self) {
        NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:@"foo"];
        self.session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:nil];
    }
    return self;
}

- (void)startDownload:(NSURL *)url {
    [self.session downloadTaskWithURL:url];
}

- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location {
    NSLog(@"%s: %@", __FUNCTION__, downloadTask.originalRequest.URL.lastPathComponent);

    NSError *error;
    NSURL *documents = [[NSFileManager defaultManager] URLForDirectory:NSDocumentDirectory inDomain:NSUserDomainMask appropriateForURL:nil create:false error:&error];
    NSAssert(!error, @"Docs failed %@", error);

    NSURL *localPath = [documents URLByAppendingPathComponent:downloadTask.originalRequest.URL.lastPathComponent];
    if (![[NSFileManager defaultManager] moveItemAtURL:location toURL:localPath error:&error]) {
        NSLog(@"move failed: %@", error);
    }
}

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
    NSLog(@"%s: %@ %@", __FUNCTION__, error, task.originalRequest.URL.lastPathComponent);
}

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

    // UILocalNotification *notification = [[UILocalNotification alloc] init];
    // notification.fireDate = [NSDate date];
    // notification.alertBody = [NSString stringWithFormat:NSLocalizedString(@"Downloads done", nil. nil)];
    //
    // [[UIApplication sharedApplication] scheduleLocalNotification:notification];

    if (self.savedCompletionHandler) {
        self.savedCompletionHandler();
        self.savedCompletionHandler = nil;
    }
}

@end

And, finally, the view controller code that initiates the request:

//  ViewController.m

#import "ViewController.h"
#import "SessionManager.h"

@implementation ViewController

- (IBAction)didTapButton:(id)sender {

    NSArray *urlStrings = @[@"http://spaceflight.nasa.gov/gallery/images/apollo/apollo17/hires/s72-55482.jpg",
                            @"http://spaceflight.nasa.gov/gallery/images/apollo/apollo10/hires/as10-34-5162.jpg",
                            @"http://spaceflight.nasa.gov/gallery/images/apollo-soyuz/apollo-soyuz/hires/s75-33375.jpg",
                            @"http://spaceflight.nasa.gov/gallery/images/apollo/apollo17/hires/as17-134-20380.jpg",
                            @"http://spaceflight.nasa.gov/gallery/images/apollo/apollo17/hires/as17-140-21497.jpg",
                            @"http://spaceflight.nasa.gov/gallery/images/apollo/apollo17/hires/as17-148-22727.jpg"];

    for (NSString *urlString in urlStrings) {
        NSURL *url = [NSURL URLWithString:urlString];
        [[SessionManager sharedSession] startDownload:url];
    }

    // explicitly kill app if you want to test background operation
    //
    // exit(0);
}

- (void)viewDidLoad {
    [super viewDidLoad];

    // if you're going to use local notifications, you must request permission

    UIUserNotificationSettings *settings = [UIUserNotificationSettings settingsForTypes:UIUserNotificationTypeAlert categories:nil];
    [[UIApplication sharedApplication] registerUserNotificationSettings:settings];
}

@end


回答2:

As stated by Apple:

If an iOS app is terminated by the system and relaunched, the app can use the same identifier to create a new configuration object and session and retrieve the status of transfers that were in progress at the time of termination. This behavior applies only for normal termination of the app by the system. If the user terminates the app from the multitasking screen, the system cancels all of the session’s background transfers. In addition, the system does not automatically relaunch apps that were force quit by the user. The user must explicitly relaunch the app before transfers can begin again.