之前写过一篇文章
APP启动流程(android12源码)
中介绍到,AMS通知Zygote去fork进程的时候,使用的是socket的方式,而不是binder。我们都知道,安卓中默认跨进程的方式是binder,而为什么这里偏偏使用Socket呢?
目前网上的说明众说纷纭,甚至有的都不能自圆其说。总结一下,主要有以下几大类:
1.锁的问题
2.启动顺序问题
3.安全问题(这个就有点离谱了,socket不会比binder更安全)
按照我个人的理解,主要有以下五个原因:
首先,先看下安卓系统启动顺序:
从图中可以知道,binder驱动是早于init进程加载的。而init进程是安卓系统启动的第一个进程。
那么,为什么有时序问题呢?
安卓中一般使用的binder引用,都是保存在ServiceManager进程中的,而如果想从ServiceManager中获取到对应的binder引用,前提是需要注册,而注册的行为是在对应的逻辑代码执行时才会去注册的。
流程上,是Init产生Zygote进程和ServiceManager进程,然后Zygote进程产生SystemServer进程。如果AMS想通过binder向Zygote发送信号,必须向ServiceManager获取Zygote的binder引用,而前提是Zygote进程中必须提前注册好才行。
而实际上,Init进程是先创建ServiceManager,后创建Zygote进程的。虽然Zygote更晚创建,但是并不能保证Zygote进程去注册binder的时候,ServiceManager已经初始化好了,因为两者属于两个进程,是并行的。
PS:我们可以通过进程ID来确定哪个进程更优先创建,如下,ServiceManager会更早创建。
system 311 1 11612 3204 binder_ioctl_write_read 0 S servicemanager
root 380 1 4014532 164032 poll_schedule_timeout 0 S zygote64
root 381 1 1457084 145624 poll_schedule_timeout 0 S zygote
当然有人说,等ServiceManager完全初始化好再去注册不就好了吗?这当然可以了,但是什么时候才能可以注册呢?这个时间点无法保证,如果想保证就必须通过另外一种跨进程通讯的方式来保证,这样设计不就变得复杂了吗?
还有人说,我Zygote启动后,延时10秒或者20秒再去向ServiceManager进程注册不就好了吗?这样做自然也是可以的,但是有没有考虑下,如果用户恰好在这期间点击了应用图标尝试去启动APP应该怎么办呢?岂不就是没有反应了吗?
所以,AMS无法获取到Zygote的binder引用,是原因之一。
Linux中,fork进程其实并不是完美的fork,linux设计之初只考虑到了主线程的fork,也就是说如果主进程中存在子线程,那么fork进程中,其子线程的锁状态,挂起状态等等都是不可恢复的,只有主进程的才可以恢复。
而binder作为典型的CS模式,其在Server是通过线程来实现的,Server等待请求状态时,必然是处于一种挂起的状态。所以如果使用binder机制,zygote进程fork子进程后,子进程的binder的Server永远处于一种挂起不可恢复的状态,这样的设计无疑是非常差的。
所以,zygote如果使用binder,会导致子进程中binder线程的挂起和锁状态不可恢复,这是原因之二。
我们都知道,Binder基于mmap机制,只会进行一次拷贝,所以效率是很高的。那么Socket就一定低吗?
其实答案也许会出乎你的意料。如果这个问题你问GPT,GPT会告诉你,localsocket的效率是高于binder的。其原因,是虽然binder只有一次拷贝,比socket的两次更少,但是拷贝次数只是一个很重要的原因,但并不是所有影响的因素。binder因为涉及到安全验证等等环节,所以实际上效率反而没有localsocket高。据说腾讯小程序跨进程通讯使用的就是LocalSocket的方式。
所以,LocalSocket效率其实并不低,这是原因之三。
普遍认为Socket是不安全的,因为它缺乏PID校验,所以导致任何进程都可以访问。但是LocalSocket也是不安全的吗?我们就实际去操作一下。
代码如下,我们尝试直接给zygote的进程发送socket消息,看zygote进程是否可以接受。如果可以接受的话,那么我们只要发送指定格式的字符串,APP也能启动其它应用进程了。相关代码是直接拷贝AMS中的。
val localSocket = LocalSocket()
val localSocketAddress =
LocalSocketAddress("zygote", LocalSocketAddress.Namespace.RESERVED)
localSocket.connect(localSocketAddress)
val outputStream = localSocket.outputStream
outputStream.write(1)
outputStream.flush()
实验下来果然不出所料,实验结果如下。所以LocalSocket也是有权限验证的。
connectLocal在native层的实现是socket_connect_local方法。
socket_connect_local(JNIEnv *env, jobject object,
jobject fileDescriptor, jstring name, jint namespaceId)
{
int ret;
int fd;
if (name == NULL) {
jniThrowNullPointerException(env, NULL);
return;
}
fd = jniGetFDFromFileDescriptor(env, fileDescriptor);
if (env->ExceptionCheck()) {
return;
}
ScopedUtfChars nameUtf8(env, name);
ret = socket_local_client_connect(
fd,
nameUtf8.c_str(),
namespaceId,
SOCK_STREAM);
if (ret < 0) {
jniThrowIOException(env, errno);
return;
}
}
所以最终的校验逻辑应该在Linux层的socket_local_client_connect方法,我这里没有Linux的源码,所以就不继续往下研究的。如果有知晓这方面知识的大佬,麻烦告之我一下。
所以,LocalSocket其实也有权限校验,并不意味着可以被所有进程随意调用,这是原因之四。
进程的fork,是拷贝一个和原进程一摸一样的进程,其中的各种内存对象自然也会被拷贝。所以用来接收消息去fork进程的binder对象自然也会被拷贝。但是这个拷贝对于APP层有用吗?那自然是没用的,所以就凭白多占用了一块无用的内存区域。
说到这你自然想问,如果通过socket的方式,不也平白无故的多占用一块Socket内存区域吗?是的,确实是,但是fork出APP进程之后,APP进程会去主动的关闭掉这个socket,从而释放这块区域。相关代码在ZygoteConnection的processCommand方法中:
try {
if (pid == 0) {
// in child
zygoteServer.setForkChild();
zygoteServer.closeServerSocket();
IoUtils.closeQuietly(serverPipeFd);
serverPipeFd = null;
return handleChildProc(parsedArgs, childPipeFd,
parsedArgs.mStartChildZygote);
} else {
// In the parent. A pid < 0 indicates a failure and will be handled in
// handleParentProc.
IoUtils.closeQuietly(childPipeFd);
childPipeFd = null;
handleParentProc(pid, serverPipeFd);
return null;
}
}
说到这肯定有人想问,那我释放掉Binder内存不就一样了吗?Binder的特殊性在于其是成对存在的,其分为Client端对象和Server端对象。假设我们使用binder,那么因为APP端的binder是拷贝自Zygote进程的,所以如果要释放掉APP的Server端binder引用对象,就必须释放掉AMS中的Client端binder对象,那这样就会导致AMS失去binder从而无法正常向Zygote发送消息。
所以,使用binder会造成额外的内存占用,这是原因之五。
综上所述,如果选择binder的话,会有各种各样的问题。
如果说非要使用binder可以吗?我认为是可以的,但是这样会让设计变得更复杂,而且出问题的可能性会更高。
所以,回答本文题目中的问题:为什么安卓选择使用socket而不是Binder?不是说使用binder绝对不行,而是通过各个层面的综合衡量,socket会比binder更为合适,所以安卓最终选择了socket的方式。架构设计,不正式寻求一种简单且安全的体系嘛。
由于写文本的时候,查询的文章过多,而且有的还是转载的,所以实在无法一一列举,所以这里说一声抱歉,有相关问题可以联系我,我会补上相关资料的原始链接来源。
本文使用到的相关资料来源:
@享学课堂Alvin老师
等
本文是结合网上的文章,以及作者自身的源码阅读和实际的实验得出来的结论,不能代表google官方的说法,相关答案仅供参考。
如果有想讨论的点,欢迎评论留言。