Android笔记:IPC

原文地址: http://tianma.space/post/3352500871/

IPC (Interprocess Communication) 即进程间通信,需要用到 IPC 主要有以下原因:

  • 应用内自身原因需要采用多进程,比如,大应用模块多,需要的内存大,而 Android 对单进程内存有大小限制,所以需要多进程获取更多的内存空间;
  • 当前应用需要获取其他应用数据。

Android 多进程模式

开启多进程模式

Android 中开启多进程有两种方式:

  1. 给四大组件在 AndroidManifest 中指定 android:process 属性
  2. 通过 JNI 在 nativefork 子进程(属于Linux处理方式)
<activity 
    android:name=".FirstActivity"
    android:label="FirstActivity" />
<activity 
    android:name=".SecondActivity"
    android:label="SecondActivity"
    android:process=":remote" />
<activity
    android:name=".ThirdActivity"
    android:label="ThirdActivity"
    android:process="com.github.tianma8023.ipclearn.remote" />

这里,应用程序的包名是 com.github.tianma8023.ipclearn,其中 FirstActivity 运行在以包名为进程名的默认进程中;而SecondActivity 在启动时,会运行在名为 com.github.tianma8023.ipclearn:remote 的进程中;ThirdActivity 在启动时,会运行在名为 com.github.tianma8023.ipclearn.remote 的进程中。
: 开头的进程是当前应用的私有进程,其他应用程序的四大组件不会和它跑在同一个进程中;不以 : 开头的进程是全局进程,其他应用可以通过 ShareUID 的方式和其跑在同一个进程中。

多进程模式弊端

多进程模式会造成如下问题:

  1. 静态成员变量以及单例模式失效:
    Android系统为每个进程都分配有独立的虚拟机,不同的虚拟机有着不同的内存空间分配,在不同虚拟机下访问同一个类对象会分配到不同的地址空间,也就是属于不同的对象。
  2. 线程同步机制失效:
    在不同的内存地址空间中,同步锁(无论是对象锁还是全局锁)也不一样,故没办法在不同进程间进行线程同步。
  3. SharedPreferences 可靠性降低
    SharedPreferences 底层通过读写 XML 文件来实现,在不同进程中并发读写是会产生同步问题的。
  4. Application 会多次创建:
    每个进程有独立的虚拟机,自然也有独立的 Application 对象

从以上信息可以得出:位于不同进程的四大组件之间,但凡通过内从共享数据、变量的,都会共享失败。

Android 中的 IPC 方式

Bundle

Activity, Service, BroadcastReceiver 都支持在 Intent 中传递 Bundle 数据,而因为 Bundle 实现了 Parcelable 接口,所以可以在不同的进程间传输,也就是进行了进程间单向通信。

共享文件

既然共享内存会失效,那就通过共享文件的方式,但是也会存在并发读写的问题,所以文件共享方式适合对数据同步要求不高的进程间通信。
虽然 SharedPreferences 本质是文件读写,但由于 Android 系统对其有缓存策略,即在内存中也会持有 SharedPreferences 的缓存,因此进程间通信不宜用 SharedPreferences

AIDL

AIDL(Android Interface Definition Language) 的进程间通信方式主要依靠 Binder 实现。

定义一个实体类 Book

// Book.java
package com.github.tianma8023.ipclearn.aidl;

public class Book implements Parcelable {

    public int bookId;
    public String bookName;

    // constructor ...
    // implements Parcelable ...
}

创建 Book.aidl

// Book.aidl
package com.github.tianma8023.ipclearn.aidl;

// 声明自定义的Parcelable对象
parcelable Book;

创建 IBookManager.aidl 欲实现对 Book 的管理操作:

// IBookManager.aidl
package com.github.tianma8023.ipclearn.aidl;

// 显式导入自定义的数据类型
import com.github.tianma8023.ipclearn.aidl.Book;

interface IBookManager {
    List<Book> getBookList();
    void addBook(in Book book);
}

AIDL 中仅支持一下几种数据类型:

  1. 基本数据类型(int, long, char, boolean, double 等);
  2. String 和 CharSequence;
  3. List:只支持 ArrayList,且 List 中的每个元素必须是 AIDL 支持的数据类型;
  4. Map:只支持 HashMap,且 Map 中的每个元素都必须是 AIDL 支持的数据类型;
  5. Parcelable:实现了Parcelable的对象;
  6. AIDL:所有的 AIDL 接口本身也可以在其他 AIDL 文件中使用。

