macOS で iOS/Android(ADB) デバイスの接続・切断を検出したくて、この数日ずっと調べていてようやく形にできた。
備忘録的にどうやっているのかを簡単に書いておこうと思う。
IONotificationPort を CFRunLoop に接続する
まず、IOKit からの通知を受け取る IONotificationPortRef が必要なので IONotificationPortCreate で取得する。ここで発生したイベントがランループで発生するよう、IONotificationPortGetRunLoopSource で CFRunLoopSourceRef を取得して、CFRunLoopAddSource で CFRunLoop に接続する。
このとき、この処理を呼び出すスレッドがどこになるかわからないので、NSThread を専用に作った方がいいと思う。
bself.runLoop = CFRunLoopGetCurrent(); bself.notificationPort = IONotificationPortCreate(kIOMasterPortDefault); bself.notificationRunLoopSource = IONotificationPortGetRunLoopSource(bself.notificationPort); CFRunLoopAddSource(bself.runLoop, bself.notificationRunLoopSource, kCFRunLoopDefaultMode);
IOServiceAddMatchingNotification で通知を受け取る
IOServiceAddMatchingNotification で通知を受け取る関数を登録する。
- 第1引数は先ほど作った IONotificationPortRef
- 第2引数は受け取りたいイベント
- 第3引数はマッチ条件の CFMutableDictionaryRef。
- IOServiceMatching などの関数で作れる。
- IOService 系のメソッドの MatchingDictionary の Retain Count を 1 減らす。すぐ作って使ってなくなるなら CFRelease は不要だし、他で使うなら CFRetain をしておく必要がある。
- 第4引数はコールバック関数
- 適宜
void Callback(void* refcon, io_iterator_t iterator)
というシグネチャで定義しておく。- 最初
static void Callback(void* refcon, io_iterator_t iterator)
にしてて詰んでた。
- 最初
- 適宜
- 第5引数は任意のデータ。
- コールバック関数の第1引数に渡される。
- 第6引数はイテレータを受けるポインタ
- IOServiceAddMatchingNotification を呼び出したタイミングで条件に該当するデータが入っているので、必要があれば中身を処理しておく。
- 通知の受け取りを終了するまで IOObjectRelease してはいけない。
- これでめっちゃハマった。
CFMutableDictionaryRef matchingDict = IOServiceMatching(kIOUSBDeviceClassName); CFDictionaryAddValue(matchingDict, CFSTR(kIOPropertyMatchKey), (CFDictionaryRef) @{ @"SupportsIPhoneOS": @YES }); IOServiceAddMatchingNotification(bself.notificationPort, kIOMatchedNotification, matchingDict, EBIMobileDeviceWatcherIOSDeviceMatchedCallback, (__bridge void *)(bself), &iosDeviceMatchedIter); [bself onIOSDeviceMatchedCallback:iosDeviceMatchedIter];
CFRunLoop を回す
NSThread を新規に立てている場合は CFRunLoopRun で RunLoop を回す。
通知の受け取りを止める。
- NSThread を外から止めるときは、CFRunLoopStop で止めればいい(プロパティなどに CFRunLoopRef を保持しておくといい)。
- CFRunLoopRemoveSource で RunLoop から Notification の RunLoopSource を取り除く。
- IOObjectRelease で IOServiceAddMatchingNotification から戻ってきたイテレータを解放する
- IONotificationPortDestroy で IONotificationPortRef を解放する。
iOS/Android デバイスの接続/切断検知
- iOS デバイスは IOServiceMatching(kIOUSBDeviceClassName) でウォッチできる。かつ SupportsIPhoneOS というプロパティをもっているので、これでマッチさせれば一発でとれる。
- Android デバイスは、デバイスだけでは Android かどうかを判別できない。また、端末ロック中は ADB インターフェイスが認識されなかったりする。そのため、IOServiceMatching(kIOUSBInterfaceClassName) で ADB インターフェイスをウォッチして、親の USB デバイスを取る、ということをする必要がある。
- 切断は USB デバイスだけウォッチして、iOS は一致を、Android はシリアルナンバーの一致で切断判定している。
IOKit を使った情報はかなり少なくて苦労したけど、腰も砕けよ 膝も折れよ さんのところがいろいろ詳しく書いてあってすごく助かった。ありがとうございました。