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|darwinIOKitmacOSxnu內核系統

 

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

 

 
 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM