《高效能iOS 應用開發》之降低你 APP 的電量消耗

奧卡姆剃鬚刀發表於2018-02-07

高效能iOS 應用開發

在編寫高效能 程式碼時, 電量消耗是一個需要重點處理的重要因素, 就執行時間和 CPU 資源的利用而言, 我們不僅要實現高效的資料結構和演算法, 還需要考慮其他的因素,如果某個應用是個電池黑洞,那麼一定不會有人喜歡他 電量消耗除了 CPU 外,還有一些硬體模組:網路硬體, 藍芽,GPS, 麥克風,加速計,攝像頭,揚聲器,和螢幕. 我們可以帶著以下問題來看這篇文章:

  • 消耗電量的關鍵領域有哪些
  • 如何降低電量的消耗
  • 如何在 IOS 應用中分析電源, CPU 和資源的使用

一 CPU

不論使用者是否正在直接使用, CPU 都是應用所使用的主要硬體, 在後臺操作和處理推送通知時, 應用仍然會消耗 CPU 資源

iOS 裝置與處理器

應用計算的越多,消耗的電量越多.在完成相同的基本操作時, 老一代的裝置會消耗更多的電量(換電池呀 哈哈哈 開個玩笑),計算量的消耗取決於不同的因素

  • 對資料的處理
  • 待處理的資料大小---更大的螢幕允許軟體在單個檢視中展示更多的資訊,但這也意味著要處理更多的資料
  • 處理資料的演算法和資料結構
  • 執行更新的次數,尤其是在資料更新後,觸發應用的狀態或 UI 進行更新(應用收到的推送通知也會導致資料更新,如果此使用者正在使用應用,你還需要更新 UI)

沒有單一原則可以減少裝置中的執行次數,很多規則都取決於操作的本質, 以下是一些可以在應用中投入使用的最佳實踐

  • 針對不同的情況選擇優化的演算法 例如,當你在排序時,如果列表少於43個例項, 則插入排序優於歸併排序, 但例項對於286時, 應當使用快速排序,要優先使用雙樞軸快速排序而不是傳統的單樞軸快速排序
  • 如果應用從伺服器接受資料,儘量減少需要在客戶端進行的處理 例如如果一段文字需要在客戶端進行渲染,儘可能在伺服器將資料清理乾淨 我曾經做個一個專案, 因為伺服器的實現主要用於服務桌面使用者,所以返回的文字中包含 HTML 標籤, 清理 HTML 標籤的工作並沒有放在客戶端進行, 而是放在了服務端實現,從而減少了裝置上的計算過程, 降低了處理時間
  • 優化靜態編譯(ahead-of-time,AOT)處理 動態編譯處理的缺點在於他會強制使用者等待操作完成, 但是激進的 AOT 處理則會導致計算資源的浪費, 需要根據應用和裝置選擇精確定量的 AOT 處理. 例如,在 UITableView 中渲染一組記錄時,在載入列表是處理全部的記錄並不是明智之舉,基於單元格的高度,如果裝置可以渲染 N 條記錄, 那麼3N 或4N 則是一個理想的資料載入規模, 類似的,使用者快速滑動,則不應立即載入記錄,而應推遲帶滾動速度下降到某一閾值.精確的閾值應該由每個單元格的處理時間和單元格的 UI 的複雜性來決定

二 網路

智慧的網路訪問管理可以讓應用響應的更快,並有助於延長電池壽命.在無法訪問網路時,應該推遲後續的網路請求, 直到網路連線恢復為止. 此外,應避免在沒有連線 WiFi 的情況下進行高寬頻消耗的操作.比如視訊流, 眾所周知, 蜂窩無線系統(LTE,4G,3G等)對電量的消耗遠遠大於 WiFi訊號, 根源在於 LTE 裝置基於多輸入,多輸出技術,使用多個併發訊號以維護兩端的 LTE 連結,類似的,所有的蜂窩資料連結都會定期掃描以尋找更強的訊號. 因此:我們需要

  • 在進行任何網路操作之前,先檢查合適的網路連線是否可用
  • 持續監視網路的可用性,並在連結狀態發生變化時給與適當的反饋

