Android中实现IPC的几种方式详细分析及比较

1.使用Bundle ----> 用于android四大组件间的进程间通信

android的四大组件都可使用Bundle传递数据,所以如果要实现四大组件间的进程间通信,完全可以使用Bundle来实现简单方便 。

2.使用文件共享 ---->用于单线程读写

这种方式在单线程读写的时候比较好用 如果有多个线程并发读写的话需要限制线程的同步读写 另外 SharePreference是个特例 它底层基于xml实现 但是系统对它的读写会基于缓存,也就是说再多进程模式下就变得不那么可靠了,有很大几率丢失数据

3.使用Messenger ---->用于可存放在message中的数据的传递

使用这个方式可以在不同进程间传递message对象 这是一种轻量级的IPC方案 当传递的对象可以放入message中时 可以考虑用这种方式 但是msg.object最好不要放因为不一定可以序列化 使用它的步骤如下:假设这样一个需求 需要在客户端A发送消息给服务端B接受 然后服务端B再回复给客户端A

  • 首先是客户端A发送消息给服务端B 所以在客户端A中 声明一个Handler用来接受消息 并创建一个Messenger对象 用Handler作为参数构造 然后onBinder方法返回messenger.getBinder() 即可

  • 在客户端A自然是需要发送消息给服务端B的 所以需要在服务绑定完成之后 获取到binder对象 之后用该对象构造一个Messenger对象 然后用messenger发送消息给服务端即可

  • 由于在服务端接收到了客户端的消息还需要回复 所以在服务端代码中获取 msg中的replyTo对象 用这个对象发送消息给 客户端即可 在客户端需要创建一个handler和Messenger 将发送的msg.replyTo设置成Messenger对象

4.AIDL android 接口定义语言 ---->主要用于调用远程服务的方法的情况 还可以注册接口

使用方法很简单,在服务端定义aidl文件 自动生成java文件,然后在service中实现这个aidl,在onbind中返回这个对象,在客户端把服务端的aidl文件完全复制过来,包名必须完全一致,在onServiceConnected方法 中 把Ibinder对象用asInterface方法转化成 aidl对象然后调用方法即可。

需要注意的地方:

在aidl文件中并不是支持所有类型 仅支持如下6种类型:

  • 基本数据类型---- int long char boolean double String charSequence
  • List 只支持ArrayList,CopyOnWriteArrayList也可以,里面元素也必须被aidl支持。
  • Map 只支持HashMap,ConCurrentHashMap也可以,里面元素也必须支持aidl。
  • Parcelable 所有实现了此接口的对象。
  • AIDL 所有的AIDL接口,因此,如果需要使用接口,必须使用AIDL接口
  • 其中自定义的类型和AIDL对象必须显示import进来,不管是不是在一个包中。
  • 如果AIDL文件中用到了自定义的Parcelable对象,必须创建同名的AIDL文件,并声明为Parcelable类型。
  • AIDL文件中除了基本数据类型外,其他类型必须标上方向(in、out、inout)
  • AIDL接口中只支持方法 不支持声明静态常量。
  • 在使用aidl时,最好把所有aidl文件都放在一个包中,这样方便复制到客户端,其实所有的跨进程对象传递都是对象的序列化与反序列化,所以必须包名一致。
场景用例

现在加入有这样一个需求 如果服务端是 图书馆添加和查看书的任务 客户端可以查看和添加书 这时候需要添加一个功能 当服务端每添加了一本书 需要通知客户端注册用户 有一本新书上架了 这个功能如何实现?想想可知 这是一个观察者模式 如果在同一进程中很容易实现,只需要在服务端中的代码中维护一个集合 里面放的是注册监听的用户 然后用户需要实现一个新书到来的回调接口当有新书上架时 遍历这个集合 调用每个注册者的接口方法 即可实现 现在我们是跨进程通信 所以自然不能如此简单了 但也不是很复杂 想一想 其实就是把以往的接口定义 变成了aidl接口定义 然后其他的一样即可 但是这样还是存在一个问题 如果注册了listener 我们又想解除注册 是不是在客户端传入listener对象 在服务端把它移除就可以呢? 其实是不可以的 因为这是跨进程的 所以对象并不是真正的传递 只是在另一个进程中重新创建了一个一样的对象 内存地址不同 所以根本不是同一个对象所以是不可以的 如果要解决这个问题 需要使用RemoteCallbackList 类 不要使用CopyWriteArrayList 在RemoteCallBackList中封装了一个Map 专门用来保存所有的AIDL回调 key为IBinder value是CallBack 使用IBinder 来区别不同的对象 ,因为跨进程传输时会产生很多个不同的对象 但这些对象的底层的Binder都是同一个对象 所以可以 在使用RemoteCallBackList时 add 变为 register remove 变为 unregister 遍历的时候需要先 beginBroadcast 这个方法同时也获取集合大小 获取集合中对象使用 getBoardCastItem(i) 最后不要忘记finishBoardCast方法。

