iOS7多任务处理

Posted 微个日光日

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了iOS7多任务处理相关的知识,希望对你有一定的参考价值。

第11章-中级-多任务

Chapter 11 - Intermediate Multitasking

By Pietro Rea

在上一章中你学习了如何实现后台获取,它可以使你的应用内容及时更新,这样用户每次启动应用时都能看到最新的内容。

In the previous chapter,you learned how to implement background fechting,which allows you to keep your app content up to date so the user always sees fresh content when they launch your app

在本章你将会学习Apple在ios 7引入的另外两个多任务处理API:后台传输和静音推送通知。

In this chapter, you’ll learn about the other two multitasking APIs Apple introduced in iOS 7: background transfers and silent push notifications. 

你将会继续修改从本书本件中下载的NASA TV应用程序。本章中的更改并不受最后一章中更改的影响,所以如果你在最后一章中没有实现所有的功能,请不用担心,你只需从最开始的工程中进行即可。

You’ll continue to make modifications to the NASA TV application that you downloaded with this book’s files. The changes in this chapter are not dependent on last chapter’s changes, so if you didn’t implement all of the functionality in the last chapter, don’t worry – you can just begin with the starter project as-is.

后台传输

Background transfers

在之前版本的iOS中,如果在上传或者下载期间退出你的应用程序,那么就意味着你的传输进程将会被暂停(或者完全关闭)。在本章中,你将会对NSURLSession感兴趣,因为它可以让后台传输不中断变成可能。

In previous versions of iOS, quitting an app in the middle of an upload or download meant that your transfer would be paused — or killed altogether. In this chapter, you are going to focus on NSURLSession, which makes background transfers possible.

注意:如果你想学习更多关于NSURLSession的内容,请阅读第16章中的“使用NSURLSession联网”,此章节深入研究了iOS7里的API配置以实现一般的网络任务。

Note: If you want to learn more about NSURLSession, make sure to read Chapter 16, “Networking with NSURLSession.” It deals in depth with the new set of APIs in iOS 7 to perform common networking tasks.

本章的第一个任务就是在NASA TV应用中实现下载视频到本地观看的功能。但必须保证即使应用程序不在前台运行时,一个活动的下载进程继续执行而不会被中断。

Your first task in this chapter is to add the ability to download videos for offline viewing in NASA TV. An active download should continue even if the app isn’t running in the foreground.

开始之前,你需要添加一个“download”按钮和一个进度条到VideoDetailViewController中。打开Main.storyboard并在左侧选择VideoDetailViewController场景。

To start out, you’re going to add a “Download” button and a progress bar to VideoDetailViewController. Open Main.storyboard and select the VideoDetailViewController scene on the far right.

将一个UIBarttonItem拖拽到VideoDetailViewController导航条的右侧。在属性查看器中,将UIBarttonItem的Title属性改为Download,如下图所示:

Drag a UIBarButtonItem to the right side of VideoDetailViewController’s navigation bar. In the Attributes inspector, change the UIBarButtonItem’s Title property to Download as shown below: 

同样的,拖拽一个UIProgressView到刚刚放置在导航条上的UIBarButtonItem的左侧,并将其默认的颜色改为白色。

Similarly, drag a UIProgressView and place it to the left of the Download UIBarButtonItem you just dragged into the navigation bar, and change the default tint color to white.

接下来,在代码里将UIBarButton和UIProgressView连接到IBOutlets。在storyboard上选择VideoDetailViewController上的黄色视图控制器图标。

Next, connect the UIBarButton and UIProgressView to IBOutlets in code. Select the yellow View Controller icon on VideoDetailViewController’s dock in the storyboard. 

然后,选择Xcode右上角的Assistant Editor图标;此图标有点像一个男管家系着一条弯曲的领带。这样便会在storyboard之后打开VideoDetailViewController.m文件。

After that, select the Assistant Editor icon on the top right in Xcode; it looks like a butler wearing a bow tie. This will open VideoDetailViewController.m next to the storyboard.

按住control键将Download按钮拖拽到VideoDetailViewController.m的@interface部分,并将其命名为downloadButton。对UIProgressView执行相同的操作(确保你在barbutton里选择的是progress view,而不是bar button),并将其命名为progressView。

Control-drag the Download button to the @interface section of VideoDetailViewController.m and name it downloadButton. Do the same with your UIProgressView (make sure you select the progress view inside the bar button item, not the bar button item itself) and name it progressView.

到此,Xcode应该已经为你创建了两个新的属性,如下所示:

At this point, Xcode should have created two new properties for you:

@property (weak, nonatomic) IBOutlet 

  UIBarButtonItem *downloadButton;

@property (weak, nonatomic) IBOutlet 

  UIProgressView *progressView;

在代码中,下载按钮已经连接到了一个IBOutlet,但是点击它并不会触发任何事情。为了解决这个问题,按住Control键将此按钮拖拽到VideoDetailViewController.m中的@implementation部分。

The download button is now connected to an IBOutlet in code, but it won’t trigger anything when tapped. To fix this, control-drag from the button to the @implementation section in VideoDetailViewController.m.

这个连接将会是一个action(动作)而不是一个outlet(插槽)。将IBAtion的方法命名为downloadButtonTapped;此代码如下:

This connection is going to be an action instead of an outlet. Name the IBAction method downloadButtonTapped; its code representation will look like the following:

- (IBAction)downloadButtonTapped:(id)sender 

构建并运行你的工程;点击Videos标签显示视频列表,然后点击视图列表里的任意一个视频。出现在视图中的VideoDetailViewController将会是如下图所示的那样:

Build and run your project; tap on the Videos tab to display the list of videos and then tap any video in the collection view. The VideoDetailViewController that is pushed into view should look similar to the one below:

我们终于成功了!嘿嘿,高兴地有点早哦。下载按钮和进度视图看起来很酷,但是现在它们还不能做任何事情。你需要重新回到VideoDetailViewController.m文件中,并添加一些代码来实现这些功能。

Houston, we have liftoff! Well, not quite. The download button and the progress view look great but they don’t do anything at the moment. You need to revisit VideoDetailViewController.m and add some code to do perform those tasks.

下载视频

Downloading videos

为了下载视频文件,先在VideoDetailViewController.m文件声明的@interface中添加如下协议:

To add the code to download the video files, start by adding the following protocols to the @interface declaration in VideoDetailViewController.m:

@interface VideoDetailViewController() <NSURLSessionDelegateNSURLSessionTaskDelegateNSURLSessionDownloadDelegate>

这三个协议是追踪下载状态所需的。另外,它们也允许你更新刚刚加到用户接口中的UIProgressView。

Those three protocols are necessary for monitoring the status of the download. Among other things, they allow you to update the UIProgressView that you just added to the user interface.

接下来将如下三个属性添加到@interface部分:

Next, add the following three properties to the @interface section:

