iOS后臺模式開發指南

自從古老的iOS4以來,當用戶點擊home建的時候,你可以使你的APP們在內存中處于suspended(掛起)狀態.即使APP仍停留在內存中,它的所有操作是被暫停的直到用戶再次運行它.

當然這個規則中有例外情況.在特定的情況下,這個APP仍然可以在后臺中執行某些操作.這個教程會教你在什么時候怎么去用最常用的一些后臺操作.

每一次iOS的發布都會在后臺操作和細節上的放寬限制,以此提升用戶體驗和延長電池壽命.對于在iOS中實現"真正"的多任務來說,后臺模式不是一個神奇的解決辦法.當用戶切換到其他的APP應用時,大多數的APP應用仍然會完全的暫停運行.你的應用只被允許在很特殊的情況下才能在后臺中繼續運行.例如,這些包括播放音頻,獲取位置更新,或者從服務器獲取最新內容的情況.

iOS7之前,APP應用在真正暫停之前會有連續10分鐘的時間去完成它們當前的操作.隨著NSURLSession的出現,有了一種更為優雅的方式去應對大量的網絡切換.因此,對于可用的后臺運行時間已經減少到只有幾分鐘,而且不再必須為連續的.

這樣的后臺模式可能不適合你.但如果合適,請繼續閱讀!

接下來的學習中,將會有幾個幾個后臺模式提供給你.在本教程中你將建立一個關于簡單標簽應用的工程,來探索從連續播放視頻到周期性的獲取更新內容的四種常見模式.

開始

在深入這個工程之前,這里有一個iOS可用的基礎后臺模式的快速預覽.在Xcode 6中,你通過點擊目標程序的Capabilities(功能)選項卡能夠看到如下列表:

打開后臺模式功能列表(1)在項目導航欄中選擇項目(2)選擇目標應用(3)選擇功能選項卡(4)把后臺模式開關打開.

在這個教程中,你會研究四種后臺進程處理方式.

*視頻播放:APP可以在后臺播放或錄制視頻

*獲取位置更新:該應用會隨著設備位置的改變繼續回調結果.

*執行一定的任務:通常在沒有限制的情況下,這時APP會在有限的時間內運行任意的代碼.

*后臺獲取:通過iOS的更新計劃獲取最細的內容.

這個教程將按照上面的順序,在本教程的每個部分中介紹如何使用這四個模板.

從這個像觀光車一樣的工程開始,通過它熟悉一下iOS后臺機制,首先下載這個上手工程.有個好消息:用戶界面已經為你預配置好了.

運行這個示例項目,檢查一下你的四個選項卡.

這些選項卡是本教程剩余部分的路線圖.第一站:后臺視頻

提示:為了使后臺模式充分發揮作用,你應該使用一個真正的設備.根據我的經驗,如果你忘記配置設置,該APP在模擬器的后臺能很好的運行運行.然而,當你切換到真正的設備時,它將不會運行.

音頻播放

這里有iOS播放音頻的幾種方法,他們中的大部分需要實現回調函數去提供更多用來播放的音頻數據.當用戶使你的APP做某些事情,會調用回調函數(比如委托模型),在這種情況下,會把音波存儲在內存緩存區中.

如果你想播放流數據中的音頻,你可以開啟一個網絡連接,連接的這些回調函數提供連續的音頻數據.

當你激活音頻后臺模式后,即使你的APP現在沒在活動,iOS將繼續執行這些回調函數.音頻后臺模式是自動的,這么說很正確.你只是激活它,恰好為管理它提供了基礎設備.

對于我們這些有點小心思的人來說,如果你的APP確實為用戶播放音頻,你應該只使用后臺音頻模式.如果你嘗試使用這個模式只是為了獲取當程序安靜運行的時候使用CPU的時長,蘋果將拒絕你APP的運行.

在這部分,你將在你的APP中添加一個音頻播放器,打開后臺模式,為你演示它的運行過程.

為了獲取到音頻播放裝置,你需要學習 AV Foundation.打開AudioViewController.swift,在文件頂部import UIKit后面添加引用.

import AVFoundation

Override viewDidLoad() with the following implementation: 用下面的實現代碼重寫viewDidLoad()

override func viewDidLoad() {

super.viewDidLoad()

var error: NSError?

var success = AVAudioSession.sharedInstance().setCategory(

AVAudioSessionCategoryPlayAndRecord,

withOptions: .DefaultToSpeaker, error: &error)

if !success {

NSLog("Failed to set audio session category.? Error: \(error)")

}

}

