几种Cocoa开发中的IPC方案

​ iOS系统相对于Android系统来说非常的封闭,每个应用的活动范围被严格地限制在各自的沙盒中,也许这是为了保证系统的安全性,避免恶意的应用去偷取其他应用的隐私信息。不过这同时也就阻碍了系统中应用之间或者与Extension间某些必要的跨进程通信。

​ Unix系统提供很多进程间通信的渠道,比如pipe、FIFO、共享内存、消息队列、信号等方式,但是iOS非越狱系统都对这些层级的API加了权限,调用这些接口会出现Permission Denied的错误警告。

尽管如此,iOS还是提供了若干IPC的策略方案,下面就来整理一下:

一、URL Schema

​ 简单来说,URL Schema就是iOS内的应用调用协议,应用A可以声明自定义的调用协议,就如http/https那样,当另一个应用B打算在应用内打开应用A时,可以打开使用A自定义的协议开头的URL来打开A,除了协议头,URL中还可以附加其他参数。此外,这些协议还可以通过Safari访问打开某个特定的应用。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lvRbJxqP-1577180499914)(/Users/aesthetic/Desktop/屏幕快照 2019-03-18 下午5.08.28.png)]

在Xcode的Info.plist里面可以自由定义该应用的协议头,然后通过形似“协议头://xxx”的URL便可以打开了。

消息接收端(在AppDelegate.m文件里):

- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation {
    if ([sourceApplication isEqualToString: @"xxx.xxx.xxx"]) {
        //...解析URL的参数
    }
    return YES;
}

消息发送端:

//URL中://后可以跟需要传递的参数
NSURL* url = [NSURL URLWithString: @"xxx://xxx"];
[[UIApplication sharedApplication] openURL: url];

这种方式普遍存在于现在很多应用的分享SDK中,比如微信、微博、QQ等。

不过这种方式有以下缺点

1、同一时刻还是只能有一个进程在前台,主动呼叫的应用在调用成功后必须进入后台,所以能传递的只有URL中所带的参数或annotation中所带的参数。

2、调用的过程中会出现应用之间的切换,用户体验不好。即不能够直接在本应用内进行远程分享。

二、Distributed Notifications

(1)NSDistributedNotificationCenter

​ 每 一个进程都有一个默认的DistributedNotificationCenter,你可以通过访问 NSDistributedNotificationCenter 的 +defaultCenter方法来得到它。这种类型的NotificationCenter负责管理一台机器上多个进程之间的Notification。

​ 发送一个DistributedNotification是非常昂贵的。 Notification首先会被发送到一个系统级别的服务器上,然后在分别分发到每一个注册过的进程里。从发消息到消息被接受到之间的延迟理论上来说是无限的。事实上,如果太多的Notification被发送到服务器上,那么服务器上的Notification队列可能会被撑满,这就有可能会造成Notification的丢失。

​ DistributedNotification会在一个进程的主循环里被发送出去。一个进程必须保证有一个主循环在其内部运行,例如 NSDefaultRunLoopMode,然后才能接受到DistributedNotification。如果接收进程是多线程的, 那么Notification并不一定会被主线程接受到。 一般来说Notification会被分发到主线程的主循环,但是其他线程一样可以接收到。NSDistributedNotificationCenter的使用方法如下:

//发送方发送通知
/*
notificationSender:被发送的对象(只能是字符串,但可以通过NSData和NSString的相互转换来传递数据)  
userInfo:关于该通知的信息(有时候通过它来传递数据比字符串方便)
*/
- (void)postNotificationName:(NSString *)notificationName object:(id)notificationSender userInfo:(NSDictionary *)userInfo;

//接收方接收通知
//1、订阅通知
 - (void)addObserver:id selector:(SEL)selector name:(NSString *)notificationName object:id object;
//2、定义一个Selector方法
- (void)selectorName:(NSNotification *)note;

​ 一般类型的NotificationCenter可以注册所有Object的Notification,但是DistributedNotificationCenter只能注册字符串类型的Notification。因为发送者和接受者可能在不同进程里,Notification里面包含的Object不能保证指向同一个Object。所以,DistributedNotificationCenter只能接受包含字符串类型的Notification。 Notification会基于字符串来匹配。

(2)CFNotificationCenter

CFNotificationCenter有三种类型 - DistributedCenter,LocalCenter和DarwinNotifyCenter。
在这里插入图片描述
其中分发式通知只能在OSX和WIN32系统使用,而DarwinNotifyCenter有些局限,不能够传递object和userInfo,下面是苹果文档的注释:

// The Darwin Notify Center is based on the <notify.h> API.
// For this center, there are limitations in the API. There are no notification "objects",
// "userInfo" cannot be passed in the notification, and there are no suspension behaviors

所以在iOS上的使用就有些局限了,仅能够传递同步信号;不过还有一个比较理想的使用环境,因为DarwinNotifyCenter是基于Darwin层传递通知的,所以给监听系统级别的通知开辟了渠道。下面是一个监听SpringBoard的锁开屏状态的应用:

#include<notify.h>
//处理锁屏通知
static void handleLockStateNotification(CFNotificationCenterRef center, void *observer, CFStringRef name, const void *object, CFDictionaryRef userInfo) {
    uint64_t state;
    int token;
    notify_register_check("com.apple.springboard.lockstate", &token);
    notify_get_state(token, &state);
    notify_cancel(token);
    if ((uint64_t)1 == state) {
        //...屏幕锁定
    }
    else {
        //...屏幕未锁定
    }
}
//com.apple.springboard.lockstate:设备锁屏状态通知名
//其他的SpringBoard层面通知可以参考:http://iphonedevwiki.net/index.php/SpringBoard.app/Notifications
CFNotificationCenterAddObserver(CFNotificationCenterGetDarwinNotifyCenter(), NULL, handleLockStateNotification, CFSTR("com.apple.springboard.lockstate"), NULL, CFNotificationSuspensionBehaviorDeliverImmediately);

三、CFMessagePort

CFMessagePort是Core Foundation框架层的API,而且它是比较适合用于简单的一对一通讯的。几行代码就可以进行端口的搭建,在接收端定义一个本地端口附属到runloop源上,只要获取到消息就执行回调。

#define SERVER_MSG_PORT_NAME "groupid.port.server"
//远程端请求CallBack回调
static CFDataRef recvMessageCallback(CFMessagePortRef port, SInt32 messageID, CFDataRef data, void *info) {
    // 实现数据解析
}
//定义并创建一个本地端口
CFMessagePortRef localPort = CFMessagePortCreateLocal(nil, CFSTR(SERVER_MSG_PORT_NAME), recvMessageCallback, nil, nil);
//初始化Runloop源
CFRunLoopSourceRef runLoopSource = CFMessagePortCreateRunLoopSource(nil, localPort, 0);
//附属到Runloop源
CFRunLoopAddSource(CFRunLoopGetCurrent(), runLoopSource, kCFRunLoopCommonModes);

发送数据只需要定义好远程端口,封装好待发送的消息,设置好发送接收的超时时间,然后由CFMessagePortSendRequest 完成数据的发送:

#define CLIENT_MSG_PROT_NAME "groupid.port.client"
CFDataRef sendData, recvData;
SInt32 messageID = 0x1111; // Arbitrary
CFTimeInterval timeout = 10.0;
//定义并创建一个远程端口
CFMessagePortRef remotePort = CFMessagePortCreateRemote(nil, CFSTR(CLIENT_MSG_PROT_NAME));
//发送端口请求
SInt32 status = CFMessagePortSendRequest(remotePort, messageID, data, timeout, timeout, kCFRunloopDefaultMode, &recvData);
if (status == kCFMessagePortSuccess) {
    // ...解析返回数据
}

此外我们可以通过以下方式取消端口的监听:

CFMessagePortInvalidate(port);
CFRelease(port);

这种方式相比于上面阉割了的分发通知来说有明显的优势,可以传输的不受约束的数据类型和数据大小。但是比较遗憾的是在iOS7及以后系统中,CFMessagePort的通信机制不再可用。官方文档如下所说:

This method is not available on iOS 7 and later—it will return NULL and log a sandbox violation in syslog. See Concurrency Programming Guide for possible replacement technologies.

权限被阻止了,但是我们还是可以使用在非越狱系统,前提是在进行App Extension的开发中或者是共用一套包含Group的开发者证书的APP。因为经过测试,打开App Groups权限后,以GroupID的前缀作为CFMessagePort中端口名来定义,就可以进行Host与Extension之间的通信,百度输入法和搜狗输入法的语音数据就是通过此方法来进行传输的。

此外,CFMessagePort是基于Mach Port端口的通信方式,不但可以用于进程通信,也可以用于线程间通信,只是线程间通信有了GCD和Cocoa提供的原生方法,已经能很方便的实现了,没必要再使用CFMessagePort。