需要注意:

  • 自定义的 Parcelable 对象 和 AIDL 对象必须显式的 import 进来,无论它们是否在同一个包内。
  • 如果 AIDL 文件中引用了自定义的 Parcelable 对象,则该 Parcelable 对象必须创建一个与它同名的 AIDL 文件,并在其中声明它为 parcelable 类型
  • AIDL 文件中除了基本数据类型之外,其他类型参数都需标上:in(输入型参数), out(输出型参数) 或者 inout(输入输出型参数)。因为不同标识的参数底层开销不一样,所以最好别滥用 inout

之后 build 操作,Android Studio 会在模块 /build 目录下面生成对应的 IBookManager.java 文件,大致内容如下:

// IBookManager.java
/*
 * This file is auto-generated.  DO NOT MODIFY.
 * Original file: F:\\AndroidStudio\\IPCLearn\\app\\src\\main\\aidl\\com\\github\\tianma8023\\ipclearn\\aidl\\IBookManager.aidl
 */
 package com.github.tianma8023.ipclearn.aidl;

 public interface IBookManager extends android.os.IInterface {

    /** Local-side IPC implementation stub class. */
    public static abstract class Stub extends android.os.Binder implements com.github.tianma8023.ipclearn.aidl.IBookManager {
        private static final java.lang.String DESCRIPTOR = "com.github.tianma8023.ipclearn.aidl.IBookManager";
        /** Construct the stub at attach it to the interface. */
        public Stub() {
            // ...
        }

        /**
        * Cast an IBinder object into an com.github.tianma8023.ipclearn.aidl.IBookManager interface,
        * generating a proxy if needed.
        */
        public static com.github.tianma8023.ipclearn.aidl.IBookManager asInterface(android.os.IBinader 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) {
            // ...
        }

        private static class Proxy implements com.github.tianma8023.ipclearn.aidl.IBookManager {
            private android.os.IBinder mRemote;

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

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

            @Override
            public java.util.List<com.github.tianma8023.ipclearn.aidl.Book> getBookList() throws android.os.RemoteException {
                // ...
            }

            @Override
            public void addBook(com.github.tianma8023.ipclearn.aidl.Book book) throws android.os.RemoteException {
                // ...
            }
        }

    }

    public java.util.List<com.github.tianma8023.ipclearn.aidl.Book> getBookList() throws android.os.RemoteException;

    public void addBook(com.github.tianma8023.ipclearn.aidl.Book book) throws android.os.RemoteException;
}

AS 自动生成的 aidl 对应的 java 类,可以很好的了解 Binder 的工作机制,所以有必要读一读这里的源码。 这里大致介绍下各个字段及方法的含义:

  • Stub#DESCRIPTOR:
    Binder 的唯一标识
  • Stub#asInterface(android.os.IBinader obj):
    服务端 的 Binder 对象转化成 客户端 所需要的 AIDL 接口类型的对象。当服务端和客户端位于同一进程中时,此方法返回的就是服务端对象本身。当它们不在同一个进程中时,返回的是由系统封装后的 Stub.Proxy 对象(也就是需要转换)
  • Stub#asBinder():
    返回当前 Binder 对象
  • Stub#onTransact(int code, Parcel data, Parcel reply, int flags)
    该方法会运行在服务端的 Binder 线程池中。客户端发起的跨进程请求会通过系统底层封装后交由此方法处理。服务端通过 code 参数确认接下来调用执行的目标方法。
    如果该方法返回 false,则客户端的请求会失败,所以可以利用此特性进行权限认证。
  • Proxy#getBookList():
    此方法运行在客户端。客户端使用此方法进行跨进程调用时,会转化为让服务端(远端)的 Binder 执行 transact 方法,与此同时客户端线程挂起,并最终调用服务端的 onTransact,即调用上一条的 Stub#onTransact() 方法,当服务端相应方法执行完毕后返回结果后,客户端当前线程结束挂起,继续执行,并取出服务端返回的结果。
  • Proxy#addBook()
    与上述的 Proxy#getBookList() 类似

Binder 的工作机制图:

Binder 工作机制

当客户端发起远程请求时,客户端当前线程会被挂起,等待服务端进程返回结果,所以,如果服务端进程很耗时,则不能在 UI 线程中发起远程请求。

远程服务端实现 Service

public class BookManagerService extends Service {
    private static final String TAG = "BookManagerService";