还有一个情况 由于onServiceConnected方法 是在主线程执行的 如果在这里执行服务端的耗时代码 会ANR 所以需要开启一个子线程执行 同理在服务端中 也不可以运行客户端的耗时程序 总结起来就是 在执行其他进程的耗时程序时 都需要开启另外的线程防止阻塞UI线程 如果要访问UI相关的东西 使用handler

为了程序的健壮性 有时候Binder可能意外死亡 这时候需要重连服务 有2种方法:

  • 1.在onServiceDisconnected方法中,重连服务。
  • 2.给Binder注册DeathRecipient监听,当binder死亡时,我们可以收到回调 这时候我们可以重连远程服务。

最后有时候我们不想所有的程序都可以访问我们的远程服务 所以可以给服务设置权限和过滤:

第一种方法:在onBind中进行验证(permission验证)

首先在AndroidMenifest中声明所需权限

<permission android:name="com.example.test1.permission.ACCESS_BOOK_SERVICE"android:protectionLevel="normal" />
<uses-permission android:name="com.example.test1.permission.ACCESS_BOOK_SERVICE" />

验证

@Override
    public IBinder onBind(Intent intent) {
        int check = checkCallingOrSelfPermission("com.ryg.chapter_2.permission.ACCESS_BOOK_SERVICE");
        Log.d(TAG, "onbind check=" + check);
        if (check == PackageManager.PERMISSION_DENIED) {
            return null;
        }
        return mBinder;
    }
第二种方法:在onTransact中进行验证(包名验证)
public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws RemoteException {
            // 权限验证
            int check = checkCallingOrSelfPermission("com.example.test1.permission.ACCESS_BOOK_SERVICE");
            L.d("check:"+check);
            if(check==PackageManager.PERMISSION_DENIED){
                L.d("Binder 权限验证失败");
                return false;
            }
            // 包名验证
            String packageName=null;
            String[] packages = getPackageManager().getPackagesForUid(getCallingUid());
            if(packages!=null && packages.length>0){
                packageName = packages[0];
            }
            if(!packageName.startsWith("com.example")){
                L.d("包名验证失败");
                return false;

            }
            return super.onTransact(code, data, reply, flags);
        };

5.ContentProvider方式 实现对另一个应用进程开放provider数据的查询

此方法使用起来也比较简单 底层是对Binder的封装 使之可以实现进程间通信,使用方法如下:

  1. 在需要共享数据的应用进程中建立一个ContentProvider类,重写它的CRUD 和getType方法,在这几个方法中调用对本应用进程数据的调用,然后在AndroidMinifest.xml文件中声明provider
 <provider 
        android:authorities="com.yangsheng.book"  //这个是用来标识provider的唯一标识  路径uri也是这个
        android:name=".BookProdiver"
        android:process=":remote_provider"/>   //此句为了创建多进程  正常不需要使用
  1. 在需要获取共享数据的应用进程中调用getContentResolver().crud方法 即可实现数据的查询

需要注意的问题:
1.关于 sqlite crud的各个参数的意义,query函数,参数
Cursor query(boolean distinct, String table, String[] columns,String selection, String[] selectionArgs, String groupBy,String having, String orderBy, String limit)
第一个参数 distinct 英语单词意思,独特的,如果true 那么返回的数据都是唯一的,意思就是实现查询数据的去重。
第二个参数 table,表名
第三个参数 columns,要查询的行的名字数组 例如 new String[]{"id","name","sex"}
第四个参数 selection 选择语句 sql语句中where后面的语句 值用?代替 例如 "id=? and sex=?"
第五个参数 selectionArgs 对应第四个参数的 ? 例如 new String[]{"1","男"}
第六个参数 groupBy 用于分组
第七个参数 having 筛选分组后的数据
第八个参数 orderby 用于排序 desc/asc 升序和降序 例如 id desc / id asc
最后一个参数 limit 用于限制查询的数据的个数 默认不限制其他几个函数 根据query函数的参数猜想即可。

2.由于每次ipc操作 都是靠uri来区别 想要获取的数据位置 所以provider在调取数据的时候根据uri并不知道要查询的数据是在哪个位置,所以我们可以通过 UriMatcher 这个类来给每个uri标上号 根据编号 对应适当的位置 例如:

public static final int BOOK_CODE = 0;
            public static final int USER_CODE = 1;
            public static UriMatcher matcher = new UriMatcher(UriMatcher.NO_MATCH);

            static {
                matcher.addURI("book uri", "book", BOOK_CODE);
                matcher.addURI("user uri", "user", USER_CODE);
            }
            这样我们可以通过 下面这个样子来获取位置(此处是表名 其他类型也一样)
            private String getTableName(Uri uri) {
                switch (matcher.match(uri)) {
                    case BOOK_CODE:
                        return "bookTable";
                    case USER_CODE:
                        return "userTable";
                }
                return "";
            }

3.另外ContentProvider除了crud四个方法外,还支持自定义调用 通过ContentProvider 和ContentResolver的 call方法 来实现

6.Socket方法实现Ipc 这种方式也可以实现 但是不常用

需要权限:
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
这种方式需要一个服务端socket 和一个客户端socket 建立连接后 通过流循环获取消息即可。
1.在服务端开启一个serverSocket 不断获取客户端连接 注意要在子线程中开启

