无标题文章

Android IPC

IPC简介

IPC即Inter-Process Communication(进程间通信)。指的是两个进程间进行数据交换的过程。Android间进程通信的方式并不完全继承于Linux,它有自己独特的一套进程间通信的方式。Android中实现IPC中最有特色的是Binder,通过Binder可以轻松地实现IPC。另外,Android还支持Socket,不仅可以实现一个设备上的两个进程间通信,还可以实现两个终端之间的通信。

Android中的多进程模式

开启多进程模式

正常情况下,Android中的多进程是指一个应用中存在多进程的情况,因此,此处暂不讨论两个应用之间的多进程情况。
  在Android中使用多进程只有一个方法,在manifest文件中给四大组件制定android:process属性,另外还有一种非常规的方法,在JNI在natice层中fork一个新的进程,暂不讨论。

<activity android:name=".MainActivity">
    <intent-filter>
        <action android:name="android.intent.action.MAIN"/>

        <category android:name="android.intent.category.LAUNCHER"/>
    </intent-filter>
</activity>
<activity android:name=".SecondActivity" android:process=":remote">
</activity>
<activity android:name=".ThirdActivity"  android:process="com.daijie.mulitprocesstest.remote">
</activity>

上述代码中,分别为SecondActivity和ThirdActivity指定了process属性,这意味着在启动这两个Acitvity的时候,系统将为它创建一个单独的进程,进程名分别为"com.daijie.mulitprocesstest:remote"和"com.daijie.mulitprocesstest.remote"。而MainActivity没有指定process属性,则运行在默认的进程中,默认的进程名是包名。
  可以看到,在process中“:”开头的含义则是在当前进程名前面附上当前的包名,而另外一种不包含“:”的则是一种完整的命名方式。其次,以“:”开头的进程属于当前应用的私有进程,不可以与其他应用的组件跑在同一个进程中,而不以“:”开头的进程则属于全局进程,其他应用可以通过ShareUID的方式和它跑在同一个进程中
   Android系统会为每一个应用分配一个唯一的UID,具有相同UID的应用才能共享数据。两个应用间通过ShareID跑在同一个进程是有要求的,不仅需要ShareID相同,还需要签名相同才可以。这种情况下,不管它们是否运行在同一进程中,它们可以互相访问对方的私有数据(比如data目录和组件信息等),而如果跑在同一进程中,则还可以共享内存数据,就像一个应用的两个部分。
  以上的方法看起来很简单就开启了多进程,但是多进程远远没那么简单,还有许多事情要进行处理。

多进程的运行机制

开启多进程模式后,会出现各种奇怪的现象,比如以下的全局静态变量。

public class UserManager {
    public static int sUserId = 1;
}

在MainActivity中重新给sUserId赋值为2,然后在运行在另一个进程中的SecondActivity打印sUserId的值,发现还是原来的1。
  出现这种情况的原因是,SecondActivity运行在另一个进程中。因为Android中为每一个进程分配了一个独立的虚拟机,而不同的虚拟机在内存分配上有着不同的地址空间,这就导致了在不同虚拟机上访问同一个类的对象可能产生多份副本。而MainActivityg和SecondActivity由于运行在不同进程上,所以所持有的UserManaer并非同一个对象,修改后只会影响当前进程中的对象。
  一般情况下,使用多进程可能产生以下的问题。

  1. 静态成员和单例模式失效
  2. 线程同步机制完全失效
  3. SharePreferences的可靠性下降
  4. Application会多次创建

第1个问题和第2个问题是显而易见的,毕竟都不在同一块内存了,无论是锁对象还是全局类都无法保证线程同步,因为不同的进程锁的都不是同一个对象。第3个问题是因为SharePreferences不支持两个进程同时进行写操作,否则会造成一定几率的数据缺失,因为其底层是通过读/写xml文件实现的,所以并发读/写都可能出现问题。
  第4个问题是因为当一个组件跑在一个新的进程中,系统在创建新的进程的同时要分配独立的虚拟机,而这个过程其实就是一个启动一个应用的过程,相当于把这个应用又重新启动的一遍,既然都重新启动了,那么自然是要创建新的Application对象。所以可以这么理解,运行在同一个进程的组件是属于同一个虚拟机和同一个Application的,换言之,运行在不同进程的组件是属于两个不同的虚拟机和Application的。

IPC基础概念