优点: 比较容易部署,且数据传输功能能满足一般需要。

缺点: 使用场景比较局限,现在为止只能够进行App Extension的开发进程通信或者同一套包含GroupID证书开发的应用之间。

四、Local Socket

这种方式即是本地建立Socket连接,是一种特别实用的方法。它的原理很简单,一个App1在本地的端口Port:12345进行TCP的bind和listen,另外一个App2在同一个端口Port:12345发起TCP的connect连接,这样就可以建立正常的TCP连接,进行TCP通信了。

下面就是运用CFSocket来创建服务端和接收端的具体实现:

服务端: 配置和启动socket服务,把Socket地址定义为本地,端口定义统一

#import <sys/socket.h>
#import <netinet/in.h>
#import <arpa/inet.h>
#import <unistd.h>

#define TEST_IP_PROT 12345
#define TEST_IP_ADDR "xxx.xxx.xxx.xxx"//需要通过<ifaddrs.h>进行动态获取本地IP地址
CFSocketRef _socket = CFSocketCreate(kCFAllocatorDefault,
                                     PF_INET,/*指定协议族,如果参数为0或者负数,则默认为PF_INET*/
                                     SOCK_STREAM,/*指定Socket类型,如果协议族为PF_INET,且该参数为0或者负数,则它会默认为SOCK_STREAM,如果要使用UDP协议,则该参数指定为SOCK_DGRAM*/
                                     IPPROTO_TCP ,/*指定通讯协议。如果前一个参数为SOCK_STREAM,则默认为使用TCP协议,如果前一个参数为SOCK_DGRAM,则默认使用UDP协议*/
                                     kCFSocketAcceptCallBack,/*指定下一个函数所监听的事件类型*/
                                     TCPServerAcceptCallBack,
                                     NULL);
//定义sockaddr_in类型的变量,该变量将作为CFSocket的地址
struct sockaddr_in Socketaddr;
memset(&Socketaddr, 0, sizeof(Socketaddr));
Socketaddr.sin_len = sizeof(Socketaddr);
Socketaddr.sin_family = AF_INET;
//设置服务器监听地址
Socketaddr.sin_addr.s_addr = inet_addr(TEST_IP_ADDR);
//设置服务器监听端口
Socketaddr.sin_port = htons(TEST_IP_PROT);
//将IPv4的地址转换为CFDataRef
CFDataRef address = CFDataCreate(kCFAllocatorDefault, (UInt8 *)&Socketaddr, sizeof(Socketaddr));
//将CFSocket绑定到指定IP地址
if (CFSocketSetAddress(_socket, address) != kCFSocketSuccess) {
   ...
}

下面是将Socket包装成Source加到当前Runloop中以循环监听客户端连接:

CFRunLoopRef cfRunLoop = CFRunLoopGetCurrent();
//将_socket包装成CFRunLoopSource
CFRunLoopSourceRef source = CFSocketCreateRunLoopSource(kCFAllocatorDefault, _socket, 0);
//为CFRunLoop对象添加source
CFRunLoopAddSource(cfRunLoop, source, kCFRunLoopCommonModes);
CFRelease(source);
//运行当前线程的CFRunLoop
CFRunLoopRun();

创建可写Stream来进行数据的传输:

CFWriteStreamRef writeStreamRef;
if (kCFSocketAcceptCallBack == type) { 
    //创建一组可写的CFStream
    writeStreamRef = NULL;  
    //创建一个和Socket对象相关联的读取数据流
    CFStreamCreatePairWithSocket(kCFAllocatorDefault, //内存分配器
                                 nativeSocketHandle, //准备使用输入输出流的socket
                                 NULL,            //输入流
                                 &writeStreamRef);//输出流
    if (writeStreamRef) {        
        const char *sendMsg = "message";
        //向客户端输出数据
        CFWriteStreamWrite(writeStreamRef, (UInt8 *)sendMsg, strlen(sendMsg) + 1);
    }
} 

客户端: 连接远程服务端,主要是进行地址和端口的绑定