@property (strongnonatomicNSURLSession* urlSession;

@property (strongnonatomic

NSURLSessionDownloadTask* downloadTask;

@property (strongnonatomicNSString* videosDirectoryPath;

NSURLSession和NSURLSessionDownloadTask对象是执行下载操作的重要任务,而videoDirectoryPath则指向用于存储视频的文件系统的目录。

The NSURLSession and NSURLSessionDownloadTask objects perform the heavy lifting in the download operation, while videosDirectoryPath points to the directory in the file system where the videos are going to be stored. 

下载和保存视频是一个相当大的任务,所以你需要将其进行分解。现在先把重心放在进度条视图上,当点击下载按钮时使其能够正常工作。

The methods that that download and save the video are fairly large in size, so you’re going to tackle it in small chunks. For now, just focus on making the progress view work properly when you tap on the download button.

找到viewWillAppear:这个方法,并添加如下两行代码到此方法的最上面:

Go to viewWillAppear: and add the following two lines to the top of the method:

- (void)viewWillAppear:(BOOL)animated 

    [super viewWillAppear:animated];

    

    self.progressView.progress = 0.0f;

    self.progressView.hidden = YES;

这两行代码使进度视图隐藏,并且在进度视图第一次被推送到视图中时将其重置。

Those two lines ensure that the progress view is hidden and reset when the view controller first gets pushed into view.

接下来,大概地浏览一下如下所示的downloadButtonTapped:实现代码:

Next, flesh out the implementation of downloadButtonTapped: as shown below:

- (IBAction)downloadButtonTapped:(id)sender 

    

    //1

    if ([self.video.availableOffline boolValue]) return;

    

    //2

    self.downloadButton.enabled = NO;

    self.progressView.hidden = NO;

    

    //3

    if (!self.urlSession

        

        NSURLSessionConfiguration* config =

        [NSURLSessionConfiguration defaultSessionConfiguration];

        

        self.urlSession =

        [NSURLSession

         sessionWithConfiguration:config

         delegate:self

         delegateQueue:[NSOperationQueue mainQueue]];

    

    

    NSURLRequest *request = [NSURLRequest

                             requestWithURL:self.videoURL];

    

    self.downloadTask = [self.urlSession

                         downloadTaskWithRequest:request];

    

    //4

    [self.downloadTask resume];

这个方法实现不长,但是之后将会变得很重要。在上面的代码里,主要执行了以下一些动作:

00001The method is short but it has important pieces that will come into play later. In the code above, you take the following actions:

1.检查当前视频的availableOffline的属性是否为@(YES),如果是则退出程序;此属性的意义是视频已经开始下载并防止你重复下载。

1. Check if the current video's availableOffline property is set to @(YES) and if so, exit; this property indicates that a video has already been downloaded and prevents you from downloading it twice.

2. 使下载按钮变成不可用状态,这样可以防止在下载操作进行中再次启动一个NDURLSessionDownloadTask副本;另外,其将用于显示下载进度的进度条显示给用户。

2.Disable the download button while the download operation is in progress to prevent launching a duplicate NSURLSessionDownloadTask; additionally, reveal the progress bar to show the download progress to the user.

3.延迟NSURLSession的实例化。默认的配置对一个简单的下载任务是可行的,但是你将需要更改它,使其能启动后台传输。你将会在稍后完成此任务。

3. Lazy instantiation of NSURLSession. The default configuration is fine for a simple download task but you’ll have to change it to something else to enable background transfers. You’ll deal with this a bit later.

4. 最后,开始执行下载任务。

4.Finally, start the download task.

在downloadButtonTapped:方法下面添加如下方法的实现代码:

Below downloadButtonTapped: add the following method implementation:

#pragma mark - NSURLSessionDownloadTask methods

- (void)URLSession:(NSURLSession *)session

      downloadTask:(NSURLSessionDownloadTask *)downloadTask

      didWriteData:(int64_t)bytesWritten

 totalBytesWritten:(int64_t)totalBytesWritten

totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite 

    

    dispatch_async(dispatch_get_main_queue(), ^

        self.progressView.progress =

        (double)totalBytesWritten /

        (double)totalBytesExpectedToWrite;

    );

这个方法分配给主线程去更新VideoDetailViewController导航栏上的UIProgressView,这样用户就可以看到下载的进度了。

This method dispatches to the main thread to update the UIProgressView in VideoDetailViewController’s navigation bar so the user can see how the download is progressing.

构建并运行你的应用;选择任意一个视频并点击Download;你将会看到进度条沿着左侧向右侧移动。下载完了,但是视频文件在哪呢?

Build and run your app; navigate to any video and tap Download; you should see the progress bar moving along from left to right. The download clearly finishes…but where’s the video file? 

NSURLSessionDownloadTask将视频下载到了一个临时文件中,但是如果你没有及时将其复制并保存到永久存储的位置上,这个文件将会消失。所以你需要将其保存到某个地方。

NSURLSessionDownloadTask downloads the video into a temporary file — but if you don’t immediately copy it to a permanent location, the file will simply vanish. Looks like you need to persist it somewhere. 

还是在VideoDetailViewController.m文件中,重写VideoDirectoryPath的属性的getter,如下所示:

Still in VideoDetailViewController.m, override the getter for the videosDirectoryPath property as shown below: 

- (NSString*)videosDirectoryPath 

    

    if (!_videosDirectoryPath

        

        NSArray* paths =

        NSSearchPathForDirectoriesInDomains(NSCachesDirectory,

                                            NSUserDomainMask,

                                            YES);

        _videosDirectoryPath = [paths[0]

                                stringByAppendingPathComponent:

                                @"com.razeware.videos"];

        

        BOOL directoryExists =

        [[NSFileManager defaultManager]

         fileExistsAtPath:_videosDirectoryPath];

        

        if (!directoryExists) 

            

            NSError* error;

            if (![[NSFileManager defaultManager]

                  createDirectoryAtPath:_videosDirectoryPath

                  withIntermediateDirectories:NO

                  attributes:nil

                  error:&error]) 

                

                /* Could not create directory */

                /* Handle NSFileManager error */

            

        

        

    return _videosDirectoryPath;

上面的代码在你的Caches目录里创建了一个文件夹(如果需要的话),并返回此目录的一个引用给调用者。

Caches是推荐使用的一个本地存储,也许在将来它会被重复产生或者重复被下载。

The code above creates a folder (if necessary) in your Caches directory and returns the directory reference to the caller. Caches is the recommended location to store files that may be regenerated or re-downloaded in the future.

接下来,在你之前在NSURLSessionDownloadTask中实现的第一个委托方法的下面添加如下方法:

Next, add the following method below the first NSURLSessionDownloadTask delegate method you implemented earlier:

- (void)URLSession:(NSURLSession *)session

      downloadTask:(NSURLSessionDownloadTask *)downloadTask

didFinishDownloadingToURL:(NSURL *)downloadURL 

    

    //1

    NSString* lastPathComponent =

    [downloadTask.originalRequest.URL lastPathComponent];

    

    //2

    NSString* destinationPath =

    [self.videosDirectoryPath

     stringByAppendingPathComponent:lastPathComponent];

    

    NSURL* destinationURL =

    [NSURL fileURLWithPath:destinationPath];

    

    //3

    NSError* error;

    

    BOOL copySuccessful =

    [[NSFileManager defaultManager]

     copyItemAtURL:downloadURL

     toURL:destinationURL

     error:&error];

    

    if (!copySuccessful) 

        /* Could not copy file to destinationURL */

        /* Handle NSFileManager error */        

    

    

    //4

    dispatch_async(dispatch_get_main_queue(), ^        

        self.video.availableOffline = @(YES);

        NSManagedObjectContext* moc =

        self.video.managedObjectContext;

        

        NSError* error;

        [moc save:&error];

        

        if (error) NSLog(@"Core Data error");

        
        //5

        self.progressView.hidden = YES;

        self.downloadButton.title = @"Downloaded";

    );

这里有一大堆的代码,但是下面将会进行详细的解释:

1.http://192.168.1.111:44447/videos/video-name.mp4,这是视频文件URL的格式,此路径的最后部分是为文件名服务的。这里整段路径是为提取文件名来建立一个合理的文件URL。使用它你可以将视频永久保存起来。

The video URLs are in the format http://192.168.1.111:44447/videos/video-name.mp4, where the path’s lastComponent serves as the name of the file. The goal here is to extract the file name to build a reasonable file URL to which you can save the video permanently.

2.destinationPath代表用于视频文件存储的完全文件URL;它是videoDirectoryPath和视频文件名合起来的结果。例如,discovery.mp4将会被存储在…/Caches/com.razeware.videos/discovery.mp4.

2.destinationPath represents the full file URL where the video is to be saved; it’s the result of concatenating videosDirectoryPath to the video’s file name. For example, discovery.mp4 would be saved as …/Caches/com.razeware.videos/discovery.mp4.

3.这里神奇的事情发生了。第一个参数是将视频下载到临时位置的URL,而第二个参数提供了在文件消失之前用于存储视频的位置。

3.This is where the magic happens. The first parameter is the URL for the downloaded video in its temporary location, and the second parameter gives the permanent location to save the video to before the file vanishes in a puff of digital smoke.

4.更新video的videoAvailableOffline属性来提示这个视频已经下载成功,downloadButtonTapped:则用于避免重复下载视频。在self.video指向的video被分配给主线程时这个动作必须先分配给主线程。

4.Update the videoAvailableOffline attribute of video to indicate that this video has been successfully downloaded, which downloadButtonTapped: uses to avoid re-downloading a video. This action must be dispatched to the main thread since the video that self.video points to was fetched on the main thread.

注意:在本章中,Core Data的细节并不重要,但是如果你想阅读更多关于使用多线程里的Core Data,请阅读苹果官方文档Core Data Programming Guide.

Note: The Core Data details are not important for this chapter, but if you want to read more about using Core Data from multiple threads make sure to read Apple’s Core Data Programming Guide.

5.最后,隐藏进度条并将导航栏里的文本内容“Download”改为“Downloaded”。现在看来这些后期细小视觉效果并不重要,但是之后当你实现后台传输的时候将会对你有很大的帮助。

5. Finally, hide the progress bar and change the text in the navigation bar from “Download” to “Downloaded”. These small visual cues may seem unimportant now, but they will be helpful later when you’re implementing background transfers.

还是在VideoDetailViewController.m文件中,添加如下空白方法:

Still in VideoDetailViewController.m, add the following empty method:

- (void)URLSession:(NSURLSession *)session

      downloadTask:(NSURLSessionDownloadTask *)downloadTask

 didResumeAtOffset:(int64_t)fileOffset

expectedTotalBytes:(int64_t)expectedTotalBytes 

    

    

这是NSURLSessionDownloadTask委托所需的方法;你并不准备使用它,将其放置此处是为了避免来自Xcode的警告。

This is a required NSURLSessionDownloadTask delegate method; you’re not going to use it but it’s included here to silence an Xcode warning.

现在你还没有做任何错误处理的工作。虽然在移动网络上进行下载文件的过程中并没有产生任何的错误,但是这样做是一个很好的习惯,但是还是要防止一些意外发生。

You haven’t yet taken care of the error handling. Although nothing ever goes wrong when downloading files over mobile networks, it’s good practice to have it there just in case something glitches. .J

如果下载时遇到了一个错误,一般的做法是将硬盘上的临时文件删除,因为通常此文件是没有用的。添加如下方法实现:

If you encounter an error with the download, delete the temporary file from disk as the file will likely be of no use. Add the method implementation as shown below:

#pragma mark - NSURLSessionTaskDelegate methods

- (void)URLSession:(NSURLSession *)session

              task:(NSURLSessionTask *)task

didCompleteWithError:(NSError *)error 

    

    if (error) 

        NSString* lastPathComponent =

        [task.originalRequest.URL lastPathComponent];

        

        NSString* filePath =

        [self.videosDirectoryPath

         stringByAppendingPathComponent:lastPathComponent];

        

        [[NSFileManager defaultManager]

         removeItemAtPath:filePath error:nil];

    

如果错误发生了,上面的代码将从视频的URL产生这个文件的URL(如同你之前做的),并且删除这个临时文件。

If an error occurs, the above code generates the file URL from the video URL as you did before and deletes the temporary file.

构建并运行你的应用程序。尝试如下试验:选择一个视频,下载它,然后(确保程序一直开着)切换到Setting(设置)并打开Airplane mode(飞行模式)来模拟离线。然后切换回你的应用程序并尝试播放你下载的视频。没有任何响应!为什么?

Build and run your app. Try the following experiment: choose a video, download it, then (with the app still open) switch to Settings and go into Airplane mode to simulate being offline. Switch back to your app and attempt to play your downloaded video — nothing happens! Why?

启用离线视图

Enabling offline viewing

视频出现在了文件系统中,但是影音播放器并不知道如何找到它。为解决这个问题,修改viewWillAppear:里的代码,如下所示:

The video is present in the file system, but the movie player doesn’t know how to find it. Solve this problem by modifying the code in viewWillAppear: as follows:

- (void)viewWillAppear:(BOOL)animated 

    [super viewWillAppear:animated];

    

    self.progressView.progress = 0.0f;

    self.progressView.hidden = YES;

    

    //1

    BOOL videoAvailableOffline =

    [self.video.availableOffline boolValue];

    

    NSURL* playbackVideoURL;

    

    //2

    if (videoAvailableOffline) 

        self.downloadButton.enabled = NO;

        self.downloadButton.title = @"Downloaded";

        

        /* Play local content if available */

        NSString* lastPathComponent =

        [self.videoURL lastPathComponent];

        

        NSString* videoPath =

        [self.videosDirectoryPath

         stringByAppendingPathComponent:lastPathComponent];

        

        playbackVideoURL = [NSURL fileURLWithPath:videoPath];

    

    

    else 

        self.downloadButton.enabled = YES;

        playbackVideoURL = self.videoURL;

    

    //3

    self.moviePlayerViewController =

    [[MPMoviePlayerController alloc]

     initWithContentURL:playbackVideoURL];

    

    [self.moviePlayerViewController prepareToPlay];

    

    [self.moviePlayerViewController

     setControlStyle:MPMovieControlStyleDefault];

    

    [self.moviePlayerViewController.view

     setFrame:self.view.bounds];

    

    [self.view addSubview:self.moviePlayerViewController.view];

    

    [self.moviePlayerViewController play];

现在花点时间来看看刚刚你添加的那些代码:

1.记住在下载成功时你需要更新availableOffline属性。你需要将这个BOLL进行拆箱(unbox)操作,因为Core Data将其保存成一个NSNumber类型的数据。

1.Recall that you had to update the availableOffline property when the download finished successfully. You have to unbox this BOOL because Core Data saves it as an NSNumber.

2.如果视频在本地是可用的,那么将产生此文件的URL;否则使用self.video里的URL流。如果需要的话,你可以利用availableOffline来启用或者禁用下载按钮,因为如果将视频保存到本地之后就没必要让下载按钮保持可用状态了。

2.If the video is available locally, generate the file URL like you’ve been doing all along; otherwise, use the streaming URL in self.video. You can also use availableOffline to enable or disable the download button as needed, as there’s no need to keep the download button active if you already have the video saved locally.

这里你给MPMoviePlayerController添加了一个名为playbackVideoURL临时变量,它代表self.videoURL。根据视频在本地是否可用来确定playbackVideoURL是否是正确的URL。

3.Here you’re feeding MPMoviePlayerController a temporary variable named playbackVideoURL instead of self.videoURL. playbackVideoURL should have the correct URL based on whether or not the video is available locally.

构建并运行NASA TV应用,选择任一视频并点击Download。下载完毕后,保持应用一直开着,切换到Setting并禁用Wi-Fi,拔掉以太网卡,或者做任何可以断开网络的操作。切换回你的应用程序,找到刚才那个视频并打开它,这时发现你的视频现在可以播放了。

00002Build and run NASA TV, navigate to any video and tap Download. After the download is complete, with the app still open switch to Settings and disable Wi-Fi, unplug the Ethernet cable, or do whatever you need to do to disconnect from the Internet. Switch back to your app, navigate to the same video and voila — your video now plays. 

如果你有一个真实的设备,在此设备上构建并运行你的应用程序;在真实设备上下载明显变慢了,这就你你接下来要处理的任务。

00003If you have a physical device, build and run your app on that as well; downloads are noticeably slower on physical devices, which will come in handy for your next task. 

重新连接到互联网,找到任一视频,再一次点击下载。但是,这次在下载完成之前快速按下Home键。记住你退出NASA TV时进度条的进度节点。

等大概十秒钟,然后重新启动你的NASA TV应用;下载进度将会重新回到你离开时那个节点的位置。对于用户来说这可能并不是一件非常愉悦的事情,但是这正是在后台传输中我们将要修正的地方。

00004Wait about ten seconds, then restore your NASA TV app; the download should resume from exactly the same point at which you left it. That’s not terribly pleasing to the user — but that’s exactly what you’re going to fix with background transfers.

执行后台传输

Performing background transfers

不同于其他的后台模式,使用后台传输并需要你在应用的Info.plist上注册一个特殊的后台模式。

添加如下方法到AppDelegate.m文件中:

Unlike other background modes, using background transfers does not require you to register for a special background mode in your application’s Info.plist.

Add the following method to AppDelegate.m:

#pragma mark - Background Transfer

- (void)application:(UIApplication *)application

handleEventsForBackgroundURLSession:(NSString *)identifier

  completionHandler:(void (^)())completionHandler 

    

    NSDictionary* userInfo =

  @@"completionHandler" : completionHandler,

    @"sessionIdentifier" : identifier;

    

    [[NSNotificationCenter defaultCenter]

     postNotificationName:@"BackgroundTransferNotification"

     object:nil

     userInfo:userInfo];

当一个后台传输完成后,系统会调用application:handleEventsForBackGroundURLSession:completionhandler:方法,此方法会传给你一个完成处理器,和之前一章中介绍的后台获取中的是一样的。

When a background transfer completes, the system calls application:handleEventsForBackgroundURLSession:completionHandler: which hands you a completion handler, just as was demonstrated in the previous chapter with background fetch. 

在这种情况下,处理后台传输的工作将在其他地方完成。这个委托方法通过将其包含在通知里的userInfo目录来传递完成处理器。

In this case, the work to handle the background download work is done elsewhere. The delegate method delivers the completion handler by including it in the notification’s userInfo dictionary. 

一个应用程序可以有几个传输在传输队列中,所以NSURLSession的身份标识同样会被发布,这样接受者就能明确那个传输已经完成了。

An app can have several transfers queued up, so the NSURLSession identifier is posted as well so the receiver can identify which transfer completed.

打开VideoDetailViewController.m文件,并添加如下代码到viewWillAppear的底部:

Open VideoDetailViewController.m and add the following snippet of code to the bottom of viewWillAppear:

[[NSNotificationCenter defaultCenter]

 addObserver:self

 selector:@selector(handleBackgroundTransfer:)

 name:@"BackgroundTransferNotification"

 object:nil]; 

这里添加了一个观察者到在AppDelegate.m发布的后台传输通知中。

This simply adds an observer to the background transfer notification posted in AppDelegate.m.

现在定位到VideoDetailViewController.m的底部并添加如下代码:

Now scroll to the bottom of VideoDetailViewController.m and add the following:

#pragma mark - Background Transfers

- (void)handleBackgroundTransfer:(NSNotification*)notification 

    

    // 1

    NSString* sessionIdentifier =

    notification.userInfo[@"sessionIdentifier"];

    

    NSArray* components =

    [sessionIdentifier componentsSeparatedByString:@"."];

    

    NSString* videoID = [components lastObject];

    

    // 2

    if ([self.video.videoID integerValue] ==

        [videoID integerValue]) 

        

        // 3

        dispatch_async(dispatch_get_main_queue(), ^

            self.downloadButton.title = @"Downloaded";

            self.progressView.hidden = YES;

            

            void(^completionHandler)(void) =

            notification.userInfo[@"completionHandler"];

            

            if (completionHandler) 

                completionHandler();

            

        );

    

当一个下载完成的通知来了,handlebackgroundTransfer:方法将会被调用。在这个方法中引入了一些新的概念:

handleBackgroundTransfer: executes when a download complete notification arrives. There’s a few new concepts introduced in this method:

1.解包来自通知的userInfo目录中的NSURLSession身份标识。此身份标识的格式和反转的DNS标记法类似,所以最后的那部分包含的是视频的ID。

1. Unpack the NSURLSession identifier from the notification’s userInfo dictionary. The identifier is formatted similar to reverse DNS notation so the last component contains the video’s ID.

2.如果有多个下载进度,那么每个都会对应一个VideoDetailViewController实例。如果在屏幕上的视频和通知里的身份标识不是同一个,那么在继续执行之前将会检查他们是否匹配。

3.If there are multiple downloads in progress, each one will have a VideoDetailViewController instance. Since it’s possible that the video on the screen is not the same one identified in the notification, check that they match before continuing.

3.在主线程中执行UI的更新:将“Download”改为“Downloaded”并隐藏进度条。完成之后,执行保存在userInfo目录里的完成事件。

2. Perform the UI updates on the main thread: change “Download” to “Downloaded” and hide the progress bar. After that’s done, execute the completion handler stored in the userInfo dictionary.

你已经在viewWillAppear:方法中完成了注册,所以同样别忘了修改viewWillDisappear:方法对通知进行解除注册:

You’ve registered for the notification in viewWillAppear: so be sure to modify viewWillDisappear: to unregister for the notification as well:

- (void)viewWillDisappear:(BOOL)animated 

    [super viewWillDisappear:animated];

    [self.moviePlayerViewController stop];

    

    [[NSNotificationCenter defaultCenter]

    removeObserver:self];

现在,这些通知和处理器会帮助文件在后台传输中顺利完成传输,剩下的事情就是配置NSURLSession,使下载视频任务继续在后台执行。

The notifications and handlers now facilitate file transfers that will run to completion in the background. All that’s left to do is configure NSURLSession to continue downloading the video in the background.

定位到downloadButtonTapped:方法,并找到你l延迟下载SURLSession的地方并修改如下代码:

Go to downloadButtonTapped: and find the place where you lazy-load NSURLSession. Change that block of code as shown below: 

    //3

    if (!self.urlSession

        

        NSString* sessionID =

        [@"com.razeware.backgroundsession."

         stringByAppendingFormat:@"%d",

         [self.video.videoID integerValue]];

        

        NSURLSessionConfiguration* config =

        [NSURLSessionConfiguration

         backgroundSessionConfiguration:sessionID];

        

        self.urlSession =

        [NSURLSession

         sessionWithConfiguration:config

         delegate:self

         delegateQueue:

         [NSOperationQueue mainQueue]];

    

创建一个后台NSURLSessionConfiguration需要一段ID;此ID与委托调用的application:handleEventForBackgroundURLSession:completionHandler:方法返回的ID是一样的。

Creating a background NSURLSessionConfiguration requires a session ID; this is the same ID that comes back in the delegate call application:handleEventsForBackgroundURLSession:completionHandler:. 

在本段代码中,你使用一个反向DNS标记并在最后添加此视频的ID。在创建唯一的连续性ID时,要小心点。这些ID会显示在debugger里,并如果出现某些意外错误时可以帮助你追踪相关的视频。

In this case, you are using a reverse DNS notation and appending the video ID to the end. Take care to create unique session IDs— these IDs will also show up in the debugger and help you track down the pertinent video if something goes wrong.

别着急着测试,你得先分别在AppDelegate.m里的application:handleEventsForBackgroundURLSession:completionHandler:和VideoViewController.m里的handleBackgroundTransfer:的两个方法中的完成处理器执行的后一句添加一个段点。

You’re almost ready to test. But first add a breakpoint to AppDelegate.m inside application:handleEventsForBackgroundURLSession:completionHandler: and another one to VideoViewController.m inside handleBackgroundTransfer: right after the completion handler gets executed.

在你的真实设备上构建并运行此应用程序;在设备上运行时的慢速下载给你创造了更多的时间在下载期间退出此应用,从而完成测试工作。

Build and run on your physical device; the slower download speed on the device gives you a little more time to quit the app mid-download to test your work.

现在找到任一视频,点击Download并在进度条没跑完之前按Home键。

Now navigate to any video, tap Download and press the Home button once the progress bar is halfway done. 

稍稍离开一下你的电脑休息一下吧。你可以吃点三明治或者喝杯咖啡,更甚者去解决一下千禧年大奖的问题,反正随你意。当你回来的时候,你发现Xcode的debugger将会停在刚才你在AppDelegate.m设置的断点处,如下图所示:

Step away from your computer for a bit; maybe grab a sandwich or a coffee, or solve a Millennium Prize Problem, whatever you choose. When you come back, Xcode’s debugger will be paused at the breakpoint you inserted in AppDelegate.m, as shown below:

点击Continue跳转到下一个断点,在VideoDetailViewController.m的completion handler执行完之后立即使debugger暂停。再次点击Continue恢复正常运行。

Click on Continue to move to the next breakpoint, which pauses the debugger immediately after executing the completion handler in VideoDetailViewController.m. Click on Continue once again to resume normal execution.

这样就证明了completion handler像预期的那样被执行了——但是应用的切换截图是否更新了呢?为了证明,双击你设备上Home键,进入应用切换管理器并找到NASA TV的快照,如下图所示:

按钮的右上角清楚的显示着“Downloaded”,这就表明视频已经被复制到了文件系统中。这样就没必要重新启动NASA TV去查看下载进度了,应用切换管理器中快照已经告诉你:下载已经成功完成了!这就证明了完成处理器在恰当的时候被调用了。

Notice that the button in the top right reads “Downloaded”, which means that the video has been copied permanently into the file system. At no point did you re-launch NASA TV to check on the download’s progress, yet the app switcher snapshot tells you that the download completed successfully! This is your proof that the completion handler was called at the right moment.

自由传输

Discretionary transfers

在本章前半部分,你在应用运行在前台的时候安排了一个后台传输。然而,在后台完成开始到结束传输的整个过程也就变得可能了,例如当应用因一个后台获取的操作而被唤醒,或者响应一个静音推送通知。

In the previous section you queued up a background transfer while the app was running in the foreground. However, it is also possible to start and finish a transfer entirely from the background, such as when the app wakes up for a background fetch operation or responds to a silent push notification. 

从后台开始执行的后台传输叫discretionary transfers(自由传输),这意味着它具有管理权限,并且只在Wi-Fi上工作。

Background transfers started from the background are discretionary transfers, which means they are power-managed and will only work over Wi-Fi. 

你可以将NSURLSessionConfiguration中的一个BOOL属性设置成discretionary,从而使前台传输变成自由传输。

You can optionally set foreground transfers to be discretionary by setting a BOOL property in NSURLSessionConfiguration named discretionary.

在你实现后台传输的时候请把自由传输记住在心上。用户可以通过检查Setting应用就很容易知道你的应用是否具有重要数据要更新,传输是否足够重要这完全取决于你的决定。

Keep discretionary transfers in the back of your mind as you implement background transfers. Users can easily see if your app is being a data hog by checking the Settings app, so it’s up to you to determine whether the transfer is important enough.

静音推送通知

Silent push notifications

传统的推送通知的目的是提醒用户一些新鲜有趣事物,即使此刻用户并没有在使用你的应用。比如一些重要新闻,一位好友更新了Facebook的状态或者近期你喜欢的主题杂志更新了。

The goal for traditional push notifications is to alert the user of something interesting going on — even if they’re not using your app at the moment. This could be something like breaking news, a friend responding to a Facebook status or the latest issue of your favorite magazine becoming available.

推送通知的本质是模拟多任务处理,它让你的应用看起来一直不断地在后台更新一些新的信息,一旦有新的感兴趣东西出现了就会提醒你。iOS 7通过引进静音推送通知拓展了这个概念,它允许第三方开发者在不打扰用户的情况下触发后台更新。

Push notifications essentially simulate multitasking, making it seem like your app is constantly polling new information in the background and alerting you anytime something interesting happens. iOS 7 extends this concept by introducing silent push notifications, which allow third-party developers to trigger background refreshes without bothering the user. 

当一台设备接受了一个静音推送通知,并不会在屏幕上显示提醒信息。这并不是出了什么错误,而是你的应用在

以上是关于iOS7多任务处理的主要内容,如果未能解决你的问题,请参考以下文章

iOS7多任务处理

iOS7如何以编程方式为iPhone多任务屏幕显示黑屏? [复制]

在 iOS 7 中检测静音模式

iOS 7 多任务切换器:导航栏出现黑色

使用嵌套滚动视图实现 iOS 7 跳板多任务 UI

使用 C# 检测 WAV 文件中的音频静音