《高性能iOS 應用開發》讀書筆記(二):能耗

在現如今的開發中, 電量消耗是一個應用運行效果的一個重要的衡量標準,尤其是直播,運動應用。 設備中的每個硬件模塊都會消耗電量。電量的最大消費者是CPU,但這只是系統的一個方面。一個編寫良好的應用需要謹慎地使用電能。用戶往往會刪除耗電量大的應用。
除CPU外,耗電量高、值得關注的硬件模塊還包括網絡硬件、藍牙、GPS、麥克風、加速計、攝像頭、揚聲器和屏幕。
如何降低電量的消耗,是延長使用時間的關鍵。我們要關注以下:

  • 判斷電池的剩余電量及充電狀態
  • 如何分析電源
  • 如何在 iOS 應用中分析電源, CPU 和資源的使用

1. CPU

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


image

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

  • 對數據的處理
  • 待處理的數據大小----更大的顯示屏允許軟件在單個視圖中展示更多的信息,但這也意味著要處理更多的數據
  • 處理數據的算法和數據結構
  • 執行更新的次數,尤其是在數據更新后,觸發應用的狀態或 UI 進行更新(應用收到的推送通知也會導致數據更新,如果此用戶正在使用應用,你還需要更新 UI)

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

  • 針對不同的情況選擇優化的算法
  • 如果應用從服務器接受數據,盡量減少需要在客戶端進行的處理
  • 優化靜態編譯(ahead-of-time,AOT)處理
    動態編譯處理的缺點在于他會強制用戶等待操作完成, 但是激進的 AOT 處理則會導致計算資源的浪費, 需要根據應用和設備選擇精確定量的 AOT 處理.

2. 網絡

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

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

官方提供了檢查和監聽網絡狀態的變化的代碼,大多數人使用的網絡庫----AFNetWorking也提供了類似的代碼,我們可以任選其一,亦或是自己編寫(這段代碼并不復雜)

3. 定位管理器和 GPS

定位服務包括GPS(或GLONASS)和WIFI硬件以及蜂窩網絡

原文中只寫了前兩種,而我們知道iOS的定位是有三種的

  • 衛星定位
  • 蜂窩基站定位
  • Wi-Fi定位(WIFI定位的故事和緣由很有的一講,在后面會說)

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

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

計算坐標會不斷的使用 CPU 和 GPS 的硬件資源,因此他們會迅速的消耗電池電量

先來看一下初始化CLLocationManager并高效接受地理位置更新的典型代碼

.h文件
@interface LLLocationViewController :UIViewController<CLLocationManagerDelegate>
@property (nonatomic, strong)CLLocationManager *manager;
@end

.m文件
@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 后臺定位服務

這里iOS 10 之后變化比較大,參考即可

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 在應用關閉后重啟

當應用位于后臺時,任何定時器或線程都會掛起。但如果你在應用位于后臺狀態時申請了定位,那么應用會在每次收到更新后被短暫的喚醒。在此期間,線程和計時器都會被喚醒。

3.6 在應用關閉后重啟

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

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

4 屏幕

屏幕非常耗電, 屏幕越大就越耗電.當然,如果你的應用在前臺運行且與用戶進行交互,則勢必會使用屏幕并消耗電量 這里仍然有一些方案可以優化屏幕的使用

4.1 動畫

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

4.2 視頻播放

在視頻播放期間,最好保持屏幕常量.可以使用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 的永久用戶

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容