這個框架能做什麼
顧名思義:External:外部的;Accessory:配件。應該是和外部設備相關的一個框架。
ExternalAccessory框架,就是可以用來和Lightning接口的硬件,或者藍牙(2.1)設備進行連接、通訊的這麼一個框架。(當然,也可以和30-pin接口的硬件連接、通訊——不過現在幾乎沒有這種接口的設備了吧~)
就是你現在有一個Lightning耳機(iPhone7, 7Plus的耳機~),或者有一個藍牙2.1的音箱,你要寫一個App去控制這些設備,你要選用的框架,就是ExternalAccessory。
比如我前公司,幫美國公司代工的一款藍牙2.1的音箱,寫了一個App進行控制(燈光、音效);還有現在公司,做Lightning設備的App,用來對耳機進行簡單的控制、固件升級。這都需要用到ExternalAccessory框架。
框架簡介
ExternalAccessory框架的主要功能,就是提供一個管道,讓外圍設備可以和基於iOS系統的設備進行通訊。
主要的幾個類:
EAAccessory:表示你連接的設備。
EAAccessoryManager:有一個重要的屬性connectedAccessories,用來獲取已經連接上手機的設備。
EASession:這個類主要用來建立通道,讓App和設備可以進行數據的傳輸(發送和接收)
設備的連接
其實設備的連接、斷開,都是系統自動完成的。
EAAccessoryManager類中有一個屬性connectedAccessories(一個array),裡面就已經包含了所有已經連接的外圍設備(EAAccessory對象)。像什麼設備名稱、制造廠商、硬件型號、固件型號等等信息,都可以在EAAccessory對象中拿得到。
但是,ExternalAccessory框架,並不會自動幫你監控設備的斷開、連接狀態。如果你想拿到設備連接、斷開的回調,則需要手動敲一些代碼了:
拿到連接、斷開的回調
需要注冊通告,即調用EAAccessoryManager的方法registerForLocalNotifications。
當有硬件連接,ExternalAccessory框架就會發送EAAccessoryDidConnectNotification這個通告,當有硬件斷開連接,就會發出EAAccessoryDidDisconnectNotification通告。所以,要監聽、接收這兩個通告。
// 注冊通告
[[EAAccessoryManager sharedAccessoryManager] registerForLocalNotifications];
// 監聽EAAccessoryDidConnectNotification通告(有硬件連接就會回調Block)
[[NSNotificationCenter defaultCenter] addObserverForName:EAAccessoryDidConnectNotification
object:nil
queue:nil
usingBlock:^(NSNotification * _Nonnull note) {
// 從已經連接的外設中查找我們的設備(根據協議名稱來查找)
[self searchOurAccessory];
}];
[[NSNotificationCenter defaultCenter] addObserverForName:EAAccessoryDidDisconnectNotification
object:nil
queue:nil
usingBlock:^(NSNotification * _Nonnull note) {
// Do something what you want
}];此外,硬件斷開連接,除了通告回調,框架還提供了Delegate的回調方式,遵守EAAccessoryDelegate協議,並實現accessoryDidDisconnect:這個可選方法(這個協議中的唯一一個方法),也可以拿到硬件斷開連接的回調。(好奇怪,Apple為什麼單單只弄這麼一個方法?)
識別硬件
好了,我們知道硬件連接進行了,那怎麼知道是不是我們的硬件呢?
蘋果公司將這個能識別硬件身份的東東叫做「協議」。本質上就是一個字符串,一個由反向域名組成的字符串,例如om.apple.myProtocol。
而這個協議(字符串)的定義,是由硬件的生產廠商定義的,所以App開發人員,要和廠商溝通拿到這部分的資料。
所以我們要做幾件事件:
導入框架(這個不用說了吧~)#import
在Info.plist中,增加UISupportedExternalAccessoryProtocols這個key,然後值賦為協議名稱(就是那個反向域名字符串)。(其實是一個array,所以這裡可以支持多個協議,不分順序)
在硬件已經連接的回調中,遍歷所有已經連接的設備,根據協議名稱找到自己的硬件(實現上述代碼的searchOurAccessory方法):
// 從已經連接的外設中查找我們的設備(根據協議名稱來查找)
- (void)searchOurAccessory {
NSMutableString *info = [[NSMutableString alloc] init];
// search our device
for (EAAccessory *accessory in [EAAccessoryManager sharedAccessoryManager].connectedAccessories) {
if ([kSPKLightingHeadphoneProtocolString isEqualToString:[accessory.protocolStrings firstObject]] == YES) {
// 硬件的協議字符串和硬件廠商提供的一致,這個就是我們要找的設備了!
// log:可以打印一下該硬件的相關資訊
for (NSString *proStr in accessory.protocolStrings) {
[info appendFormat:@"protocolString = %@\n", proStr];
}
[info appendFormat:@"\n"];
[info appendFormat:@"manufacturer = %@\n", accessory.manufacturer];
[info appendFormat:@"name = %@\n", accessory.name];
[info appendFormat:@"modelNumber = %@\n", accessory.modelNumber];
[info appendFormat:@"serialNumber = %@\n", accessory.serialNumber];
[info appendFormat:@"firmwareRevision = %@\n", accessory.firmwareRevision];
[info appendFormat:@"hardwareRevision = %@\n", accessory.hardwareRevision];
// Log...
}
}
}另外,監視硬件連接的通告Block回調,NSNotification * _Nonnull note這個參數,其實是包含了EAAccessory對象,我們也可以直接通過EAAccessoryKey這個key拿到EAAccessory對象,再對比協議字符串是否相同,從而直接拿到已經連接的硬件,無須遍歷connectedAccessories數組。
傳輸數據(指令)
創建EASession、打開輸入、輸出通道
App和外圍設備通訊、數據傳輸,靠的是NSInputStream和NSOutputStream對象,而這兩個對象是EASession的兩個屬性。所以我們要創建EASession對象,謂曰:打開傳輸通道()。
遵守NSStreamDelegate協議,類似:@interface YourClassName()
創建EASession並打開輸入、輸出通道,類似如下代碼:
- (BOOL)openSession {
// 根據已經連接的EAAccessory對象和這個協議(反向域名字符串)來創建EASession對象,並打開輸入、輸出通道
self.session = [[EASession alloc] initWithAccessory:self.accessory forProtocol: kSPKLightingHeadphoneProtocolString];
if(self.session != nil) {
// open input stream
self.session.inputStream.delegate = self;
[self.session.inputStream scheduleInRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
[self.session.inputStream open];
// open output stream
self.session.outputStream.delegate = self;
[self.session.outputStream scheduleInRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
[self.session.outputStream open];
}
else {
NSLog(@"Failed to create session");
}
return (nil != self.session);
}到此為止,就完整創建了一個包含accessory對象、並已經可以進行數據發送和接收的EASession對象了。
stream:handleEvent:回調:
不過,雖然數據傳輸通道已經打開了,但是怎麼發送、接收數據呢?或者說,怎麼知道什麼時候可以發送數據,什麼時候要接收數據?
注意我們剛剛遵守了NSStreamDelegate協議,這裡就是利用delegate回調來監聽input stream和output stream的數據。
// delegate回調的方法
- (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode {
switch (eventCode) {
case NSStreamEventNone:
break;
case NSStreamEventOpenCompleted:
break;
case NSStreamEventHasBytesAvailable:
//NSLog(@"Input stream is ready");
// 接收到硬件數據了,根據指令定義對數據進行解析。
[self readFromDevice];
break;
case NSStreamEventHasSpaceAvailable:
//NSLog(@"Output stream is ready");
// 可以發送數據給硬件了
[self writeToDevice];
break;
case NSStreamEventErrorOccurred:
break;
case NSStreamEventEndEncountered:
break;
default:
break;
}
}HasBytesAvailable:表示stream中有數據需要讀取(硬件發送了數據給App)
HasSpaceAvailable:表示stream中可以接收數據的寫入(App發送了數據給硬件)——當然,不是每次都需要等到這個回調執行,App才能發送數據給硬件,你可以判斷stream的hasBytesAvailable屬性,如果為Yes,照樣可以直接發送數據給硬件。類似如下:
BOOL isAvailable = self.session.outputStream.hasSpaceAvailable;
if (isAvailable == YES) {
[self writeToDevice];
}發送數據、接收數據的具體方法:
發送數據:
outputStream的write:maxLength:方法,類似如下:
[self.session.outputStream write:[self.writeData bytes] maxLength:self.writeDataLen];
接收數據:
inputStream的read:maxLength:方法,類似如下:
[self.session.inputStream read:buffer maxLength:SPK_INPUT_DATA_BUFFER_LEN];
到此,我們用ExternalAccessory框架,進行了從識別硬件連接、獲取硬件、打開傳輸通道、發送數據、接收數據的完整過程。
調試、Debug
我們開發的是一個Lightning接口設備的App,當手機連接硬件時,就沒辦法連接電腦進行調試,當手機連接電腦時,就沒辦法連接硬件進行測試。所以整個開發調試、Debug無從下手。網站上咨詢了蘋果,也在StackOverflow上提問,都沒有得到解決方案。
後來我就腦洞大開,把需要打印的日志收集起來,通過一個TextView,顯示到App上做調試用(如下圖)。也算是一個權宜之計,誰有更好的辦法麼~

將Log轉移到App界面上進行Debug
如有謬誤,敬請斧正。