iOS後臺模式藉助位置更新實現

小東邪發表於2019-03-04

需求:iOS系統下使我們的app在後臺下(點選Home鍵進入後臺)仍能繼續執行任務.


閱讀前提:

  • 瞭解後臺任務機制
  • 瞭解獲取位置基本原理

GitHub地址(附程式碼) : iOS後臺模式藉助位置更新實現

簡書地址 : iOS後臺模式藉助位置更新實現

部落格地址 : iOS後臺模式藉助位置更新實現

掘金地址 : iOS後臺模式藉助位置更新實現


原理

iOS下預設app中所有執行緒在進入後臺後(點選Home鍵或上滑退出)所有執行緒處於掛起狀態,即不支援後臺執行程式,當再次點選進入app後,所有執行緒恢復執行,因此,如果要實現後臺模式,即所有執行緒在進入後臺後仍處於活躍狀態。

蘋果官方提供了開啟後臺模式開關的操作,但是需要說明使用後臺模式的場景,如下專案配置圖1中所示,但我們不能平白無故使用例如後臺播放音樂,VOIP等等功能(否則會上架被拒),綜合考慮,我們可以選用後臺實時更新位置以實現支援後臺模式,因為位置可以作為app的資料分析統計等等。

流程

  • 實現獲取地理位置資訊(必須給予app始終允許的位置許可權)
  • 開啟後臺模式-更新地理位置資訊
  • 進入後臺後輪循開始後臺任務(1個後臺任務有效時間為3分鐘)

專案配置

實現後臺模式需要藉助例如位置更新,後臺播放音樂,VOIP等等功能以實現後臺app處於活躍狀態

  • 開啟後臺模式:Project -> Capabilities -> Background Modes -> Location updates

    Background_Mode

  • plist檔案中新增位置許可權

	<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
	<string>Give me location authority</string>
	<key>NSLocationWhenInUseUsageDescription</key>
	<string>Give me location authority</string>
複製程式碼

location_authority

  • 匯入靜態庫CoreLocation.framework, 然後在需要的檔案中匯入標頭檔案#import <CoreLocation/CoreLocation.h>與#import <CoreLocation/CLLocation.h>
  • 在啟動app時給予始終允許的許可權(否則無法支援後臺模式)

Note : 因為我們是通過後臺任務來實時獲取地理位置以實現後臺模式,所以如果不給予app始終允許的位置許可權則無法實時在後臺獲取地理位置,也無法實現我們的需求。

實現

1.實現獲取地理位置(CoreLocation)

1.1 初始化並配置locationManager物件

遵循CLLocationManagerDelegate協議,按照如下設定完成後即可在回撥函式中獲取經緯度等位置資訊

@property (nonatomic, strong) CLLocationManager   *locationManager;

    _locationManager = [[CLLocationManager alloc] init];
    _locationManager.delegate                           = self;
    _locationManager.desiredAccuracy                    = kCLLocationAccuracyBestForNavigation;
    _locationManager.pausesLocationUpdatesAutomatically = NO;
    self.locationManager.distanceFilter                 = kCLDistanceFilterNone; // 不移動也可以後臺重新整理回撥
    
    if([[UIDevice currentDevice].systemVersion floatValue]>= 8.0) {
        [self.locationManager requestAlwaysAuthorization];
    }
    
    [self.locationManager startUpdatingLocation];
複製程式碼

1.2 回撥函式中獲取地理位置資訊

如果開啟後臺模式,我們需要使用_isCollectLocation標記當前是否正在定位,如果當前停止定位我們則需要重啟定位功能,我們在這裡設定120秒後開啟一個後臺任務並在10秒後停止一個後臺任務,因為一個後臺任務的有效時間為3分鐘,我們就讓前一個後臺任務執行到2分鐘左右時停止掉(以防時間控制本身不精確所以設定2分鐘),然後開啟一個新的後臺任務,不斷迴圈改過程,即可實現後臺模式。

-(void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray<CLLocation *> *)locations {
    CLLocation *location = [locations lastObject];    //當前位置資訊
    
    self.longitude = location.coordinate.longitude;
    self.latitude  = location.coordinate.latitude;
    
    if (self.isSupportBGMode) {
        //如果正在10秒定時收集的時間,不需要執行延時開啟和關閉定位
        if (_isCollectLocation) {
            return;
        }
        [self performSelector:@selector(restartLocation) withObject:nil afterDelay:120];
        [self performSelector:@selector(stopLocation)    withObject:nil afterDelay:10];
        _isCollectLocation = YES;//標記正在定位
    }
}