IPC中,主要包含三方面的内容,Serializable,Paercelable,Binder。其中数据的传输需要先通过序列化,而Android上进行序列化主要依赖Serializable接口和Paercelable接口,序列化后是通过Intent和Binder进行数据的传输。

Serializable接口

Serializable是Java提供的一个序列化的空接口,为对象提供了标准的序列化和反序列化操作。可以通过声明一个静态int类型的serialVersionID的值来保证序列化和反序列化的类serialVersionID相同而被正确的反序列化。

Parcelable接口

Parcelable是Android提供的一个序列化接口,需要对其提供的方法进行实现。Android为我们提供了许多实现Parcelable接口的类,比如Intent,Bundle,Bitmap等,而List和Map也可以序列化,前提是子元素也必须都是可序列化的。

Serializable和Parcelable

Serializable虽然简单但是要进行大量的I/O操作,开销很大,而Parcelable是Android中的序列化方式,更适合用在Android平台上。而通过Parcelable主要用于在内存序列化话上,虽然序列化后通过网络传输也是可以的,但是会稍显复杂,这种情况还是首选使用Serializable。

Binder

Binder是Android中的一个类,它实现了IBinder接口。从IPC的角度说,Binder是Android中的一种跨进程传输的方式,其中,Binder还可以理解为一种虚拟的物理设备,它的设备驱动是/dev/binder。
  从Android Framework的角度来说,Binder是ServiceManager连接各种Manager(Acitivty Manager、Window Manager,等等和相应ManagerService的桥梁。
  从Android应用层的角度来说,Binder是客户端和服务端进行通信的媒介,当bindService的时候,服务端会返回一个包含了服务端业务调用的Binder对象,通过这个Binder对象,客户端就可以获取服务端提供的服务或者数据,这里的数据包括普通服务和基于AIDL的服务。
  Android开发中,Binder主要用在Service中,其中包含AIDL和Messenger。其中普通Service的Binder不涉及到进程间通信,,所以较为简单,无法触及到Binder的核心,而Messenger的底层其实也属于AIDL,所以可以使用AIDL来分析Binder的工作机制。

通过基于AIDL的服务分析Binder

我们需要新建一个AIDL实例,而SDK会为我们自动产生出AIDL所对应的Binger类,然后我们就可以分析Binder的工作过程。先新建三个文件Book.java,Book.aidl,IBookManager.aidl。

Book.java
public class Book implements Parcelable {
    private int bookId;
    private String bookName;
    // setter and getter
    // parcelable的实现
}
Book.aidl
package com.daijie.aldlapp;

parcelable Book;
IBookManager.aidl
package com.daijie.aldlapp;

// Declare any non-default types here with import statements

import com.daijie.aldlapp.Book;

interface IBookManager {

    List<Book> getBookList();

    void addBook(in Book book);
}

以上代码中,Book.java是一个表示图书信息的类,它实现了Parcelable接口。Book.aidl是Book类在AIDL中的声明。IBookManager.aidl是我们定义的一个接口,里面有两个方法getBookList和addBook,分别用于远程获取书籍列表和添加一本书。
  注意,在aidl中,尽管Book.aidl和IBookManager.aidl都在同一个包中,但是IBookManager仍需要导入Book类。
  编写完上述代码后,在Android Studio执行make project,就会生成AIDL对应的java文件,代码如下。

package com.daijie.aldlapp;

public interface IBookManager extends android.os.IInterface {
    /**
     * Local-side IPC implementation stub class.
     */
    public static abstract class Stub extends android.os.Binder implements com.daijie.aldlapp.IBookManager {
        private static final java.lang.String DESCRIPTOR = "com.daijie.aldlapp.IBookManager";

        /**
         * Construct the stub at attach it to the interface.
         */
        public Stub() {
            this.attachInterface(this, DESCRIPTOR);
        }

        /**
         * Cast an IBinder object into an com.daijie.aldlapp.IBookManager interface,
         * generating a proxy if needed.
         */
        public static com.daijie.aldlapp.IBookManager asInterface(android.os.IBinder obj) {
            if ((obj == null)) {
                return null;
            }
            android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
            if (((iin != null) && (iin instanceof com.daijie.aldlapp.IBookManager))) {
                return ((com.daijie.aldlapp.IBookManager) iin);
            }
            return new com.daijie.aldlapp.IBookManager.Stub.Proxy(obj);
        }

        @Override
        public android.os.IBinder asBinder() {
            return this;
        }

        @Override
        public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException {
            switch (code) {
                case INTERFACE_TRANSACTION: {
                    reply.writeString(DESCRIPTOR);
                    return true;
                }
                case TRANSACTION_getBookList: {
                    data.enforceInterface(DESCRIPTOR);
                    java.util.List<com.daijie.aldlapp.Book> _result = this.getBookList();
                    reply.writeNoException();
                    reply.writeTypedList(_result);
                    return true;
                }
                case TRANSACTION_addBook: {
                    data.enforceInterface(DESCRIPTOR);
                    com.daijie.aldlapp.Book _arg0;
                    if ((0 != data.readInt())) {
                        _arg0 = com.daijie.aldlapp.Book.CREATOR.createFromParcel(data);
                    } else {
                        _arg0 = null;
                    }
                    this.addBook(_arg0);
                    reply.writeNoException();
                    return true;
                }
            }
            return super.onTransact(code, data, reply, flags);
        }

        private static class Proxy implements com.daijie.aldlapp.IBookManager {
            private android.os.IBinder mRemote;

            Proxy(android.os.IBinder remote) {
                mRemote = remote;
            }

            @Override
            public android.os.IBinder asBinder() {
                return mRemote;
            }

            public java.lang.String getInterfaceDescriptor() {
                return DESCRIPTOR;
            }

            @Override
            public java.util.List<com.daijie.aldlapp.Book> getBookList() throws android.os.RemoteException {
                android.os.Parcel _data = android.os.Parcel.obtain();
                android.os.Parcel _reply = android.os.Parcel.obtain();
                java.util.List<com.daijie.aldlapp.Book> _result;
                try {
                    _data.writeInterfaceToken(DESCRIPTOR);
                    mRemote.transact(Stub.TRANSACTION_getBookList, _data, _reply, 0);
                    _reply.readException();
                    _result = _reply.createTypedArrayList(com.daijie.aldlapp.Book.CREATOR);
                } finally {
                    _reply.recycle();
                    _data.recycle();
                }
                return _result;
            }

            @Override
            public void addBook(com.daijie.aldlapp.Book book) throws android.os.RemoteException {
                android.os.Parcel _data = android.os.Parcel.obtain();
                android.os.Parcel _reply = android.os.Parcel.obtain();
                try {
                    _data.writeInterfaceToken(DESCRIPTOR);
                    if ((book != null)) {
                        _data.writeInt(1);
                        book.writeToParcel(_data, 0);
                    } else {
                        _data.writeInt(0);
                    }
                    mRemote.transact(Stub.TRANSACTION_addBook, _data, _reply, 0);
                    _reply.readException();
                } finally {
                    _reply.recycle();
                    _data.recycle();
                }
            }
        }

        static final int TRANSACTION_getBookList = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
        static final int TRANSACTION_addBook = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);
    }

    public java.util.List<com.daijie.aldlapp.Book> getBookList() throws android.os.RemoteException;

    public void addBook(com.daijie.aldlapp.Book book) throws android.os.RemoteException;
}

