zoukankan      html  css  js  c++  java
  • macOS 内核之从 I/O Kit 电量管理开始

    macOS 内核之从 I/O Kit 电量管理开始

    来源 http://www.cocoachina.com/articles/87071

    来源 http://justinyan.me/post/3961

    在上一篇macOS 内核之 hw.epoch 是个什么东西?我们提到 XNU 内核包含了 BSD 和 Mach,其中 Mach Kernel 提供了 I/O Kit 给硬件厂商写驱动用的。这个部分在 NeXT 时期是用 Objective-C 提供的 API,叫做 Driver Kit,后来乔布斯回到苹果之后,升级了 BSD 和 Mach 的代码,于是在 OS X 中提供了 C++ 接口的 I/O Kit。

    根据官方的这份文档,以下系统支持 I/O Kit:

    • iOS 2.0+
    • macOS 10.0+
    • Mac Catalyst 13.0+

    I/O Kit 里我们可以通过三种不同的方式获取电池信息,位于 IOKit/pwr_mgt 的 Power Mangement 接口,位于 IOKit/ps 的 Power Sources 接口,以及通过 IOServiceGetMatchingService 获取 AppleSmartBattery Service 接口。

    1. IOPM (Power Management) API

    IOPM 接口需要使用 Mach Port 跟 IOKit 进行 IPC 通信,所以我们先来了解一点 Mach Port 的背景。

    1.1 Mach Port

    XNU 是一个混合内核,既有 BSD 又有 Mach Kernel,上层还有各种各样的技术,所以在 macOS 系统中,IPC (跨进程通信)的技术也多种多样。Mattt 在 NSHipster 上写过一篇 IPC 的文章: Inter-Process Communication - NSHipster 对此有过详解。

    Mach Port 是在系统内核实现和维护的一种 IPC 消息队列,持有用于 IPC 通信的 mach messages。只有一个进程可以从对应的 port 里 dequeue 一条消息,这个进程被持有接收权利(receive-right)。可以有多个进程往某个 port 里 enqueue 消息,这些进程持有该 port 的发送权利(send-rights)。

    如上图,PID 123 的进程往一个 port 里发送了一条消息,只有对应的接收端 PID 456 才能从 port 里取出这条消息。

    我们可以简单把 mach port 看做是一个单向的数据发送渠道,构建一个消息结构体后通过mach_msg() 方法发出去。因为只能单向发送,所以当 B 进程收到了 A 进程发来的消息之后要自己创建一个新的 Port 然后又发回去 A 进程。

    手动构建 mach message 发送是比较复杂的,大概长这个样子(代码来自 Mattt 的那篇文章):

    natural_t data;
    mach_port_t port;
    
    struct {
        mach_msg_header_t header;
        mach_msg_body_t body;
        mach_msg_type_descriptor_t type;
    } message;
    
    message.header = (mach_msg_header_t) {
        .msgh_remote_port = port,
        .msgh_local_port = MACH_PORT_NULL,
        .msgh_bits = MACH_MSGH_BITS(MACH_MSG_TYPE_COPY_SEND, 0),
        .msgh_size = sizeof(message)
    };
    
    message.body = (mach_msg_body_t) {
        .msgh_descriptor_count = 1
    };
    
    message.type = (mach_msg_type_descriptor_t) {
        .pad1 = data,
        .pad2 = sizeof(data)
    };
    
    mach_msg_return_t error = mach_msg_send(&message.header);
    
    if (error == MACH_MSG_SUCCESS) {
        // ...
    }
    

    其中最关键的是 msgh_remote_port 和 msgh_local_port。上述代码是发送消息,所以 msgh_remote_port 就是要接收这条消息的那个进程的 port。我们得先知道这个 port 信息我们才能往里面发消息。另外例子中使用的是 mach_msg_send() 函数。

    port name

    留意到在上图中,PID 123 往一个名为 0xabc 的 port 发消息,PID 456 则从名为 0xdef 的 port 里取消息。这里 port name 只对当前进程有意义,并不需要全局一致,内核会自动根据进程 ID 和名字信息找到对应的进程。

    Out-of-line memory

    我们的代码在用户层调用,需要进出内核层,这是一进一出如果消息体里带上大量的信息就会非常慢。所以如果需要使用 mach message 来发送体积较大的信息,可以使用 “out-of-line memory” descriptor。

    我们看到上面 Mattt 的代码使用 mach_msg_send() 函数来发送消息,message.body 带了一个 msgh_descriptor_count 为 1。这个 descriptor 是一个 natural_t。我看到这里的时候并没有搞懂系统是怎么做 OOL 的 copy-on-write 的。于是照例翻一下 XNU 的源码,我发现 Mattt 的例子并没有使用 OOL descriptor,而是使用了 type descriptor。

    typedef struct
    {
      natural_t         pad1;
      mach_msg_size_t       pad2;
      unsigned int          pad3 : 24;
      mach_msg_descriptor_type_t    type : 8;
    } mach_msg_type_descriptor_t;
    

    ool descriptor 的结构如下:

    typedef struct
    {
      uint64_t          address;
      boolean_t             deallocate: 8;
      mach_msg_copy_options_t       copy: 8;
      unsigned int          pad1: 8;
      mach_msg_descriptor_type_t    type: 8;
      mach_msg_size_t           size;
    } mach_msg_ool_descriptor64_t;
    

    使用时我们需要把内存地址发过去,内核只负责传递地址指针,等到进程接受到了这条消息之后才会从内存里 copy buffer。

    1.2 使用 Master Port 和 IOKit 通信

    在 IOKit 里面,所有的通信都通过 IOKit Master Port 来进行,使用以下函数可以获取 master port。

    kern_return_t
    IOMasterPort( mach_port_t   bootstrapPort,
              mach_port_t * masterPort );
    

    实际使用时如下:

    mach_port_t masterPort;
    IOMasterPort(MACH_PORT_NULL, &masterPort)
    

    默认把 bootstrapPort 置空。如果返回值是 kIOReturnSuccess 就成功构建了一个 mach_port_t 用于跟 IOKit 通信。

    bootstrapPort

    不过在这个 API 里面,获取单一 master port 好理解,那 bootstrapPort 这个参数又是用来干啥的呢?

    在上面的例子中 PID 123 和 PID 456 是在已经获知对方的 port name 的前提下才有办法互相通信的。但是如果你不知道对方的 port name 呢?于是 XNU 系统提供了 bootstrap port 这个东西,由系统提供查询服务,这样所有的进程都可以去广播自己的 mach port 接收端的名字,也可以查询其他人的名字。

    查询接口大概是这样:

    mach_port_t port;
    kern_return_t kr = bootstrap_look_up(bootstrap_port, "me.justinyan.example", &port);
    

    注册接口大概是这样:

    bootstrap_register(bootstrap_port, "me.justinyan.example", port);
    

    同时 bootstrap port 是一个特殊的 port。其他的 mach port 在父进程被 fork() 的时候,子进程是不会继承 port 的,只有 bootstrap port 可以被继承。

    但是,自从 OS X 10.5 开始,苹果引入了 Launchd 这么一个服务,同时弃用了 bootstrap_register() 接口。关于这件事情当时 darwin 开发团队有个长长的邮件列表做了激烈的讨论: Apple - Lists.apple.com

    新的接口可以参考 CFMessagePortCreateLocal() 和这篇文章: Damien DeVille | Interprocess communication on iOS with Mach messages

    IOPM 获取电池信息接口

    上面罗里吧嗦一大堆全是 mach port 的事情,现在终于到正题了。代码非常简单:

    NSDictionary* get_iopm_battery_info() {
        mach_port_t masterPort;
        CFArrayRef batteryInfo;
    
        if (kIOReturnSuccess == IOMasterPort(MACH_PORT_NULL, &masterPort) &&
            kIOReturnSuccess == IOPMCopyBatteryInfo(masterPort, &batteryInfo) &&
            CFArrayGetCount(batteryInfo))
        {
            CFDictionaryRef battery = CFDictionaryCreateCopy(NULL, CFArrayGetValueAtIndex(batteryInfo, 0));
            CFRelease(batteryInfo);
            return (__bridge_transfer NSDictionary*) battery;
        }
        return NULL;
    }
    
    
        NSDictionary *dict = get_iopm_battery_info();
        NSLog(@"iopm dict: %@", dict);
    

    输出:

    iopm dict: {
        Amperage = 0;
        Capacity = 6360;
        Current = 6360;
        "Cycle Count" = 113;
        Flags = 5;
        Voltage = 12968;
    }
    

    可以看到电池循环次数、容量之类的信息,但是不多。IOPMLib.h 的注释说 不建议大家使用这个接口,可以考虑用 IOPowerSources API 代替。

    2. IOPowerSources API

    IOPowerSources 的接口比较简单,先用 IOPSCopyPowerSourcesInfo() 取到 info, 然后取 IOPSCopyPowerSourcesList(),最后再 copy 一下就完事了。

    NSDictionary* get_iops_battery_info() {
        CFTypeRef info = IOPSCopyPowerSourcesInfo();
    
        if (info == NULL)
            return NULL;
    
    
        CFArrayRef list = IOPSCopyPowerSourcesList(info);
    
        // Nothing we care about here...
        if (list == NULL || !CFArrayGetCount(list)) {
            if (list)
                CFRelease(list);
    
            CFRelease(info);
            return NULL;
        }
    
        CFDictionaryRef battery = CFDictionaryCreateCopy(NULL, IOPSGetPowerSourceDescription(info, CFArrayGetValueAtIndex(list, 0)));
    
        // Battery is released by ARC transfer.
        CFRelease(list);
        CFRelease(info);
    
        return (__bridge_transfer NSDictionary* ) battery;
    }
    
        NSDictionary *iopsDict = get_iops_battery_info();
        NSLog(@"iops dict: %@", iopsDict);
    

    输出:

    iops dict: {
        "Battery Provides Time Remaining" = 1;
        BatteryHealth = Good;
        Current = 0;
        "Current Capacity" = 100;
        DesignCycleCount = 1000;
        "Hardware Serial Number" = D**********;
        "Is Charged" = 1;
        "Is Charging" = 0;
        "Is Present" = 1;
        "Max Capacity" = 100;
        Name = "InternalBattery-0";
        "Power Source ID" = 9764963;
        "Power Source State" = "AC Power";
        "Time to Empty" = 0;
        "Time to Full Charge" = 0;
        "Transport Type" = Internal;
        Type = InternalBattery;
    }
    

    可以看到信息多了很多,还有 BatteryHealth 等信息,我们看到我的 MacBook 的电池设计循环次数是 DesignCycleCount = 1000,然后我已经循环 113 次了。

    但是,这批信息里面没有带电池的设计容量。

    3. IOPMPS Apple Smart Battery API

    IOKit 里提供了一套 IOService 相关的接口,你可以往里面注册 IOService 服务,带个名字,一样是通过 IOMasterPort() 来通信。IOKit 主要是面向硬件驱动开发者的,所以如果你的硬件依赖另外一个硬件,但是另外一个硬件还没有接入,这时候你可以往 IOService 注册一个通知。使用 IOServiceAddMatchingNotification,等到你观察的硬件接入后调用了 registerService() 你就会收到对应的通知了。

    这里我们直接用 IOServiceGetMatchingService() 来获取系统提供的 AppleSmartBattery service。

    NSDictionary* get_iopmps_battery_info() {
        io_registry_entry_t entry = 0;
        entry = IOServiceGetMatchingService(kIOMasterPortDefault, IOServiceNameMatching("AppleSmartBattery"));
        if (entry == IO_OBJECT_NULL)
            return nil;
    
        CFMutableDictionaryRef battery;
        IORegistryEntryCreateCFProperties(entry, &battery, NULL, 0);
        return (__bridge_transfer NSDictionary *) battery;
    }
    
        NSDictionary *iopmsDict = get_iopmps_battery_info();
        NSLog(@"iopmsDict: %@", iopmsDict);
    

    输出:

    iopmsDict: {
        AdapterDetails =     {
            Current = 4300;
            PMUConfiguration = 2092;
            Voltage = 20000;
            Watts = 86;
        };
        AdapterInfo = 0;
        Amperage = 0;
        AppleRawAdapterDetails =     (
                    {
                Current = 4300;
                PMUConfiguration = 2092;
                Voltage = 20000;
                Watts = 86;
            }
        );
        AppleRawCurrentCapacity = 6360;
        AppleRawMaxCapacity = 6360;
        AvgTimeToEmpty = 65535;
        AvgTimeToFull = 65535;
        BatteryData =     {
            AdapterPower = 1106486026;
            CycleCount = 113;
            DesignCapacity = 6669;
            PMUConfigured = 0;
            QmaxCell0 = 6812;
            QmaxCell1 = 6859;
            QmaxCell2 = 6784;
            ResScale = 200;
            StateOfCharge = 100;
            SystemPower = 4625;
            Voltage = 12968;
        };
        BatteryFCCData =     {
            DOD0 = 128;
            DOD1 = 144;
            DOD2 = 128;
            PassedCharge = 0;
            ResScale = 200;
        };
        BatteryInstalled = 1;
        BatteryInvalidWakeSeconds = 30;
        BatterySerialNumber = D**********;
        BestAdapterIndex = 3;
        BootPathUpdated = 1571194014;
        CellVoltage =     (
            4323,
            4322,
            4323,
            0
        );
        ChargerData =     {
            ChargingCurrent = 0;
            ChargingVoltage = 13020;
            NotChargingReason = 4;
        };
        CurrentCapacity = 6360;
        CycleCount = 113;
        DesignCapacity = 6669;
        DesignCycleCount70 = 0;
        DesignCycleCount9C = 1000;
        DeviceName = bq20z451;
        ExternalChargeCapable = 1;
        ExternalConnected = 1;
        FirmwareSerialNumber = 1;
        FullPathUpdated = 1571290629;
        FullyCharged = 1;
        IOGeneralInterest = "IOCommand is not serializable";
        IOReportLegend =     (
                    {
                IOReportChannelInfo =             {
                    IOReportChannelUnit = 0;
                };
                IOReportChannels =             (
                                    (
                        7167869599145487988,
                        6460407809,
                        BatteryCycleCount
                    )
                );
                IOReportGroupName = Battery;
            }
        );
        IOReportLegendPublic = 1;
        InstantAmperage = 0;
        InstantTimeToEmpty = 65535;
        IsCharging = 0;
        LegacyBatteryInfo =     {
            Amperage = 0;
            Capacity = 6360;
            Current = 6360;
            "Cycle Count" = 113;
            Flags = 5;
            Voltage = 12968;
        };
        Location = 0;
        ManufactureDate = 19722;
        Manufacturer = SMP;
        ManufacturerData = {length = 27, bytes = 0x00000000 *** };
        MaxCapacity = 6360;
        MaxErr = 1;
        OperationStatus = 58433;
        PackReserve = 200;
        PermanentFailureStatus = 0;
        PostChargeWaitSeconds = 120;
        PostDischargeWaitSeconds = 120;
        Temperature = 3067;
        TimeRemaining = 0;
        UserVisiblePathUpdated = 1571291169;
        Voltage = 12968;
    }
    

    可以看到比前面的两次输出多了很多。

    CurrentCapacity = 6360;
    DesignCapacity = 6669;
    

    有了当前电池容量和设计容量,就可以得到我的电池还剩 95% 的容量。

    4. 列出所有 IOService

    以上三种方法我都是从 Hammerspoon 的源码中习得。通过阅读这部分接口学习了相关的一些内核层 API 的概念,很有意思。那么在 #3 中 Hammerspoon 的作者是怎么知道系统有一个 IOService 叫做 "AppleSmartBattery" 的呢?我们不妨把系统所有的 IOService 打印出来,然后 grep 看看里面有没有带 battery 或者 energy 关键字的。

    IOKitLib.h 里有一个接口 IORegistryCreateIterator() 可以创建一个迭代器,把所有已注册的 IOService 取出来。

    核心代码如下:

    const char *plane = "IOService";
    io_iterator_t it = MACH_PORT_NULL;
    IORegistryCreateIterator(kIOMasterPortDefault, plane, kIORegistryIterateRecursively, &it) 
    

    有一个开源库实现了这个功能,有兴趣的读者朋友可以看看这里: Siguza/iokit-utils: Dev tools for probing IOKit

    ➜  iokit-utils ./ioprint| grep -i battery
    AppleSmartBatteryManager(AppleSmartBatteryManager)
    AppleSmartBattery(AppleSmartBattery)
    

    结果出来两个 battery 相关的,AppleSmartBattery 就是上述例子用到的,AppleSmartBatteryManager 则打印出如下结果:

    iopmsDict: {
        CFBundleIdentifier = "com.apple.driver.AppleSmartBatteryManager";
        CFBundleIdentifierKernel = "com.apple.driver.AppleSmartBatteryManager";
        IOClass = AppleSmartBatteryManager;
        IOMatchCategory = IODefaultMatchCategory;
        IOPowerManagement =     {
            CapabilityFlags = 2;
            CurrentPowerState = 1;
            MaxPowerState = 1;
        };
        IOProbeScore = 0;
        IOPropertyMatch =     {
            IOSMBusSmartBatteryManager = 1;
        };
        IOProviderClass = IOSMBusController;
        IOUserClientClass = AppleSmartBatteryManagerUserClient;
    }
    

    只是一堆苹果自家驱动的信息而已。

    5. 用于 iOS 系统

    我在运行了 iOS 13.1.2 的 iPhone Xs Max 机器上进行了测试。iOS 工程引入 IOKit 会比较麻烦,因为这个 Framework 是不公开的,所以你得把所有的头文件导出来,并且把 #import <IOKit/xxx.h> 的地方都改掉。可以参考此文: [Tutorial] Import IOKit framework into Xcode project | Gary's ...Lasamia

    实测 IOPMCopyBatteryInfo 在 iOS 上无效,估计是 iOS 直接不给 mach port 权限到上层。 IOPSCopyPowerSourcesList 和 IOServiceNameMatching 能用。

    iops dict: {
        "Battery Provides Time Remaining" = 1;
        "Current Capacity" = 100;
        "Is Charged" = 1;
        "Is Charging" = 0;
        "Is Present" = 1;
        "Max Capacity" = 100;
        Name = "InternalBattery-0";
        "Play Charging Chime" = 1;
        "Power Source ID" = 2490467;
        "Power Source State" = "AC Power";
        "Raw External Connected" = 1;
        "Show Charging UI" = 1;
        "Time to Empty" = 0;
        "Time to Full Charge" = 0;
        "Transport Type" = Internal;
        Type = InternalBattery;
    }
    iopmsDict: {
        BatteryInstalled = 1;
        ExternalConnected = 1;
    }
    

    可以看到信息比 macOS 的少了很多,并且没有包含 cycleCount 这个信息。

    5.1 奇技淫巧 hack 之

    但是毕竟 iOS 是有 IOKit 框架的,那么有没有什么奇技淫巧可以拿到 IOKit 的信息呢?eldade/UIDeviceListener: Obtain power information (battery health, charger details) for iOS without any private APIs.这个库可以在 iOS 7 - iOS 9.3 上捕获这部分信息。

    所使用之操作也是非常有趣。从 iOS 3.0 开始,UIDevice 增加了 batteryState 和 batteryLevel 这两个参数,并且允许开启电池监控 batteryMonitoringEnabled。通过上文我们已经知道,这些操作最终都是通过 IOKit 来进行的。

    IOKit 会从 IORegistry 获取一份最新的电池信息,就像我们的 get_iopmps_battery_info() 方法一样。留意到从 IORegistry 取数据的接口长这样:

    IORegistryEntryCreateCFProperties(
        io_registry_entry_t entry,
        CFMutableDictionaryRef * properties,
            CFAllocatorRef      allocator,
        IOOptionBits        options );
    

    重点在第三个参数 CFAllocatorRef,通常情况下系统会用默认的 CFAllocatorGetDefault()。我们看看这个 allocator 长啥样CoreFoundation/CFBase.c:

    typedef const struct CF_BRIDGED_TYPE(id) __CFAllocator * CFAllocatorRef;
    
    
    // CFAllocator structure must match struct _malloc_zone_t!
    // The first two reserved fields in struct _malloc_zone_t are for us with CFRuntimeBase
    struct __CFAllocator {
        CFRuntimeBase _base;
        CFAllocatorRef _allocator;
        CFAllocatorContext _context;
    };
    

    以及 CoreFoundation 提供了不少操作:

    CFAllocatorGetDefault();
    CFAllocatorGetContext();
    CFAllocatorCreate();
    CFAllocatorSetDefault();
    

    如果能把系统的默认 allocator 替换成自己的实现,那么当我们打开 batteryMonitoringEnabled 然后电池发生变更的时候,系统就回去用 IORegistry 取一份电池信息,就会掉进我们替换掉的 allocator。这时候就能截取 allocator 刚刚 allocate 的内存信息了。真的佩服作者的脑洞。详细的实现大家可以看原来的库: eldade/UIDeviceListener,我们只看关键代码:

    // 获取默认 allocator
    _defaultAllocator = CFAllocatorGetDefault();
    
    
    CFAllocatorContext context;
    
    // 获取默认 allocator 的 context    
    CFAllocatorGetContext(_defaultAllocator, &context);
    
    // 全部改成自己的实现, myAlloc/myRealloc/myFree 都是 C 函数
    context.allocate = myAlloc;
    context.reallocate = myRealloc;
    context.deallocate = myFree;
    
    // 用修改后的 context 创建新的 allocator
    _myAllocator = CFAllocatorCreate(NULL, &context);
    
    // 把自己创建的 allocator 替换掉系统的默认 allocator
    CFAllocatorSetDefault(_myAllocator);
    

    接下来看看 myAlloc 的实现:

    void * myAlloc (CFIndex allocSize, CFOptionFlags hint, void *info)
    {
        // 做一下线程检查
        VERIFY_LISTENER_THREAD();
    
        // 实现一个新的 allocation
        void *newAllocation = CFAllocatorAllocate([UIDeviceListener sharedUIDeviceListener].defaultAllocator, allocSize, hint);
    
        // 失败就放过
        if (newAllocation == NULL)
            return newAllocation;
    
        // 有东西了,赶紧把新的内容塞进准备好的 allocations 变量里,这是个 C++ 的 std::set<void *>
        if (hint & __kCFAllocatorGCObjectMemory)
        {
            [UIDeviceListener sharedUIDeviceListener].allocations->insert(newAllocation);
        }
        return newAllocation;
    }
    

    与此同时,通过 KVO 观察 UIDevice 公开的 batteryLevel 属性,接收 KVO 回调:

    - (void) observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context
    {
        if ([change objectForKey: NSKeyValueChangeNewKey] != nil)
        {
            std::set<void *>::iterator it;
            for (it=_allocations->begin(); it!=_allocations->end(); ++it)
            {
                CFAllocatorRef *ptr = (CFAllocatorRef *) (NSUInteger)*it;
                void * ptrToObject = (void *) ((NSUInteger)*it + sizeof(CFAllocatorRef));
    
                if (*ptr == _myAllocator && // Just a sanity check to make sure the first field is a pointer to our allocator
                    [self isValidCFDictionary: ptrToObject])   // Check for valid CFDictionary
                {
                    CFDictionaryRef dict = (CFDictionaryRef) ptrToObject;
    
                    if ([self isChargerDictionary: dict]) // Check if this is the charger dictionary
                    {
                        // Found our dictionary. Let's clear the allocations array:
                        _allocations->clear();
    
                        // We make a deep copy of the dictionary using the default allocator so we don't
                        // get callbacks when this object and any of its descendents get freed from the
                        // wrong thread:
    
                        CFDictionaryRef latestDictionary = (CFDictionaryRef) CFPropertyListCreateDeepCopy(_defaultAllocator, dict, kCFPropertyListImmutable);
    
                        if (latestDictionary != nil)
                        {
                            // Notify that new data is available, but that has to happen on the main thread.
                            // Because of the CFAllocator replacement, we generally shouldn't
                            // do ANYTHING on this thread other than stealing this dictionary from UIDevice...
                            dispatch_sync(dispatch_get_main_queue(), ^{
                                // Pass ownership of the CFDictionary to the main thread (using ARC):
                                NSDictionary *newPowerDataDictionary = CFBridgingRelease(latestDictionary);
                                [[NSNotificationCenter defaultCenter] postNotificationName:kUIDeviceListenerNewDataNotification object:self userInfo:newPowerDataDictionary];
                            });
                        }
    
                        return;
                    }
                }
            }
        }
    }
    

    上面一堆嵌套代码判断了一层又一层,最后做了一个 CFPropertyListCreateDeepCopy 然后通过通知转发出去。

    CFDictionaryRef latestDictionary = (CFDictionaryRef) CFPropertyListCreateDeepCopy(_defaultAllocator, dict, kCFPropertyListImmutable);
    

    严格来说这种写法并没有用到私有 API,但是非常取巧。如果内核实现代码不用 default allocator 来取 IORegistry 的信息这里就失效了。事实上从 iOS 10 开始这个做法确实也失效了。但是整个思路非常有趣,值得观摩。

    5.2 遍历所有的 IOService

    上面我们在 macOS 上通过取 AppleSmartBattery 这个 IOService 可以获得更多电池信息,但是在 iOS 上没有。那么我们还能不能寻找其他的 IOService 看看是否有携带了电池信息的呢?

    此文iOS IOKit Browser - Christopher Lyon Anderson 使用私有 API 遍历了 iOS 上所有的 IOService,并且在他的截屏中是包含了电池信息的。我 clone 下来发现已经没有 cycleCount 信息了,但是这个项目有个地方挺有意思:

    NSString *bundlePath = [[NSBundle bundleWithPath:@"/System/Library/Frameworks/IOKit.framework"] bundlePath];
        NSURL *bundleURL = [NSURL fileURLWithPath:bundlePath];
        CFBundleRef cfBundle = CFBundleCreate(kCFAllocatorDefault, (CFURLRef)bundleURL);
    
        self.IORegistryGetRootEntryShim = CFBundleGetFunctionPointerForName(cfBundle, CFSTR("IORegistryGetRootEntry"));
    

    先取系统的 IOKit.framework,然后用 CoreFoundation 的接口来取函数指针,然后就可以使用这批 IOKit 的私有函数了。可惜此方法亦已无效。

    6. 小结

    iOS 方面暂时还未找到能展示 cycleCount 信息的方法,想必 Battery Health App 应该用了更加厉害的黑科技。可能只有越狱逆向一下才知道它是怎么做到的了。

    之前因为 sysctl() 的缘故看了一下 XNU 的源码,结果发现内核层还是有不少有意思的东西。IOKit 作为驱动层的 API,除了获取电池信息之外还能干很多事情。

    本文通过 IOKit 的简单接口,扩展学习了 XNU 的 IPC 通信机制 mach port。希望后续能通过这些工具做出点有意思的东西来。

    内核系列文章

    参考资料

     iOSMac|

    ============= End

     
     
  • 相关阅读:
    Linux简介
    在VMware上安装Ubuntu软件步骤与遇到的相关问题及解决方案
    深度学习框架之TensorFlow的概念及安装(ubuntu下基于pip的安装,IDE为Pycharm)
    Windows下安装Python及Eclipse中配置PyDev插件
    结构体定义struct和typedef struct
    定义与声明
    error LNK2005:错误改正方法
    OPENCV 笔记
    RANSANC算法
    梯度下降法和牛顿法
  • 原文地址:https://www.cnblogs.com/lsgxeva/p/14622775.html
Copyright © 2011-2022 走看看