在 上一篇 文章裡介紹了 iOS APP 的狀態切換,其中一個狀態是 Background, 也就是後臺,在這個狀態,程式只被允許執行非常有限的一點點時間,然後就會隨時被掛起,不再執行任何程式碼,但顯然是有應用場景需要不斷地在後臺執行一些任務,比如 音樂播放 APP, 健康記步軟體等。本文就來介紹 iOS 所提供的實現這些場景的技術。
後臺任務分類
首先 Apple 官方為我們界定了 3 類後臺執行任務的場景:
- Background Tasks:APP 在前臺時啟動某項任務,然後在未結束之前突然 切換到了後臺,那麼 APP 可以在切換回撥裡使用某些 API 來繼續向系統請求一些時間來繼續完成這個任務;完成之後通知系統,之後系統會將 APP 掛起;
- Downloading:在後臺啟動從網路下載檔案的任務 – 對於檔案下載,iOS 有專門的機制;
- Specific Backgournd Tasks:應用需要在後臺一直執行程式碼;
這三種型別的後臺任務,在實現時各有不同,下面來一一介紹。
Background Tasks
Apple 文件建議,如果要啟動一個後臺任務(非同步任務),可以使用 API beginBackgroundTaskWithExpirationHandler來指定,即使啟動任務的時候,程式是處在前臺的,也沒有關係,當位於前臺時,該方法請求得到的時間是DBL_MAX ,也就是 double 資料型別最大值,你可以認為是無限大,當任務執行過程中 APP 被切換到後臺時,任務還沒有完成,這個時間又會自動調整為一個時間片段(具體多少我沒找到文件說明,都是說可以通過backgroundTimeRemaining屬性得到)。需要注意的是, 這個方法是成對使用的,對於一個固定 task ,每次呼叫 beginBackgroundTaskWithExpirationHandler,都會產生一個 token 值(UIBackgroundTaskIdentifier實際是個整型),必須在任務執行結束時,呼叫 endBackgroundTask並傳遞這個 token,來結束後臺任務。另外,作為最佳實踐,都應該傳遞一個 超時 handler,以防申請到的時間片段內,還是沒能完成任務的話,做最後的清理和標註工作!如果不傳的話,那麼結果就是 iOS 直接 kill 掉你的APP,閃退咯,因為它覺得我們騙了它嘛,哈哈。。。下面是一段在進入後臺時啟動非同步任務的例子;
// 在某處定義一個 token 變數
UIBackgroundTaskIdentifier _bgTaskToken;
// 進入後臺 委派方法回撥
- (void)applicationDidEnterBackground:(UIApplication *)application
{
_bgTaskToken = [application beginBackgroundTaskWithName:@"MyTask" expirationHandler:^{
// 時間到了,任務還沒完成,只能清理
...
// 取消後臺任務
[application endBackgroundTask:_bgTaskToken];
_bgTaskToken = UIBackgroundTaskInvalid;
}];
// 非同步啟動任務,這樣不會阻塞 本委派方法回撥
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 巴拉巴拉,做自己的任務
...
// 任務在時間限制內結束啦,取消後臺任務
[application endBackgroundTask:_bgTaskToken];
_bgTaskToken = UIBackgroundTaskInvalid;
});
}
複製程式碼
Background Downloading
這類後臺任務,必須使用 iOS 指定的機制才可以,那就是 NSURLSession。使用 NSURLSession 建立的下載任務,會被系統直接在另外一個獨立的系統程式裡進行管理,不會因 APP 進入後臺或掛起等而受到影響,iOS 會統一管理所有的下載任務。並且,即使你的 APP 已經掛掉啦,下載任務還是會繼續,等到下載完成啦,系統會喚起你的 APP 程式,並通知你,但如果是使用者主動殺掉的你的程式,那麼系統會自動取消下載任務。具體使用方法:
- 1, 使用 NSURLSessionConfiguration類的 backgroundSessionConfigurationWithIdentifier方法建立一個 NSURLSessionConfiguration 物件,引數為一個字串,作為一個 token ,完成時會用到,不能為空或 nil;
- 2, 設定上一步建立出的物件的 sessionSendsLaunchEvents屬性為 YES;
- 3, 如果開啟下載任務時,是位於前臺的,將 discretionary屬性也設定為 YES;
- 4, 設定你需要的其他屬性值;
- 5,使用配置好的 NSURLSessionConfiguration物件,作為引數,建立 NSURLSession例項物件;
- 6,使用 NSURLSession開始下載task,這個這裡不細講啦,需要看 《URL Session Programming Guide》;
如果在下載完成之前,你的APP已經掛起或者死掉啦,那麼當系統完成下載之後,系統會喚醒你的 APP,並回撥 你的 app 委託方法 application:handleEventsForBackgroundURLSession:completionHandler:,在這其中,引數會傳進來一個 token,這個就是你第一步裡 傳入的 字串,使用這個 字串,再重新建立一個 NSURLSessionConfiguration,並進行與開始任務之前一樣的配置,那麼就可以使用這些物件來獲取已經完成的任務的詳細情況了。
Background Long-Running Tasks
在 iOS 裡只有特定的一些應用型別才會被允許可以在後臺一直執行,APP 必須顯式的宣告一些特定許可權,才可以在後臺進行長時間執行而不被掛起。一些應用型別有 6 種:
- 需要在後臺播放音訊 – 如 Music Player;
- 需要在後臺錄音;
- 在後臺時也需要不斷通知使用者位置變動的,比如導航;
- 支援 VoIP 電話的 – 如 skype 網路電話;
- 需要在後臺有規律的下載和處理網路內容的;
- 在後臺有規律的從其他外設(第三方配件)獲取並更新資料的;
要實現這些型別服務的 APP,需要進行專門的宣告,這樣系統才會採取相應的操作。 先來看看怎麼宣告。
宣告後臺服務型別
通過 XCode 的 project setting 裡就可以配置型別,選擇之後會自動 在你 工程的 Info.plist 檔案裡 增加 UIBackgroundModes鍵值對;一個 APP 可以同時宣告多種支援的後臺長期任務型別,在 XCode 裡勾選上即可;下表給出了所有 在 XCode 可選的 型別 及 具體含義;
Xcode background mode | UIBackgroundModes 值 | 描述 |
---|---|---|
Audio and AirPlay | audio | 應用可以在後臺播放或錄製音訊,包括 Apple 自家的 AirPlay 流媒體音視訊;對於錄製,需要在APP 第一次執行時,使用者授予許可權才可進行。 |
Location updates | location | APP 不斷更新 GPS 位置資訊,並通知給使用者,即使 APP 處於後臺 |
Voice over IP | voip | APP 提供通過網路連線來打電話的功能 |
Newsstand downloads | newsstand-content | 雜誌應用,可以在後臺下載雜誌並處理 |
External accessory communication | external-accessory | 一些外設控制 APP, 比如一些控制 第三方 MFI 配件的應用,宣告這種 型別,可以讓APP 在後臺不斷的與 外設進行溝通 |
Uses Bluetooth LE accessories | bluetooth-central | iPhone 作為藍芽中心裝置使用,也就是做為 server;需要在後臺不斷更新藍芽狀態的 |
Acts as a Bluetooth LE accessory | bluetooth-peripheral | iPhone 作為藍芽外圍裝置使用,也就是做 client,需要在後臺不斷的訪問其他藍芽裝置獲取資料的 |
Background fetch | fetch | APP 需要在後臺不斷地 頻繁有規律的從網路獲取資料 |
Remote notifications | remote-notification | APP 先在後臺關注某個 push 推送,但這個 push 推送到達的時候,及時在後臺開始對應的下載任務,以儘可能減少使用者直接點開 通知 後 檢視內容的等待時間 |
Playing and Recording Background Audio
一些典型的應用例子:
- 音樂播放軟體
- 錄音APP
- 支援 AirPlay 音視訊播放的APP
- 網路通話軟體
當你在 Info.plist 裡宣告瞭 UIBackgroundModes為 audio的時候,在後臺進行 audio 的相關操作時,系統 audio API 會自動阻止系統將你的 APP 程式掛起,所以不需要 APP 自己再進行其他額外的處理,只需要處理自己的軟體邏輯即可。 【Note】:手機上是有可能會有多個 APP 同時擁有後臺 audio 操作許可權的,這時候系統會根據 每個 APP 開始操作音訊時的 audio session 配置來決定如何進行操作,而且你應該非常小心的處理一些中斷事件,如來電,其他系統提示音等,這些都有相關的 API 和機制,可以參考 《Audio Session Programming Guide》
Tracking the User’s Location
有三種方式來實現 位置的訪問:
- The significant-change location service(這也是官方推薦的方式)
- Foreground-only location services
- Background location services
前兩種都不需要在 Info.plist 裡宣告 UIBackgroundModes,只有最後一種需要。 The significant-change location service,字面理解,就是隻有位置有變化時才會發出通知,有人說這個時機是依據基站,切換了基站時,就會發出一次通知,所以頻率會受基站的密度影響,所以市區更新頻率會比郊區高。但好處是這個服務不管你的 APP 是在前臺還是後臺,不管是否已經被掛起,或已經死掉了,他都會喚醒你的程式進行相應處理,所以應該是最省電的。 後兩種都是標準的定位服務,只不過一個只能工作在前臺,而一個可以在後臺工作;【Note】:官方對於使用後臺定位服務的 APP 稽核是非常嚴格的,所以使用時一定要小心,並提供足夠的說明和解釋。 至於如何實現一個定位 APP ,請看 《Location and Maps Programming Guide 》
Implementing a VoIP App
iOS8之前: 網路通話軟體,skype 就是其中一個。這樣的軟體使用 internet 連線來進行語音通話,為了提供健全的電話功能,這類軟體必須一直保持一個長期的網路連線,以便監聽到來電。實現類似的功能,APP 自己並非一直在後臺不被掛起,而是交由系統監聽 網路連線,有資料進來時,系統會喚醒 APP,並將 socket 轉交給 APP 進行處理;大致步驟: 在 Info.plist 裡進行UIBackgroundModes配置;
- 配置一個 socket 連線用於 VoIP;
- 在進入後臺時,呼叫 setKeepAliveTimeout:handler:
- 方法傳遞一個回撥,用來處理事件;
- 配置要使用到的 audio session;
【Note】:貌似對於 VoIP 的實現, iOS 8 有變化,改為使用 remote notification 的方式來做啦,誰說 iOS 沒有碎片化的啊,有!具體實現請參考 Tips for Developing a VoIP App 簡單說就是伺服器發個特殊的通知,系統喚醒app來處理,連socket都不用保留了.
Downloading Newsstand Content in the Background
雜誌應用,居然還有專門的處理。但我看介紹,跟前面講解的 後臺下載檔案沒啥區別啊!!另外好像也是用 通知推送 觸發啊。About Newsstand Kit Framework
Communicating with an External Accessory
外設裝置有很多,比如一些心率監控器,會在必要的時候向手機推送資料。宣告瞭UIBackgroundModes為 external-accessory 後,系統就不會主動關閉 APP 與 外設之間的連線,而是替 APP 監視這個連線,但有資料過來時,會喚醒 APP 進行處理,每次喚醒 APP 只有 10 S鍾時間進行資料處理,所以應當越快越好,萬不得已,如果10S不夠,需要使用 beginBackgroundTaskWithExpirationHandler:方法再申請一段時間進行處理; 【Note】:Apple 要求此類應用 需要提供一個 開啟 和 關閉 連線的介面供使用者使用;
Communicating with a Bluetooth Accessory
類似上一節的 配件,如果心率監控器跟 手機之間使用的連線方式是藍芽,那麼就一模一樣啦,連 喚醒的時間限制都一樣,都是 10 S !!!略啦。。。
Fetching Small Amounts of Content Opportunistically
有人依靠這種手段來實現後臺永存,但現在不好使啦,除非你是真的每次都在下載東西,而且每次時間都很短。使用者的流量啊。因為宣告瞭這個 mode 之後,並不保證 系統一定會給你分配時間來執行後臺任務,因為它自己有一套邏輯,如果你經常性喚醒,但卻每次都耗時很久,又沒有做從網路下載東西的操作,那麼以後你被分配給喚醒的機率就會越來越小。另外還有稽核!!!! 正常情況下,宣告瞭這個型別之後,系統在你的 APP 進入後臺後,會間隔性的給機會將你的 APP 喚醒,並回撥你的 委託方法 application:performFetchWithCompletionHandler:,你需要在這個回撥裡檢查是否有新內容可用,如果有,就開啟後臺下載,推薦使用 NSURLSession來建立,下載完成後,你必須呼叫這個方法出入 的 completionHandler並傳入一個 整型值 來表示 你的處理是否正常,UI是否已經更新,讓系統來決定更新 snapshot等;
Using Push Notifications to Initiate a Download
這個方式,是你的應用中包含通知功能時,你在服務端推送的通知內容里加入 鍵值對 content-available= 1,那麼 手機收到這個通知後,會自動啟動 APP 到後臺,或 喚醒(依舊保持後臺執行),並回撥 委託方法 application:didReceiveRemoteNotification:fetchCompletionHandler:,在這個方法裡進行內容下載。 【Note】:需要服務端推送配合
哪些情況系統會喚醒掛起程式
當一些特定事件發生時,系統會喚醒已經被掛起的程式,轉換到後臺執行狀態,這些事件針對不同型別的APP 有所不同:
location apps
- 系統產生了符合 APP 配置的定位要求的位置更新;
- 裝置進入或離開了一個網路註冊的區域,你可以理解為基站;
audio apps
- audio framework 需要 app 處理資料的時候–任何 播放、錄製;
Bluetooth apps
- 當手機扮演中心裝置時,收到了其他藍芽裝置發來的資料;
- 當手機扮演外圍裝置時,收到了藍芽服務端發來的資料;
background download apps
- 本應用的一個包含 content-available= 1的推送通知到達了手機; background fetch 型別,系統給予了 APP 喚醒的機會;
- 使用 NSURLSession進行後臺下載的APP,在下載過程完成或出現問題時,系統會主動喚醒對應 APP;
- 雜誌應用,下載完成時喚醒 APP;
【Note】:絕大多數情況下,系統不會重啟被使用者手動強制關閉的 APP,但在 iOS 8 之後, location apps 是個例外。其他的所有被使用者手動強制關閉的APP 都不會被系統主動喚起,直到 使用者再次 主動啟動這個 APP,或者手機重啟並在使用者輸入瞭解鎖密碼之後才會恢復機制。
做一個盡責的後臺APP
- Apple 教育我們,如果你要實現一個後臺 APP,應該做一個有責任的APP,不要亂搞,哈哈。
- 不要在後臺呼叫任何 OpenGL ES 介面,在進入後臺之前也要保證這些呼叫都已結束,否則你的 APP 將直接被 kill;
- 取消所有 Bonjour相關的操作,還不清楚這個是啥東西,不過 Apple 說即使你不取消,它在把你掛起之前也會都給你取消;
- 如果有網路操作,做好容錯處理;
- 儲存 APP 狀態,進入後臺前持久化一些資料,以便恢復;
- 儘可能多的釋放記憶體,尤其是強引用;
- 停止使用共享的系統資源,比如 電話本,日曆等,進入後臺前,release他們;
- 不要在後臺進行 UI 的更新操作;
- 做好對外設配件的 連線 和斷開 事件的響應;這個是 外設程式設計的機制啦,需要 參考 External Accessory Programming Topics ;
- 關閉彈出視窗和彈出選單等;
- 移除視窗上的一些敏感資訊;
- 在後臺的執行儘可能小的任務;
最後, Apple 建議能不後臺就不後臺,那當然。。。