轉自:http://blog.csdn.net/chowpan/article/details/22417247
翻譯自:http://www.raywenderlich.com/29948/backgrounding-for-ios
(代碼部分若亂碼,請移步原鏈接拷貝)
自ios4開始,用戶點擊home按鈕時,你可以將app設計爲掛起狀態。app在內存中,除非用戶再次返回到app,否則該app暫停運行。都是這種情況嗎?
當然不是,在一些例外的情況下,app仍然可以在後臺保持運行。這篇文章將介紹如何以及何時應用(幾乎)所有這些後臺操作功能。
應用後臺運行模式實際上有很嚴格的限制條件,在ios上實現真正的多任務上,這並非一個奇特的解決辦法。app退回後臺時,比如用戶切換到了另一個app,更多的是完全被掛起。
允許app在後臺仍然運行的情況僅限於以下幾種:
1,播放音頻文件(playing audio)
2,獲取定位更新(getting location updates)
3,雜誌app中下載新的期刊(downloading new issues for newsstand apps)
4,VoIP 呼叫(handing VoIP calls)
假如你的app沒有用到如上任一功能,那麼非常不幸運。需要特別注意的是:針對所有的app,在它們真正被掛起之前,有最多10分鐘的時間去完成其正在執行的任務。
所以後臺模式可能並非如你所願,但仍請看官細品!
接下來你將學習到,在IOS中5種基本的可用的後臺模式。本章你創建的項目是一個基於tabbed的簡單應用,每個tab展示了上述5種基本後臺模式效果之一,即從‘播放音頻’開始到‘接聽Voice - over - IP’鏈接。
贅言不絮,開始正文。
初探--”後臺模式“
在深入探討之前,下面預覽下這5種基本後臺模式:
1,音頻播放:在後臺app依然可以播放/錄製音頻。
2,實時接收定位更新:app依然可以獲取設備位置更新的回調。
3,執行一個有限時長的任務:通用的情況,在限定的時間內,app可以運行任意作用的代碼。
4,雜誌下載:對於雜誌類app,允許其在後臺下載更新的內容。
5,提供VoIP服務:允許app在後臺運行任意作用的代碼。當然前提是你的app必須提供了VoIP服務。
本文接下來將依次介紹上述5種後臺模式,你若僅對其中的一種或幾種模式感興趣,可以選擇閱讀。
首先下載本文介紹的項目,本文demo下載鏈接:sample project ,你也可以follow該項目的GitHub頁面 ,其中有詳盡的項目創建過程,儘管本文着重介紹的是後臺模式的操作。
好消息:用戶接口已經爲你預先配置好了,這更有利於專注後臺模式的學習。
運行項目,效果如下:
上面的tabs將是本節闡述的引導圖。首先我們將進行音頻後臺模式。
附:爲了達到測試的最優效果,你英愛在真機上運行本程序,因爲有些後臺功能在模擬器上不能闡釋的那麼完善(甚至完全沒有效果)。
音頻播放:
在IOS上有若干方法進行音頻的播放,這些方法中的多數均要求繼承回調來提供後續的音頻數據進行播放。回調(如委託方法)即是在適當時間進行某種操作,使用音頻流填充緩衝區。
假如打算以數據流方式來進行音頻播放,你可以開啓一個網絡連接,並用該連接回調驚醒持續的數據流接受。
當使能音頻後臺模式播放時,既是app已經在後臺,即不是當前活躍的app,IOS仍然可以調用上述回調方法---就是這樣,這是本文介紹的後臺模式中的4個之一,音頻後臺模式幾乎是自動完成的,你僅需要激活它,並提供合適的基礎操作即可。
僅當你的app是真的提供給用戶音頻播放功能,你才能使用音頻後臺模式。若我們抱有僥倖心理,爲了獲得CPU更多時間而利用該模式播放一段無聲的音頻,apple將會拒絕此類app。
本節,你將添加音頻播放到項目,並開啓後臺模式演示其效果。
爲了實現音頻播放,首先需要學習AV Foundation,打開 TBFirstViewController.m文件,添加頭文件:
#import <AVFoundation/AVFoundation.h>
在viewDidLoad中,添加如下代碼:
// Set AVAudioSession
NSError *sessionError = nil;
[[AVAudioSession sharedInstance] setDelegate:self];
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayAndRecord error:&sessionError];
// Change the default output audio route
UInt32 doChangeDefaultRoute = 1;
AudioSessionSetProperty(kAudioSessionProperty_OverrideCategoryDefaultToSpeaker,
sizeof(doChangeDefaultRoute), &doChangeDefaultRoute);
代碼初始化了audio session對象,並確定用揚聲器來播放而非聽筒。
以下變量用來跟蹤播放進程:
@property (nonatomic, strong) AVQueuePlayer *player;
@property (nonatomic, strong) id timeObserver;
聲明在如下位置:
@interface TBFirstViewController ()
// Insert code here
@end
項目開始包含了一個音頻文件,來自favorit rotalty-free music websites 。你可以免費使用其中的音頻文件,所有的文件由Kevin Macleod 提供,所以,感謝Kevin!
在IOS上播放音樂,一種最簡單的方法之一即是應用AV Foundations AVPlayer。 故我們的實例將會使用一個AVPlayer的子類叫做AVQueuePlayer 。AVQueuePlayer允許我們設置一個AVPlayerItems隊列,用來依次並自動的播放音頻文件。
在viewDidLoad結尾:
NSArray *queue = @[
[AVPlayerItem playerItemWithURL:[[NSBundle mainBundle] URLForResource:@"IronBacon" withExtension:@"mp3"]],
[AVPlayerItem playerItemWithURL:[[NSBundle mainBundle] URLForResource:@"FeelinGood" withExtension:@"mp3"]],
[AVPlayerItem playerItemWithURL:[[NSBundle mainBundle] URLForResource:@"WhatYouWant" withExtension:@"mp3"]]];
self.player = [[AVQueuePlayer alloc] initWithItems:queue];
self.player.actionAtItemEnd = AVPlayerActionAtItemEndAdvance;
代碼首先創建了一個含有AVPlayerItems對象的數組,接着以該數組初始化AVQueuePlayer對象,並設置其爲連續播放。
在播放進程中,爲了更新音樂名字,你需要註冊監聽player的currentItem屬性,該功能代碼添加至viewDidLoad的結尾:
[self.player addObserver:self
forKeyPath:@"currentItem"
options:NSKeyValueObservingOptionNew
context:nil];
當player的currentItem屬性變化時,將會回調監聽事件。添加監聽事件到viewDidLoad下面:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if ([keyPath isEqualToString:@"currentItem"])
{
AVPlayerItem *item = ((AVPlayer *)object).currentItem;
self.lblMusicName.text = ((AVURLAsset*)item.asset).URL.pathComponents.lastObject;
NSLog(@"New music name: %@", self.lblMusicName.text);
}
}
當監聽方法被調用時,首先應該確定的是更新的屬性是我們需要關注的。但在本例中,因爲僅監聽一個屬性,故判斷語句不是必須的。但是判斷檢測是一個很好的習慣,也是以防後期會添加另外的屬性監聽。如是‘currentItem’屬性變化,測更新lb1MusicName標籤。
你或許需要更新當前播放條目的已播時間,實現的最好方法是利用:addPeriodicTimeObserverForInterval:queue:usingBlock:方法,在指定的queue中提供回調block。
在viewDidLoad結尾添加:
void (^observerBlock)(CMTime time) = ^(CMTime time) {
NSString *timeString = [NSString stringWithFormat:@"%02.2f", (float)time.value / (float)time.timescale];
if ([[UIApplication sharedApplication] applicationState] == UIApplicationStateActive) {
self.lblMusicTime.text = timeString;
} else {
NSLog(@"App is backgrounded. Time is: %@", timeString);
}
};
self.timeObserver = [self.player addPeriodicTimeObserverForInterval:CMTimeMake(10, 1000)
queue:dispatch_get_main_queue()
usingBlock:observerBlock];
首先創建一個block,當時間更新時,它將被調用。假如你對block不熟悉,可以閱讀:How to User Blocks in IOS5 tutorial 。該block基於app的狀態,創建一個顯示音樂播放時間的字串。
在此之後,便是調用-(id)addPeriodicTimeObserverForInterval:(CMTime)interval queue:(dispatch_queue_t)queue usingBlock:(void(^)(CMTime time))block 開始獲取更新信息。
附:關於app的狀態
你的app始終處在以下5種狀態的其中之一,概要如下:
Not running:app在啓動前在此狀態
Active:一旦app開啓,即此狀態
Inactive:app在運行時有事件中斷它的執行,比如一個電話呼叫到來,它將進入該狀態。inactive意味着app仍然在前臺運行但卻不接收事件。
Backgrounded:此狀態,app不在前臺,但其仍能執行代碼
Suspended:當不執行代碼時,app即進入該狀態
若你希望瞭解上述狀態的更詳盡信息,請移步apple官網:App States and Multitasking。
通過調用[[UIApplication sharedApplication]applicationState]來檢測app的狀態,不過要注意的是你僅可以獲取以下3種狀態之一:
UIApplicationStateActive;UIApplicationStateInactive;UIApplicationStateBackground。suspended和not running很明顯不可能在運行代碼期間檢測到,所以沒有它們無對應的值。
返回代碼,加入app處在active狀態,你需要更新label標籤,否則,將更新信息打印到控制檯即可。在後臺狀態時仍需要更新label標籤,但現在需要演示的是app進入後臺後,仍能接收回調。
現在要做的是完成play/pause按鈕的工作,即實現didTapPlayPause方法,添加該方法至TBFirstViewController.m中:
- (IBAction)didTapPlayPause:(id)sender
{
self.btnPlayPause.selected = !self.btnPlayPause.selected;
if (self.btnPlayPause.selected)
{
[self.player play];
}
else
{
[self.player pause];
}
}
所有代碼完成,運行之:
點擊‘player’按鈕,音樂將起,good。
現在我們來測試下後臺模式的工作情況,點擊home(模擬器:Cmd+shift+H)後,但是此刻音樂也隨之停止了。爲什麼?因爲還有關鍵的一塊沒有完成。
大多數後臺模式(除了3,有限時長任務外),你需要在info.plist中添加一個key,來聲明該app在後臺時要運行代碼。
返回Xcode,操作以下:
1,點擊項目
2,點擊info
3,點擊“+”
4,在出現的列表中,選擇‘Required Background Modes’
當選擇了4中的條目後,Xcode將會在該條目下創建一個數組,並含有一個空條目。點擊該子條目右側,並選擇‘App
plays audio’。在顯示的列表中,課餘ikandao所有本文介紹的後臺模式,當然也包含一些基於某些硬件的條目信息。
再次運行項目,播放音樂,然後點擊‘Home’鍵,app進入後臺,但音樂照就播放了。
假如仍然沒有出現上述效果,可能是因爲你用的是模擬器,試着用真機測試,應該沒問題。
你也可以通過查看在Xcode控制檯的時間更新來證明在後臺app仍是工作的。
GitHub上關於本後臺模式的演示項目:BackgroundMusic
2,實時接收定位更新
在定位後臺模式中,即便app處在後臺模式,它仍能根據定位委託事件來接收用戶的位置更新信息。
需要提醒的是:僅當你的app確實能夠根據後臺定位來提供有益於用戶的價值,纔可使用該模式。否則,你用了該模式,但對apple看來,用戶毫無獲益,你的app將會被拒。有時apple也會要求你在app添加一段警告,即告知用戶你的app會增加電池的使用量。
演示項目的第二個tab就是關於定位更新的。打開TBSecondViewController.m文件,將以下聲明
@property (nonatomic, strong) CLLocationManager *locationManager;
@property (nonatomic, strong) NSMutableArray *locations;
添加在:
@interface TBSecondViewController ()
// add code here
@end
中。
CLLocationManager用來獲取設備的定位更新信息。
loactions數組用來存貯多個將被標記到map上的位置信息。
在viewDidLoad末尾添加:
self.locations = [[NSMutableArray alloc] init];
self.locationManager = [[CLLocationManager alloc] init];
self.locationManager.desiredAccuracy = kCLLocationAccuracyBest;
self.locationManager.delegate = self;
將保存位置更新信息的數組初始化。
初始化CLLocationManager對象,並設置其精度(這個可以根據需要設置其值,接下來將會介紹更多的精度設置)屬性。
完成方法:
- (IBAction)accuracyChanged:(id)sender
{
const CLLocationAccuracy accuracyValues[] = {
kCLLocationAccuracyBestForNavigation,
kCLLocationAccuracyBest,
kCLLocationAccuracyNearestTenMeters,
kCLLocationAccuracyHundredMeters,
kCLLocationAccuracyKilometer,
kCLLocationAccuracyThreeKilometers};
self.locationManager.desiredAccuracy = accuracyValues[self.segmentAccuracy.selectedSegmentIndex];
}
accuracyValue數組包含CLLocationManager的desireAccuracy屬性中所有可能的值。
或許你認爲這是很笨拙的方法,爲什麼不能將accuracy屬性一直設置爲最高精度吶?這是因爲考慮到電量的消耗。精度越低,電量使用越少。
所以綜上,當你的app不需要精度太高時,儘量選擇精度和你需求的最接近的值即可。你也可以根據需要隨時修改它。
不考慮desiredAccuracy的值distanceFilter時,還有一個屬性用來控制app多久進行一次定位更新。當設備移動達到一定的距離時(m),該屬性來控制locationManager接收定位更新。爲了節省電量,該屬性應該在滿足要求情況下,越高越好。
添加如下代碼來開始獲取/暫停定位更新;
- (IBAction)enabledStateChanged:(id)sender
{
if (self.switchEnabled.on)
{
[self.locationManager startUpdatingLocation];
}
else
{
[self.locationManager stopUpdatingLocation];
}
}
在xib文件中有UISwitch控件,並鏈接至該方法,以開啓/關閉定位追蹤。
實現CLLocationManagerDelegate方法,來獲取更新的信息:
#pragma mark - CLLocationManagerDelegate
/*
* locationManager:didUpdateToLocation:fromLocation:
*
* Discussion:
* Invoked when a new location is available. oldLocation may be nil if there is no previous location
* available.
*
* This method is deprecated. If locationManager:didUpdateLocations: is
* implemented, this method will not be called.
*/
- (void)locationManager:(CLLocationManager *)manager
didUpdateToLocation:(CLLocation *)newLocation
fromLocation:(CLLocation *)oldLocation
{
// Add another annotation to the map.
MKPointAnnotation *annotation = [[MKPointAnnotation alloc] init];
annotation.coordinate = newLocation.coordinate;
[self.map addAnnotation:annotation];
// Also add to our map so we can remove old values later
[self.locations addObject:annotation];
// Remove values if the array is too big
while (self.locations.count > 100)
{
annotation = [self.locations objectAtIndex:0];
[self.locations removeObjectAtIndex:0];
// Also remove from the map
[self.map removeAnnotation:annotation];
}
if (UIApplication.sharedApplication.applicationState == UIApplicationStateActive)
{
// determine the region the points span so we can update our map's zoom.
double maxLat = -91;
double minLat = 91;
double maxLon = -181;
double minLon = 181;
for (MKPointAnnotation *annotation in self.locations)
{
CLLocationCoordinate2D coordinate = annotation.coordinate;
if (coordinate.latitude > maxLat)
maxLat = coordinate.latitude;
if (coordinate.latitude < minLat)
minLat = coordinate.latitude;
if (coordinate.longitude > maxLon)
maxLon = coordinate.longitude;
if (coordinate.longitude < minLon)
minLon = coordinate.longitude;
}
MKCoordinateRegion region;
region.span.latitudeDelta = (maxLat + 90) - (minLat + 90);
region.span.longitudeDelta = (maxLon + 180) - (minLon + 180);
// the center point is the average of the max and mins
region.center.latitude = minLat + region.span.latitudeDelta / 2;
region.center.longitude = minLon + region.span.longitudeDelta / 2;
// Set the region of the map.
[self.map setRegion:region animated:YES];
}
else
{
NSLog(@"App is backgrounded. New location is %@", newLocation);
}
}
本文並非關於專注位置相關技術,故簡要說明:當app是active時,將依據位置信息更新地圖;app是background時,只將位置更新信息打印到Xcode的控制檯上。
接下來我們來修改info.plist,以支持後臺模式,別重蹈覆轍。添加另外一個條目(‘app register for location updates’)至數組,app便會在進入後臺後,仍能繼續接收位置更新信息。
運行項目,並切換至第二個tab,講switch控件置爲‘ON’狀態。
當app首次運行時,將會彈出一個對話框提示是否允許該app訪問location定位服務。點擊ok並移動設備,你應該看到位置更新情況,在模擬器上也可以模擬。
大致效果如下:
點擊Home鍵,將app置於後臺,移動設備,可以看到在控制檯上輸出更新信息。片刻,將app置於前臺,將會看到所有的地圖標註pins都更新爲後臺獲取的新的位置。
假如應用模擬器,可以模擬移動效果,工具如圖:
將location設置爲Freeway Drive,點擊home鍵。當你定位到一加州高速路上時,將會看到控制檯上打印出來的信息。
2013-03-07 22:31:11.667 TheBackgrounder[52611:c07] App is backgrounded. New location is <+37.33500926,-122.03272188> +/- 5.00m (speed 7.74 mps / course 246.09) @ 3/7/13, 10:31:11 PM Eastern Daylight Time 2013-03-07 22:31:12.670 TheBackgrounder[52611:c07] App is backgrounded. New location is <+37.33497737,-122.03281282> +/- 5.00m (speed 9.18 mps / course 251.37) @ 3/7/13, 10:31:12 PM Eastern Daylight Time 2013-03-07 22:31:13.669 TheBackgrounder[52611:c07] App is backgrounded. New location is <+37.33494812,-122.03292120> +/- 5.00m (speed 10.78 mps / course 251.72) @ 3/7/13, 10:31:13 PM Eastern Daylight Time 2013-03-07 22:31:14.658 TheBackgrounder[52611:c07] App is backgrounded. New location is <+37.33492222,-122.03304215> +/- 5.00m (speed 12.11 mps / course 254.18) @ 3/7/13, 10:31:14 PM Eastern Daylight Time |
有關該模式的演示代碼:BackgroundLocation 。
第三個tab即第三種後臺模式
有限時長的後臺任務(performing finite-Length Tasks --or,whatever)
這個模式官方稱謂叫‘Executing a finite-Length Task in the background’,簡稱爲‘whatever’。
從技術上講,這不是一個後臺模式,因爲你不需要在info.plist文件中聲明你要用這種後臺模式。而是依據一個API,當app進入後臺後,在有限的時間內允許執行任意的代碼。
比如,你可以應用此模式完成一個上傳或者下載任務。形如你創建了一個圖片分享app(或者其它),用戶選擇了一個圖片,並切離該app,可能沒有時間去將圖片傳至服務器上,但是利用此模式的API,你將獲取一定CPU時間來完成上傳。
上述只是一例,該後臺執行代碼是任意的,你可以利用此API去做任何事。執行很複雜的運算,給圖片添加過濾器,渲染一個3D等等。但是需要注意的是你僅獲得有限的時間,而非無限制的。
app進入後臺後,這個執行時間有多長,將取決於IOS。獲取的時長不確定,但是你可以一直檢測着UIApplication的backgroundTimeRemaning屬性,它講告訴你還剩有多久。
普遍但非經API文檔說明,你可能有10分鐘的時間--所以不要依賴這個數字,你也可能只獲得5分鐘甚至於5秒都有可能,故app需要時刻做好任何可能的打算。
下面是一個普通任務:廣爲熟知的斐波拉切數列FibonacciSequence。下面將演示在後臺進行運算。
打開TBThirdViewController.m文件,添加屬性:
@property (nonatomic, strong) NSDecimalNumber *previous;
@property (nonatomic, strong) NSDecimalNumber *current;
@property (nonatomic) NSUInteger position;
@property (nonatomic, strong) NSTimer *updateTimer;
@property (nonatomic) UIBackgroundTaskIdentifier backgroundTask;
到
@interface TBThirdViewController ()
// add code here
@end
中。
NSDecimalNumber將持有數列中2個上次的值,NSDecimalNumber對象可保存非常大的數,所以此處應用非常適合。
position計數器,在序列中告知你當前數的位置。
應用updateTimer演示持續計算過程,並能減緩計算過程,以方便觀察。
加如下代碼至viewDidLoad結尾:
self.backgroundTask = UIBackgroundTaskInvalid;
關鍵:
- (IBAction)didTapPlayPause:(id)sender
{
self.btnPlayPause.selected = !self.btnPlayPause.selected;
if (self.btnPlayPause.selected)
{
self.previous = [NSDecimalNumber one];
self.current = [NSDecimalNumber one];
self.position = 1;
self.updateTimer = [NSTimer scheduledTimerWithTimeInterval:0.5
target:self
selector:@selector(calculateNextNumber)
userInfo:nil
repeats:YES];
self.backgroundTask = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
NSLog(@"Background handler called. Not running background tasks anymore.");
[[UIApplication sharedApplication] endBackgroundTask:self.backgroundTask];
self.backgroundTask = UIBackgroundTaskInvalid;
}];
}
else
{
[self.updateTimer invalidate];
self.updateTimer = nil;
if (self.backgroundTask != UIBackgroundTaskInvalid)
{
[[UIApplication sharedApplication] endBackgroundTask:self.backgroundTask];
self.backgroundTask = UIBackgroundTaskInvalid;
}
}
}
詳解上述代碼,探討如何使用API的。
通過切換按鈕的selected屬性來決定是否進行計算,或者準備開始或者結束。
首先設置數列,接着創建NSTimer,並開啓,使之每隔0.5秒執行一次calculateNextNumber方法。
下面即是最精要代碼:調用beginBackgroundTaskWithExpirationHander:方法,告知IOS一旦app進入後臺,將需要更多的時間來完成某些操作。調用之後,app若進入後臺,仍獲取部分CPU時長,直至調用endBackgroudTask:方法。
當然,如你不調用endBackgroudTask:方法,經過一段的後臺時長,IOS將調用beginBackgroundTaskWithExpirationHander中定義的Block,讓你結束正在執行的代碼,所以此處是調用endBackgroudTask的好地方,告知IOS已經完成相關功能。若你不理會此方法的調用,依然執行代碼的話,app將會被強制終止。
if之後使timer無效(invalidates):並調用endBackgroudTask:方法告知IOS不再需要額外的後臺時間。
每次調用beginBackgroundTaskWithExpirationHander時調用endBackgroudTask:是很重要的。假如調用beginBackgroundTaskWithExpirationHander兩次,而只調用endBackgroudTask一次,app將仍請求有CPU時間,直到用第二次backgroundtask的值再一次調用endBackgroudTask爲止。這也是需要background值的原因。
補全代碼:
- (void)calculateNextNumber
{
NSDecimalNumber *result = [self.current decimalNumberByAdding:self.previous];
if ([result compare:[NSDecimalNumber decimalNumberWithMantissa:1 exponent:40 isNegative:NO]] == NSOrderedAscending)
{
self.previous = self.current;
self.current = result;
self.position++;
}
else
{
// This is just too much.... Let's start over.
self.previous = [NSDecimalNumber one];
self.current = [NSDecimalNumber one];
self.position = 1;
}
NSString *currentResultLabel = [NSString stringWithFormat:@"Position %d = %@", self.position, self.current];
if (UIApplication.sharedApplication.applicationState == UIApplicationStateActive)
{
self.txtResult.text = currentResultLabel;
}
else
{
NSLog(@"App is backgrounded. Next number = %@", currentResultLabel);
NSLog(@"Background time remaining = %.1f seconds", [UIApplication sharedApplication].backgroundTimeRemaining);
}
}
此處比較有意思的是backgroundTimeRemaining的值。此代碼僅當調用beginBackgroundTaskWithExpirationHander的block時才停止。
運行項目,並切換至第三個tab。
點擊開始,查看app計算情況。點擊home鍵,查看控制檯輸出情況,you should see the app still updating the numbers while the time remaining goes down。
在大多數情況下,這個後臺時長始以600s(10分鐘)到最低5s。當時長達到5s即過期時(也可能是其它時長),過期block將被調用,app停止輸出信息。此時返回app,timer將重新運行。
在上面代碼中僅有一個bug,由此可以解釋什麼是‘後臺通知’。
假設將app置於後臺,知道過期,app將調用過期block,並執行endBackgroudTask。結束後臺時間需求。
此刻再次重回app,timer將繼續fire。但再次置於後臺,將不會再有後臺時長。這是爲什麼?因爲在過期後和再次進入後臺時沒有再次調用beginBackgroundTaskWithExpirationHander。
有多種解決方式來修正此bug,其中之一是利用app狀態更改監聽來更正之。
2種獲取狀態更改通知:
1,通過main app delegate方法。
2,監聽IOS發送來的監聽事件。
UIApplicationWillResignActiveNotification和applicationWillResignActive:當app將要置爲inactive狀態時,前者將被sent,後者將被調用。此時,app尚未進入後臺--仍是前臺app但不會接收任何UI事件。
UIApplicationDidEnterBackgroundNotification和applicationDidEnterBackground:app置爲後臺時將被sent和調用。此時,app不再active,並且這是最後運行代碼的機會。這也是調用beginBackgroundTaskWithExpirationHander的執行時間,若想獲得更多的CPU時長的話。
UIapplicationWillEnterForegroundNotification和applicationWillEnterForeground:app將至active狀態時將被sent和調用。app此時仍在後臺,但你已經可以開始你要做的代碼了。也是調用endBackgroudTask的好時刻,若是在進入後臺時調用beginBackgroundTaskWithExpirationHander的話。
UIApplicationDidBecomeActiveNotification和applicationDidBecomeActive:在上面的事件發生後,自後臺後,將被sent和調用;或者在app被臨時中斷後,比如一個電話呼叫。並非真正的進入後臺,將會收到UIApplicationWillResignActiveNotification監聽事件。
你可以在apple文檔中查看上述流程的圖列:App States and Multitasking 。
下一部分將會介紹如何使用這些監聽,解決beginBackgroundTaskWithExpirationHander 的bug將留爲一個課後聯繫。
演示代碼:BackgroundWhatever 。
雜誌下載
在IOS5中,apple介紹了雜誌API:允許建立雜誌類報紙類的app,並且具有一些別於其它app的特性。用雜誌API創建的app在安裝到原生雜誌app裏以前它不具有app icon的。
雜誌後臺模式非常便於雜誌類app。他提供了一個API集來使app下載大文件(一般爲期刊雜誌)變得容易。即使你的app不是在active狀態,它也支持下載知道完成。
附:如果你想更具體瞭解雜誌類app,IOS5 by Tutorials有相關的章節。
實例項目的xib中,在UIWebView控件上有一個載有URL的UITextField,默認的URL爲:https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/ProgrammingWithObjectiveC.pdf
這個一個PDF的鏈接文件,方便展示app進入後臺後,仍然可以繼續下載的效果。其他的大文件下載鏈接亦可。
開始在TBFourthViewController.m文件中添加2個屬性:
@property (nonatomic, strong) NKIssue *currentIssue;
@property (nonatomic, strong) NSString *issueFilename;
完善UITextFieldDelegate方法,響應編輯好textfiled,並點擊return後的操作:
#pragma mark - UITextFieldDelegate
- (BOOL)textFieldShouldReturn:(UITextField *)textField
{
self.webView.hidden = YES;
self.progress.progress = 0.0f;
self.progress.hidden = NO;
NKLibrary *library = [NKLibrary sharedLibrary];
for (NKIssue *issue in [library.issues copy])
{
[library removeIssue:issue];
}
self.currentIssue = [library addIssueWithName:@"test" date:[NSDate date]];
NSURL *downloadURL = [[NSURL alloc] initWithString:self.txtURL.text];
NSURLRequest *request = [NSURLRequest requestWithURL:downloadURL];
NKAssetDownload *assetDownload = [self.currentIssue addAssetWithRequest:request];
[assetDownload downloadWithDelegate:self];
[textField resignFirstResponder];
return YES;
}
首先講webView隱藏,並顯示置零的progress。
NKLibrary提供一個單例來管理app中雜誌/報紙的issues。利用它來創建NKIssue對象。
移除所有現存的issue以防添加issue時出現重名錯誤。
創建issue,命名。在真實的生產環境下,名稱需是唯一,issue名稱或者號碼序列。
以textfield的值建立NSURL對象,並創建NSURLRequest,將其添加至NKIssue對象中,返回NKAssetDownload對象,開啓下載任務,並設置self爲delegate。
實現NSURLConnection的一個委託方法(可選),即用於接收下載的進度:
#pragma mark - NSURLConnectionDownloadDelegate
- (void)connection:(NSURLConnection *)connection
didWriteData:(long long)bytesWritten
totalBytesWritten:(long long)totalBytesWritten
expectedTotalBytes:(long long)expectedTotalBytes
{
float progress = (float)totalBytesWritten / (float)expectedTotalBytes;
if (UIApplication.sharedApplication.applicationState == UIApplicationStateActive)
{
self.progress.progress = progress;
}
else
{
NSLog(@"App is backgrounded. Progress = %.1f", progress);
}
}
- (void)connectionDidFinishDownloading:(NSURLConnection *)connection
destinationURL:(NSURL *)destinationURL
{
self.issueFilename = destinationURL.pathComponents.lastObject;
NSURL *fileURL = [self.currentIssue.contentURL URLByAppendingPathComponent:self.issueFilename];
[[NSFileManager defaultManager] moveItemAtURL:destinationURL
toURL:fileURL
error:nil];
if (UIApplication.sharedApplication.applicationState == UIApplicationStateActive)
{
self.webView.hidden = NO;
self.progress.hidden = YES;
NSURL *fileURL = [self.currentIssue.contentURL URLByAppendingPathComponent:self.issueFilename];
[self.webView loadRequest:[NSURLRequest requestWithURL:fileURL]];
}
else
{
NSLog(@"App is backgrounded. Download finished");
}
}
第一個方法在下載數據有更新時被調用。不需要太多操作,OS進行下載數據,app只要趕住UI更新即可,當然區分app處的狀態。
第二個方法是當下載完成時被調用。然後將下載的文件遷移到NewsstandAPI期望的目錄下。並且當設備空間不足時允許API進行刪除老的issues。
文件遷移之後,當app爲active時更新UI,否則打印至控制檯上。
但當app處後臺時,下載完成,將如何處理?webVIew不會更新PDF。我們可以利用監聽事件來時刻關注上述問題。
首先在上述方法中找到並提取出更新代碼,避免重複代碼(時刻銘記保持代碼DRY),在本m文件的任何地方添加如下方法:
- (void)updateWebView
{
self.webView.hidden = NO;
self.progress.hidden = YES;
NSURL *fileURL = [self.currentIssue.contentURL URLByAppendingPathComponent:self.issueFilename];
[self.webView loadRequest:[NSURLRequest requestWithURL:fileURL]];
}
從connectionDidFinishDownloading:destinationURL:移除多餘代碼,使之爲下:
if (UIApplication.sharedApplication.applicationState == UIApplicationStateActive)
{
[self updateWebView];
}
else
{
NSLog(@"App is backgrounded. Download finished");
}
添加如下方法,當app爲active狀態時調用
- (void)appBecameActive
{
if (self.currentIssue && self.currentIssue.downloadingAssets.count == 0 && self.webView.hidden)
{
[self updateWebView];
}
}
最後,在viewDidLoad最後添加:
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(appBecameActive)
name:UIApplicationDidBecomeActiveNotification
object:nil];
無論何時添加監聽,不要忘記對應的添加移除操作,在dealloc中:
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
最後一步,在info.plst中添加鍵值。這次需要2個鍵值對來使app成爲一個雜誌類app。
在Required background modes數組下添加:
接着創建一個新的key,並選擇application
presents content in Newsstand,在value中改爲boolean屬性,並設置爲YES:
運行之,效果:
ok,它在前臺工作很好,但是它還未經真實的測試。
重新運行,在下載時點擊HOME,將看到打印到控制檯的信息,重回APP,將看到webview已經更新PDF,ok,正確。
假如網絡很好的情況下,可能還沒有來得及看到控制檯的信息,下載過程就很快完成了。這種情況,你可以在設備上運行該app,並使用較大文件下載,或者將網絡調至成較差的情況。
需要注意的是你的app並不含有一個正規的icon圖片,它是在newsstandAPP中的並且有個默認的雜誌icon。
演示項目:BackgroundNewsstand。
5,提供VoIP服務
最後一個是一個強大的後臺模式,它允許你的APP在後臺時運行任意代碼。這個後臺模式相較‘任意時長(whatever)’的更好,因爲它沒有時長限制即不限時。
更重要的,app若崩潰或者重啓設備,APP仍然自動在後臺運行。good。
當然,使用它的前提是:你的APP必須提供給用戶VoIP功能纔可以,否則,apple將會拒掉。
VoIP app的創建超出本文所述,但我會闡述基本。
VoIP:Voice over IP或者網絡電話。本文中,你將建立一個簡單app並鏈接至服務器,在後臺時仍保持鏈接,app接收訊息時回調。
打開TBFifthViewController.m文件,添加如下屬性聲明:
@property (nonatomic, strong) NSInputStream *inputStream;
@property (nonatomic, strong) NSOutputStream *outputStream;
@property (nonatomic, strong) NSMutableString *communicationLog;
@property (nonatomic) BOOL sentPing;
在@implementation TBFifthViewController之前,定義字串常量,在鏈接中會用到:
const uint8_t pingString[] = "ping\n";
const uint8_t pongString[] = "pong\n";
下面是一個簡便方法添加到TextView上的。用它來判斷是否鏈接完成,是否中斷或者接收到訊息。
- (void)addEvent:(NSString *)event
{
[self.communicationLog appendFormat:@"%@\n", event];
if (UIApplication.sharedApplication.applicationState == UIApplicationStateActive)
{
self.txtReceivedData.text = self.communicationLog;
}
else
{
NSLog(@"App is backgrounded. New event: %@", event);
}
}
app是active時其追加字串並更新UI,否則處在後臺模式時僅打印在控制檯上。
下面方法在用戶點擊連接按鈕時調用:
- (IBAction)didTapConnect:(id)sender
{
if (!self.inputStream)
{
// 1
CFReadStreamRef readStream;
CFWriteStreamRef writeStream;
CFStreamCreatePairWithSocketToHost(NULL, (__bridge CFStringRef)(self.txtIP.text), [self.txtPort.text intValue], &readStream, &writeStream);
// 2
self.sentPing = NO;
self.communicationLog = [[NSMutableString alloc] init];
self.inputStream = (__bridge_transfer NSInputStream *)readStream;
self.outputStream = (__bridge_transfer NSOutputStream *)writeStream;
[self.inputStream setProperty:NSStreamNetworkServiceTypeVoIP forKey:NSStreamNetworkServiceType];
// 3
[self.inputStream setDelegate:self];
[self.outputStream setDelegate:self];
[self.inputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
[self.outputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
// 4
[self.inputStream open];
[self.outputStream open];
// 5
[[UIApplication sharedApplication] setKeepAliveTimeout:600 handler:^{
if (self.outputStream)
{
[self.outputStream write:pingString maxLength:strlen((char*)pingString)];
[self addEvent:@"Ping sent"];
}
}];
}
}
看起來有點複雜,因爲創建了2條stream。
1,應用CFStreamCreatePairWithSocketToHost方法是最簡便的方式,意味着此後更多的橋接(bridging)可以應用NSInputStream和NSOutputStream類。
2,以CFStreamCreatePairWithSocketToHost創建2個stream對後,將它們轉換成oc類,setProperty:forKey方法調用非常重要,由此告知OS,app在後臺時,鏈接仍需保持。該設置只需在input stream完成即可。
3,設置s2個tream的委託對象爲self,並將runloop設置爲main runloop。OS需要確定委託方法需要在哪個runloop下被調用,本例中最適合的runloop便是和app主線程相關的。爲了在接收到信息後更新UI,故需要設置在main runloop中。
4,設置好後,開啓2個stream。
5,調用setKeepAliveTimeout:handler:,app在後臺時將會定期調用handler。運行在其中做任何事情,但是它應當用於發送‘ping’到服務器,來保持鏈接可用。上述代碼設置爲每隔10 min進行ping操作(文檔所示的最小值)。在此唯一做的便是ping服務器,並打印輸出。
添加stream委託事件來接收鏈接更新:
#pragma mark - NSStreamDelegate
- (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode
{
switch (eventCode) {
case NSStreamEventNone:
// do nothing.
break;
case NSStreamEventEndEncountered:
[self addEvent:@"Connection Closed"];
break;
case NSStreamEventErrorOccurred:
[self addEvent:[NSString stringWithFormat:@"Had error: %@", aStream.streamError]];
break;
case NSStreamEventHasBytesAvailable:
if (aStream == self.inputStream)
{
uint8_t buffer[1024];
NSInteger bytesRead = [self.inputStream read:buffer maxLength:1024];
NSString *stringRead = [[NSString alloc] initWithBytes:buffer length:bytesRead encoding:NSUTF8StringEncoding];
stringRead = [stringRead stringByTrimmingCharactersInSet:[NSCharacterSet newlineCharacterSet]];
[self addEvent:[NSString stringWithFormat:@"Received: %@", stringRead]];
if ([stringRead isEqualToString:@"notify"])
{
UILocalNotification *notification = [[UILocalNotification alloc] init];
notification.alertBody = @"New VOIP call";
notification.alertAction = @"Answer";
[[UIApplication sharedApplication] presentLocalNotificationNow:notification];
}
else if ([stringRead isEqualToString:@"ping"])
{
[self.outputStream write:pongString maxLength:strlen((char*)pongString)];
}
}
break;
case NSStreamEventHasSpaceAvailable:
if (aStream == self.outputStream && !self.sentPing)
{
self.sentPing = YES;
if (aStream == self.outputStream)
{
[self.outputStream write:pingString maxLength:strlen((char*)pingString)];
[self addEvent:@"Ping sent"];
}
}
break;
case NSStreamEventOpenCompleted:
if (aStream == self.inputStream)
{
[self addEvent:@"Connection Opened"];
}
break;
default:
break;
}
}
該方法涵蓋了鏈接中所有可能的事件,大多數是簡單的自我說明的。
NSStreamEventHasBytesAvailable:一般發生在inputstream中,但是你必須要檢測。讀取數據至字串中,消去換行符等,打印出來。
附:如果事件是‘notify’,創建一個本地通知。在真實的VoIP中,該行爲應是對應一個接入呼叫。即使在後臺,本地通知亦展示出來。
如果是‘ping’,應該回應服務器‘pong’。
NSStreamEventHasSpaceAvailable:輸出的數據爲空,僅當首次接收到時,發送ping。
然後打開info.plist文件,在Required background Modes數組下添加App provides Voice over IP services:
運行項目前,需要一個測試服務器。你可以應用mac已有的工具,叫做netcat。可以實現簡單的基於文本的服務器。
打開終端,鍵入:
nc -l 10000
命令開始了一個在端口10000上運行的服務器,並開啓監聽鏈接,返回Xcode,運行項目:
若在模擬器上運行,可以設置地位爲127.0.0.1,若真機測試,應找出測試mac的ip地址。
點擊connect,鏈接正常開始的話,可以看到控制檯的輸出:
如果要測試通知命令,需要在真機上測試。有關通知的內容目前在模擬器上無法測試。
點擊HOME進入後臺,在終端發送ping,你仍可在控制檯輸出中看到pong的迴應。、
如果運行在真機上,當app是後臺模式時,試着發送nitify命令給它,可以看到:
演示項目:backgroundVoIP 。
何去何從?
你可下載關於本文的所有的資源文件:full Project。
若想閱讀關於本文技術相關的apple文檔,請移步:Background Execution and multitasking 。文檔很好的的解釋了每個後臺模式,每個部分均有合適的外鏈文檔。
本文特別值得關注的是:being a responsible background app。在發佈你的具有後臺模式的app之前,有一些可行/不可行的細節。
全部項目在my GitHub,下載,fork,並修複相關bug,祝愉快。