-(void)restartLocation {
    self.locationManager.delegate       = self;
    self.locationManager.distanceFilter = kCLDistanceFilterNone; // 不移動也可以後臺重新整理回撥
    if ([[UIDevice currentDevice].systemVersion floatValue]>= 8.0) {
        [self.locationManager requestAlwaysAuthorization];
    }
    [self.locationManager startUpdatingLocation];
    [self.bgModeManager beginNewBackgroundTask];
}

-(void)stopLocation {
    log4cplus_debug("XDXBGModeManager", "%s - Stop Background Mode Location service !",ModuleName);
    _isCollectLocation = NO;
    [self.locationManager stopUpdatingLocation];
}

複製程式碼

1.3 開啟與關閉後臺模式

停止後臺模式即停止位置更新,delegate也失效同時停止當前所有的後臺任務,停止監聽app進入後臺的通知 開啟後臺模式即重新設定代理並開始位置更新,同時註冊使用者進入後臺的通知以便在使用者進入後臺後開啟輪循的後臺任務

- (void)openBGMode {
    self.isSupportBGMode = YES;
    // Note : You need to open background mode in the project setting, Otherwise the app will crash.
    _locationManager.allowsBackgroundLocationUpdates = YES;
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationEnterBackground) name:UIApplicationDidEnterBackgroundNotification object:nil];
}

- (void)closeBGMode {
    self.isSupportBGMode = NO;
    [self.bgModeManager endAllBGTask];
    [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidEnterBackgroundNotification object:nil];
}

-(void)applicationEnterBackground {
    [self startBGLocationService];
    [self.bgModeManager beginNewBackgroundTask];
}

- (void)startBGLocationService {
    if ([CLLocationManager locationServicesEnabled] == NO) {
        log4cplus_error("XDXBGModeManager", "%s - You currently have all location services for this device disabled", ModuleName);
    }else {
        CLAuthorizationStatus authorizationStatus = [CLLocationManager authorizationStatus];
        
        if(authorizationStatus == kCLAuthorizationStatusDenied || authorizationStatus == kCLAuthorizationStatusRestricted) {
            log4cplus_error("XDXBGModeManager", "%s - AuthorizationStatus failed",ModuleName);
        }else {
            log4cplus_info("XDXBGModeManager", "%s - Start Background Mode Location service !",ModuleName);
            self.locationManager.distanceFilter = kCLDistanceFilterNone;
            
            if([[UIDevice currentDevice].systemVersion floatValue]>= 8.0) {
                [self.locationManager requestAlwaysAuthorization];
            }
            [self.locationManager startUpdatingLocation];
        }
    }
}
複製程式碼

1.4 處理獲取地理位置失敗的情況

當使用者拒絕提供位置許可權或當前網路故障時,應該給予使用者提示

- (void)locationManager: (CLLocationManager *)manager didFailWithError: (NSError *)error {
    log4cplus_error("XDXBGModeManager", "%s - locationManager error:%s",ModuleName, [NSString stringWithFormat:@"%@",error].UTF8String);
    
    self.latitude  = 0;
    self.longitude = 0;
    
    switch([error code])
    {
        case kCLErrorNetwork:
            log4cplus_error("XDXBGModeManager", "%s - %s : Please check the network connection !",ModuleName, __func__);
            
            static dispatch_once_t onceToken;
            dispatch_once(&onceToken, ^{
                UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Location Warning" message:
                                            @"Please check the network connection" preferredStyle:UIAlertControllerStyleAlert];
                UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:@"OK" style:
                                               UIAlertActionStyleCancel handler:nil];
                [alert addAction:cancelAction];
                UIViewController *vc = [UIApplication sharedApplication].windows[0].rootViewController;
                [vc presentViewController:alert animated:YES completion:nil];
            });
            
            break;
        case kCLErrorDenied:
        {
            log4cplus_error("XDXBGModeManager", "%s - %s : Please open location authority on the setting if you want to use our service!",ModuleName, __func__);
            
            static dispatch_once_t onceToken2;
            dispatch_once(&onceToken2, ^{
                UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Location Warning" message:
                                            @"Please allow our app access the location always on the setting->Privacy->Location Services !" preferredStyle:UIAlertControllerStyleAlert];
                UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:@"OK" style:
                                               UIAlertActionStyleCancel handler:nil];
                [alert addAction:cancelAction];
                UIViewController *vc = [UIApplication sharedApplication].windows[0].rootViewController;
                [vc presentViewController:alert animated:YES completion:nil];
            });
            
        }
            break;
        default:
            break;
    }
}
複製程式碼

