谈谈Mac进程间通信--XPC

5,814 阅读7分钟

概述

XPC Service是一种整合了GCDlaunchd的一种轻量级进程间通信机制,其主要目的是提供:权限分离和稳定性。

权限分离:利用xpc服务具有自己的沙箱环境,将应用程序分割为若干个小组件来控制权限,来实现严格的沙箱环境,减小被攻击的风险;其中xpc服务是私有的,仅用于包含它的主应用有效;其运行在更严格的沙箱环境中,如最小的文件访问权限、网络访问等,且不支持将其服务权限提升为root权限;

稳定性:通过将应用不稳定的功能与应用核心功能分开,来避免稳定性对整个应用的影响;

XPC Service由launchd来管理其启动、监视及终止,比如崩溃恢复,服务完成或者闲置会被kill -9终止,更好地管理XPC服务的生命周期。

通过find /System/Library/Frameworks -name \*.xpc或者find /Applications -name \*.xpc搜索系统框架及应用下的XPC服务,可以发现:XPC被广泛使用在系统框架、系统应用及第三方应用中,如XcodeChromeCleanMyMac等。不过对于iOS,只能苹果使用对于第三方开发者无法使用(xcode中也未提供相应的模版)。

使用及原理实现

苹果提供了两种处理xpc服务端api:

  • 基于纯c实现的服务api(包含在libSystem中),包含两部分:

    • xpc.h,定义了XPC支持的对象和数据类型及其操作接口,以及服务启动、事务管理接口;

    • connection.h,定义了用于XPC服务连接的建立/激活/暂停、消息发送/响应、连接属性信息获取及设置事件处理程序及目标队列等的接口;

xpc api

  • 基于Objective-C实现的NSXPCConnection接口,提供了远程过程调用机制,允许本地进程通过代理对象调用另一个进程的方法,并自动处理数据序列化及反序列化,主要由以下几部分:

    • NSXPCConnection,用于两个进程的双向通信;
    • NSXPCInterface,用于约定通信双方的调用行为;
    • NSXPCListener,用于监听传入的连接并设定NSXPCListenerDelegate协议的代理对象来接收处理传入的连接;
    • NSXPCListeneEndpoint,一个可以唯一标识NSXPCListener实例的类,可以使用NSXPCConnection将其发送到远程进程。这允许一个进程在其他两个进程之间构造一个直接的通信通道;

下面将介绍下典型的使用上述接口来创建使用XPC服务。

创建XPC服务

XPC服务典型应用就是应用内组件之间通信以实现权限分离保证核心功能稳定性,其创建相对简单,xcode已经提供了相应的模版,编译后会直接添加到相应的应用包中,路径为/Contens/XPCSercices,其包结构与应用包结构类似,都包含二进制程序、Info.plist文件及添加的资源文件;

xcode创建服务模版默认使用NSXPCConnection方式,Info.plist中配置的XPCService字典服务类型ServiceTypeApplicationInfo.plist还包含其他定义服务属性及类型字段,如下:

  • CFBundleIdentifier,服务bundle id,命名应符合反向DNS风格,如com.fengyunsky.xpc.xpcmain
  • CFBundlePackageType,服务类型,必须为XPC!
  • XPCService,服务属性字段,包含
    • EnvironmentVariables字典属性来设置环境变量值;
    • JoinExistingSession BOOL类型属性,用于指示是否需要创建新的安全会话,默认为false,即创建新会话;若为true,则可以访问当前用户的keychain、剪贴板及其他用户的资源及服务;
    • RunLoopType,字符串类型,用于指示服务runloop类型,默认为dispatch_main,或者NSRunLoop;
    • ServiceType,服务类型,默认为Application,也可以为User或者System

dispatch_main()pthread_exit的包装,其主要作用是退出主线程并继续执行其他子线程,对于添加到主队列的block由workqueue线程来处理;

创建服务并监听连接典型使用如下:

static void XPCService_peer_event_handler(xpc_connection_t peer, xpc_object_t event)
{
  	xpc_type_t type = xpc_get_type(event);
    if (type == XPC_TYPE_ERROR)
    {
        if (event == XPC_ERROR_CONNECTION_INVALID)
        {
            //连接无效
        }
        else if (event == XPC_ERROR_TERMINATION_IMMINENT)
        {
            //即将终止
        }
    } else {
        //处理连接业务,如发送消息
        const char* data = "hello world!";
        xpc_object_t dictionary = xpc_dictionary_create(NULL, NULL, 0);
        xpc_dictionary_set_data(dictionary, "msg", data, strlen(data));
        xpc_connection_send_message(peer, dictionary);
    }
}

static void XPCService_event_handler(xpc_connection_t peer)
{
    xpc_connection_set_event_handler(peer, ^(xpc_object_t event) {
        XPCService_peer_event_handler(peer, event);
    });
    xpc_connection_resume(peer);
}

int main(int argc, const char *argv[]) {
    xpc_main(XPCService_event_handler);
 
    // The xpc_main() function never returns.
    exit(EXIT_FAILURE);
}

xpc_main函数来启动服务并设置事件处理函数,其中事件处理函数XPCService_event_handler主要设置事件连接处理函数并xpc_connection_resume恢复连接;默认连接处理队列是DISPATCH_TARGET_QUEUE_DEFAULT,可以通过xpc_connection_set_target_queue修改GCD处理队列;xpc_connect提供了同步或者异步的发送/接收消息接口,如下:

其中xpc_connection_send_message_with_reply_sync为同步接口,阻塞直到收到应答消息,其余为异步接口;xpc_connection_send_barrier可以设定最后一条消息发送完成后的执行block,类似dispatch栅栏接口。

