主旨
本章主要介绍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是一套实现应用册到传输层的接口实现。配合下面这张图会更加清晰一些。
至于为什么需要socket,文中也有解释,只要是两点原因。
- 上三层协议主要处理应用层信息(FTP/HTTP)而不关注通信细节,而下四层主要处理通信细节而不关注应用层信息。
- 上三层作用在用户进程,而下四层通常作为OS内核的一部分提供。
所以在第四和第五层之间构建这样一个沟通上下的接口就可以理解了。
socket的工作方式
Unix网络编程中有一个实现基于TCP的例子(4.1章),我们可以用这个来解释。
- server端先创建socket连接->绑定本地地址->监听端口->接受请求。
- client端建立socket连接->向server端请求建立连接。
- 连接建立之后,client向server请求数据,server读取数据之后,给client写数据。
- client收到数据之后读取出来。
- 所有任务结束之后断开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 | - (EDOSocket *)edo_createListenSocket:(UInt16)port { |
edo_createListenSocket的代码里,我们可以看到两部分内容,一部分是EDOSocket创建一个socket监听指定端口,另一部是在回调里处理数据。具体细节能在EDOSocket和startReceivingRequestsForChannel中找到。
EDOSocket
调用顺序 |
---|
[EDOSocket listenWithTCPPort:queue:connectedBlock:] |
[EDOListenSocket listenSocketWithSocket:connectedBlock:] |
EDOSocket的原理基本上都在上面两个方法中了,我们一个一个看。
1 | + (EDOSocket *)listenWithTCPPort:(UInt16)port |
可以看到,上面大致做了3件事
- 调用了edo_CreateSocket创建了socket,这是封装后的方法,内部其实直接调用原始socket的socket(AF_INET, SOCK_STREAM, 0)。
- 紧接着调用通过bind绑定本地地址。
- 调用listen监听连接。
- 调用accept接受连接
- 通过回调处理数据。
至此,初始化HostService的任务基本结束,从代码上看,和标准的socket初始化没什么区别。初始化完之后就剩下数据的接受和处理了。
Unix中I/O相关的资源都使用文件描述符操作,所以这里socket创建之后返回的也是文件描述符。Everything is a file!
Host接受数据
startReceivingRequestsForChannel之前,创建了一个channel来作为发送和接收数据的通道。
| 调用顺序 |
| :—– |
| [EDOSocketChannel channelWithSocket:] |
| [EDOSocketChannel initWithSocket:] |
| [EDOSocketChannel initWithDispatchIO:] |
| [EDOSocket releaseAsDispatchIO] |
1 | - (nullable dispatch_io_t)releaseAsDispatchIO { |
可以看到,EDOSocketChannel的本质是创建了一个Dispatch I/O通道来读取client通过socket传送过来的数据。
1 | - (void)receiveDataWithHandler:(EDOChannelReceiveHandler)handler { |
EDOSocketChannel内部再从Dispatch I/O中不断的读取拼接数据并将数据通过handler发送出去。
当通道数据发送到外面之后,就需要对收到的数据进行具体的处理。
1 | - (void)startReceivingRequestsForChannel:(id<EDOChannel>)channel { |
这里主要解析出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 | + (void)connectWithTCPPort:(UInt16)port |
明显可以看到,这里也是遵循标准socket的流程,做了两件事。
- 调用socket()创建socket。
- 调用connect和Host建立连接。
现在两边连接已经建立,下面就到了Client调用Host的时候。
EDOObject
官方README是这样说的
1 | - (void)someMethod { |
这里的FooClass就是一个在Host中实现的普通类,之所以在Client进程中能够直接调用,是因为EDOClientService返回的是EDOObject,method1的实现并不是在Client进程,所以看起来直接调用method1能成功,是因为真正的调用会被EDOObject发送到Host端,然后将数据返回,这中间就用到OC的消息转发+socket通信。
1 | //EDOObject+Invocation |
EDOObject+Invocation文件中,覆写了methodSignatureForSelector和forwardInvocation,因为method1方法在EDOObject中肯定不存在,所以会直接走转发流程,所以消息会被封装,通过EDOChannel发送到Host进程,从Host进程拿返回数据。
总结
通过以上代码分析,可以总结出,eDistantObject使用了标准的socket流程在Host进程和Client进程建立了socket连接,然后将Client中的方法调用全部以消息转发的形式发送到Host进程,由Host进程处理完毕再返回被Client进程,达到我们表面上看起来的跨进程调用方法。
eDistantObject在README中也给出了一张原理图,也很好的诠释了这一过程。