三 定位管理器和 GPS

這個知識點我專案中並沒有用到定位相關的功能 ,不過也總結一下書中所講的知識點 有用的定位功能的朋友可以參考此知識點來優化自己的 app

我們都知道定位服務是很耗電的,使用 GPS 計算座標需要確定兩點資訊:

  • 時間鎖 每個 GPS 衛星每毫秒廣播唯一一個1023位隨機數, 因而資料傳播速率是1.024Mbit/s GPS 的接收晶片必須正確的與衛星的時間鎖槽對齊
  • 頻率鎖 GPS 接收器必須計算由接收器與衛星的相對運動導致的多普勒偏移帶來的訊號誤差

計算座標會不斷的使用 CPU 和 GPS 的硬體資源,因此他們會迅速的消耗電池電量 先來看一下初始化CLLocationManager並高效接受地理位置更新的典型程式碼


#import "LLLocationViewController.h"
#import <CoreLocation/CoreLocation.h>

@interface LLLocationViewController ()<CLLocationManagerDelegate>
@property (nonatomic, strong)CLLocationManager *manager;
@end

@implementation LLLocationViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.manager = [[CLLocationManager alloc]init];
    self.manager.delegate = self;    
}

- (void)enableLocationButtonClick:(UIButton *)sender{
    
    self.manager.distanceFilter = kCLDistanceFilterNone;
    // 按照最大精度初始化管理器
    self.manager.desiredAccuracy = kCLLocationAccuracyBest;
    
    if (IS_IOS8) {
        [self.manager requestWhenInUseAuthorization];
    }
    [self.manager startUpdatingLocation];
}

- (void)locationManager:(CLLocationManager *)manager
     didUpdateLocations:(NSArray<CLLocation *> *)locations{
    
    CLLocation *loc = [locations lastObject];
    // 使用位置資訊
}
複製程式碼
3.1 最佳的初始化
  • distanceFilter 只要裝置的移動超過了最小的距離, 距離過濾器就會導致管理器對委託物件的 LocationManager:didUpdateLocations:事件通知發生變化,該距離單位是 M
  • desiredAccuracy 精度引數的使用直接影響了使用天線的個數, 進而影響了對電池的消耗.精度級別的選取取決於應用的具體用途,精度是一個列舉 我們應該依照不同的需求去恰當的選取精度級別

距離過濾器只是軟體層面的過濾器,而精度級別會影響物理天線的使用.當委託方法 LocationManager:didUpdateLocations:被呼叫時,使用距離範圍更廣泛的過渡器只會影響間隔.另一方面,更高的精度級別意味著更多的活動天線,這會消耗更多的能量

3.2 關閉無關緊要的特性

判斷何時需要跟蹤位置的變化, 在需要跟蹤的時候呼叫 startUpdatingLocation方法, 無須跟蹤時呼叫stopUpdatingLocation方法.

當應用在後臺執行或使用者沒有與別人聊天時,也應該關閉位置跟蹤,也就說說,瀏覽媒體庫,檢視朋友列表或調整應用設定時, 都應該關閉位置跟蹤

3.3 只在必要時使用網路

為了提高電量的使用效率, IOS 總是儘可能地保持無線網路關閉.當應用需要建立網路連線時, IOS 會利用這個機會向後臺應用分享網路會話, 以便一些低優先順序能夠被處理, 如推送通知, 收取電子郵件等 關鍵在於每當使用者建立網路連線時,網路硬體都會在連線完成後多維持幾秒的活動時間.每次集中的網路通訊都會消耗大量的電量 要想減輕這個問題帶來的危害,你的軟體需要有所保留的的使用網路.應該定期集中短暫的使用網路,而不是持續的保持著活動的資料流.只有這樣,網路硬體才有機會關閉

3.4 後臺定位服務