ServerSocket serverSocket = new ServerSocket(8688);
        while(isActive) { //表示服务生存着
                try {
                    final Socket client = serverSocket.accept();  //不断获取客户端连接
                    System.out.println("---服务端已获取客户端连接");
                    new Thread(){
                        @Override
                        public void run() {
                            try {
                                dealWithMessageFromClient(client);  //处理客户端的消息 就是开启一个线程循环获取out 和 in  流 进行通信
                            } catch (IOException e) {
                                e.printStackTrace();
                            }
                        }
                    }.start();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

2.在客户端开启一个线程 使用ip和端口号连接服务端socket 连接成功后 一样 开启子线程 循环获取消息 处理

Socket socket = null;
                while(socket==null){  //失败重连
                    try {
                        socket = new Socket("localhost",8688);
                        out = new PrintWriter(socket.getOutputStream(),true);
                        handler.sendEmptyMessage(1);
                        final Socket finalSocket = socket;
                        new Thread(){
                            @Override
                            public void run() {
                                try {
                                    reader = new BufferedReader(new InputStreamReader(finalSocket.getInputStream()));
                                } catch (IOException e) {
                                    e.printStackTrace();
                                }
                                while(!MainActivity.this.isFinishing()){  //循环获取消息  这里必须用 循环 否则 只能获取一条消息 服务端也一样
                                    try {
                                        String msg = reader.readLine();
                                        System.out.println("---"+msg);
                                        if (msg!=null){
                                            handler.sendMessage(handler.obtainMessage(2,msg));
                                        }
                                    } catch (IOException e) {
                                        e.printStackTrace();
                                    }
                                }
                            }
                        }.start();
                    } catch (IOException e) {
                        SystemClock.sleep(1000);
                        e.printStackTrace();
                    }
                }

7.Binder 连接池的使用 很好用

有一种情况,假如有多个业务模块需要通过AIDL进程间通信,如果按照之前AIDL的实现方式,我们就需要创建对应的多个Service。显然这样是不可取的,不仅耗费系统资源,而且让应用看上去很重量级。我们可以通过Binder连接池的方法解决以上问题。
实现步骤:

  1. 首先,为每个业务模块创建AIDL接口并实现此接口及其业务方法。
  2. 创建IBinderPool的AIDL接口,定义IBinder queryBinder(int BinderCode)方法。外部通过调用此方法传入对应的code值来获取对应的Binder对象。
  3. 创建BinderPoolService,通过new BinderPool.BinderPoolImpl实例化Binder对象,通过onBind方法返回出去。
  4. 创建BinderPool类,单例模式,在构造方法中绑定Service,在onServiceConnected方法获取到BinderPoolImpl对象,这个BinderPoolImpl类是BinderPool的内部类,并实现了IBinderPool的业务方法。BinderPool类中向外暴露了queryBinder方法,这个方法其实调用的是BinderPoolImpl对象的queryBinder方法。

代码连接:https://github.com/huivs12/IPCDemo2BinderPool.git

最后 总结了这么多IPC通信方式 那我们该如何选择合适的IPC方式呢 针对这几种IPC通信方式分析一下优缺点
1.bundle :简单易用 但是只能传输Bundle支持的对象 常用于四大组件间进程间通信
2.文件共享:简单易用 但不适合在高并发的情况下 并且读取文件需要时间 不能即时通信 常用于并发程度不高 并且实时性要求不高的情况
3.AIDL :功能强大 支持一对多并发通信 支持即时通信 但是使用起来比其他的复杂 需要处理好多线程的同步问题 常用于一对多通信 且有RPC 需求的场合(服务端和客户端通信)
4.Messenger :功能一般 支持一对多串行通信 支持实时通信 但是不能很好处理高并发情况 只能传输Bundle支持的类型 常用于低并发的无RPC需求一对多的场合
5.ContentProvider :在数据源访问方面功能强大 支持一对多并发操作 可扩展call方法 可以理解为约束版的AIDL 提供CRUD操作和自定义函数 常用于一对多的数据共享场合
6.Socket :功能强大 可以通过网络传输字节流 支持一对多并发操作 但是实现起来比较麻烦 不支持直接的RPC 常用于网络数据交换。

总结起来
当仅仅是跨进程的四大组件间的传递数据时 使用Bundle就可以 简单方便 当要共享一个应用程序的内部数据的时候 使用ContentProvider实现比较方便 当并发程度不高 也就是偶尔访问一次那种 进程间通信 用Messenger就可以 当设计网络数据的共享时 使用socket 当需求比较复杂 高并发 并且还要求实时通信 而且有RPC需求时 就得使用AIDL了 文件共享的方法用于一些缓存共享 之类的功能

最后附上自制的一个ipc通讯的demo,实现了AIDL和Messenger方式。
地址:http://git.oschina.net/elensliu/IPC_demo

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,219评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,363评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,933评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,020评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,400评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,640评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,896评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,597评论 0 199
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,327评论 1 244
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,581评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,072评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,399评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,054评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,083评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,849评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,672评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,585评论 2 270

推荐阅读更多精彩内容