In the previous tutorial, we started creating a simple podcast client to put what we’ve learned about NSURLSession
into
practice. So far, our podcast client can query the iTunes Search API, download a podcast feed, and display a list of episodes. In this tutorial, we zoom in on another interesting aspect of NSURLSession
,
out-of-process downloads. Let me show you how this works.
在前一篇教程中,我們創建瞭一個簡單的播客客戶端,對 NSURLSession 的所學進行瞭實踐。目前,我們的播客客戶端可以 query the iTunes Search API, download
a podcast feed, 和 display a list of episodes。在這篇教程中,將探究 NSURLSession 另一個有趣的部分:out-of-process download。下面開始吧!
Introduction
In this fourth and final tutorial about NSURLSession
, we’ll
take a closer look at out-of-process tasks, download tasks in particular. Our podcast client is already able to show a list of episodes, but it currently lacks the ability to download inpidual episodes. That’ll be the focus of this tutorial.
在這第四篇也是最後一篇有關 NSURLSession 的教程,我們將深入瞭解 out-of-process task 尤其是 download task。目前播客客戶端可以 show
a list of episodes,但是還不能下載 inpidual episodes,這將是本篇教程的關註點。
Background Uploads and Downloads
Adding support for background uploads and downloads is surprisingly easy withNSURLSession
.
Apple refers to them as out-of-process uploads and downloads as the tasks are managed by a background daemon, not your application. Even if your application crashes during an upload or download task, the task continues in the background.
對於 NSURLSession ,是支持後臺下載和上傳的。Apple 將其稱之 進程之外(out-of-process)的上傳和下載,因為這些任務都是由後臺的守護進程完成的,而非應用程序本身。即使應用程序奔潰瞭,上傳或者下載任務都可以在後臺進行執行。
Overview
I’d like to take a few moments to take a closer look at how out-of-process tasks work. It’s pretty simple once you have a complete picture of the process. Enabling background uploads and downloads is nothing more than flipping a switch in your session’s configuration.
With a properly configured session object, you are ready to schedule upload and download tasks in the background.
先看看 out-of-process 的任務是如何工作的。一旦你對整個過程有瞭全面的瞭解,這將變得很簡單。啟動後臺上傳和下載無非就是在會話配置中進行簡單的設置(如同扳開開關),通過會話配置對象中的屬性設置,你就可以在後臺執行上傳和下載的任務。
When an upload or download is initiated, a background daemon comes into existence. The daemon takes care of the task and sends updates to the application through the delegate protocols declared in the NSURLSession
API.
If your application stops running for some reason, the task continues in the background as it’s the daemon managing the task. The moment the task finishes, the application that created the task is notified. It reconnects with the background session that created
the task and the daemon managing the task informs the session that the task finished and, in the case of a download task, hands the file over to the session. The session then invokes the appropriate delegate methods to make sure your application can take the
appropriate actions, such as moving the file to a more permanent location. That’s enough theory for now. Let’s see what we need to do to implement out-of-process downloads in Singlecast.
當一個上傳或者下載的任務啟動,一個後臺守護進程就存在瞭。通過 NSURLSession 委托協議中的 API ,守護進程關註維護這個任務,並且發送更新消息給應用程序。如果由於某些原因,應用程序停止運行,守護進程會在後臺管理這個任務的繼續執行。一旦任務執行結束,就會通知創建該任務的應用程序。它(應用程序)會和後臺這個任務的會話重新連接;同時,後臺守護進程通知會話對象,任務執行結束,如果是一個下載任務,會將下載文件提交給會話對象。會話對象接著會調用相應的委托方法,確保應用程序執行恰當的動作,例如,將文件移動到某一持久化存儲位置。這些理論解釋差不多就是這樣瞭,下面看看如何在
Singlecast 項目中實現 out-of-process 的下載。
1. Subclass UITableViewCell
Step 1: Update Main Storyboard
At the moment, we are using prototype cells to populate the table view. To give us a bit more flexibility, we need to create a UITableViewCell
subclass.
Open the main storyboard, select the table view of the MTViewController
instance
and set the number of prototype cells to 0
.
目前,在table view中我們使用的是原型的表單元。為瞭處理的靈活性,我們自定義創建 UITableViewCell 的子類。打開 storyboard,選中MTViewController
的 table view,設置其
prototype cells 數量為0.
Step 2: Create Subclass
Open Xcode”s File menu and choose New > File…. Create a new Objective-C class, name it MTEpisodeCell
,
and make sure it inherits from UITableViewCell
. Tell Xcode
where you’d like to store the class files and hit Create.
新建一個
Objective-C 類文件,名為
MTEpisodeCell ,繼承自UITableViewCell。
Step 3: Update Class Interface
The interface of MTEpisodeCell
is simple
as you can see in the code snippet below. All we do is declare a prZ喎?/kf/ware/vc/” target=”_blank” class=”keylink”>vcGVydHkgPGNvZGU+cHJvZ3Jlc3M8L2NvZGU+IG9mCiB0eXBlIDxjb2RlPmZsb2F0PC9jb2RlPi4gV2U=”ll use this to update and display the progress
of the download task that we’ll use for downloading an episode.
MTEpisodeCell 類的interface 是比較簡單的,代碼片段如下。在其中聲明瞭一個
float 類型的屬性
progress 。這個屬性將用來更新和顯示下載任務的進度。
#import @interface MTEpisodeCell : UITableViewCell @property (assign, nonatomic) float progress; @end
Step 4: Implement Class
The implementation of MTEpisodeCell
is
a bit more involved, but it isn’t complicated. Instead of using an instance of UIProgressView
,
we’ll fill the cell’s content view with a solid color to show the progress of the download task. We do this by adding a subview to the cell’s content view and updating its width whenever the cell’s progress
property
changes. Start by declaring a private property progressView
of
type UIView
.
MTEpisodeCell 類的 implementation
是有些復雜,但不難實現。我們用純色填充表單元視圖以顯示下載任務的進度,而不使用進度條
UIProgressView。在表單元中添加一個子視圖更新顯示
progress 屬性的變化,添加一個
UIView 類型的私有屬性
progressView (在實現文件的 interface
中添加 property 即是私有屬性)。
#import "MTEpisodeCell.h" @interface MTEpisodeCell () @property (strong, nonatomic) UIView *progressView; @end
We override the class’s designated initializer as shown below. Note how we ignore thestyle
argument
and pass UITableViewCellStyleSubtitle
to
the superclass’s designated initializer. This is important, because the table view will passUITableViewCellStyleDefault
as
the cell’s style when we ask it for a new cell.
覆蓋該類的指定初始化方法如下。註意到style 參數,傳遞UITableViewCellStyleSubtitle 給超類的初始化方法。這一點很重要,因為創建一個新的cell的時候,table
view 會傳遞UITableViewCellStyleDefault 給cell 的類型。
In the initializer, we set the background color of the text and detail text labels to [UIColor
and create the progress view. Two details are especially important. First, we insert the progress view as a subview of the cell’s content view at index
clearColor]0
to
make sure that it’s inserted below the text labels. Second, we invoke updateView
to
make sure that the frame of the progress view is updated to reflect the value of progress
,
which is set to 0
during the cell’s initialization.
在初始化方法中,設置文本(text label)和詳細文本(detail text label)的背景顏色為[UIColor
clearColor],並創建一個進度視圖。有兩個細節比較重要,首先,插入一個進度顯示視圖作為表單元的子視圖,並置 index 為 0 使得進度顯示位於 text label 之下;其次,我們調用updateView 方法確保進度顯示視圖及時更新反映progress 的值,在表單元初始化的時候設置progress
的值為 0.
- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier { self = [super initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:reuseIdentifier]; if (self) { // Helpers CGSize size = self.contentView.bounds.size; // Configure Labels [self.textLabel setBackgroundColor:[UIColor clearColor]]; [self.detailTextLabel setBackgroundColor:[UIColor clearColor]]; // Initialize Progress View self.progressView = [[UIView alloc] initWithFrame:CGRectMake(0.0, 0.0, size.width, size.height)]; // Configure Progress View [self.progressView setAutoresizingMask:(UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleWidth)]; [self.progressView setBackgroundColor:[UIColor colorWithRed:0.678 green:0.886 blue:0.557 alpha:1.0]]; [self.contentView insertSubview:self.progressView atIndex:0]; // Update View [self updateView]; } return self; }
Before we take a look at the implementation of updateView
,
we need to override the setter method of the progress
property.
The only change we make to the default implementation of setProgress:
is
invoke updateView
when the _progress
instance
variable is updated. This ensures that the progress view is updated whenever we update the cell’s progress
property.
在實現
updateView 方法之前,我們需要實現
progress 屬性的 setter 方法。和默認的
setProgress: 實現方法,唯一有所不同的是,當
_progress 實例變量更新時,調用
updateView 方法。這樣當表單元的 屬性發生更新變化時,進度顯示視圖可以及時更新顯示。
- (void)setProgress:(CGFloat)progress { if (_progress != progress) { _progress = progress; // Update View [self updateView]; } }
In updateView
, we calculate the new width of the progress
view based on the value of the cell’s progress
property.
在
updateView 方法中,我們基於表單元的 progress 屬性計算進度顯示視圖的寬度值。
- (void)updateView {
// Helpers
CGSize size = self.contentView.bounds.size;
// Update Frame Progress View
CGRect frame = self.progressView.frame;
frame.size.width = size.width * self.progress;
self.progressView.frame = frame;
}
Step 5: Use MTEpisodeCell
To make use of the MTEpisodeCell
, we need
to make a few changes in theMTViewController
class. Start
by adding an import statement for MTEpisodeCell
.
為瞭在 MTViewController 類中使用MTEpisodeCell
,首先需要添加 import 語句。
#import "MTViewController.h" #import "MWFeedParser.h" #import "SVProgressHUD.h" #import "MTEpisodeCell.h" @interface MTViewController () @property (strong, nonatomic) NSDictionary *podcast; @property (strong, nonatomic) NSMutableArray *episodes; @property (strong, nonatomic) MWFeedParser *feedParser; @end
In the view controller’s viewDidLoad
method, invoke setupView
,
a helper method we’ll implement next.
在
viewDidLoad
方法中,調用setupView 方法,這個方法接著會實現它。
- (void)viewDidLoad { [super viewDidLoad]; // Setup View [self setupView]; // Load Podcast [self loadPodcast]; // Add Observer [[NSUserDefaults standardUserDefaults] addObserver:self forKeyPath:@"MTPodcast" options:NSKeyValueObservingOptionNew context:NULL]; }
In setupView
, we invoke setupTableView
,
another helper method in which we tell the table view to use the MTEpisodeCell
class
whenever it needs a cell with a reuse identifier of EpisodeCell
.
在
setupView 方法中,調用
setupTableView 方法,這個方法通知 table view 使用
MTEpisodeCell 類創建 cell ,並使用
EpisodeCell 作為 identifier。
- (void)setupView { // Setup Table View [self setupTableView]; } - (void)setupTableView { // Register Class for Cell Reuse [self.tableView registerClass:[MTEpisodeCell class] forCellReuseIdentifier:EpisodeCell]; }
Before we build the project and run the application, we need to update our implementation of tableView:cellForRowAtIndexPath:
as
shown below.
在編譯運行項目之前,還要更新實現
tableView:cellForRowAtIndexPath: 方法如下:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { MTEpisodeCell *cell = (MTEpisodeCell *)[tableView dequeueReusableCellWithIdentifier:EpisodeCell forIndexPath:indexPath]; // Fetch Feed Item MWFeedItem *feedItem = [self.episodes objectAtIndex:indexPath.row]; // Configure Table View Cell [cell.textLabel setText:feedItem.title]; [cell.detailTextLabel setText:[NSString stringWithFormat:@"%@", feedItem.date]]; return cell; }
Step 6: Build and Run
Run your application in the iOS Simulator or on a test device to see the result. If nothing has changed, then you’ve followed the steps correctly. All that we’ve done so far is replacing the prototype cells with instances of MTEpisodeCell
.
在模擬器或者真機上運行該程序查看結果,如果沒有異常,則說明以上內容都順利的完成瞭。誠然,所有的這些都隻是將原型表單元替換為自定義的表單元
MTEpisodeCell 。
2. Create Background Session
To enable out-of-process downloads, we need a session that is configured to support out-of-process downloads. This is surprisingly easy to do with the NSURLSession
API.
There a few gotchas though.
為瞭實現
out-of-process 下載,我們需要一個會話對象,其配置是支持 out-of-process 下載的。對於使用 NSURLSession 的 API 這是很容易實現的,幾步即可。
Step 1: Create session
Property
Start by declaring a new property session
of
type NSURLSession
in theMTViewController
class
and make the class conform to the NSURLSessionDelegate
andNSURLSessionDownloadDelegate
protocols.
在 MTViewController 中聲明一個NSURLSession
屬性
session ,還有使這個類遵循
NSURLSessionDelegate 和
NSURLSessionDownloadDelegate 協議。
#import "MTViewController.h" #import "MWFeedParser.h" #import "SVProgressHUD.h" #import "MTEpisodeCell.h" @interface MTViewController () @property (strong, nonatomic) NSDictionary *podcast; @property (strong, nonatomic) NSMutableArray *episodes; @property (strong, nonatomic) MWFeedParser *feedParser; @property (strong, nonatomic) NSURLSession *session; @end
In viewDidLoad
, we set the session
property
by invoking backgroundSession
on the view controller instance.
This is one of the gotchas I was talking about.
在 viewDidLoad 方法中,調用 backgroundSession
方法設置 session 屬性,下面將介紹這個方法:
- (void)viewDidLoad { [super viewDidLoad]; // Setup View [self setupView]; // Initialize Session [self setSession:[self backgroundSession]]; // Load Podcast [self loadPodcast]; // Add Observer [[NSUserDefaults standardUserDefaults] addObserver:self forKeyPath:@"MTPodcast" options:NSKeyValueObservingOptionNew context:NULL]; }
Let’s take a look at the implementation of backgroundSession
.
In backgroundSession
, we statically declare a session
variable
and use dispatch_once
(Grand Central Dispatch) to instantiate
the background session. Even though this isn’t strictly necessary, it emphasizes the fact that we only need one background session at any time. This is a best practice that’s also mentioned in the WWDC
session on the NSURLSession
API.
下面看看
backgroundSession 方法的實現。在
backgroundSession 方法中,聲明一個靜態變量 session ,使用dispatch_once
(GCD)實例化這個後臺會話對象。雖然這並不是絕對需要的,但是在隻需要一個後臺會話對象的時候是有必要的這麼處理的( dispatch_once)。這也是在WWDC 中提到的最佳實踐。(原話:So,
I do this inside of a dispatch once here to emphasize the fact that you should only be creating a session with a given identifier once.)
In the dispatch_once
block, we start by creating a NSURLSessionConfiguration
object
by invoking backgroundSessionConfiguration:
and passing
a string as an identifier. The identifier we pass uniquely identifies the background session, which is key as we’ll see a bit later. We then create a session instance by invokingsessionWithConfiguration:delegate:delegateQueue:
and
passing the session configuration object, setting the session’s delegate
property,
and passing nil
as the third argument.
在
dispatch_once block 中,通過調用 backgroundSessionConfiguration: 方法,傳遞一個字符串作為 identifier(標識符)
創建一個 NSURLSessionConfiguration 對象。這個標識符是唯一的。然後通過調用 sessionWithConfiguration:delegate:delegateQueue:
方法傳遞會話配置對象作為參數創建一個會話對象實例,同時設置會話對象的 delegate 屬性為 self ,傳遞 nil 給第三個參數。
- (NSURLSession *)backgroundSession { static NSURLSession *session = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ // Session Configuration NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration backgroundSessionConfiguration:@"com.mobiletuts.Singlecast.BackgroundSession"]; // Initialize Session session = [NSURLSession sessionWithConfiguration:sessionConfiguration delegate:self delegateQueue:nil]; }); return session; }
By passing nil
as
the third argument ofsessionWithConfiguration:delegate:delegateQueue:
,
the session creates a serial operation queue (串行操作隊列)for us. This operation queue is used for performing the delegate method calls and completion handler calls.
3. Download Episode
Step 1: Create Download Task
It’s time to make use of the background session we created and put the MTEpisodeCell
to
use. Let’s start by implementing tableView:didSelectRowAtIndexPath:
,
a method of the UITableViewDelegate
protocol. Its implementation
is straightforward as you can see below. We fetch the correct MWFeedItem
instance
from the episodes
array and pass it todownloadEpisodeWithFeedItem:
.
是時候開始使用剛創建的後臺會話對象和
MTEpisodeCell 類瞭。先實現 UITableViewDelegate 的委托方法:tableView:didSelectRowAtIndexPath:
。實現過程是簡單的,從
episodes 數組中獲取到對應的 MWFeedItem 實例對象,傳遞給downloadEpisodeWithFeedItem: 方法。
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { [tableView deselectRowAtIndexPath:indexPath animated:YES]; // Fetch Feed Item MWFeedItem *feedItem = [self.episodes objectAtIndex:indexPath.row]; // Download Episode with Feed Item [self downloadEpisodeWithFeedItem:feedItem]; }
In downloadEpisodeWithFeedItem:
, we extract the remote URL
from the feed item by invoking urlForFeedItem:
, create
a download task by calling downloadTaskWithURL:
on the
background session, and send it a message of resume
to
start the download task.
在 downloadEpisodeWithFeedItem: 方法中,調用 urlForFeedItem:
方法提取出 URL ,然後後臺會話對象調用
downloadTaskWithURL: 方法創建一個下載任務,調用
resume 啟動下載任務。
- (void)downloadEpisodeWithFeedItem:(MWFeedItem *)feedItem { // Extract URL for Feed Item NSURL *URL = [self urlForFeedItem:feedItem]; if (URL) { // Schedule Download Task [[self.session downloadTaskWithURL:URL] resume]; } }
As you may have guessed, urlForFeedItem:
is a convenience
method that we use. We’ll use it a few more times in this project. We obtain a reference to the feed item’senclosures
array,
extract the first enclosure, and pull out the object for the url
key.
We create and return an NSURL
instance.
正如你所猜測的那樣,urlForFeedItem: 方法是一個輔助方法,我們已經在項目中多次使用到瞭。在該方法中,先獲取到feed item的enclosures
數組,取出第一項,根據 key 值獲取 value ,然後創建一個 NSURL 實例,並返回。
- (NSURL *)urlForFeedItem:(MWFeedItem *)feedItem { NSURL *result = nil; // Extract Enclosures NSArray *enclosures = [feedItem enclosures]; if (!enclosures || !enclosures.count) return result; NSDictionary *enclosure = [enclosures objectAtIndex:0]; NSString *urlString = [enclosure objectForKey:@"url"]; result = [NSURL URLWithString:urlString]; return result; }
We’re not done yet. Is the compiler giving you three warnings? That’s not surprising as we haven’t implemented the required methods of the NSURLSessionDelegate
andNSURLSessionDownloadDelegate
protocols
yet. We also need to implement these methods if we want to show the progress of the download tasks.
是不是編譯器給出三個警告吧,所以任務還沒有完成。這並不奇怪,因為我們還沒有實現 NSURLSessionDelegate 和 NSURLSessionDownloadDelegate
協議中的方法,所以為瞭顯示下載任務的進度,需要實現這些方法。
Step 2: Implementing Protocol(s)
The first method we need to implement is URLSession:downloadTask:didResumeAtOffset:
.
This method is invoked if a download task is resumed. Because this is something we won’t cover in this tutorial, we simply log a message to Xcode’s console.
第一個需要實現的是 URLSession:downloadTask:didResumeAtOffset: 方法,下載任務重新啟動的時候就會調用到該方法,這裡隻是簡單的在Xcode終端中輸出一個消息。
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didResumeAtOffset:(int64_t)fileOffset expectedTotalBytes:(int64_t)expectedTotalBytes { NSLog(@"%s", __PRETTY_FUNCTION__); }
More interesting is the implementation ofURLSession:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite:
.
This method is invoked every time a few bytes have been downloaded by the session. In this delegate method, we calculate the progress, fetch the correct cell, and update the cell’s progress property, which in turn updates the cell’s progress view. Have you
spotted the dispatch_async
call? There’s no guarantee that
the delegate method is invoked on the main thread. Since we update the user interface by setting the cell’s progress, we need to update the cell’s progress
property
on the main thread.
對於 URLSession:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite: 方法,當會話對象下載到數據時就會調用到。在這個委托方法中,先計算進度,獲取到正確的表單元,更新這個表單元的進度屬性,同時更新表單元的進度顯示視圖。註意到
dispatch_async 瞭嗎?我們無法保證這個委托方法一定會在主線程中調用,但一旦該委托方法被調用到,那麼就一定要對表單元的progress
屬性在主線程中進行更新。
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite { // Calculate Progress double progress = (double)totalBytesWritten / (double)totalBytesExpectedToWrite; // Update Table View Cell MTEpisodeCell *cell = [self cellForForDownloadTask:downloadTask]; dispatch_async(dispatch_get_main_queue(), ^{ [cell setProgress:progress]; }); }
The implementation of cellForForDownloadTask:
is straightforward.
We pull the remote URL from the download task using its originalRequest
property
and loop over the feed items in the episodes
array until
we have a match. When we’ve found a match, we ask the table view for the corresponding cell and return it.
cellForForDownloadTask: 方法的實現比較簡單。根據 downloadTask 參數中的 originalRequest
屬性提取到 URL。然後遍歷 episodes 數組中的 feed item,從 table view 中找到一個匹配對應的 cell,並返回。
- (MTEpisodeCell *)cellForForDownloadTask:(NSURLSessionDownloadTask *)downloadTask { // Helpers MTEpisodeCell *cell = nil; NSURL *URL = [[downloadTask originalRequest] URL]; for (MWFeedItem *feedItem in self.episodes) { NSURL *feedItemURL = [self urlForFeedItem:feedItem]; if ([URL isEqual:feedItemURL]) { NSUInteger index = [self.episodes indexOfObject:feedItem]; cell = (MTEpisodeCell *)[self.tableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:index inSection:0]]; break; } } return cell; }
The third delegate method of the NSURLSessionDownloadDelegate
protocol
that we need to implement is URLSession:downloadTask:didFinishDownloadingToURL:
.
As I mentioned in the previous tutorials, one of the advantages of the NSURLSession
API
is that downloads are immediately written to disk. The result is that we are passed a local URL inURLSession:downloadTask:didFinishDownloadingToURL:
.
However, the local URL that we receive, points to a temporary file. It is our responsibility to move the file to a more permanent location and that’s exactly what we do inURLSession:downloadTask:didFinishDownloadingToURL:
.
NSURLSessionDownloadDelegate 協議的第三個需要實現的是 URLSession:downloadTask:didFinishDownloadingToURL:
方法。正如我在前面教程中提到的, NSURLSession 的API 是將下載內容寫入磁盤。其結果是,傳遞一個本地 URL 參數給
URLSession:downloadTask:didFinishDownloadingToURL: 方法,然後我們接收到的 URL 參數卻是指向一個臨時文件。所以在該方法中我們有必要將文件移到到一個持久化保存的固定位置。
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location { // Write File to Disk [self moveFileWithURL:location downloadTask:downloadTask]; }
In moveFileWithURL:downloadTask:
, we extract the episode’s
file name from the download task and create a URL in the application’s Documents directory by invokingURLForEpisodeWithName:
.
If the temporary file that we received from the background session points to a valid file, we move that file to its new home in the application’s Documents directory.
在 moveFileWithURL:downloadTask: 方法中,我們從下載任務中提取到情節(episode)文件名,通過調用URLForEpisodeWithName:
方法創建一個該應用程序 Documents 目錄路徑的 URL。如果從後臺會話對象中接收到的臨時文件是一個有效的文件,我們將該文件移動到應用程序的 Documents 目錄下。
- (void)moveFileWithURL:(NSURL *)URL downloadTask:(NSURLSessionDownloadTask *)downloadTask { // Filename NSString *fileName = [[[downloadTask originalRequest] URL] lastPathComponent]; // Local URL NSURL *localURL = [self URLForEpisodeWithName:fileName]; NSFileManager *fm = [NSFileManager defaultManager]; if ([fm fileExistsAtPath:[URL path]]) { NSError *error = nil; [fm moveItemAtURL:URL toURL:localURL error:&error]; if (error) { NSLog(@"Unable to move temporary file to destination. %@, %@", error, error.userInfo); } } }
I use a lot of helper methods in my iOS projects, because it makes for DRY code.
It’s also good practice to create methods that only do one thing. Testing becomes much easier that way.(譯者註: DRY:Don‘t repeat yourself)
URLForEpisodeWithName:
is another helper method, which invokes episodesDirectory
.
InURLForEpisodeWithName:
, we append the name
argument
to the Episodes directory, which is located in the application’s Documents directory.
在 URLForEpisodeWithName: 方法中,調用 episodesDirectory
方法,將 name 參數追加到情節文件目錄,它位於應用程序的 Documents 目錄。
- (NSURL *)URLForEpisodeWithName:(NSString *)name { if (!name) return nil; return [self.episodesDirectory URLByAppendingPathComponent:name]; }
In episodesDirectory
, we create the URL for the Episodes
directory and create the directory if it doesn’t exist yet.
在 episodesDirectory 方法中,創建一個指向Episodes 目錄的URL,並返回。
- (NSURL *)episodesDirectory {
NSURL *documents = [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject];
NSURL *episodes = [documents URLByAppendingPathComponent:@"Episodes"];
NSFileManager *fm = [NSFileManager defaultManager];
if (![fm fileExistsAtPath:[episodes path]]) {
NSError *error = nil;
[fm createDirectoryAtURL:episodes withIntermediateDirectories:YES attributes:nil error:&error];
if (error) {
NSLog(@"Unable to create episodes directory. %@, %@", error, error.userInfo);
}
}
return episodes;
}
Step 3: Build and Run
Run the application and test the result by downloading an episode from the list of episodes. You should see the table view cell’s progress view progress from left to right reflecting the progress of the download task. There are a few issues though. Have you
tried scrolling through the table view? That doesn’t look right. Let’s fix that.
運行程序,從
episodes 列表中選擇一個 episode 下載進行測試。你應該可以看到選中的表視圖單元的進度顯示從左往右,反映出下載任務的進度。但還有幾個問題,試一下滾動表視圖,這看起來好像不對吧,接下來解決它吧!
4. Create a Progress Buffer
Because the table view reuses cells as much as possible, we need to make sure that each cell properly reflects the download state of the episode that it represents. We can fix this in several ways. One approach is to use an object that keeps track of the progress
of each download task, including the download tasks that have already completed.
由於table view中存在表單元的復用機制,而我們需要每一個表單元都顯示各自代表的 episode 文件下載狀態。有幾種方式解決。一種解決方式是使用一個對象來保存每一個下載任務的進度,包括已經下載完畢的任務。
Step 1: Declare a Property
Let’s start by declaring a new private property progressBuffer
of
typeNSMutableDictionary
in the MTViewController
class.
在 MTViewController 類中聲明一個 NSMutableDictionary
類型的私有屬性 progressBuffer 。
#import "MTViewController.h" #import "MWFeedParser.h" #import "SVProgressHUD.h" #import "MTEpisodeCell.h" @interface MTViewController () @property (strong, nonatomic) NSDictionary *podcast; @property (strong, nonatomic) NSMutableArray *episodes; @property (strong, nonatomic) MWFeedParser *feedParser; @property (strong, nonatomic) NSURLSession *session; @property (strong, nonatomic) NSMutableDictionary *progressBuffer; @end
Step 2: Initialize Buffer
In viewDidLoad
, we initialize the progress
buffer as shown below.
在
viewDidLoad 方法中,初始化
progress buffer 這個對象。
- (void)viewDidLoad { [super viewDidLoad]; // Setup View [self setupView]; // Initialize Session [self setSession:[self backgroundSession]]; // Initialize Progress Buffer [self setProgressBuffer:[NSMutableDictionary dictionary]]; // Load Podcast [self loadPodcast]; // Add Observer [[NSUserDefaults standardUserDefaults] addObserver:self forKeyPath:@"MTPodcast" options:NSKeyValueObservingOptionNew context:NULL]; }
Step 3: Update Table View Cells
The key that we’ll use in the dictionary is the remote URL of the corresponding feed item. With this in mind, we can update the tableView:cellForRowAtIndexPath:
method
as shown below. We pull the remote URL from the feed item and ask progressBuffer
for
the value for the key that corresponds to the remote URL. If the value isn’t nil
,
we set the cell’s progress
property to that value, otherwise
we set the progress
property of the cell to 0.0
,
which hides the progress view by setting its width to 0.0
.
字典中的 key 值是每一 feed item對應的下載鏈接地址 URL。考慮到這一點,我們就可以在 tableView:cellForRowAtIndexPath: 方法中進行如下的修改。先從
feed item 中獲取到下載鏈接地址 URL,然後對應的從
progressBuffer 字典中獲取到 value 值,也即是下載的進度。如果這個value值不是 nil,那麼就將表單元
progress 屬性設置為該 value 值;否則將其置為 0.0,這樣進度條顯示寬帶為 0.0。
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { MTEpisodeCell *cell = (MTEpisodeCell *)[tableView dequeueReusableCellWithIdentifier:EpisodeCell forIndexPath:indexPath]; // Fetch Feed Item MWFeedItem *feedItem = [self.episodes objectAtIndex:indexPath.row]; NSURL *URL = [self urlForFeedItem:feedItem]; // Configure Table View Cell [cell.textLabel setText:feedItem.title]; [cell.detailTextLabel setText:[NSString stringWithFormat:@"%@", feedItem.date]]; NSNumber *progress = [self.progressBuffer objectForKey:[URL absoluteString]]; if (!progress) progress = @(0.0); [cell setProgress:[progress floatValue]]; return cell; }
Step 4: Avoid Duplicates
We can also use the progress buffer to prevent users from downloading the same episode twice. Take a look at the updated implementation oftableView:didSelectRowAtIndexPath:
.
We take the same steps we took intableView:cellForRowAtIndexPath:
to
extract the progress value from the progress buffer. Only when the progress value is nil
,
we download the episode.
我們根據 progress buffer 還可以阻止用戶對同一 episode 文件下載兩次。更新 tableView:didSelectRowAtIndexPath:
方法如下。如 方法中那樣從 progress buffer 中獲取到某一表單元cell的進度的值,隻有進度值為 nil,我們才需要進行下載。
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { [tableView deselectRowAtIndexPath:indexPath animated:YES]; // Fetch Feed Item MWFeedItem *feedItem = [self.episodes objectAtIndex:indexPath.row]; // URL for Feed Item NSURL *URL = [self urlForFeedItem:feedItem]; if (![self.progressBuffer objectForKey:[URL absoluteString]]) { // Download Episode with Feed Item [self downloadEpisodeWithFeedItem:feedItem]; } }
Step 5: Update Buffer
The progress buffer only works in its current implementation if we keep it up to date. This means that we need to update theURLSession:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite:
method
as well. All we do is store the new progress value in the progress buffer.
progress buffer 隻有在以下方法執行的時候才進行更新,這就意味著需要對 URLSession:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite:
方法進行修改如下。我們隻是將新的下載進度值保存到 progress buffer中。
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite { // Calculate Progress double progress = (double)totalBytesWritten / (double)totalBytesExpectedToWrite; // Update Progress Buffer NSURL *URL = [[downloadTask originalRequest] URL]; [self.progressBuffer setObject:@(progress) forKey:[URL absoluteString]]; // Update Table View Cell MTEpisodeCell *cell = [self cellForForDownloadTask:downloadTask]; dispatch_async(dispatch_get_main_queue(), ^{ [cell setProgress:progress]; }); }
In downloadEpisodeWithFeedItem:
, we set the progress value
to 0.0
when the download task starts.
在
downloadEpisodeWithFeedItem: 方法中,下載任務啟動的時候,設置其進度值為 0.0
- (void)downloadEpisodeWithFeedItem:(MWFeedItem *)feedItem { // Extract URL for Feed Item NSURL *URL = [self urlForFeedItem:feedItem]; if (URL) { // Schedule Download Task [[self.session downloadTaskWithURL:URL] resume]; // Update Progress Buffer [self.progressBuffer setObject:@(0.0) forKey:[URL absoluteString]]; } }
The session delegate is notified when a download task finishes. InURLSession:downloadTask:didFinishDownloadingToURL:
,
we set the progress value to 1.0
.
當下載任務結束的時候,會通知會話的委托方法,我們在 URLSession:downloadTask:didFinishDownloadingToURL: 委托方法中設置下載任務的進度值為
1.0。
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location { // Write File to Disk [self moveFileWithURL:location downloadTask:downloadTask]; // Update Progress Buffer NSURL *URL = [[downloadTask originalRequest] URL]; [self.progressBuffer setObject:@(1.0) forKey:[URL absoluteString]]; }
Step 6: Restore Buffer
At the moment, the progress buffer is only stored in memory, which means that it’s cleared between application launches. We could write its contents to disk, but to keep this application simple we are going to restore or recreate the buffer by checking which
episodes have already been downloaded. The feedParser:didParseFeedItem:
method,
part of the MWFeedParserDelegate
protocol, is invoked for
every item in the feed. In this method, we pull the remote URL from the feed item, create the corresponding local URL, and check if the file exists. If it does, then we set the corresponding progress value for that feed item to 1.0
to
indicate that it’s already been downloaded.
目前,progress buffer 對象隻是保存在內存中,這就意味著應用程序退出的時候就會被清除。我們可以將其寫入磁盤中持久保存,但為瞭保持應用程序的簡潔,我們可以通過檢查episodes 已經下載的進度,然後重新創建恢復
progress buffer。 MWFeedParserDelegate 協議的feedParser:didParseFeedItem:
方法,feed 中的每一個 item 都會調用這個方法。在這個方法中,從 item 中提取到下載鏈接remote URL ,然後得到對應的本地保存路徑 local URL,檢查下載文件是否存在。如果存在,則設置對應的下載進度值為
1.0 ,表明已經下載完畢瞭。
- (void)feedParser:(MWFeedParser *)parser didParseFeedItem:(MWFeedItem *)item { if (!self.episodes) { self.episodes = [NSMutableArray array]; } [self.episodes addObject:item]; // Update Progress Buffer NSURL *URL = [self urlForFeedItem:item]; NSURL *localURL = [self URLForEpisodeWithName:[URL lastPathComponent]]; if ([[NSFileManager defaultManager] fileExistsAtPath:[localURL path]]) { [self.progressBuffer setObject:@(1.0) forKey:[URL absoluteString]]; } }
Step 7: Rinse and Repeat
Run the application one more time to see if the issues with the table view are resolved. The application should now also remember which episodes have already been downloaded.
再一次運行應用程序,看看表視圖有關的問題是否都解決瞭。現在應用程序應該知道哪些已經下載瞭。
5. Being a Good Citizen
It’s important that our application is a good citizen by not wasting more CPU cycles or consume more battery power than needed. What does this mean for our podcast client. When a download task is started by our application and the application goes to the background,
the background daemon that manages our application’s download task notifies our application through the background session that the download task has finished. If necessary, the background daemon will launch our application so that it can respond to these
notifications and process the downloaded file.
應該好的應用程序應該充分利用CPU的運行周期,並且不消耗更多電量。對於我們的播客客戶端,啟動一個下載任務,如果應用程序退到後臺,那麼後臺的守護進程就管理這個應用程序的下載任務,而且當後臺會話的下載任務完成下載,就會通知應用程序,如果有必要,後臺守護進程將會啟動應用程序,以便其可以處理下載的文件。
In our example, we don’t need to do anything special to make sure that our application reconnects to the original background session. This is taken care of by theMTViewController
instance.
However, we do have to notify the operating system when our application has finished processing the download(s) by invoking a background completion handler.
在這個例子中,我們不需要對應用程序做額外特殊的處理使得應用程序和後臺會話重新連接。但是,當下載完成後,我們也要通過調用完成處理程序塊通知操作系統該應用程序的下載任務完成瞭。
When our application is woken up by the operating system to respond to the notifications of the background session, the application delegate is sent a message ofapplication:handleEventsForBackgroundURLSession:completionHandler:
.
In this method, we can reconnect to the background session, if necessary, and invoke the completion handler that is passed to us. By invoking the completion handler, the operating system knows that our application no longer needs to run in the background.
This is important for optimizing battery life. How do we do this in practice?
當應用程序被操作系統喚醒,對後臺會話通知做出響應時,應用程序會調用 application:handleEventsForBackgroundURLSession:completionHandler:
委托方法。在這個方法中,應用程序和後臺會話對象重新建立連接,如果有必要,也會執行傳遞參數:完成處理程序塊。通過調用完成處理程序塊,操作系統就知道,應用程序不再需要在後臺繼續運行瞭,這對於優化電池壽命是恒友好處的。如何將這一點付諸實踐呢?
Step 1: Declare a Property
We first need to declare a property on the MTAppDelegate
class
to keep a reference to the completion handler that we get fromapplication:handleEventsForBackgroundURLSession:completionHandler:
.
The property needs to be public. The reason for this will become clear in a moment.
首先需要在
MTAppDelegate 類中聲明一個公有屬性:完成處理程序塊(completion handler),保存在application:handleEventsForBackgroundURLSession:completionHandler: 委托方法中的參數。這麼做的原因後面就知道瞭。
#import @interface MTAppDelegate : UIResponder @property (strong, nonatomic) UIWindow *window; @property (copy, nonatomic) void (^backgroundSessionCompletionHandler)(); @end
Step 2: Implement Callback
In application:handleEventsForBackgroundURLSession:completionHandler:
,
we store the completion handler in backgroundSessionCompletionHandler
,
which we declared a moment ago.
在
application:handleEventsForBackgroundURLSession:completionHandler: 方法中,保存
backgroundSessionCompletionHandler 。
- (void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)())completionHandler { [self setBackgroundSessionCompletionHandler:completionHandler]; }
Step 3: Invoke Background Completion Handler
In the MTViewController
class, we start
by adding an import statement for theMTAppDelegate
class.
在 MTViewController 類中,添加
MTAppDelegate
類的 import 語句。
#import "MTViewController.h" #import "MWFeedParser.h" #import "MTAppDelegate.h" #import "SVProgressHUD.h" #import "MTEpisodeCell.h" @interface MTViewController () @property (strong, nonatomic) NSDictionary *podcast; @property (strong, nonatomic) NSMutableArray *episodes; @property (strong, nonatomic) MWFeedParser *feedParser; @property (strong, nonatomic) NSURLSession *session; @property (strong, nonatomic) NSMutableDictionary *progressBuffer; @end
We then implement another helper method, invokeBackgroundSessionCompletionHandler
,
which invokes the background completion handler stored in the application delegate’sbackgroundSessionCompletionHandler
property.
In this method, we ask the background session for all its running tasks. If there are no tasks running, we get a reference to the application delegate’s background completion handler and, if it isn’t nil
,
we invoke it and set it to nil
.
接著實現 invokeBackgroundSessionCompletionHandler 方法,這個方法會調用保存著應用程序委托的 backgroundSessionCompletionHandler
屬性。在這個方法中,我們查看後臺會話是否有任務在運行,如果沒有且不為 nil,則將其置為 nil。
- (void)invokeBackgroundSessionCompletionHandler { [self.session getTasksWithCompletionHandler:^(NSArray *dataTasks, NSArray *uploadTasks, NSArray *downloadTasks) { NSUInteger count = [dataTasks count] + [uploadTasks count] + [downloadTasks count]; if (!count) { MTAppDelegate *applicationDelegate = (MTAppDelegate *)[[UIApplication sharedApplication] delegate]; void (^backgroundSessionCompletionHandler)() = [applicationDelegate backgroundSessionCompletionHandler]; if (backgroundSessionCompletionHandler) { [applicationDelegate setBackgroundSessionCompletionHandler:nil]; backgroundSessionCompletionHandler(); } } }]; }
Wait a minute. When do we invoke invokeBackgroundSessionCompletionHandler
?
We do this every time a download task finishes. In other words, we invoke this method inURLSession:downloadTask:didFinishDownloadingToURL:
as
shown below.
那麼什麼時候調用
invokeBackgroundSessionCompletionHandler 方法呢?我們在每一次下載任務完成的時候進行調用。換言之,在 URLSession:downloadTask:didFinishDownloadingToURL:
方法中進行調用,如下:
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location { // Write File to Disk [self moveFileWithURL:location downloadTask:downloadTask]; // Update Progress Buffer NSURL *URL = [[downloadTask originalRequest] URL]; [self.progressBuffer setObject:@(1.0) forKey:[URL absoluteString]]; // Invoke Background Completion Handler [self invokeBackgroundSessionCompletionHandler]; }
6. Wrapping Up
I hope you agree that our podcast client isn’t ready for the App Store just yet since one of the key features, playing podcasts, is still missing. As I mentioned in the previous tutorial, the focus of this project wasn’t creating a full-featured podcast client.
The goal of this project was illustrating how to leverage the NSURLSession
API
to search the iTunes Search API and download podcast episodes using data and out-of-process download tasks respectively. You should now have a basic understanding of theNSURLSession
API
as well as out-of-process tasks.
你應該同意,現在我們實現的播客客戶端還不能上 App Store,因為隻實現瞭一個功能,播放功能還沒有實現。正如我在前面教程中所提到的,我們關註的重點不是實現一個完整功能的播客客戶端。我們的目標是通過這個項目展示如何利用 NSURLSession 的data task 對 iTunes 的 API 進行查詢;out-of-process
download task 下載播客節目。現在你應該對此有一個基本的瞭解瞭吧!
Conclusion
By creating a simple podcast client, we have taken a close look at data and download tasks. We’ve also learned how easy it is to schedule download tasks in the background. The NSURLSession
API
is an important step forward for both iOS and OS X, and I encourage you to take advantage of this easy to use and flexible suite of classes. In the final installment of this series, I will take a look at AFNetworking 2.0. Why is it a milestone release? When
should you use it? And how does it compare to theNSURLSession
API?
通過創建一個簡單的播客客戶端,我們已經對數據任務和下載任務有瞭一定的瞭解。我們也瞭解到安排下載任務在後臺執行也是簡單的。NSURLSession 是 iOS 和 OS X開發重要的一個內容,我建議你根據其優勢在開發中進行使用。最後,我會看看 AFNetworking
2.0 ,它是一個裡程碑意義的版本,什麼時候可以使用它?它和 NSURLSession
API
比較有怎樣?