這使用了音頻回話的單例模式sharedInstance()去設置播放的類別,也確保了聲音是通過手機揚聲器而不是通過手機聽筒傳播的.如果它執行了,他會檢查調用是否失敗并記錄錯誤.一個真正的APP在發生錯誤后會顯示一個隊伍的對話框,作為對錯誤的回應,但是我們不需要因為這些小細節而糾結.

接下來,你要把播放器這個成員屬性添加到AudioViewController中:

var player: AVQueuePlayer!

這是個隱式的可拓展的屬性,最初為nil,你將在viewDidLoad()對它進行初始化.

這個上手項目包含來自主要收納免版權稅的音樂網站incompetech.com的音頻文件.認證之后你可以免費的使用它上面的音樂.你這里使用的全部歌曲來自incompetech.com 上Kevin MacLeod的作品.謝謝Kevin!

返回viewDidLoad(),在此函數的末尾處添加如下方法:

let songNames = ["FeelinGood", "IronBacon", "WhatYouWant"]

let songs = songNames.map {

AVPlayerItem(URL: NSBundle.mainBundle().URLForResource($0, withExtension: "mp3"))

}

player = AVQueuePlayer(items: songs)

player.actionAtItemEnd = .Advance

這樣可以獲取到歌曲的列表,把它們映射到主程序包的路徑中并把它們轉化為可以在AVQueuePlayer上播放的AVPlayerItems.此外,這個隊列被設置為循環播放.

為了在隊列進程中更新歌曲名字,你需要觀察播放器中的currentItem.為了達到上述目的,需要在viewDidLoad()的末尾處添加如下代碼:

player.addObserver(self, forKeyPath: "currentItem", options: .New | .Initial , context: nil)

這使得每當播放器中currentItem改變,類觀察者的回調被初始化.

現在你可以添加觀察者模式方法.把下面代碼放到viewDidLoad()下面.

override func observeValueForKeyPath(keyPath: String, ofObject object: AnyObject, change: [NSObject : AnyObject], context: UnsafeMutablePointer) {

if keyPath == "currentItem", let player = object as? AVPlayer,

currentItem = player.currentItem?.asset as? AVURLAsset {

songLabel.text = currentItem.URL?.lastPathComponent ?? "Unknown"

}

}

當這個函數被調用的時候,你首先要確保這個被更新的屬性是你所關注的.在這種情形下,它不是那么重要了因為只有一個屬性被觀察,但是在你之后添加更多的觀察者的情況下去檢查,是個不錯的方法.如果它是currentItem鍵,你將使用它通過文件名更新songLabel.如果由于某些原因,當前項的URL不能獲取到,它將使songLabel顯示字符串"Unknown".

你也需要一個去更新timeLabel的方法來顯示當前播放項消耗的時間.使用addPeriodicTimeObserverForInterval(_:queue:usingBlock:)是達到當前目的最好的方法,該函數講調用給定的隊列當中提供的塊.在viewDidLoad()的末尾處添加如下代碼:

player.addPeriodicTimeObserverForInterval(CMTimeMake(1, 100), queue: dispatch_get_main_queue()) {

[unowned self] time in

let timeString = String(format: "%02.2f", CMTimeGetSeconds(time))

if UIApplication.sharedApplication().applicationState == .Active {

self.timeLabel.text = timeString

} else {

println("Background: \(timeString)")

}

}

這添加給播放器一個周期性的觀察者,如果這個APP在前臺,這個觀察者每一秒的1/100就會被調用一次并且更新UI.

重要提示:由于你想在結束時更新UI,你必須確保這些代碼在主隊列中被調用.這就是你指定dispatch_get_main_queue()參數的原因.

在這里暫停一下,思考應用的狀態.

你的應用處于下面五個狀態之一中.簡單地說,他們是:

*未運行:你的APP在開啟之前處于這個狀態.

*激活:一旦你的APP被開啟,它變成活躍狀態.

*未激活:當你的APP正在運行,但是一些事情打斷它的動作,比如有電話打進來,它變成inactive狀態.休眠意味著這個APP仍然在前臺運行,只是它沒有接收事件.

*后臺:在這個狀態下,你的APP不在前臺顯示了但是它仍然在執行代碼.

*掛起:你的APP進入不再運行代碼的狀態.

如果你想更深入的了解這些狀態之間的區別,蘋果網站的Execution States for Apps對此有很詳細介紹.

你可以通過讀取UIApplication.sharedApplication().applicationState來檢查APP的轉臺.記住:你只能獲取三種狀態的返回值: .Active, .Inactive, and .Background.當你的APP在執行代碼的時候,掛起狀態和未運行狀態很明顯不可能出現.

讓我們將目光繼續放在之前代碼上,如果該應用處于激活狀態,你需要更新音樂標題欄.在后臺中,你仍然能夠更新這個label的文字,但是這點知識證明了當你APP在后臺的時候繼續接受回調.