CLLocationManager提供了一個替代的方法來監聽位置的更新. [self.manager startMonitoringSignificantLocationChanges]可以幫助你在更遠的距離跟蹤運動.精確的值由內部決定,且與distanceFilter無關 使用這一模式可以在應用進入後臺後繼續跟蹤運動,典型的做法是在應用進入後臺時執行startMonitoringSignificantLocationChanges方法,而當應用回到前臺時執行startUpdatingLocation 如下程式碼

- (void)applicationDidEnterBackground:(UIApplication *)application {
    [self.manager stopUpdatingLocation];
    [self.manager startMonitoringSignificantLocationChanges];
}
- (void)applicationWillEnterForeground:(UIApplication *)application {    
    [self.manager stopMonitoringSignificantLocationChanges];
    [self.manager startUpdatingLocation];    
}
複製程式碼
3.5 在應用關閉後重啟

在其他應用需要更多資源時, 後臺的應用可能會被關閉.在這種情況下, 一旦發生位置變化,應用會被重啟,因而需要重新初始化監聽過程,若出現這種情況,application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions方法會受到鍵值為UIApplicationLaunchOptionsLocationKey的條目 如下程式碼: 在應用關閉後重新初始化監聽

- (void)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // 因缺乏資源而關閉應用後, 監測應用是否因為位置變化而被重啟
    if (launchOptions[UIApplicationLaunchOptionsLocationKey]) {
// 開啟監測位置的變化
        [self.manager startMonitoringSignificantLocationChanges];
    }
}
複製程式碼

###四 螢幕 螢幕非常耗電, 螢幕越大就越耗電.當然,如果你的應用在前臺執行且與使用者進行互動,則勢必會使用螢幕並消耗電量 這裡仍然有一些方案可以優化螢幕的使用

4.1 動畫

當應用在前臺時, 使用動畫, 一旦應用進入了後臺,則立即暫停動畫.通常來說,你可以通過監聽 UIApplicationWillResignActiveNotificationUIApplicationDIdEnterBackgroundNotification的通知事件來暫停或停止動畫,也可以通過監聽UIApplicationDidBecomeActiveNotification的通知事件來恢復動畫

4.2 視訊播放

我在上家公司就是做視訊類App的,當時就採用了這個技術 保持螢幕常亮

在視訊播放期間,最好保持螢幕常量.可以使用UIApplication物件的 idleTimerDisabled屬性來實現這個目的.一旦設定了 YES, 他會阻止螢幕休眠,從而實現常亮. 與動畫類似,你可以通過相應應用的通知來釋放和獲取鎖

4.3 多螢幕

使用螢幕比休眠鎖或暫停/恢復動畫要複雜得多

如果正在播放電影或執行動畫, 你可以將它們從裝置的螢幕挪到外部螢幕,而只在裝置的螢幕上保留最基本的設定,這樣可以減少裝置上的螢幕更新,進而延長電池壽命

處理這一場景的典型程式碼會涉及一下步驟

  • 1 在啟動期間監測螢幕的數量 如果螢幕數量大於1,則進行切換
  • 2 監聽螢幕在連結和斷開時的通知. 如果有新的螢幕加入, 則進行切換. 如果所有的外部螢幕都被移除,則恢復到預設顯示

@interface LLMultiScreenViewController ()
@property (nonatomic, strong)UIWindow  *secondWindow;
@end

@implementation LLMultiScreenViewController

- (void)viewDidAppear:(BOOL)animated{
    [super viewDidAppear:animated];
    [self updateScreens];
}

- (void)viewDidDisappear:(BOOL)animated{
    [super viewDidDisappear:animated];
    [self disconnectFromScreen];
    
}

- (void)viewDidLoad {
    [super viewDidLoad];    
    [self registerNotifications];    
}

- (void)registerNotifications{
    
    NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
    [nc addObserver:self selector:@selector(scrensChanged:) name:UIScreenDidConnectNotification object:nil];
}

- (void)scrensChanged:(NSNotification *)nofi{
    [self updateScreens];
}

