iPhone (iOS 12.1.1)でバッテリ残量を取得する

iOS でバッテリ残量を取得するAPIについて調査しました.作業メモを公開します

結論から言うと

  • iOS 11までは隠しAPIが存在.残量,充電回数(サイクル数)など詳細が取得できた.
  • iOS 12でもAPIはあるが,情報がほぼ何も取れなくなった.
  • iOS 12でもエミュレータなら,従来通りの情報が取れる.

と言う感じでした

サンプルコード

Object-c で書きました.

まず隠しAPIのエントリポイントを見つけて,情報を取り出す処理です.

概要としては,内部で dlopen関数 で /System/Library/Frameworks/IOKit.framework/Versions/A/IOKit というライブラリをダイナミックロードして,その中になる関数を呼び出します.

#import <Foundation/Foundation.h>
#include <dlfcn.h>

NSDictionary *FCPrivateBatteryStatus()
{
    static mach_port_t *s_kIOMasterPortDefault;
    static kern_return_t (*s_IORegistryEntryCreateCFProperties)(mach_port_t entry, CFMutableDictionaryRef *properties, CFAllocatorRef allocator, UInt32 options);
    static mach_port_t (*s_IOServiceGetMatchingService)(mach_port_t masterPort, CFDictionaryRef matching CF_RELEASES_ARGUMENT);
    static CFMutableDictionaryRef (*s_IOServiceMatching)(const char *name);
    
    static CFMutableDictionaryRef g_powerSourceService;
    static mach_port_t g_platformExpertDevice;
    
    static BOOL foundSymbols = NO;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        void* handle = dlopen("/System/Library/Frameworks/IOKit.framework/Versions/A/IOKit", RTLD_LAZY);
        s_IORegistryEntryCreateCFProperties = dlsym(handle, "IORegistryEntryCreateCFProperties");
        s_kIOMasterPortDefault = dlsym(handle, "kIOMasterPortDefault");
        s_IOServiceMatching = dlsym(handle, "IOServiceMatching");
        s_IOServiceGetMatchingService = dlsym(handle, "IOServiceGetMatchingService");
        
        if (s_IORegistryEntryCreateCFProperties && s_IOServiceMatching && s_IOServiceGetMatchingService) {
            g_powerSourceService = s_IOServiceMatching("IOPMPowerSource");
            g_platformExpertDevice = s_IOServiceGetMatchingService(*s_kIOMasterPortDefault, g_powerSourceService);
            foundSymbols = (g_powerSourceService && g_platformExpertDevice);
        }
    });
    
    if (! foundSymbols) return nil;
    
    CFMutableDictionaryRef prop = NULL;
    s_IORegistryEntryCreateCFProperties(g_platformExpertDevice, &prop, 0, 0);
    return prop ? ((NSDictionary *) CFBridgingRelease(prop)) : nil;
}

使い方は

    NSDictionary *prop = FCPrivateBatteryStatus();

という感じです.情報は NSDictionary に格納されています.

NSDictionaryの中身を見るために,簡単な iOSのアプリを書きました.

と言っても UITextView を使ってNSDictionaryの中身を全部テキストで表示するだけのアプリです.

コードはこんな感じ. viewDidLoad を用意するだけです.

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    
    NSDictionary *prop = FCPrivateBatteryStatus();

    UITextView *text = [[UITextView alloc] init];
    NSMutableString *str = [NSMutableString string];
 
    for (id key in [prop keyEnumerator]) {
        NSLog(@"Key:%@ Value:%@", key, [prop valueForKey:key]);
        [str appendString: [NSString stringWithFormat:@"Key:%@ Value:%@ \n", key, [prop valueForKey:key] ]];
    }
    text.text = str;
    [text sizeToFit];
    [self.view addSubview:text];
}

iOS 12以降の実機で実行すると

  • 充電中か,否か
  • バッテリーを搭載しているか,否か

しか表示されませんでした.

エミュレータで実行すると

 .... 省略....
LegacyBatteryInfo {
    Amperage = 2181;
    Capacity = 5917;
    Current = 4240;
    "Cycle Count" = 384;
    Flags = 7;
    Voltage = 8370;
}
 .... 省略....

という感じで情報が取れるんですが...Appleの方針なので諦めるしかないようです.