現在,把剩余的代碼添加到playPauseAction(:)的實現中,讓播放/暫停按鈕工作.在AudioViewController中,把下面代碼添加到playPauseAction(:)的實現中:

@IBAction func playPauseAction(sender: UIButton) {

sender.selected = !sender.selected

if sender.selected {

player.play()

} else {

player.pause()

}

}

很好,這是你全部的代碼.創建并運行,你將看到下面的樣子:

現在,點播放,音樂將開始.很好!

測試后臺模式是否起作用.按home按鈕(如果你正在使用模擬器,按Cmd-Shift-H).如果你在真正的設備上運行(不是Xcode 6.1的模擬器)音樂將停止.這是為什么呢?還有很重要的一塊落下了!)

對于大多數的后臺模式("Whatever"模式除外)你需要在Info.plist中添加一個key用來指明APP在后臺中運行的代碼.幸運的是,在Xcode6可以通過復選框進行選擇.

回到Xcode,按照以下步驟進行操作:

1.在項目管理器中點擊工程

2.點擊目標TheBackgrounder

3.點擊功能標簽

4.滑動背景模式并設置為ON

5.選中 Audio和AirPlay

重新編譯并且運行.開始運行音樂并且點擊home鍵,盡管這個APP在后臺運行,這次你就會依舊能夠聽到音樂.

You should also see the time updates in your Console output in Xcode, proof that your code is still working even though the app is in the background. You can download the partially finished sample project up to this point.

在Xcode的輸出里你也能夠在控制臺看到實時的更新,著就證明了雖然你的APP在后臺運行,但是你的代碼依舊在工作.現在你可以下載部分完成的示例代碼了.

以上第一個模式結束了,如果你想學完整個教程--那就繼續往下讀吧!

接收位置更新

當在后臺模式進行定位時,你的APP依舊會隨著用戶更新位置而接收到位置信息,甚至APP在后臺的時候.你可以控制這些位置更新的準確性,甚至改變精度.

如果你的app真正需要這些信息來為用戶提供價值,你只能使用后臺模式.如果你使用這個模式并且Apple看到用戶將要獲得這些信息,你的應用程序將會被拒絕.有時蘋果也將要求你向app添加一個警告的描述說明app將導致增加電量的使用.

第二步是為了位置更新,打開LocationViewController.swift并且向里面增加一些屬性用來初始化LocationViewController.

var locations = [MKPointAnnotation]()

lazy var locationManager: CLLocationManager! = {

let manager = CLLocationManager()

manager.desiredAccuracy = kCLLocationAccuracyBest

manager.delegate = self

manager.requestAlwaysAuthorization()

return manager

}()

你將使用locations來存儲能夠繪制在地圖上的位置信息.CLLocationManager可以使你能夠從設備上獲取位置更新.你使用延遲的方法實例化它,所以當你第一次訪問該屬性被調用的函數時,它才被初始化.

代碼可以設置位置管理器的精確度來實現最高的精確,你可以調節到你的app所需要的精確度.你會了解更多關于其他精度設置和它們的重要性.注意你也可以調用requestAlwaysAuthorization().這是在IOS8中的要求,并且為用戶提供了接口來允許用戶在后臺使用位置.

現在你可以填寫空的accuracyChanged(_:)的實現在LocationViewController里:

@IBAction func accuracyChanged(sender: UISegmentedControl) {

let accuracyValues = [

kCLLocationAccuracyBestForNavigation,

kCLLocationAccuracyBest,

kCLLocationAccuracyNearestTenMeters,

kCLLocationAccuracyHundredMeters,

kCLLocationAccuracyKilometer,

kCLLocationAccuracyThreeKilometers]

locationManager.desiredAccuracy = accuracyValues[sender.selectedSegmentIndex];

}

accuracyValues是由CLLocationManager的desiredAccuracy可能值構成的數組.這些變量控制了你的位置的精確度.

你可能認為這種方式是愚蠢的.為什么位置管理器不能夠給你最精確的位置信息呢?最重要的原因是為了節省電量.低精確意味著耗電量較低.

這就意味著你應該選擇最少的值實現你的app可以承受的最低限度的精確度.你隨時可以修改這些值在你的需求.

另一個性能就是你可以控制你的app接收位置更新的頻率,忽視desiredAccuracy: distanceFilter的值.當你的設備移動到了一定的值(以米計算)時,這個性能告訴位置管理器你只想接收位置更新.這個值應該最大限度的節省你的電池消耗.

現在你可以在enabledChanged(_:)中添加代碼來實現獲取位置更新:

@IBAction func enabledChanged(sender: UISwitch) {

if sender.on {

locationManager.startUpdatingLocation()

} else {

locationManager.stopUpdatingLocation()

}

}

這個代碼示例有一個與動作相關的UISwitch,這個UISwitch實現了位置跟蹤的開啟與關閉.

下一步你可以通過添加一個CLLocationManagerDelegate方法來接收位置更新.添加以下方法到LocationViewController中.

// MARK: - CLLocationManagerDelegate

func locationManager(manager: CLLocationManager!, didUpdateToLocation newLocation: CLLocation!, fromLocation oldLocation: CLLocation!) {

// Add another annotation to the map.

let annotation = MKPointAnnotation()

annotation.coordinate = newLocation.coordinate

// Also add to our map so we can remove old values later

locations.append(annotation)

// Remove values if the array is too big

while locations.count > 100 {

let annotationToRemove = locations.first!

locations.removeAtIndex(0)

// Also remove from the map

mapView.removeAnnotation(annotationToRemove)

}

if UIApplication.sharedApplication().applicationState == .Active {

mapView.showAnnotations(locations, animated: true)

} else {

NSLog("App is backgrounded. New location is %@", newLocation)

}

}

如果app的狀態是激活狀態,這些代碼將更新地圖.如果這個app在后臺運行,你應該在xcode的控制臺來看位置更新的log.

現在你已經知道了后臺模式,現在你不應該犯以前的相同的錯誤了.現在你可以在Location updates中設置使得ios知道你的app想在后臺運行時繼續接受位置更新.

除了更改這個之外,你應該在你的Info.plist中設置一個關鍵詞來允許你向使用者解釋為什么后臺更行數據是需要的.如果不被允許后臺更新,位置更新就會慢慢地失敗.

步驟如下:

1.選擇Info.plist文件

2.點擊+號來添加一個關鍵詞

3.點擊這個關鍵詞的名字:NSLocationAlwaysUsageDescription

4.描述為什么你需要在后臺位置更新,能夠另使用者信服.

現在你可以編譯并且運行你的程序了.切換到第二個選項卡并打開開關.

當你第一次運行的時候,你會看到你寫入到Info.plist中的信息.點擊allow出去走走,或者圍繞你周圍的建筑轉一轉.這時候你就開始看到位置信息的更新,在模擬器里也可以實現.

過一會,你將會看到如下的一些東西:

如果你在后臺運行你的app,你將會在你的控制臺log看到你的app位置更新信息.重新打開你的app,你就會發現地圖上有所有的位置點,這些就是你的app在后臺 運行時候更新的數據.

如果你使用的是模擬器,你也可以使用這個app來模擬這個動作.打開菜單Debug \ Location:

設置location選項為Freeway Drive然后點擊home按鈕.這時候你就會看到在控制臺打印出你的程序運行的狀態,就像你在模擬你開車在加利福尼亞的高速公路上.

2014-12-21 20:05:13.334 TheBackgrounder[21591:674586] App is backgrounded. New location is <+37.33482105,-122.03350886> +/- 5.00m (speed 15.90 mps / course 255.94) @ 12/21/14, 8:05:13 PM Pacific Standard Time

2014-12-21 20:05:14.813 TheBackgrounder[21591:674586] App is backgrounded. New location is <+37.33477977,-122.03369603> +/- 5.00m (speed 17.21 mps / course 255.59) @ 12/21/14, 8:05:14 PM Pacific Standard Time

2014-12-21 20:05:15.320 TheBackgrounder[21591:674586] App is backgrounded. New location is <+37.33474691,-122.03389325> +/- 5.00m (speed 18.27 mps / course 257.34) @ 12/21/14, 8:05:15 PM Pacific Standard Time

2014-12-21 20:05:16.330 TheBackgrounder[21591:674586] App is backgrounded. New location is <+37.33470894,-122.03411085> +/- 5.00m (speed 19.27 mps / course 257.70) @ 12/21/14, 8:05:16 PM Pacific Standard Time

現在你可以下載這個示例程序了,到第三個選項卡和第三個后臺模式.

執行有限長任務等

下一個后臺模式在可以正式的稱為后臺執行有限長的任務(Executing a Finite-Length Task in the Background).

嚴格的說這并不是真正意義上的后臺模式,因為你并沒有在Info.plist中聲明在你的app中使用這個模式(或者在復選框中使用Background Mode).相反,它只是一個api你可以讓你的任意代碼運行有限的時間,當你的app在后臺運行的時候.

在過去,這個模式只是在上傳或者下載或者運行某一段時間來完成某一項任務.但是如果這個鏈接很緩慢或者這個進行一直不結束怎么辦?它會讓你的應用程序在一個奇怪的狀態,你必須添加大量的代碼來處理錯誤使得程序穩健地工作. 因為這樣的原因,Apple介紹了NSURLSession.

