iOS後臺模式開發指南

GitHub - MollyMmm發表於2015-05-10

自從古老的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<Void>) {
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.

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

相關文章