2014年4月12日

淺談iOS應用程式背景執行(一) - 在有限時間內在背景執行任意工作

為了能有效率地掌握各個iOS應用程式所使用的資源,及確保iOS應用程式的執行效能,iOS對於應用程式在背景執行的限制相當嚴苛。 一般情況下,只有更新地理位置、下載少量資訊...等輕量級的任務,又或像是音樂播放、VOIP等非得要能在背景運作的任務,才可以在背景執行。

然而,自iOS 4之後,iOS應用程式其實可以向請求在背景執行程式,而不限制執行工作的類型! 只不過相對條件是,在背景執行程式的時間,多半不能超過10分鐘。一旦超過時限,將被iOS強制中止。
但即便如此,10分鐘也或許足以滿足某些特定應用的需求。聊勝於無!

iOS應用程式執行的五種狀態

在開始學習如何在背景執行程式前,應先了解到iOS應用程式執行時的幾種狀態:

上圖參考自Apple官方文件的App States and Multitasking,說明五種狀態之間的變化。

Not running

應用程式尚未被執行的狀態。

Active

應用程式在前景,且正在執行中。

Suspended

應用程式被暫停,無法執行程式。 當使用者按下Home鍵,退出當前應用程式,或是切換至其他應用程式後。 若原應用程式不具有在背景執行的能力,則會立即進入Suspended狀態。
而應用程式中的資訊(如:所使用的變數)...等仍然在記憶體中,只要使用者切換該應用程式, 則該應用程式會接回之前的狀態,繼續執行。
此外,若iOS系統察覺記憶體資源不足時,會清理掉記憶體中Suspended狀態的程式,使應用程式回到Not running的狀態。

Inactive

應用程式在前景,但是可能發生了其他事情(如:接到電話),因此應用程式無法接收任何事件(像是使用者點擊按鈕的UI事件)。 但應用程式仍然在執行中。

Background

當使用者按下Home鍵,退出當前應用程式,或是切換至其他應用程式後。 原應用程式具有在背景執行的能力,因此仍然在執行程式。 如何使應用程式進入此狀態,則是本篇討論的主題。

取得當前應用程式執行狀態

以下API可以取得當前應用程式狀態:
UIApplicationState st = [[UIApplication sharedApplication] applicationState];
UIApplicationState為enum型態,定義如下:
typedef enum : NSInteger {
   UIApplicationStateActive,
   UIApplicationStateInactive,
   UIApplicationStateBackground
} UIApplicationState;
由於Not running與Suspended狀態下無法執行程式,因此自然不會有其定義。

應用程式執行狀態變化時會呼叫的方法

在UIApplicationDelegate中,有幾個方法,會在應用程式執行狀態發生變化時被呼叫:
- (void)applicationWillResignActive:(UIApplication *)application
//當應用程式由Active變為Inactive時會被呼叫。

- (void)applicationDidBecomeActive:(UIApplication *)application
//當應用程式由Inactive變為Active時會被呼叫。


- (void)applicationDidEnterBackground:(UIApplication *)application
//當應用程式由Active變為Background時會被呼叫。

- (void)applicationWillEnterForeground:(UIApplication *)application
//當應用程式由Background回到Active時會被呼叫。
舉幾個例子來說,當應用程式未執行時,點擊應用程式icon開啟後,applicationDidBecomeActive會被呼叫;
當應用程式在前景執行時,按下Home鍵,applicationWillResignActiveapplicationDidEnterBackground會被依序呼叫;
若再度點擊應用程式icon使其回到前景,則applicationWillEnterForegroundapplicationDidBecomeActive會依序被呼叫。
另一種情況是:當應用程式在前景執行時,接到電話或是連按兩下Home鍵進入最近使用的App列表時, 此時,該應用程式還在前景,只是變為Inactive狀態,因此只有applicationWillResignActive會被呼叫。
關於這幾個method被呼叫的時機,也可以參考此篇討論: http://stackoverflow.com/questions/3712979/applicationwillenterforeground-vs-applicationdidbecomeactive-applicationwillre

請求在有限時間內在背景執行任意工作

以下,我們以一個極其簡單的例子,展示如何使iOS應用程式在背景執行工作。
首先,我們將使用一個Timer,每秒在console下印出數字,並累加:
//啟動Timer ...
NSTimer* cntTimer = [NSTimer scheduledTimerWithTimeInterval:1
                             target:self
                             selector:@selector(runCounter)
                             userInfo:nil
                             repeats:YES];

//runCounter 方法
-(void) runCounter {
    NSLog(@"runCounter, count=%i",self.count);
    self.count++;
}
此時,此應用程式還沒有背景執行的能力。
當我們啟動應用程式時,可以看到console下不斷印出數字;但是,一旦按下Home鍵之後,就停止了。
因此,我們需要請求在背景執行。
首先,在AppDelegate類別宣告UIBackgroundTaskIdentifier型態的property。
@property (nonatomic) UIBackgroundTaskIdentifier backgroundTask;
接著,在進入背景時,請求在背景執行程式:
- (void)applicationDidEnterBackground:(UIApplication *)application
{      
    self.backgroundTask = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
        NSLog(@"Background handler called. Not running background tasks anymore.");
        [[UIApplication sharedApplication] endBackgroundTask:self.backgroundTask];
        self.backgroundTask = UIBackgroundTaskInvalid;
    }];
}
呼叫beginBackgroundTaskWithExpirationHandler方法後,當應用程式退到背景時,就不會進入Suspended狀態,而是在Background狀態。
此時可以看到console也繼續印出訊息。
然而,此方法使應用程式在Background狀態的時間是有限的,一般來說大約為10分鐘。 時間一到,iOS會呼叫當初呼叫beginBackgroundTaskWithExpirationHandler方法時, 所傳入的block,讓應用程式知道即將被停止執行。
應用程式可以在此block中執行最後的處理,並呼叫endBackgroundTask自行中止背景執行。
而倘若不呼叫endBackgroundTask,iOS系統最終還是會終止該應用程式的背景執行。

結語

iOS對於應用程式在背景執行的限制較為嚴苛,但請求在有限時間內執行任意工作的方法並不複雜。 即便只是短短的10分鐘,換個角度思考,或許也足夠滿足部分需求。
此外,在學習使用如何在背景執行程式時,也應了解iOS應用程式執行的五種狀態,以利於掌握應用程式的執行。