#define TEST_IP_PROT 12345
#define TEST_IP_ADDR "xxx.xxx.xxx.xxx"//需要通过<ifaddrs.h>进行获取本地的IP地址
//先创建一个Socket
_socketRef = CFSocketCreate(kCFAllocatorDefault, PF_INET, SOCK_STREAM, IPPROTO_TCP, kCFSocketConnectCallBack,ServerConnectCallBack, NULL);
//创建sockadd_in的结构体,该结构体作为Socket的地址
struct sockaddr_in Socketaddr;
memset(&Socketaddr, 0, sizeof(Socketaddr));
Socketaddr.sin_len = sizeof(Socketaddr);
Socketaddr.sin_family = AF_INET;
Socketaddr.sin_port = htons(TEST_IP_PROT);
Socketaddr.sin_addr.s_addr = inet_addr(TEST_IP_ADDR);
//将地址转化为CFDataRef
CFDataRef dataRef = CFDataCreate(kCFAllocatorDefault,(UInt8 *)&Socketaddr, sizeof(Socketaddr));
//建立连接
CFSocketConnectToAddress(_socketRef, dataRef, -1);   
//加入Runloop循环中
CFRunLoopRef runLoopRef = CFRunLoopGetCurrent();
CFRunLoopSourceRef sourceRef = CFSocketCreateRunLoopSource(kCFAllocatorDefault, _socketRef, 0);
CFRunLoopAddSource(runLoopRef, sourceRef, kCFRunLoopCommonModes);
CFRelease(sourceRef);
}

连接成功后读取服务端传入数据:

void ServerConnectCallBack ( CFSocketRef s, CFSocketCallBackType callbackType, CFDataRef address, const void *data, void *info ) {
        [self performSelectorInBackground:@selector(readStreamData) withObject:nil];
    }
}
- (void)readStreamData {
    //定义一个字符型变量
    char buffer[512];
    long readData;
    while((readData = recv(CFSocketGetNative(_socketRef), buffer, sizeof(buffer), 0))) {
        //处理接收数据
        NSString *content = [[NSString alloc] initWithBytes:buffer length:readData encoding:NSUTF8StringEncoding];
    }
}

这种方式最大的特点就是灵活,只要连接保持着,随时都可以传任何相传的数据,而且带宽足够大。

注意: 就是因为iOS系统在任意时刻只有一个app在前台运行,但是iOS允许App的一个Socket在App切换到后台后仍然保持连接,需要通信的另外一方具备在后台运行的权限,像导航或者音乐类app。

使用场景: 它的常用使用场景就是某个App1具有特殊的能力,比如能够跟硬件进行通信,在硬件上处理相关数据。而App2则没有这个能力,但是它能给App1提供相关的数据,这样App2跟App1建立本地Socket连接,传输数据到App1,然后App1在把数据传给硬件进行处理。

五、APP Groups

​ App Groups用于使用同一个Group开发证书的App之间,包括App和Extension之间共享同一份读写空间,进行数据共享。同一个团队开发的多个应用之间如果能直接数据共享,大大提高用户体验。

​ 只需要在需要共享数据的APP中分别打开App Groups权限就可以,需要保证的是描述文件支持权限并且两端Group名称要一致。
在这里插入图片描述
下面是具体的使用:

//获取App Group的共享目录
NSURL *groupURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:@"group.com.sogou.sogouinput-"];
NSURL *fileURL = [groupURL URLByAppendingPathComponent:@"AppGroup.txt"];
//...写入文件
//...读取文件

缺点: 不能够进行实时的数据传输。

六、Pasteboard

​ 剪贴板是iOS比较常见的进程间通信机制,这个在淘宝链接分享以及红包活动分享上面都有运用到。当用户跨应用拷贝了一段文字,图片,文档,这时候通过MachPortcom.apple.pboard服务媒介进行从一个进程到另一个进程的数据交换。

​ 具体用法是在每个应用中通过[UIPasteboard generalPasteboard]拿到全局的一个Pasteboard,然后再获取它的传递信息。

总结:

​ 虽然iOS系统的沙盒机制阻断了底层的一些进程间通信手段,但苹果还是给开发者提供了一些安全可控范围内的IPC方案,以上列举的是目前IPC的常见的执行方案,它们之间各有优缺点,也有各自不同的使用场景。

​ 除此之外还有KeyChains,UIDocumentInteractionController,AirDrop以及UIActivityViewController等方案提供间接的进程间通信。最终,对开发者来说,我们可以在适当的场景运用这些IPC方法来解决数据共享问题和提高用户的体验。

参考文档:

官方文档:Inter-App Communication

发布了71 篇原创文章 · 获赞 34 · 访问量 9万+

猜你喜欢

转载自blog.csdn.net/TuGeLe/article/details/103687163
今日推荐