NSURLSession在面對后臺運行甚至設備重啟時具有魯棒性,并且以減少設備能耗的方式完成任務.如果你想處理大規模的下載,請查看我們的NSURLSession tutorial.

這種后臺運行模式對完成一些長時間的任務還是一種非常有效的方法,比如在相機相冊中進行渲染和寫入一個視頻.

但是這只是一個例子.你可以運行的代碼是任意的,你可以用這個api來實現任意的事情:運行長時間的計算,將過濾器應用到圖像處理,渲染一個復雜3 d網格...whatever!只要是你想在長時間運行你的程序你都可以用這個api.

你的app在后臺運行的時間取決于ios系統.對于后臺運行時間你可以在UIApplication中查詢backgroundTimeRemaining,它將會告訴你剩余多長時間.

一般來說你會有3分鐘時間來實現.但是在api文檔中并沒有給一個大約的時間,所以你不能依賴這個時間,可能是5分鐘也可能是5秒.所以你的app需要準備發生的任何事情.

這里給一個計算機學生都熟悉的任務:斐波納契數列.

這里的意義是,你會在后臺計算這些數字!

打開WhateverViewController.swift并且在WhateverViewController里面添加屬性.

var previous = NSDecimalNumber.one()

var current = NSDecimalNumber.one()

var position: UInt = 1

var updateTimer: NSTimer?

var backgroundTask: UIBackgroundTaskIdentifier = UIBackgroundTaskInvalid

NSDecimalNumbers將保存序列中的前兩個數的值.NSDecimalNumbers可以保存大的數據,因此非常適合你的目標.Position只是一個計數器來告訴你這個這個數在當前序列中的位置.

你將使用updateTimer證明甚至計時器繼續使用這個API時,也稍微放慢速度的計算,這樣你就可以觀察他們.

在WhateverViewController中添加一些實用方法來重置斐波那契計算,啟動和停止能夠后臺運行的任務:

func resetCalculation() {

previous = NSDecimalNumber.one()

current = NSDecimalNumber.one()

position = 1

}

func registerBackgroundTask() {

backgroundTask = UIApplication.sharedApplication().beginBackgroundTaskWithExpirationHandler {

[unowned self] in

self.endBackgroundTask()

}

assert(backgroundTask != UIBackgroundTaskInvalid)

}

func endBackgroundTask() {

NSLog("Background task ended.")

UIApplication.sharedApplication().endBackgroundTask(backgroundTask)

backgroundTask = UIBackgroundTaskInvalid

}

現在到了重要部分,在didTapPlayPause(_:)添加空的實現:

@IBAction func didTapPlayPause(sender: UIButton) {

sender.selected = !sender.selected

if sender.selected {

resetCalculation()

updateTimer = NSTimer.scheduledTimerWithTimeInterval(0.5, target: self,

selector: "calculateNextNumber", userInfo: nil, repeats: true)

registerBackgroundTask()

} else {

updateTimer?.invalidate()

updateTimer = nil

if backgroundTask != UIBackgroundTaskInvalid {

endBackgroundTask()

}

}

}

按鈕改變選擇狀態取決于計算已經停止,應該開始或者是計算已經開始,應該停止.

首先你必須設置斐波那契序列變量.然后你可以創建一個NSTimer,沒秒啟動兩次,并且調用 calculateNextNumber()函數.

現在到了一個重要的時刻:調用registerBackgroundTask()函數,反過來調用beginBackgroundTaskWithExpirationHandler(_:).這個方法告訴了ISO你需要時間在后臺運行你的app.這些調用完成之后,在你調用endBackgroundTask()之前你的app會一直獲取cpu時間.

嗯,差不多.如果你的app在后臺運行一段時間后沒有調用endBackgroundTask(),IOS將調用關閉程序定義,這是在你調用beginBackgroundTaskWithExpirationHandler(_:)時給你機會來停止執行代碼.所以調用endBackgroundTask()告訴IOS你已經完成工作了是非常好的一個主意.如果你不執行上面所說的而是繼續執行你的代碼,你的app將會終止.

第二部分關于if的語句是很簡單的:它只是使定時器失效,并且調用endBackgroundTask()來告訴ios不再需要額外的CPU 時間.

在你每次調用beginBackgroundTaskWithExpirationHandler(:)時調用endBackgroundTask()是非常重要的.如果你在一個任務里調用 beginBackgroundTaskWithExpirationHandler(:)兩次而只調用endBackgroundTask()一次,你將仍然獲取cpu時間,直到你在運行第二次的后臺任務是調用endBackgroundTask()才能結束.這就是為什么你需要backgroundTask.