- (void)updateScreens{
    
    NSArray *screens = [UIScreen screens];
    if (screens.count > 1) {
        UIScreen *secondScreen = [screens objectAtIndex:1];
        CGRect rect =secondScreen.bounds;
        if (self.secondWindow == nil) {
            self.secondWindow = [[UIWindow alloc]initWithFrame:rect];
            self.secondWindow.screen = secondScreen;
            
            LLScreen2ViewController *svc = [[LLScreen2ViewController alloc]init];
            svc.parent = self;
            self.secondWindow.rootViewController = svc;
        }
        self.secondWindow.hidden = NO;
    }else{
        [self disconnectFromScreen];
    }
}

- (void)disconnectFromScreen{
    
    if (self.secondWindow != nil) {
        // 斷開連線並釋放記憶體
        self.secondWindow.rootViewController = nil;
        self.secondWindow.hidden = YES;
        self.secondWindow = nil;
    }
}

- (void)dealloc{
    
    [[NSNotificationCenter defaultCenter] removeObserver:self];
    
}
複製程式碼

五 其他硬體

當你的應用進入後臺是, 應該釋放對這些硬體的鎖定:

  • 藍芽
  • 相機
  • 揚聲器,除非應用是音樂類的
  • 麥克風

基本規則: 只有當應用處於前臺時才與這些硬體進行互動, 應用處於後臺時應停止互動

不過揚聲器和無線藍芽可能例外, 如果你正在開發音樂,收音機或其他的音訊類應用,則需要在應用進入後臺後繼續使用揚聲器.不要讓螢幕僅僅為音訊播放的目的而保持常量.類似的, 若應用還有未完成的資料傳輸, 則需要在應用進入後臺後持續使用無線藍芽,例如,與其他裝置傳輸檔案

六 電池電量與程式碼感知

這一條我發現 摩拜單車小程式 做的挺好的,如果晚上騎車掃描二維碼的話是需要開閃光燈達到照亮二維碼的效果, 但是如果你的手機處於低電量的話 ,你的閃光燈是打不開的, 這一個細節就說明了使用者體驗很重要,他首先會保證不讓你的手機因為閃光燈而直接關機

一個智慧的應用會考慮到電池的電量和自身的狀態, 從而決定是否執行資源密集消耗性的操作.另外一個有價值的點是對充電的判斷,確定裝置是否處於充電狀態

來看一下此處的程式碼實施

- (BOOL)shouldProceedWithMinLevel:(NSUInteger)minLevel{
    
    UIDevice *device = [UIDevice currentDevice];
    // 開啟電池監控
    device.batteryMonitoringEnabled = YES;
    
    UIDeviceBatteryState state = device.batteryState;
    // 在充電或電池已經充滿的情況下,任何操作都可以執行
    if (state == UIDeviceBatteryStateCharging ||
        state == UIDeviceBatteryStateFull) {
        return YES;
    }    
    // UIdevice 返回的 batteryLevel 的範圍在0.00 ~ 1.00
    NSUInteger batteryLevel = (NSUInteger)(device.batteryLevel * 100);
    if (batteryLevel >= minLevel) {
        return YES;
    }
    return NO;
}
複製程式碼

我們也可以得到應用對 CPU 的利用率

// 需要匯入這兩個標頭檔案
#import <mach/mach.h>
#import <assert.h>

