最近又做了一遍基于Replaykit Extension實現(xiàn)iOS錄屏直播的需求闰挡,其中宿主App和Extension間用到了CFMessagePort進(jìn)行進(jìn)程間通信椅棺,記錄一下瞄桨,并試圖封裝出一個小輪子(doing)
有啥用
已經(jīng)使用:
- 錄屏直播場景下,主App起定時器通過port時不時ping一下ReplaykitExtension是否在運行宁玫,沒有的話提示開啟
- ReplaykitExtension時不時ping一下主App控制臺是否在選擇背景圖固惯,如果是,則推流默認(rèn)圖片洛心,保護(hù)用戶相冊隱私
- 結(jié)合UserDefault固耘,在需要數(shù)據(jù)同步時發(fā)送message通知另一方去更新數(shù)據(jù)
設(shè)想:
- 部分場景下讓同一個AppGroup下的另一個后臺App/Extension停止播放/數(shù)據(jù)更新等
- 與同一個AppGroup下的另一個App/Extension進(jìn)行數(shù)據(jù)交互,(token共享词身、偏好共享等)
通信的前提條件
- 兩個通信進(jìn)程需要在同一個AppGroup中
- CFMessagePortRef的portName必須是以 AppGroupId為前綴厅目,
例如com.clc.group.portName,其中com.clc.group是AppGroupId - 如果是一個前臺一個后臺兩個進(jìn)程,需要后臺彼鸱螅活(Replaykit不必葫笼,沒有后臺狀態(tài))
結(jié)構(gòu)是怎樣的
先貼一下思考的時候畫的圖(一邊想一變畫的,大概意思就是兩者通過MessagePort通信)
如圖拗馒,左邊是Extension進(jìn)程路星,右邊是App進(jìn)程。
通過在Runloop上的Source1位置掛上一個Listener(即一個Port)作為接收端诱桂,在漫長的跑圈里奥额,等待Port來訪。
創(chuàng)建Listener
- (void)startListener
CFStringRef portName = (__bridge CFStringRef)(self.PortName);
//回調(diào)
CFMessagePortCallBack callback = onRecvMessageCallBack;
//創(chuàng)建端口
CFMessagePortRef port = CFMessagePortCreateLocal(kCFAllocatorDefault, portName, callback, NULL, NULL);
//端口封裝成source
CFRunLoopSourceRef source = CFMessagePortCreateRunLoopSource(kCFAllocatorDefault, port, 0);
//source掛上runloop
CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
}
再寫一下間聽到信息時的回調(diào)函數(shù)
CFDataRef onRecvMessageCallBack(CFMessagePortRef local ,SInt32 msgid,CFDataRef cfData, void*info)
{
//偽代碼
//處理收到的data
handler(cfData);
//準(zhǔn)備好伴手禮數(shù)據(jù)回調(diào)給發(fā)送者
return responseData;
}
那么一個Listener就創(chuàng)建完成了访诱,那么接下來讓我們打開這個Listener
CLCListener *listener = [[CLCListener alloc] initWithPortName:portName];
//開啟listener
NSThread *listenerThread = [[NSThread alloc] initWithBlock:^{
[listener startListener];
CFRunLoopRun();
}];
listener.runingThread = listenerThread;
[listenerThread start];
OK垫挨,那么接收端這邊就準(zhǔn)備好了,需要注意的是
發(fā)送端發(fā)送-->Listener收到信息進(jìn)入回調(diào)函數(shù)-->完成回調(diào)給發(fā)送放
這一整個流程都是同步進(jìn)行的触菜,所以如果在回調(diào)函數(shù)里避免耗時操作九榔。
如果不可避免,則應(yīng)先進(jìn)行回調(diào)涡相,再想辦法在處理完成后進(jìn)行一次通信(比如發(fā)送方開一個Listener等待哲泊,處理方完成后主動通知發(fā)送方,發(fā)送方收到后再關(guān)閉用于等待處理的Listener)
進(jìn)行一次美好的調(diào)用
讓我們看看發(fā)送方是怎么向Listener發(fā)送信息的催蝗,
首先這里會用CFMessagePortCreateRemote創(chuàng)建一個Port切威。
其中的portName就是和上面創(chuàng)建Listener時的portName。
- (void)doRequestWithMessage:(NSString *)message
portName:(NSString *)portName
timeOut:(NSTimeInterval)timeout
callBack:(void(^)(id data))callBack
CFStringRef cPortName = (__bridge CFStringRef)(portName);
// 生成Remote port
CFMessagePortRef port = CFMessagePortCreateRemote(kCFAllocatorDefault, cPortName);
如果在創(chuàng)建的時候丙号,Listener還沒有被創(chuàng)建先朦,那么這里返回的port就是nil。
if (nil == port) {
callBack(nil);
return;
}
如果有值犬缨,那么槍就準(zhǔn)備好了喳魏,接下來上彈
//帶過去的參數(shù)
const char *cMessage = [message UTF8String];
CFDataRef cRequestData = CFDataCreate(NULL, (UInt8 *)cMessage, strlen(cMessage));
//用來接回調(diào)的CFDataRef
CFDataRef cResponseData = nil;
//demo里隨便寫的,可以是任意數(shù)值怀薛,在接受方可以取到
SInt32 msgId = 32;
//do localRequest
CFMessagePortSendRequest(port, msgId, cRequestData, 1, timeout, kCFRunLoopDefaultMode, &cResponseData);
其中的1和timeout一個是發(fā)送timeout刺彩,一個是等待回調(diào)的timeout,還是上面那句話枝恋,因為這里是同步調(diào)用创倔,所以需要小心耗時操作阻塞
如果調(diào)用成功,那么這里的cResponseData已經(jīng)把值帶回來了
把CF對象橋過去給NS對象焚碌,就可以進(jìn)業(yè)務(wù)邏輯了
if (callBack) {
//cf to ns
NSData* response = (__bridge_transfer NSData*)cResponseData;
callBack(response);
}
通信完成畦攘,走的時候帶一下垃圾,歡迎下次光臨
//clean up
CFRelease(cRequestData);
CFMessagePortInvalidate(port);
CFRelease(port);
}
輪子能上路前還有啥要做的事情
- 數(shù)據(jù)封裝呐能,目前只是用來互相ping確認(rèn)存活提及通知去UserDefault更新數(shù)據(jù)念搬,數(shù)據(jù)傳遞能力還不夠完備易用
- 啟動關(guān)閉Listener的線程操作封裝起來抑堡,提高安全性
- 層級結(jié)構(gòu)理清,上層下層邏輯分離干凈
- 晚飯還沒吃朗徊,今天還要去理發(fā)