現在你可以實現簡單的計算機程序方法.在WhateverViewController添加以下的方法:

func calculateNextNumber() {

let result = current.decimalNumberByAdding(previous)

let bigNumber = NSDecimalNumber(mantissa: 1, exponent: 40, isNegative: false)

if result.compare(bigNumber) == .OrderedAscending {

previous = current

current = result

++position

}

else {

// This is just too much.... Start over.

resetCalculation()

}

let resultsMessage = "Position \(position) = \(current)"

switch UIApplication.sharedApplication().applicationState {

case .Active:

resultsLabel.text = resultsMessage

case .Background:

NSLog("App is backgrounded. Next number = %@", resultsMessage)

NSLog("Background time remaining = %.1f seconds", UIApplication.sharedApplication().backgroundTimeRemaining)

case .Inactive:

break

}

}

再一次,我們將展示另一個方法即使你的app在后臺運行依舊能夠顯示結果.在這種情形下,還有一個有趣的信息: backgroundTimeRemaining的數值.只有當ios調用添加調用beginBackgroundTaskWithExpirationHandler(_:)的時才會停止.

編譯并且運行,然后切換到第三個選項卡.

點擊play并且你將會看到app計算出的值.現在點擊home鍵然后查看xcode控制臺.你應該會看到app依舊會更新數字,與此同時時間依舊在向前走.

在大多數情況下,這個時間將從第180秒開始并且延續5秒鐘.如果你等待重新回到你的app,定時器將重新開始啟動并且所有的錯誤行為將繼續.

在代碼里只有一個bug,它給我機會來解釋關于后臺通知.假設你或太運行app并且等待分配的時間到期.在這種情況下,你app將調用??并且調用endBackgroundTask(),也就是終結后臺運行時間的需求.

如果你繼續返回你的app,定時器將繼續激活.但是如果你離開app,你將不會得到或太運行時間.Why?因為在超時和回到后臺期間app沒有間隙來調用beginBackgroundTaskWithExpirationHandler(_:).

你怎么解決這個問題呢?有許多方法能夠解決這個問題,并且其中一個是使用一種狀態來改變通知.

有兩種你可以得到通知并且你的app可以改變它的狀態的方法:第一種是通過你的主app委托方法;第二種是通過監聽ios發送給你的app的通知.

* 當你的app將要進入不活躍的狀態,UIApplicationWillResignActiveNotification和applicationWillResignActive(_:)將會被發送和調用.在這種情況下,你的app不是在后臺運行,它依舊在前臺運行,但是它將不會接收到任何UI事件.

* 當app進入到后臺狀態,UIApplicationDidEnterBackgroundNotification 和applicationDidEnterBackground(:)將會被發送和調用.在這種情況下,你的app將不會是在激活狀態,并且它是你最后的機會運行你的代碼.如果你想得到更多的CPU時刻,這是一個調用beginBackgroundTaskWithExpirationHandler(:)非常完美的時機.

* 當app返回激活狀態,UIApplicationWillEnterForegroundNotification 和applicationWillEnterForeground(:)將會被發送和調用.這是app依舊在后臺運行,你已經可以啟動任何你想做的事.當你真正進入后臺運行是如果你只調用了beginBackgroundTaskWithExpirationHandler(:),此時將是一個好的時機調用endBackgroundTask().

* 以防你的app從后臺運行狀態返回,在前一個通知完成后UIApplicationDidBecomeActiveNotification和applicationDidBecomeActive(_:)將會被發送和調用.如果你的app只是臨時的中斷也會被調用-舉例—如果你的app沒有真正的進入到后臺,但是你依舊會收到UIApplicationWillResignActiveNotification.

你可以在Apple’s documentation for App States for Apps中看到所有的圖像化描述(文章—有著許多非常棒的圖表)

現在是解決這個bug的時間了.首先要重寫viewDidLoad()并且訂閱UIApplicationDidBecomeActiveNotification.

override func viewDidLoad() {

super.viewDidLoad()

NSNotificationCenter.defaultCenter().addObserver(self, selector: Selector("reinstateBackgroundTask"), name: UIApplicationDidBecomeActiveNotification, object: nil)

}

不管何時這app變成激活狀態,指定的選擇器reinstateBackgroundTask將被調用.

不管何時你訂閱了一個通知你也應該想到這個訂閱的通知哪里不應該被訂閱.使用deinit來完成這個功能.按照下面的代碼加入到WhateverViewController.

deinit {

NSNotificationCenter.defaultCenter().removeObserver(self)

}

最后實現reinstateBackgroundTask().

