Android跨进程通信导论


/   今日科技快讯   /

近日,谷歌母公司Alphabet已就其处理高管性骚扰指控的一系列股东诉讼与原告达成和解,同意董事会加强监督,并承诺未来10年内在企业多元化项目上投入3.1亿美元。此外,员工将不再被强迫通过私人仲裁解决与Alphabet的纠纷,并限制谷歌对涉及这些案件的员工使用保密协议,同时禁止高管和下属之间的办公室恋情。

/   作者简介   /

大家周日好,虽然周末有点短,但是想到国庆长假即将来袭,心情瞬间变美了~

本篇文章来自西京一村夫的投稿,和大家分享了他对Android进程通信相关内容的理解,相信会对大家有所帮助!同时也感谢作者贡献的精彩文章!

西京一村夫的博客地址:

https://blog.csdn.net/yang1164851882

/   正文   /

应用层开发者想要在Android端更上一层,Android基础知识、启动过程、编译打包apk过程、安卓framework层等实现原理都是一定要掌握的,熟悉安卓四大组件的深层通信过程及原理和Android构建工具gradle的实现原理也都逐渐成为必要技能,这就是好比步入应用层中高级工程师的一道门,这道门对于应用层初学者来说却无比坚实。

本篇旨在为我们学习Android运行机制奠定基础,因为包括启动流程(了解之后可以做启动优化)、四大组件的交互、Hanlder都会关联到。

通过这些学习,你的应用可以实现最直观的启动优化。例如:很多应用后续使用过程,除了默认进程外会再启动一些服务类进程(有三方的类似推送,也有可能是我们自己创建的其他服务类进程),当应用从launch桌面启动(AS性能检测可以看到,或者自己debug),系统创建应用默认进程(与applicationId保持一致),然后在应用进程中做一些必要的初始化工作,大部分需要初始化的工作,都是默认进程所必须的,非默认进程只需要针对自身做必要的部分初始化工作即可。

也就是说,当我们做好所有的单进程(ApplicationId同名进程)初始化等工作,安全又快,也就实现了应用从最初的4-5s左右到后来的2-3s,纯原生鱼雷基本不到2s(自动登陆接口响应需要1s多),如果再配合一些其他的优化手段,画面太好。

你觉得提升2s没啥用?那是没见过做启动页开屏广告十几家广告一起来,至于显示我们不关心,但是你肯定是都要初始化的,要是代码比较任性,超10s那也常见(初始化必要工作太多的不在讨论范围),正常到3s这个差别还是很大的。本来只在默认进行做的工作,你每个进程都做;而且多进程和多线程一样,操作数据也要考虑到安全性。在正式开始Binder通信之前,我们先进行如下学习:

(1)对AIDL的操作进行简单介绍(也是我们平时使用的方式):

1.创建aidl文件MakeProject(C/S两端一样)

interface IMathAidl {
    double add(double a,double b);//加法
    double sub(double a,double b);//减法
    void play(String path);//路径信息
}

2.服务端创建Service,别忘记注册