注意:消息发送形式必须为字典对象;

消息发送实现如图:

其实质是通过mach_msg发送消息,即通过mach消息机制实现;

基于Objective-c形式如下:

//Objective-C接口形式创建
@interface ServiceDelegate : NSObject <NSXPCListenerDelegate>
@end

@implementation ServiceDelegate

- (BOOL)listener:(NSXPCListener *)listener shouldAcceptNewConnection:(NSXPCConnection *)newConnection {
    newConnection.exportedInterface = [NSXPCInterface interfaceWithProtocol:@protocol(xpcmainProtocol)];
    xpcmain *exportedObject = [xpcmain new];
    newConnection.exportedObject = exportedObject;
    [newConnection resume];
    
    return YES;
}

@end

int main(int argc, const char *argv[]) {
    MyDelegateClass *myDelegate = ...
    NSXPCListener *listener =
        [NSXPCListener serviceListener];
 
    listener.delegate = myDelegate;
    [listener resume];
 
    // The resume method never returns.
    exit(EXIT_FAILURE);
}

NSXPCConnection相关的类提供更为高级的接口,通过NSXPCListener来监听服务连接,并通过指定NSXPCListenerDelegate代理方法listener:shouldAcceptNewConnection:来处理新的连接请求;NSXPCConnection属性exportedInterfaceexportedObject来指定导出接口(通过协议实现)及对象,用于对端服务进程分别通过指定的NSXPCConnection属性remoteObjectInterfaceremoteObjectProxy来获取远端约定的导出接口及导出对象,进而实现本地调用远端方法,即远程过程调用;

消息发送

基于c实现的接口

典型使用接口如下:

//创建xpc_connection_t对象
//其中"com.fengyunsky.xpc.demo"为xpc服务的bundleID,需要指定正确否则launchd无法找到相应的服务
_connection = xpc_connection_create("com.fengyunsky.xpc.demo", NULL);
xpc_connection_set_event_handler(_connection, ^(xpc_object_t object){
		size_t len = 0;
    const char *data = xpc_dictionary_get_data(object, "msg", &len);
    fwrite(data, len, 1, stdout);
    fflush(stdout);
});
xpc_connection_resume(_connection);

//发送消息
const char *data = "hello world!";
xpc_object_t dictionary = xpc_dictionary_create(NULL, NULL, 0);
xpc_dictionary_set_data(dictionary, "msg", data, strlen(data));
xpc_connection_send_message_with_reply(_connection, dictionary, DISPATCH_TARGET_QUEUE_DEFAULT, ^(xpc_object_t  _Nonnull object) {
    xpc_type_t type = xpc_get_type(object);
    if (type == XPC_TYPE_ERROR)
    {
        if (object == XPC_ERROR_CONNECTION_INVALID)
        {
            //连接无效
        }
        else if (object == XPC_ERROR_TERMINATION_IMMINENT)
        {
            //即将终止
        }
    } else {
        const void *data = xpc_data_get_bytes_ptr(object);
        NSLog(@"reply:%s", (char *)data);
    }
});

//消息发送完成后可终止连接
xpc_connection_cancel(_connection);

主要的流程如下:

  • xpc_connection_create创建xpc连接对象;
  • xpc_connection_set_event_handler设置连接事件处理函数;
  • xpc_connection_resume启动服务开启通信;
  • xpc_connect_send_xxx调用发送消息接口来异步/同步等待响应消息;
  • launchd守护进程搜索主应用包匹配的CFBundleIdentifier服务并启动服务程序;
  • 连接的xpc服务程序连接设定的处理函数并调用;
  • 使用xpc_connect_send_xxx响应消息并发送消息;
  • 如果出现错误,就会通过xpc_connection_set_event_handler设置的事件处理函数来处理异常错误;
  • 可以通过xpc_connection_suspend暂停连接,但必须与xpc_connection_resume配对使用;
  • 连接完成后,就通过xpc_connection_cancel来终止连接;

对于创建的xpc_connection_t连接对象可通过全局对象保存,用于后续消息同一连接消息发送/接收;

基于Objective-C实现的接口

典型使用如下:

//创建NSXPCConnection对象,指定协议接口,启动连接
_connectionToService = [[NSXPCConnection alloc] initWithServiceName:"com.fengyunsky.xpc.demo"];
_connectionToService.remoteObjectInterface = [NSXPCInterface interfaceWithProtocol:@protocol(xpcmainProtocol)];
[_connectionToService resume];

//发送消息
[[_connectionToService remoteObjectProxy] upperCaseString:@"hello" withReply:^(NSString *aString) {
  // We have received a response. Update our text field, but do it on the main thread.
  NSLog(@"Result string was: %@", aString);
}];

//终止连接
[_connectionToService invalidate];

大致的流程如下图;

通过initWithServiceName来创建NSXPCConnection连接对象,interfaceWithProtocol来指定约定的协议方法,resume来启动连接;当通过remoteObjectProxy对象来调用xpc服务的方法时,launchd会搜索应用包中匹配的xpc服务并启动该服务,通过创建xpc服务的代理方法来接口连接,并执行导出接口方法。

总结

相比其他的进程间通信方式,如NSDistributedNotificationCenterMach Port、域套接字等,XPC服务实现更轻量,无需管理子进程的生命周期,并且能实现子进程崩溃恢复功能,通过NSXPCConnection相关的高级接口方便实现远程过程调用,使用更为简洁易用;

Reference

XPC

XPC介绍

XPC API

Creating XPC Services

Daemons and Services Programming Guide

Introducing XPC

利用XPC实现多进程之间的通信

《深入理解MacOS X &iOS操作系统》

进程间通信 (OSX/iOS)