func reinstateBackgroundTask() {

if updateTimer != nil && (backgroundTask == UIBackgroundTaskInvalid) {

registerBackgroundTask()

}

}

如果定時器依然運行但是后臺任務沒有運行,你只需要恢復就可以了.

把你的代碼分解成小的實用的代碼只需要做一件事就可以.當一個后臺任務不是在當前的定時器下你只需要調用registerBackgroundTask()即可.

然后你可以使用了.你可以下載這個程序.

這個課程的最后的一節是:Background Fetching.

后臺獲取

后臺獲取是iOS7中推出的讓你的APP在最大限度減少對電池損耗的時候總是展現最新的信息.舉個例子,假設你正在給你的APP填充信息.你可以通過viewWillAppear(_:).獲取最新數據來預先通知后臺模式.這個方案可以解決在新數據刷新過來之前你的用戶正在瀏覽前幾秒的數據.當用戶打開你APP的同時,最新的數據同時被神奇的展現了,這種情況再好不過了.這是后臺模式能夠為你實現的操作.

當APP被激活的時候,系統會使用慣用模式去決定什么時候執行后臺獲取.比如,如果用戶每天都在早上9點打開改APP,后臺獲取在這個時間點之前預先執行是很可能的.系統決定什么時候是安排后臺獲取的最好時間,因此你不應該用它去做緊急的更新.

這里有你為了實現后臺獲取必須做的三件事情:

* 檢查你APPCapabilities選項中后臺模式的后臺獲取選項框是否被選中.

* 使用setMinimumBackgroundFetchInterval(_:) 為你的APP創建一個合適的時間間隔.

* 在你APP委托中實現application(_:performFetchWithCompletionHandler:)去管理后臺獲取.

后臺獲取就像他名字表示的一樣,他通常涉及到從外源,比如網絡服務,中獲取信息.就這個教程的意圖,你將不會使用網絡而僅僅獲取現在的時間.這樣簡化講讓你理解在不同擔心外在的服務的時候操作并測試后臺模式所需要的每一樣東西.

對于有限長度的任務,你只有以按秒為單位的時間去執行操作,公認的時間是不超過30秒,但越短越好.如果您需要下載大量資源最為獲取的部分,這就是你需要使用NSURLSession的背景傳輸服務的地方.

開始的時間到了.首先,打開FetchViewController.swift,并將下面的屬性和方法添加到FetchViewController中.

var time: NSDate?

func fetch(completion: () -> Void) {

time = NSDate()

completion()

}

這些代碼是代替你真正的從外源(json或XML RESTful 服務)中獲取數據的一種簡化.因為它可能需要幾秒鐘來獲取和分析數據,你傳遞一個完成的handler,這個handler在進程完成后被調用.你待會兒會看到為什么很很重要.

接下來,完成view controller的代碼.將下面的方法添加到FetchViewController中.

func updateUI() {

if let time = time {

let formatter = NSDateFormatter()

formatter.dateStyle = .ShortStyle

formatter.timeStyle = .LongStyle

updateLabel?.text = formatter.stringFromDate(time)

}

else {

updateLabel?.text = "Not yet updated"

}

}

override func viewDidLoad() {

super.viewDidLoad()

updateUI()

}

@IBAction func didTapUpdate(sender: UIButton) {

fetch { self.updateUI() }

}

updateUI()格式化這個時間并顯示它.它是一個可選的類型,所以如果它沒有被創建,他將展示至今沒有更新的信息.當這個view初次被加載時(在 viewDidLoad()中)你不能獲取到,但是直接調用updateUI()函數,將會有“Not yet updated”的字樣在開始時顯示.最后,當更新按鈕被監聽的時候,它運行獲取的代碼并且會完成對UI的更新.

就這一點而言,該view controller正在工作.

然而,后臺獲取沒有起作用.

啟用后臺獲取的第一步是在Capabilities選項欄里選中Background fetch.到現在這個操作已經是老一套的了,直接找到它并選中.

接下來,打開AppDelegate.swift,通過在 application(_:didFinishLaunchingWithOptions:)中設置最小的后臺獲取時間間隔來請求后臺獲取操作.

func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {

UIApplication.sharedApplication().setMinimumBackgroundFetchInterval(

UIApplicationBackgroundFetchIntervalMinimum)

return true

}

默認的時間間隔是你想切換回去的UIApplicationBackgroundFetchIntervalNever,比如,你的用戶日志和不需要更新的內容.你也可以設置一個精確到秒的時間間隔.系統在開始執行后臺獲取之前將等待一段時間.

要小心,,不要將時間間隔設置過短,因為它會多余的消耗電池和損害服務器.結束獲取信息的確切時間是由系統決定的,但是在執行它之前將會等待一段時間.通常,UIApplicationBackgroundFetchIntervalMinimum是很好用的默認值.