    private CopyOnWriteArrayList<Book> mBookList = new CopyOnWriteArrayList<Book>();

    private Binder mBinder = new IBookManager.Stub() {

        @Override
        public List<Book> getBookList() throws RemoteException {
            return mBookList;
        }

        @Override
        public void addBook(Book book) throws RemoteException {
            mBookList.add(book);
        }
    };

    public BookManagerService() {
    }

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

    @Override
    public void onCreate() {
        super.onCreate();
        mBookList.add(new Book(1, "Think In Java"));
        mBookList.add(new Book(2, "Android Programing"));
    }
}

AndroidManifest 中将 BookManagerService 置于独立进程中,实现对进程间通信的模拟:

<service 
    android:name=".aidl.BookManagerService"
    android:process=":remote" />
  • 因为 IBookManager.Stub 类是 Binder 的一个抽象子类,所以在 Serivce 中实现 IBookManager.Stub 即可在 onBind() 中返回相应的 Binder 对象。
  • 考虑到 AIDL 中的方法是在服务端的 Binder 线程池中执行的,所以考虑同步就使用了 CopyOnWriteArrayList。注意到 CopyOnWriteArrayList 并不是 ArrayList 的子类,但其实现的最基本的底层原理和 ArrayList 一致(基于数组,可以通过下标 index 进行访问等)。虽然服务端返回的是 CopyOnWriteArrayList,但 Binder 可以通过相同的机理去访问 CopyOnWriteArrayList 中的数据并最终形成新的 ArrayList 返回给客户端。这就与之前提到的 AIDL支持的 List 只有 ArrayList 并不冲突了。

客户端跟绑定普通服务的客户端一致,比较简单:

public class BookManagerActivity extends Activity {

    private static final String TAG = "BookManagerActivity";

    private ServiceConnection mConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            IBookManager bookManager = IBookManager.Stub.asInterface(service);
            try {
                // 服务端的 getBookList() 方法可能会比较耗时,所以需要酌情考虑是否在子线程中进行访问
                List<Book> bookList = bookManager.getBookList();
                Log.i(TAG, "book list type: " + bookList.getClass().getCanonicalName());
                Log.i(TAG, "query book list: " + bookList);
            } catch(RemoteException e) {
                e.printStackTrace();
            }
        }

        public void onServiceDisconnected(ComponentName name) {
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_book_manager);

        Intent intent = new Intent(this, BookManagerService.class);
        bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
    }

    @Override
    protected void onDestroy() {
        unbindService(mConnection);
        super.onDestroy();
    }
}

以上是 AIDL 的基本用法,进阶用法请参考 Android 笔记:AIDL进阶

Messenger

使用 Messenger 可以在不同的进程之间传递 Message 对象,实现进程间数据交互通信。

Messenger 的构造函数:

public Messenger(Handler target) {
    mTarget = target.getIMessenger();
}

public Messenger(IBinder target) {
    mTarget = IMessenger.Stub.asInterface(target);
}

通过第二个构造函数可以看出来,Messegner 的底层就是 AIDL

MessengerAIDL 做了封装,由于 Messenger 机制一次只能处理一个请求,因此在服务端不需要考虑线程同步。

Messenger 远程服务端 Service:

public class MessengerServerService extends Service {
    private static final String TAG = "MessengerServerService";

    private final Messenger mMessenger = new Messenger(new MessengerHandler());

    private static class MessengerHandler extends Handler {

        @Override
        public  void handleMessage(Message msg) {
            switch(msg.what) {
                case TConstants.MSG_FROM_CLIENT:
                    // 服务端接收客户端消息
                    Log.i(TAG, "msg from client: " + msg.getData().get(TConstants.KEY_MSG));
                    // 服务端向客户端发送消息
                    Messenger client = msg.replyTo;
                    Bundle replyBundle = new Bundle();
                    replyBundle.putString(TConstants.KEY_SERVER, "Okay, I'm server, your message has been received.");
                    Message replyMsg = Message.obtain(null, TConstants.MSG_FROM_SERVER);
                    replyMsg.setData(replyBundle);
                    try {
                        client.send(replyMsg);
                    } catch (RemoteException e) {
                        e.printStackTrace();
                    }
                    break;
            }
            super.handleMessage(msg);
        }
    }

    @Override
    public IBinder onBind(Intent intent) {
        return mMessenger.getBinder();
    }
}

注册 Service 运行在单独进程中:

<service 
    android:name=".messenger.MessengerServerService"
    android:process=":remote" />

Messenger 客户端 Activity

public class MessengerClientActivity extends Activity {
    private static final String TAG = "MessengerClientActivity";

    // 客户端向服务端发送消息的Messenger
    private Messenger mMessenger;

    private ServiceConnection mConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            // 客户端向服务端发送消息
            mMessenger = new Messenger(service);
            Message msg = Message.obtain(null, TConstants.MSG_FROM_CLIENT);
            Bundle data = new Bundle();
            data.putString(TConstants.KEY_MSG, "Hello, this is client.");
            msg.setData(data);
            // 指定message的replyTo为Messenger对象
            msg.replyTo = mGetReplyMessenger;
            try {
                mMessenger.send(msg);
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            // do nothing
        }
    }

    // 客户端接收服务端消息的Messenger
    private Messenger mGetReplyMessenger = new Messenger(new MessengerHandler());

    private static class MessengerHandler extends Handler {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case TConstants.MSG_FROM_SERVER:
                    Log.i(TAG, "msg from server: " + msg.getData().getString(TConstants.KEY_REPLY));
                    break;
            }
            super.handleMessage(msg);
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_messenger_client);

        Intent intent = new Intent(this, MessengerServerService.class);
        bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
    }

    @Override
    protected void onDestroy() {
        unbindService(mConnection);
        super.onDestroy();
    }
}

注意:Messageobj 字段,在 Android 2.0 以前不支持跨进程传输,在 Android 2.0 以后也仅仅在系统提供的实现了 Parcelable 接口的对象上才能支持跨进程传输。

Messenger 原理

ContentProvider

ContentProvider 底层也是由 AIDL 实现,其主要有6个抽象方法:onCreate, getType, query, update, insert, delete, 其中除了 onCreate 由系统调用运行在主线程之外,其他的都由外界回调运行在 Binder 线程池中。

注意,ContentProvider 底层依赖的数据存储没有要求,可以用 SQLite 数据库,也可以用普通文件,亦可以用 SharedPreferences,不过通常情况下底层存储都是依赖的数据库。

在客户端要观察 ContentProvider 中的数据变化情况,可以通过 ContentResolver#registerContentObserver 方法来注册观察者,ContentResolver#unregisterContentObserver 取消注册。

Socket

Socket 也可以实现进程间通信,可以凭借 TCP 或者 UDP 的套接字来实现。与一般的 Socket 编程没有太大区别,主要是在 Android 编程中需要考虑在主线程上更新UI,在子线程发起 Socket 请求即可。

各个 IPC 方式对比

名称 优点 缺点 适用场景
Bundle 简单 只能传输Bundle支持的数据 四大组件间的进程间通信
文件共享 简单易用 不适合并发场景,无法做到进程间即时通信 无并发访问的情景,交换简单的数据,实时性要求不高的场景
AIDL 功能强大,支持一对多并发通信,支持即时通信 使用稍复杂,需要处理线程同步 一对多通信,有RPC需求
Messenger 功能一般,支持一对多串行通信,支持即时通信 不适用于高并发场景,数据只能通过Message进行传输,只能传输Bundle支持的数据类型 适用于低并发的一对多的即时通信,无RPC需求,或者不需要返回结果的PRC需求
ContentProvider 在数据源访问方面功能强大,支持一对多并发数据共享,可以通过Call扩展其他方法操作 受约束的AIDL,主要提供数据源的CRUD操作 一对多进程间的数据共享
Socket 功能强大,可以通过网络传输字节流,支持一对多并发即时通信 实现起来稍微麻烦,不支持直接的RPC 网络数据交换

代码参考

IPCLearn

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

推荐阅读更多精彩内容

  • 一、IPC简介 (1)IPC是Inter-Process Communication的缩写,含义为进程间通信或者跨...
    遥遥的远方阅读 7,193评论 0 3
  • 一、Android IPC简介 IPC是Inter-Process Communication的缩写,含义就是进程...
    SeanMa阅读 1,766评论 0 8
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,424评论 25 707
  • Jianwei's blog 首页 分类 关于 归档 标签 巧用Android多进程,微信,微博等主流App都在用...
    justCode_阅读 5,893评论 1 23
  • 前公司一位工作经历丰富的视觉设计同事,曾调侃说: 交互设计师都有一颗产品经理的心。 原因是,他见证了太多交互设计师...
    德川亮阅读 2,469评论 4 39