2.實現後臺任務的輪循

2.1 基本配置

使用bgTaskIdList記錄當前所有後臺任務的列表,使用masterTaskId記錄當前正在執行的後臺任務

@property (nonatomic, strong)   NSMutableArray             *bgTaskIdList;
@property (assign)              UIBackgroundTaskIdentifier masterTaskId;
複製程式碼

2.2 開啟後臺任務

beginBackgroundTaskWithExpirationHandler呼叫此方法可實現開啟一個後臺任務,我們需要將此後臺任務放入我們記錄的陣列中並將其設定為masterTaskId,當然,如果上次記錄的後臺記錄已經失效,就記錄新任務為主任務,如果上次開啟的後臺任務還沒結束,就提前關閉,使用最新的後臺任務

-(void)restartLocation {
    [self beginNewBackgroundTask];
}

-(UIBackgroundTaskIdentifier)beginNewBackgroundTask {
    UIApplication *application = [UIApplication sharedApplication];
    __block UIBackgroundTaskIdentifier bgTaskId = UIBackgroundTaskInvalid;
    if([application respondsToSelector:@selector(beginBackgroundTaskWithExpirationHandler:)]) {
        bgTaskId = [application beginBackgroundTaskWithExpirationHandler:^{
            log4cplus_warn("XDXBGModeManager", "%s - bg Task (%lu) expired !",ModuleName,bgTaskId);
            [self.bgTaskIdList removeObject:@(bgTaskId)];//過期任務從後臺陣列刪除
            bgTaskId = UIBackgroundTaskInvalid;
            [application endBackgroundTask:bgTaskId];
        }];
    }
    //如果上次記錄的後臺任務已經失效了,就記錄最新的任務為主任務
    if (_masterTaskId == UIBackgroundTaskInvalid) {
        self.masterTaskId = bgTaskId;
        log4cplus_warn("XDXBGModeManager", "%s - Start bg task : %lu",ModuleName,(unsigned long)bgTaskId);
    }else { //如果上次開啟的後臺任務還未結束,就提前關閉了,使用最新的後臺任務
        //add this id to our list
        log4cplus_warn("XDXBGModeManager", "%s - Keep bg task %lu",ModuleName,(unsigned long)bgTaskId);
        [self.bgTaskIdList addObject:@(bgTaskId)];
        [self endInvalidBGTaskWithIsEndAll:NO];//留下最新建立的後臺任務
    }
    return bgTaskId;
}

複製程式碼

2.3 結束後臺任務

如果僅僅結束其他後臺任務,只保留當前主任務即將陣列中除了最後一個元素外都刪除,並通過遍歷結束所有無效後臺任務

- (void)endAllBGTask {
    [self endInvalidBGTaskWithIsEndAll:YES];
}

-(void)endInvalidBGTaskWithIsEndAll:(BOOL)isEndAll
{
    UIApplication *application = [UIApplication sharedApplication];
    //如果為all 清空後臺任務陣列
    //不為all 留下陣列最後一個後臺任務,也就是最新開啟的任務
    if ([application respondsToSelector:@selector(endBackGroundTask:)]) {
        for (int i = 0; i < (isEndAll ? _bgTaskIdList.count :_bgTaskIdList.count -1); i++) {
            UIBackgroundTaskIdentifier bgTaskId = [self.bgTaskIdList[0]integerValue];
            log4cplus_debug("XDXBGModeManager", "%s - Close bg task %lu",ModuleName,(unsigned long)bgTaskId);
            [application endBackgroundTask:bgTaskId];
            [self.bgTaskIdList removeObjectAtIndex:0];
        }
    }
    
    ///如果陣列大於0 所有剩下最後一個後臺任務正在跑
    if(self.bgTaskIdList.count > 0) {
        log4cplus_debug("XDXBGModeManager", "%s - The bg task is running %lu!",ModuleName,(long)[_bgTaskIdList[0]integerValue]);
    }
    
    if(isEndAll) {
        [application endBackgroundTask:self.masterTaskId];
        self.masterTaskId = UIBackgroundTaskInvalid;
    }else {
        log4cplus_debug("XDXBGModeManager", "%s - Kept master background task id : %lu",ModuleName,(unsigned long)self.masterTaskId);
    }
}
複製程式碼

注意

後臺模式在特定情況下會app被系統自動殺死 例如:息屏後放置較長時間或app佔用大量CPU資源

結果

執行Demo程式,開啟後臺開關,進入後臺觀察控制檯仍有列印,證明後臺模式啟用成功

相關文章