接下来,可以通过这个系统生成的Binder生成的Binder类来分析Binder的工作原理。
  IBookManager是一个接口,并且继承了android.os.IInterface接口(注意,所以在Binder中传输的接口都需要继承这个接口)。在这个接口中,声明了两个方法getBookList和addBook,显然也是我们在IBookManager中声明的方法,同时它还声明了两个id用于标识这两个方法,这两个方法用于标志在transact过程中客户端请求的到底是哪个方法。它声明了一个内部类Stub,这个Stub就是一个Binder类,当客户端和服务端都位于同一个进程时,方法调用不会走跨进程的transact过程,而当两者位于不同进程的时,方法调用需要走transact过程,这个逻辑由Stub的内部代理Proxy完成。综上所述,这个接口的核心实现是它的内部类Stub和Stub的内部类Proxy。
  下面来详细介绍针对这两个类的每个方法的含义。

DESCRIPTOR

Binder类的唯一标识,一般用当前Binder类的类名表示。

asInterface(android.os.IBinder obj)

用于将服务端的Binder对象转换成客户端所需要的AIDL接口类型的对象。这种转换过程是区分进程的。如果客户端和服务端位于同一进程,则此方法返回的就是服务端的Stub对象本身,否则返回系统封装后的Stub.proxy对象

onTrancsact