最后,為了啟用后臺程序,你必須實現application(_:performFetchWithCompletionHandler:).將下列方法添加到AppDelegate.swift中.

// Support for background fetch

func application(application: UIApplication, performFetchWithCompletionHandler completionHandler: (UIBackgroundFetchResult) -> Void) {

if let tabBarController = window?.rootViewController as? UITabBarController,

viewControllers = tabBarController.viewControllers as? [UIViewController] {

for viewController in viewControllers {

if let fetchViewController = viewController as? FetchViewController {

fetchViewController.fetch {

fetchViewController.updateUI()

completionHandler(.NewData)

}

}

}

}

}

首先你需要獲取FetchViewContoller.然后,因為rootViewController在每個APP中不是必須的UITabBarController,所以它是可以選擇創建的,不過它在這個APP中,所以它絕不會出現問題.

接下來,你在選項卡控制器中循環添加所有的視圖控制器,并且將它們成功的放到FetchViewController中.在這個APP中,你知道它是最后的控制器,所以你不能對它進行硬編碼,但是在你決定以后添加或刪除選項卡的時候循環創建會提高程序的健壯性.

最后,你可以調用fetch(_:).當它執行完后,你會更新UI,然后調用將completionHandler作為參數傳遞的函數.你在這個操作的最后調用這個完成處理的程序是很重要的.你指定在獲取過程中獲取的結果作為以一個參數.它的可能值為.NewData, .NoData或者.Failed.

為了簡單起見,該教程總是指定.NewData作為永遠成功獲取時間的返回值,并且這個值和上一次的結果總是不同的.在這之后,iOS可以使用更好的時間間隔來執行后臺獲取.該系統知道在這個時間點上的系統快照,所以它可以在應用程序切換卡中顯示.以上是為了實現后臺獲取所需要的所有的操作.

提示:不是沿著信息傳遞完成對屬性的調用,而是保存一個屬性變量,并且在你獲取完成后調用它是很有誘惑力的.不這樣做的話,如果你多次調用 application(_:performFetchWithCompletionHandler:),先前的處理程序將會被覆蓋,永遠不會被調用.最好通過傳遞處理程序,并且在它不會造成這種編程錯誤的時候調用它.

測試后臺獲取

測試后臺獲取的一個方法是停下來等著系統決定去執行它.這需要大量的等待.幸運的是,Xcode體統了模擬后臺獲取的方法.有兩種你需要測試的情況,一種是當你的APP在后臺中時,另一種是你的APP處于從被掛起到繼續運行的情況.第一種方法最簡單,僅僅是一個選擇菜單.

* 在真正的設備上運行(不是模擬器);

* 在Xcode調試菜單中選擇模擬后臺獲取;

重新打開這個APP,注意被送到后臺的數據.

切換到Fetch選項卡,(注意當你模擬后臺獲取而且不是顯示“Not yet updated”的時候時間)

另一種方法是在從掛起狀態回復的時候測試后臺獲取.這里有一個啟動項讓你APP一運行就直接進入掛起狀態.因為你可能要測試這種臨界狀態,用這個選項始終建立新的Scheme是最好的.Xcode使這種情況很容易實現.

首先選擇Manage Schemes選項.

接下來,選擇列表里僅有的方案,然后點擊齒輪圖標,選擇Duplicate Scheme.

最后,用合理的名字重命名你的方案,比如 “Background Fetch”,并選中 Launch due to background fetch event的復選框.

需要注意的是在Xcode6.1中,在模擬器上這并不能可靠的運行.我自己測試的時候,我需要使用真正的設備正確的從啟動進去到掛起狀態.

用這個方案運行你的APP.你會發現,該APP沒有真正的打開,而是直接運行到了掛起狀態.現在,手動開啟它,并進入Fetch選項.你會看到,當你運行該APP時,時間會更新,而不會顯示“Not yet updated”.

使用后臺獲取能夠有效地讓你的用戶們流暢的一直獲取最新的內容.

何去何從?

你可以在這里下載完整的示例工程.

如果你想讀我們這里涉及到蘋果文檔里的內容,最佳開始地點是Background Execution.該文檔介紹了每一個后臺模式,并為每個模式鏈接到相應的位置.

該文檔有趣的部分談論了如何構建一個可靠的APP.你應該知道釋放正在后臺運行的APP中的一些細節或多或少會涉及到你到APP.

最后,如果你打算做大型網絡信息傳輸,確保檢查NSURLSession.

我們希望你能享受這個課程,如果你有任何疑問或意見, 請加入下面的論壇討論.

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容