- (float)appCPUUsage{
    kern_return_t kr;
    task_info_data_t info;
    mach_msg_type_number_t infoCount = TASK_INFO_MAX;    
    kr = task_info(mach_task_self(), TASK_BASIC_INFO, info, &infoCount);    
    if (kr != KERN_SUCCESS) {
        return -1;
    }    
    thread_array_t thread_list;
    mach_msg_type_number_t thread_count;
    thread_info_data_t thinfo;
    mach_msg_type_number_t thread_info_count;
    thread_basic_info_t basic_info_th;
    
    kr = task_threads(mach_task_self(), &thread_list, &thread_count);
    if (kr != KERN_SUCCESS) {
        return -1;
    }
    float tot_cpu = 0;
    int j;
    for (j = 0; j < thread_count; j++) {
        thread_info_count = THREAD_INFO_MAX;
        kr = thread_info(thread_list[j], THREAD_BASIC_INFO, thinfo, &thread_info_count);
        
        if (kr != KERN_SUCCESS) {
            return -1;
        }        
        basic_info_th = (thread_basic_info_t)thinfo;
        if (!(basic_info_th -> flags & TH_FLAGS_IDLE)) {
            tot_cpu += basic_info_th -> cpu_usage / TH_USAGE_SCALE * 100.0;
        }
    }
    vm_deallocate(mach_task_self(), (vm_offset_t)thread_list, thread_count * sizeof(thread_t));
    return tot_cpu;
    
}

複製程式碼

當剩餘電量較低時,提醒使用者,並請求使用者授權執行電源密集型的操作,---當然,只在 使用者同意的前提下執行 總是用一個指示符(也就是進度條百分比)顯示長時間任務的進度, 包括裝置上即將完成的計算或者只是下載一些內容.向使用者提供完成進度的估算, 以幫助他們決定是否需要為裝置充電

七 最佳實踐

以下的最佳實踐可以確保對電量的謹慎使用, 遵循以下要點,應用可以實現對電量的高效使用.

  • 最小化硬體使用. 換句話說,儘可能晚的與硬體打交道, 並且一旦完成任務立即結束使用
  • 在進行密集型任務前, 檢查電池電量和充電狀態
  • 在電量低時, 提示使用者是否確定要執行任務,並在使用者同意後再執行
  • 或提供設定的選項,允許使用者定義電量的閾值,以便在執行祕籍型操作前提示使用者

下邊程式碼展示了設定電量的閾值以提示使用者.


- (IBAction)onIntensiveOperationButtonClick:(id)sender {
    
    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    
    BOOL prompt = [defaults boolForKey:@"promptForBattery"];
    int minLevel = [defaults integerForKey:@"minBatteryLevel"];
    
    BOOL canAutoProceed = [self shouldProceeWithMinLevel:minLevel];
    
    if (canAutoProceed) {
        [self executeIntensiveOperation];
    }else{
        
        if (prompt) {
            UIAlertView *view = [[UIAlertView alloc]initWithTitle:@"提示" message:@"電量低於最小值,是否繼續執行" delegate: self cancelButtonTitle:@"取消" otherButtonTitles:@"確定"];
            [view show];
        }else{
            
            [self queueIntensiveOperation];
        }
    }
}

- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex{
    
    if (buttonIndex == 0) {
        [self queueIntensiveOperation];
    }else{
        [self executeIntensiveOperation];
    }
}
複製程式碼

程式碼對應的配圖如下

電量水平的閾值和提示選項在應用中的設定

  • 設定由兩個條目組成:promptForBattery(應用設定中的撥動開關,表明是否要在低電量時給予提示)和miniBatteryLevel(區間為0~100的一個滑塊,表明了最低電量------在此示例中,使用者可以自行調整),在實際專案中應用的開發人員通常根據操作的複雜性和密集性對閾值進行預設.不同的密集型操作可能會有不同的最低電量需求
  • 在實際執行密集操作之前,檢查當前電量是否足夠, 或者手機是否正在充電.這就是我們判斷是否可以進行後續處理的邏輯,圖中你可以有自己的定製---最低電量和充電狀態

使用者總是隨身攜帶者手機,所以編寫省電的程式碼就格外重要, 畢竟手機的移動電源並不是隨處可見,不過現在北京的街電共享充電寶好像很不錯 本人逛街會經常使用街電充電寶,但還是要儘可能的為使用者省電 在無法降低任務複雜性時, 提供一個對電池電量保持敏感的方案並在適當的時機提示使用者, 會讓使用者感覺很良好, 並且因此會成為你 APP 的永久使用者

相關文章