iOS进程间通信

主旨

本章主要介绍Google在开源项目EarlGrey中用到的进程间通信的方式eDistantObject的实现原理。

背景

在做自动化探索测试的过程中,发现Apple提供的UI测试(黑盒),对App元素的操作有点局限,获取的信息也不能完全满足要求,所以想到是否可以通过在被测试App内部获取和操作元素(白盒),然后在另一个App(测试App)中进行全流程控制,达到兼具白盒和黑盒的效果。

既然能在测试App内部操作,为什么还一定需要一个测试App来控制?因为在App内部没法完成所以自动化测试的公共,比如系统Alert的处理。这个不是本章重点,不赘述。

那么问题来了,抛开Apple的UI测试,我们怎么才能在测试App中和被测试App进行通信,这里就需要有一种通信方式来在完成App间的数据沟通。下面我们介绍下eDistantObject的大致原理和实现。

基础知识提要

什么是socket

引用Unix网络编程中的介绍(1.7章)

The sockets programming interfaces described in this book are interfaces from the upper three
layers (the “application”) into the transport layer.
大致意思是,socket是一套实现应用册到传输层的接口实现。配合下面这张图会更加清晰一些。

Layers in OSI model and Internet protocol suite.

至于为什么需要socket,文中也有解释,只要是两点原因。

  1. 上三层协议主要处理应用层信息(FTP/HTTP)而不关注通信细节,而下四层主要处理通信细节而不关注应用层信息。
  2. 上三层作用在用户进程,而下四层通常作为OS内核的一部分提供。

所以在第四和第五层之间构建这样一个沟通上下的接口就可以理解了。

socket的工作方式

Unix网络编程中有一个实现基于TCP的例子(4.1章),我们可以用这个来解释。

Socket functions for elementary TCP client/server.

  1. server端先创建socket连接->绑定本地地址->监听端口->接受请求。
  2. client端建立socket连接->向server端请求建立连接。
  3. 连接建立之后,client向server请求数据,server读取数据之后,给client写数据。
  4. client收到数据之后读取出来。
  5. 所有任务结束之后断开socket连接。

有关于图中每个方法的具体细节,可以直接通过man了解到.

eDistantObject实现

eDistantObject的README中提供的范例是先在Host端建立一个service,然后在client端根据Host端的端口去调用Host端的方法。这和我们上面的socket通信中的client/server模式很像。我们就先从Host建立service为切入点看下它的代码。

EDOHostService

调用顺序
[EDOHostService serviceWithPort:rootObject:queue:]
[EDOHostService initWithPort:rootObject:serviceName:queue:isToDevice:]
[EDOHostService edo_createListenSocket:]

从入口初始化一路向下,省略掉其中一些变量初始化的工作,我们把关注点放到edo_createListenSocket上,从命名上也能看出,edo_createListenSocket其实创建了一个socket连接,监听请求来的数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- (EDOSocket *)edo_createListenSocket:(UInt16)port {
__weak EDOHostService *weakSelf = self;
return [EDOSocket listenWithTCPPort:port
queue:nil
connectedBlock:^(EDOSocket *socket, NSError *error) {
EDOHostService *strongSelf = weakSelf;
if (!strongSelf) {
[socket invalidate];
return;
}

id<EDOChannel> clientChannel = [EDOSocketChannel channelWithSocket:socket];
[strongSelf startReceivingRequestsForChannel:clientChannel];
}];
}

edo_createListenSocket的代码里,我们可以看到两部分内容,一部分是EDOSocket创建一个socket监听指定端口,另一部是在回调里处理数据。具体细节能在EDOSocket和startReceivingRequestsForChannel中找到。

EDOSocket

调用顺序
[EDOSocket listenWithTCPPort:queue:connectedBlock:]
[EDOListenSocket listenSocketWithSocket:connectedBlock:]

EDOSocket的原理基本上都在上面两个方法中了,我们一个一个看。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
+ (EDOSocket *)listenWithTCPPort:(UInt16)port
queue:(dispatch_queue_t)queue
connectedBlock:(EDOSocketConnectedBlock)block {
// 省略
// 1 - socket()
dispatch_fd_t socketFD = edo_CreateSocket(&socketErr);
// 省略
// 2 - bind
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_len = sizeof(addr);
addr.sin_port = htons(port);
addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
if (bind(socketFD, (struct sockaddr *)&addr, sizeof(addr)) != 0) {
close(socketFD);
return nil;
}
// listen
return [EDOListenSocket listenSocketWithSocket:socketFD
connectedBlock:^(EDOSocket *socket, NSError *error) {
// dispatch the block to the user's queue
dispatch_async(queue, ^{
block(socket, nil);
});
}];
}
+ (EDOListenSocket *)listenSocketWithSocket:(dispatch_fd_t)socketFD
connectedBlock:(EDOSocketConnectedBlock)block {
// 省略
// 3 - listen
if (listen(socketFD, SOMAXCONN) != 0) {
close(socketFD);
return nil;
}

dispatch_queue_t eventQueue =
dispatch_queue_create(gListenSocketQueueLabel, DISPATCH_QUEUE_SERIAL);

dispatch_source_t source =
dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, (uintptr_t)socketFD, 0, eventQueue);

EDOListenSocket *listenSocket = [EDOListenSocket socketWithSocket:socketFD source:source];
__weak EDOListenSocket *weakSelf = listenSocket;
dispatch_source_set_event_handler(source, ^{
EDOListenSocket *strongSelf = weakSelf;

unsigned long nconns = dispatch_source_get_data(source);

while (nconns > 0) {
// 4 - accept
EDOSocket *socket = [strongSelf accept:socketFD];
// 5 - 处理数据
if (socket) {
block(socket, nil);
}
--nconns;
}
});

dispatch_source_set_cancel_handler(source, ^{
// Release the socket and reset it to -1.
EDOSocket *strongSelf = weakSelf;
[strongSelf releaseSocket];
close(socketFD);
});

dispatch_resume(source);
return listenSocket;
}

可以看到,上面大致做了3件事

  1. 调用了edo_CreateSocket创建了socket,这是封装后的方法,内部其实直接调用原始socket的socket(AF_INET, SOCK_STREAM, 0)。
  2. 紧接着调用通过bind绑定本地地址。
  3. 调用listen监听连接。
  4. 调用accept接受连接
  5. 通过回调处理数据。

至此,初始化HostService的任务基本结束,从代码上看,和标准的socket初始化没什么区别。初始化完之后就剩下数据的接受和处理了。

Unix中I/O相关的资源都使用文件描述符操作,所以这里socket创建之后返回的也是文件描述符。Everything is a file!

Host接受数据

startReceivingRequestsForChannel之前,创建了一个channel来作为发送和接收数据的通道。
| 调用顺序 |
| :—– |
| [EDOSocketChannel channelWithSocket:] |
| [EDOSocketChannel initWithSocket:] |
| [EDOSocketChannel initWithDispatchIO:] |
| [EDOSocket releaseAsDispatchIO] |

1
2
3
4
5
6
7
8
9
10
- (nullable dispatch_io_t)releaseAsDispatchIO {
dispatch_fd_t socket = [self releaseSocket];
// 省略=
dispatch_queue_t queue = dispatch_queue_create("com.google.edo.SocketIO", DISPATCH_QUEUE_SERIAL);
dispatch_io_t channel = dispatch_io_create(DISPATCH_IO_STREAM, socket, queue, ^(int error) {
//省略
});
// 省略
return channel;
}

可以看到,EDOSocketChannel的本质是创建了一个Dispatch I/O通道来读取client通过socket传送过来的数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
- (void)receiveDataWithHandler:(EDOChannelReceiveHandler)handler {
dispatch_queue_t handlerQueue = self.handlerQueue;
dispatch_io_t channel = self.channel;
//省略
dispatch_io_handler_t dataHandler = ^(bool done, dispatch_data_t data, int error) {
//省略
NSMutableData *receivedData =
[NSMutableData dataWithCapacity:dispatch_data_get_size(dataReceived)];
dispatch_data_apply(dataReceived, ^bool(dispatch_data_t region, size_t offset,
const void *buffer, size_t size) {
[receivedData appendBytes:buffer length:size];
return YES;
});
if (handler) {
dispatch_async(handlerQueue, ^{
handler(self, receivedData, nil);
});
}
};

dispatch_io_handler_t frameHandler = ^(bool done, dispatch_data_t data, int error) {
size_t payloadSize = EDOGetPayloadSizeFromFrameData(data);
//省略许多
dispatch_io_read(channel, 0, payloadSize, handlerQueue, dataHandler);
};

dispatch_io_read(channel, 0, EDOGetPayloadHeaderSize(), handlerQueue, frameHandler);
}

EDOSocketChannel内部再从Dispatch I/O中不断的读取拼接数据并将数据通过handler发送出去。
当通道数据发送到外面之后,就需要对收到的数据进行具体的处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
- (void)startReceivingRequestsForChannel:(id<EDOChannel>)channel {
//省略无关代码
EDOChannelReceiveHandler receiveHandler =
^(id<EDOChannel> targetChannel, NSData *data, NSError *error) {
EDOChannelReceiveHandler strongHandlerBlock = weakHandlerBlock;
EDOServiceRequest *request = [NSKeyedUnarchiver edo_unarchiveObjectWithData:data];
NSString *requestClassName = NSStringFromClass([request class]);
//获取匹配的handler
EDORequestHandler handler = EDOHostService.handlers[requestClassName];
__block EDOServiceResponse *response = nil;
NSError *error;
if (handler) {
__weak EDOServiceRequest *weakRequest = request;
void (^requestHandler)(void) = ^{
uint64_t currentTime = mach_absolute_time();
response = handler(weakRequest, weakSelf);
response.duration = EDOGetMillisecondsSinceMachTime(currentTime);
};
[strongSelf.executor handleBlock:requestHandler error:&error];
}
NSData *responseData = [NSKeyedArchiver edo_archivedDataWithObject:response];
// 发送respone
[targetChannel sendData:responseData withCompletionHandler:nil];
// 循环直到全部处理完
if ([strongSelf edo_shouldReceiveData:channel]) {
[targetChannel receiveDataWithHandler:strongHandlerBlock];
}
}
};
receiveHandler = [receiveHandler copy];
[channel receiveDataWithHandler:receiveHandler];
}