public class MyService extends Service {`

    private IMathAidl.Stub mStub = new IMathAidl.Stub() {
        @Override
        public double add(double a, double b) throws RemoteException {
            return a + b;
        }

        @Override
        public double sub(double a, double b) throws RemoteException {
            return a - b;
        }

        @Override
        public void play(String path) throws RemoteException {
            Message message = Message.obtain();
            message.obj = path;
            mHandler.sendMessage(message);
        }
    };

    @Override
    public IBinder onBind(Intent intent) {
        return mStub;
    }

}

<service android:name=".MyService">
            <intent-filter>
                <action android:name="com.sinitek.aidl.service"/>
                <category android:name="android.intent.category.DEFAULT"/>
            </intent-filter>
</service>

3.Client端进行关联调用Server端

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Intent intent = new Intent("com.sinitek.aidl.service");
        intent.setPackage("com.sinitek.transactionserver");//minSdkVersion 21
        bindService(intent, mConn, Context.BIND_AUTO_CREATE);
    }
    private ServiceConnection mConn = new ServiceConnection() {
        @Override
        public void onServiceDisconnected(ComponentName name) {
        }
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            IMathAidl mathAidl = IMathAidl.Stub.asInterface(service);
            try {
                double result = mathAidl.add(1, 1);
                Toast.makeText(MainActivity.this, "计算结果为:" + result, Toast.LENGTH_SHORT).show();
                mathAidl.play("请观看服务端播放:'西京一村夫'SINITEK冲刺之路.mp4");
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }
    };
}

以上代码放在:

https://github.com/BuilderPattern/AidlTransactionServer.git

https://github.com/BuilderPattern/AidlTransactionClient.git

(2)如果你是一个比较有求知欲的键盘侠,你通过一顿操作在IMathAidl.java中就会发现,该AIDL会生成java类,实际的交互都这里完成。问题来了,是不是可以不用定义aidl接口,直接采用IMathAidl.java类中的方式去实现呢?答案是:肯定可以!因为,本质上就是aidl通过构建生成对应的IMathAidl.java文件来实现具体操作,如下:

1.Server端创建一个Service并注册:

public class NoAidlService extends Service {

    public IBinder onBind(Intent t) {
        return mBinder;
    }

    private NormalBinder mBinder = new NormalBinder();

    private class NormalBinder extends Binder {
        @Override
        protected boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException {//0加,1乘
            switch (code) {
                case 0: {
                    data.enforceInterface("NoAidlService");//检测标识
                    int _arg0 = data.readInt();
                    int _arg1 = data.readInt();
                    int _result = _arg0 + _arg1;
                    reply.writeNoException();
                    reply.writeInt(_result);
                    return true;
                }
                case 1: {
                    data.enforceInterface("NoAidlService");
                    int _arg0 = data.readInt();
                    int _arg1 = data.readInt();
                    int _result = _arg0 * _arg1;
                    reply.writeNoException();
                    reply.writeInt(_result);
                    return true;
                }
            }
            return super.onTransact(code, data, reply, flags);
        }
    }

}

<service android:name=".NoAidlService">
            <intent-filter>
                <action android:name="com.sinitek.noaidl.myservice" />
                <category android:name="android.intent.category.DEFAULT" />
            </intent-filter>
</service>

2.Client端连接Server端,发送收据/等待回复:

public class MainActivity extends AppCompatActivity {

    IBinder mBinder;
    ServiceConnection mServiceConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            mBinder = service;
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {

        }
    };

    TextView mTextView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mTextView = findViewById(R.id.activity_main_operate_tv);
        Intent intent = new Intent("com.sinitek.noaidl.myservice");
        intent.setPackage("com.sinitek.transactionservernoaidl");
        bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE);
        initEvent();
    }

    private void initEvent() {
        mTextView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mRcvSnd();
            }
        });
    }

    public void mRcvSnd() {

        if (mBinder == null) {
            return;
        }
        Parcel _data = Parcel.obtain();
        Parcel _reply = Parcel.obtain();
        int _code = (int) (Math.random() * 6) % 2;
        int _result;
        try {
            _data.writeInterfaceToken("NoAidlService");//客户端标识
            _data.writeInt(6);
            _data.writeInt(6);
            mBinder.transact(_code, _data, _reply, 0);
            _reply.readException();
            _result = _reply.readInt();
            Toast.makeText(MainActivity.this, "收到回复:" + _result, Toast.LENGTH_SHORT).show();

        } catch (RemoteException e) {
            e.printStackTrace();
        } finally {
            _reply.recycle();
            _data.recycle();
        }
    }

}

有关该交互过程比较容易理解,依次:创建Server》Client连接Service》Client向Service发送数据》Service收到Client数据进行一系列分析操作,做出回应》Client拿到Service的响应结果,如下图所示:

下面,我们正式开始对Binder通信机制和过程涉及的相关内容进行阐述(如本文有理解误区,万望读者不吝赐教):

(一)进程间通信有多种方式,这里主要介绍Android进程间通信Binder特点及实现过程:

在 Android 中,一个进程通常无法访问另一个进程的内存。为了进行通信,需将其对象分解成可供操作系统理解的原语,并将其编组为可供开发人员操作的对象。如果开发人员直接编写执行该操作的代码较为繁琐,因此Android引入AIDL实现进程通信。

aidl(Android Interface Definition Language)接口定义语言,可以定义客户端与服务均认可的编程接口,实现进程通信。学习Binder前,强烈建议先了解aidl的实现过程,通过学习aidl的工作过程,我们会对进程通信的流程有大致的概念,有利于底层执行的命令、调用接口和执行上层写入/读取相对应的理解,后面学习底层的过程就好比验证每个过程的更深层实现。

比如:C/S端通信transact/onTransact方法内实现数据交互,消息源筛选、数据类型及数据的读写,特点一目了然;鉴于篇幅,有关aidl的实现过程请自行学习,尽量熟练掌握(可以学习Android开发艺术探索和张鸿洋的博客)。

特点:安全、传输性能高和操作相对简单;安全性,对比socket/管道通信方式,Binder通信过程中,系统每创建一个进程会分配对应的PID和UID(此特点还可以用于多进程项目的某些方向区分优化),进程之间通过进程id匹配确认消息来源;传输性能高,传输数据不需要tcp协议的三次握手,传输过程只需一次拷贝,操作简单;共享内存虽然不需要拷贝,但实现起来要复杂很多。

(二)了解下内存映射函数mmap和读写数据接口ioctl:

mmap用于建立用户空间和内核空间的映射关系,实现映射关系内存空间共享数据;建立映射关系的不同内存空间的数据操作,会映射到建立关系的其他内存空间,对应关系如下图:

ioctl是底层和应用层之间定义的接口协议(用于操作写入/读取数据),不提供单独的read/write接口,一次调用实现写入读取数据,满足数据交互同步。假设需要进行写入/读取数据(write_size和read_size都大于0),在操作写入和读取的过程,先将write_buffer里面的数据写入Binder;然后同步等待数据返回,从Binder中读取数据存入read_buffer。因为BINDER_WRITE_READ命令包含写入和读取数据两步操作,且是否进行写入和读取操作都是根据Binder的write_size和read_size确定的。假如只写入,设置Binder的write_size大于0,read_size等于0即可;其他情况同理。

(三)接下来对进程通信C/S端采用Binder通信过程的write/read及数据传递进行介绍:

在aidl操作的过程,有单独的write和read方法,在底层命令是一次执行,写入/读取根据顺序判断是否需要执行(write_size/read_size)对应操作。比如Server/Client端执行write/read完成后,进入等待接收数据状态,接收到数据之后做相应操作并回应reply。

Binder写入/读取的数据结构体是binder_transaction_data,该数据体包含了上述,比如:进程id、用户id用于接收方确认发送方,data_size缓存区存放的数据长度(由发送方设置,用于接收方确定接收到的数据大小)。

系统为每个应用分配一定的内存(虽然应用可以自行配置,但不能超过系统可分配的最大值),且每次处理完之后会执行BUFFER_FREE命令释放内存。上面我们提到mmap()映射函数,在Binder通信的写入/读取过程中,采用的是接收数据缓存的动态分配和释放。程序处理完某缓存区数据之后,底层会调用命令释放缓存区,否则会导致缓存区耗尽而无法接收数据。

关于Binder的C/S端通信,总结性描述:Client端将函数参数打包,在transact()中通过服务端Binder向Server发送数据包请求/等待数据返回,Server端在onTransact()中通过客户端Binder获取binder_transaction_data数据,取出进程id及其他数据做相应处理,应答Client端。

很多讲解这块儿内容的时候,都提到用对方Client/Server的Binder进行数据操作,实现数据交互。在底层本质上是,进程通过ServiceManager(简称SMgr下面会详细解释)注册,用的时候直接在注册表查找,就可以实现两端都持有彼此的引用(可以暂时理解为对象的引用遍布各处),由于应用层实现不需要修改底层代码,所以这也是便于理解的一种说法。

在Client和Server通信的过程,SMgr类似于域名服务器,当客户端向服务端发起请求的时候,通过SMgr找到初始进程对应Binder引用(类比在域名服务器中找到域名对应的主机)进行写入/读取;SMgr进程的转化及其Binder创建,如下图:

 

通信主要由四部分组成:Server、Client、SMgr和Binder驱动;前两个是需要实现通信的两端,驱动负责把Server端Binder相关信息通过数据包形式发送到SMgr,SMgr收到数据包之后解析数据注册Server端的Binder引用,后续SMgr还会负责在Binder的登记表里查找每个进程对应的Binder引用。

大致过程为:Server端创建Binder实体之后,通过驱动以transaction_data数据包的形式发送到SMgr,通过SMgr将对应引用注册到内核Binder注册表。以上发送及注册引用的具体过程为:Binder实体创建之后,驱动会在内核创建其对应的实体节点和用于传递给SMgr的Binder引用,然后把实体信息和刚刚创建的Binder内核引用等信息传递给SMgr,SMgr进程解析数据将引用插入Binder注册表,Client端就可以在表中查找该引用执行write_buffer/read_buffer等操作;其他进程发送数据,SMgr接收数据并执行注册过程,如下图:

 

实际上要完成上面的过程,首先Server和SMgr也涉及到进程间通信,SMgr可以理解成系统默认进程,其他进程和SMgr通信的时候,SMgr都是作为Server端;那么SMgr和Server端通信时,SMgr中的Binder引用到底是谁传过来的,都要它注册,那他自己的Binder谁注册?

比方SMgr进程是项目经理,其他进程是项目组织成员,项目经理给每个人的权力(Binder)去完成工作,那么项目经理分配权限,项目经理的权力哪里来?肯定需要管理层赋予他职责。接下来解释下SMgr的Binder引用哪里来,当某个进程执行BINDER_SET_CONTEXT_MGR命令将自身注册成SMgr进程时,驱动会为该进程创建好Binder实体(0号引用),并且其他所有进程都能获取到该引用,系统只能有一个SMgr进程。

后续任何进程向SMgr注册Binder时,都要通过该引用(SMgr的Binder实体引用)和SMgr进程进行通信。进程注册及进程间通信整个过程,大致理解为SMgr作为主导注册/查找对应Binder对象进行写入/读取,如下图:

所以,进程间通信就可以分为:SMgr和其他进程,Server、Client和SMgr两种。本质上都是用对应进程的Binder对象实现写入/读取等操作。区别在于进程对应的Binder对象:前者系统注册的时候就会定为0号引用;后者需要SMgr(Binder对象管理中心)实现注册,后续在注册表中根据定义的key取出对应进程的Binder引用执行操作。

本文参考资料:

https://blog.csdn.net/augfun/article/details/82343249

https://baike.baidu.com/item/mmap/1322217?fr=aladdin

https://baike.baidu.com/item/ioctl/6392403?fr=aladdin

https://developer.android.google.cn/guide/components/aidl.html?hl=zh-cn

http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2015/0319/2619.html

https://www.zhihu.com/question/20122137/answer/14049112

推荐阅读:

我的新书,《第一行代码 第3版》已出版!

妖媚的刻度尺控件,隔壁产品都馋哭了

使用MD风格,让你的项目更好看

欢迎关注我的公众号

学习技术或投稿

长按上图,识别图中二维码即可关注

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值