函数原型为onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags)
这个方法运行在服务顿的Binder线程池中,当客户端发起跨进程请求时,远程请求会通过系统底层封装后交由此方法来处理。服务端通过code可以确定客户端所请求的目标方法是什么,接着从data中取出目标方法所需要的参数(如果目标方法有参数的话),然后执行目标方法。当目标方法执行完毕后,就向reply中写入返回值(如果目标方法有返回值的话)。需要注意的是,如果这个方法返回false,那么客户端的请求就会失败,因此我们可以利用这个特性来做权限验证,避免随便一个进程都能远程调用我们的服务

Proxy#getBookList

这个方法运行在客户端,当客户端远程调用此方法时,它的内部实现是这样的:首先创建这个方法所需要的输入型Parcel对象_data、输出型Parcel对象_reply和返回值对象List;然后把该方法的参数信息写入_data(如果有参数的话);接着调用transact方法来完成RPC(远程方法调用)请求,同时将当前线程挂起;然后服务端的onTransact方法会被调用,直至RPC过程返回后,当前线程继续执行,并从_reply中取出RPC过程的返回结果;最后返回_reply的数据。

Proxy#addBook

执行过程和Proxy#getBookList是一样的,但因为addBook没有返回值所以不需要从_reply中取出返回值。

通过以上的分析,已经可以初步了解Binder的工作机制了,但是还需要注意一下两点。
  客户端发起远程请求时,由于当前线程会被挂起直至服务器进程返回值,所以如果调用的远程方法比较耗时,那么则不能在UI现在中发起这个请求,否则会出现ANR。其次由于服务端的Binder方法运行在Binder的线程池中,所以Binder无论是否耗时都必须采用同步的方式实现,因为它已经是运行在一个线程中了。
下图是Binder的工作机制图

Binder的工作机制

上述过程中,我们完全可以不使用AIDL文件即可实现Binder,而AIDL文件只是方便系统为我们生成代码的手段。

Binder死亡监听

Binder运行在服务端进程,如果服务端进程由于某种原因异常终止,这个时候客户端到服务端的Binder断裂(称之为Binder死亡)会导致远程调用失败,更为关键的是,客户端不知道Binder连接已经断裂,那么客户端功能就会受到影响。为了解决这个问题,Binder提供了两个很重要的配对方法linkToDeathunlinkToDeath,通过linkToDeath我们可以给Binder设置一个死亡代理,当Binder死亡的时候,系统回调binderDied方法,然后我们就可以移除走之前绑定的Binder死亡代理并重新绑定远程服务。

用法

首先声明一个DeathRecipient对象。DeathRecipient是一个接口,其内部只有一个方法binderDied方法。当Binder死亡的时候,系统就会回调这个方法,然后我们就可以移除之前绑定的死亡代理并重新绑定远程服务。

private IBinder.DeathRecipient mDeathRecipient = new IBinder.DeathRecipient() {
        @Override
        public void binderDied() {
            if (mMyAIDLService == null) {
                return;
            }
            mMyAIDLService.asBinder().unlinkToDeath(this, 0);
            mMyAIDLService = null;
            // 重绑定
        }
    };

其次,在客户端绑定远程服务成功之后,给binder设置死亡代理

mService =MyAIDLService.Stub.asInterface(binder);
binder.linkToDeath(mDeathRecipient,0);

以上代码中,linkToDeath的第二个参数是标记位,直接设置为0即可。经过以上两个步骤就可以给Binder设置死亡代理,当Binder死亡的时候就可以接收到通知。此外,也可以通过Binder的方法isBinderAlive判断Binder是否死亡。

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

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,444评论 25 707
  • 1.要做一个尽可能流畅的ListView,你平时在工作中如何进行优化的? ①Item布局,层级越少越好,使用hie...
    fozero阅读 710评论 0 0
  • Jianwei's blog 首页 分类 关于 归档 标签 巧用Android多进程,微信,微博等主流App都在用...
    justCode_阅读 5,893评论 1 23
  • 一、Android IPC简介 IPC是Inter-Process Communication的缩写,含义就是进程...
    SeanMa阅读 1,766评论 0 8
  • 吃晚饭,豆子坐在爸爸身上。爸爸夹了一块萝卜,但是萝卜滑掉了,掉在桌子上。张大嘴巴等萝卜的豆子说:“唉,萝卜跑掉了!”
    乐乐豆豆阅读 148评论 0 1