这里主要解析出request,然后根据request的类型,去除对应的handeler,执行完之后如果将response(包装成EDOObject)发送会client端。其中handler主要有以下几个:

  • EDOClassRequest
  • EDOInvocationRequest
  • EDOMethodSignatureRequest
  • EDOObjectAliveRequest
  • EDOObjectRequest
  • EDOObjectReleaseRequest

至此Host端(我们熟悉的socket中的Server端)以及全部结束。根据socket的流程,我们不难猜想出Client端的工作流程。

EDOClientService

调用顺序
[EDOClientService responseObjectWithRequest:onPort:]
[EDOClientService sendSynchronousRequest:onPort:]
[EDOClientService sendSynchronousRequest:onPort:withExecutor]
[EDOChannelPool channelWithPort:error:]
[EDOChannelPool edo_createChannelWithPort:error:]
[EDOSocket socketWithTCPPort:queue:error:]
[EDOSocket connectWithTCPPort:queue:connectedBlock:]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
+ (void)connectWithTCPPort:(UInt16)port
queue:(dispatch_queue_t)queue
connectedBlock:(EDOSocketConnectedBlock)block {
// 1 - socket
dispatch_fd_t socketFD = edo_CreateSocket(&socketErr);

struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
// 2 - connect
int ret = connect(socketFD, (struct sockaddr const *)&addr, sizeof(addr));
socketErr = errno;
if (ret != 0 && socketErr != EINPROGRESS) {
edo_RunHandlerWithErrorInQueueWithBlock(socketErr, queue, block);
close(socketFD);
}
}

明显可以看到,这里也是遵循标准socket的流程,做了两件事。

  1. 调用socket()创建socket。
  2. 调用connect和Host建立连接。

现在两边连接已经建立,下面就到了Client调用Host的时候。

EDOObject

官方README是这样说的

1
2
3
4
5
6
- (void)someMethod {
// The object, fetched remotely from the host is seen by the client to be the same as a local
// object.
FooClass *rootObject = [EDOClientService rootObjectWithPort:portNumber];
[rootObject method1];
}

这里的FooClass就是一个在Host中实现的普通类,之所以在Client进程中能够直接调用,是因为EDOClientService返回的是EDOObject,method1的实现并不是在Client进程,所以看起来直接调用method1能成功,是因为真正的调用会被EDOObject发送到Host端,然后将数据返回,这中间就用到OC的消息转发+socket通信。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
//EDOObject+Invocation
- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector {
// TODO(haowoo): Cache the signature.
EDOServiceRequest *request = [EDOMethodSignatureRequest requestWithObject:self.remoteAddress
port:self.servicePort
selector:selector];
EDOMethodSignatureResponse *response = (EDOMethodSignatureResponse *)[EDOClientService
sendSynchronousRequest:request
onPort:self.servicePort.hostPort];
NSString *signature = response.signature;
return signature ? [NSMethodSignature signatureWithObjCTypes:signature.UTF8String] : nil;
}

/** Forwards the invocation to the remote. */
- (void)forwardInvocation:(NSInvocation *)invocation {
[self edo_forwardInvocation:invocation selector:invocation.selector returnByValue:NO];
}

- (void)edo_forwardInvocation:(NSInvocation *)invocation
selector:(SEL)selector
returnByValue:(BOOL)returnByValue {
EDOInvocationRequest *request = [EDOInvocationRequest requestWithInvocation:invocation
target:self
selector:selector
returnByValue:returnByValue
service:service];
EDOExecutor *executor = [EDOHostService serviceForCurrentExecutingQueue].executor;
EDOInvocationResponse *response =
(EDOInvocationResponse *)[EDOClientService sendSynchronousRequest:request
onPort:self.servicePort.hostPort
withExecutor:executor];
}

EDOObject+Invocation文件中,覆写了methodSignatureForSelector和forwardInvocation,因为method1方法在EDOObject中肯定不存在,所以会直接走转发流程,所以消息会被封装,通过EDOChannel发送到Host进程,从Host进程拿返回数据。

总结

通过以上代码分析,可以总结出,eDistantObject使用了标准的socket流程在Host进程和Client进程建立了socket连接,然后将Client中的方法调用全部以消息转发的形式发送到Host进程,由Host进程处理完毕再返回被Client进程,达到我们表面上看起来的跨进程调用方法。
eDistantObject在README中也给出了一张原理图,也很好的诠释了这一过程。

Client-Host

参考文档