《Android开发艺术探索》读书笔记2.IPC机制

1. Android IPC简介

IPC是Inter-Process Communication的缩写,是指两个进程之间进行数据交换的过程。
多进程的情况分为两种,第一种情况是一个应用因为某些原因自身需要采用多进程模式来实现,另一种情况是当前应用需要向其他应用获取数据。

2. Android中的多进程模式

2.1. 开启多进程模式

在Android中使用多进程只有一种方法,那就是给四大组件在AndroidManifest中指定android:process属性。
进程名以“”开头的进程属于当前应用的私有进程,其他应用的组件不可以和它跑在同一个进程中;而进程名不以“”开头的进程属于全局进程,其他应用通过ShareUID方式可以和它跑在同一个进程中。
Android系统会为每个应用分配一个唯一的UID,具有相同UID的应用才能共享数据。两个应用通过ShareUID跑在同一个进程中需要这两个应用有相同的ShareUID并且签名相同才可以。

2.2. 多进程模式的运行机制

Android为每个进程都分配一个独立的虚拟机,不同的虚拟机在内存分配上有不同的地址空间,这就导致在不同的虚拟机中访问同一个类的对象会产生多份副本。
一般来说,使用多进程会造成如下几方面的问题:

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

3. IPC基础概念介绍

3.1. Serializable接口

Serializable是Java所提供的一个序列化接口,它是一个空接口,为对象提供标准的序列化和反序列化操作。想让一个对象实现序列化,只需要这个类实现Serializable接口并声明一个serialVersionUID即可。采用ObjectOutputStreamObjectInputStream即可进行对象的序列化和反序列化。

public class User implements Serializable {
	private static final long serialVersionUID = 519067123721295773L;

	public int userId;
	public String userName;
	public boolean isMale;
	...
}

// 序列化过程
User user = new User(0, "jake", true);
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("cache.txt"));
out.writeObject(user);
out.close();

// 反序列化过程
ObjectInputStream in = new ObjectInputStream(new FileInputStream("cache.txt"));
User newUser = (User) in.readObject();
in.close();

这个serialVersionUID是用来辅助序列化和反序列化过程的,原则上序列化后的数据中的serialVersionUID只有和当前类的serialVersionUID相同才能够正常地被反序列化。当手动指定了serialVersionUID以后,就可以在很大程度上避免发序列化过程的失败,比如当版本升级后,可能删除了某个成员变量也可能增加了一些新的成员变量,这个时候的反向序列化过程仍然能够成功,程序仍然能够最大限度地恢复数据,相反,如果不指定serialVersionUID的话,程序则会挂掉。当然如果类结构发生了非常规性改变,比如修改了类名,修改了成员变量的类型,这个时候尽管serialVersionUID验证通过了,但是反序列化过程还是会失败。
静态成员变量属于类不属于对象,所以不会参与序列化过程,其次用transient关键字标记的成员变量不参与序列化过程。

3.2. Parcelable接口

在Android中也提供了新的序列化方式,那就是Parcelable接口。只要实现这个接口,一个类的对象就可以实现序列化并可以通过IntentBinder传递。

public class MyParcelable implements Parcelable {
    private int mData;

    public int describeContents() {
        return 0;
    }

    public void writeToParcel(Parcel out, int flags) {
        out.writeInt(mData);
    }

    public static final Parcelable.Creator<MyParcelable> CREATOR
            = new Parcelable.Creator<MyParcelable>() {
        public MyParcelable createFromParcel(Parcel in) {
            return new MyParcelable(in);
        }

        public MyParcelable[] newArray(int size) {
            return new MyParcelable[size];
        }
    };
    
    private MyParcelable(Parcel in) {
        mData = in.readInt();
    }
}

从上述代码中可以看出,在序列化过程中需要实现的功能有序列化、反序列化和内容描述。
Serializable是Java中的序列化接口,其使用起来简单但是开销很大,序列化和反序列化过程需要大量I/O操作。而Parcelable是Android中的序列化方式,因此更适合用在Android平台上,它的缺点就是使用起来稍微麻烦点,但是它的效率很高,这是Android推荐的序列化方式。Parcelable主要用在内存序列化上,但是通过Parcelable将对象序列化到存储设备中或者将对象序列化后通过网络传输会稍显复杂,在这两种情况下建议使用Serializable

3.3. Binder

Binder是Android中的一个类,它实现了IBinder接口。从IPC角度来说,Binder是Android中的一种跨进程通信方式;从Android Framework角度来说,BinderServiceManager连接各种ManagerActivityManagerWindowManager,等等)和相应ManagerService的桥梁;从Android应用层来说,Binder是客户端和服务端进行通信的媒介,当bindService的时候,服务端会返回一个包含了服务端业务调用的Binder对象,通过这个Binder对象,客户端就可以获取服务端提供的服务或者数据,这里的服务包括普通服务和基于AIDL的服务。
当客户端发起远程请求时,由于当前线程会被挂起直至服务端进程返回数据,所以如果一个远程方法是很耗时的,那么不能在UI线程中发起此远程请求;由于服务端的Binder方法运行在Binder的线程池中,所以Binder方法不管是否耗时都应该采用同步的方式去实现,因为它已经运行在一个线程中了。下面给出一个Binder的工作机制图:
的工作机制

4. Android中的IPC方式

4.1. 使用Bundle

四大组件中的三大组件(Activity、Service、Receiver)都是支持在Intent中传递Bundle数据的,由于Bundle实现了Parcelable接口,所以它可以方便地在不同的进程间传输。当然,传输的数据必须能够被序列化,比如基本类型、实现了Parcelable接口的对象、实现了Serializable接口的对象以及一些Android支持的特殊对象。
读者注:intent.putExtra方法实际上是调用了bundle.putString等方法,而bundle内部是使用了ArrayMap<String, Object>来保存所有的extra。

4.2. 使用文件共享

共享文件也是一种不错的进程间通信方式,两个进程通过读/写同一个文件来交换数据。除了可以交换一些文本信息外,还可以序列化一个对象到文件系统中的同时从另一个进程中恢复这个对象。文件共享方式适合在对数据同步要求不高的进程之间进行通信,并且要妥善处理并发读/写的问题。
SharedPreferences是个特例,从本质上来说,SharedPreferences也属于文件的一种,但是由于系统对它的读/写有一定的缓存策略,即在内存中会有一份SharedPreferences文件的缓存,因此在多进程模式下,系统对它的读/写就变得不可靠。因此,不建议在进程间通信中使用SharedPreferences

4.3. 使用Messenger

通过Messenger可以在不同进程中传递Message对象,在Message中放入需要传递的数据,就可以实现数据的进程间传递了。Messenger是一种轻量级的IPC方案,它的底层实现是AIDL。由于它一次处理一个请求,因此在服务端不用考虑线程同步的问题,这是因为在服务端中不存在并发执行的情形。

  1. 服务端进程
    首先,需要在服务端创建一个Service来处理客户端的连接请求,同时创建一个Handler并通过它来创建一个Messenger对象,然后在ServiceonBind中返回这个Messenger对象底层的Binder即可。
  2. 客户端进程
    首先要绑定服务端的Service,绑定成功后用服务端返回的IBinder对象创建一个Messenger,通过这个Messenger就可以向服务端发送消息了,发消息类型为Message对象。如果需要服务端能够回应客户端,客户端还需要和服务端一样创建一个Handler并创建一个新的Messenger,并把这个Messenger对象通过MessagereplyTo参数传递给服务端,服务端通过这个replyTo参数就可以回应客户端。
    的工作原理
    Messenger是以串行的方式处理客户端发来的消息,如果有大量的并发请求,用Messenger就不太合适了。同时,Messenger的作用主要是为了传递消息,很多时候可能需要跨进程调用服务端的方法,这种情形用Messenger就无法做到了,但是可以使用AIDL来实现跨进程的方法调用。

4.4. 使用AIDL

  1. 服务端
    服务端首先要创建一个Service用来监听客户端的连接请求,然后创建一个AIDL文件,将暴露给客户端的接口在这个AIDL文件中声明,最后在Service中实现这个AIDL接口。
  2. 客户端
    首先需要绑定服务端的Service,绑定成功后,将服务端返回的Binder对象转成AIDL接口所属的类型,接着就可以调用AIDL中的方法了。
  3. AIDL接口的创建
package com.ryg.chapter_2.aidl;

import com.ryg.chapter_2.aidl.Book;

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

AIDL文件支持6种数据类型:基本数据类型、StringCharSequenceListMapParcelable、AIDL。其中自定义的Parcelable对象和AIDL对象必须要显示import进来,不管它们是否和当前的AIDL文件位于同一个包内。如果用到了自定义的Parcelable对象,那么必须新建一个和它同名的AIDL文件,并在其中声明它为Parcelable类型。

package com.ryg.chapter_2.aidl;
parcelable Book;

AIDL接口中只支持方法,不支持声明静态常量。需要注意的是,AIDL的包结构在服务端和客户端要保持一致,否则运行会出错,这是因为客户端需要反序列化服务端中和AIDL接口相关的所有类。

  1. 远程服务端Service的实现
public class BookManagerService extends Service {

    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);
        }
    };

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

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

创建一个Binder对象并在onBind中返回它,这个对象继承自IBookManager.Stub并实现了它内部的AIDL方法。注意这里采用了CopyOnWriteArrayList,它支持并发读/写。

  1. 客户端的实现
public class BookManagerActivity extends Activity {

    private static final String TAG = "BookManagerActivity";

    private ServiceConnection mConnection = new ServiceConnection() {
        public void onServiceConnected(ComponentName className, IBinder service) {
            IBookManager bookManager = IBookManager.Stub.asInterface(service);
            try {
                List<Book> list = bookManager.getBookList();
                Log.i(TAG, "query book list:" + list.toString());
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }

        public void onServiceDisconnected(ComponentName className) {
        }
    };

    @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();
    }
}

需要注意的是,服务端的方法有可能需要很久才能执行完毕,这个时候的代码就会导致ANR。


RemoteCallbackList是系统专门提供的用于删除跨进程listener的接口。虽然说多次跨进程传输客户端的同一个对象会在服务端生成不同的对象,但是这些新生成的对象有一个共同点,那就是它们底层的Binder对象是同一个。当客户端解注册的时候,RemoteCallbackList遍历服务端所有的listener,找出那个和解注册listener具有相同Binder对象的服务端listener并把它删掉即可。同时,当客户端进程终止后,RemoteCallbackList能够自动移除客户端所注册的listener。另外,RemoteCallbackList内部自动实现了线程同步的功能,所以使用它来注册和解注册时,不需要做额外的线程同步工作。


客户端调用远程服务的方法,被调用的方法运行在服务端的Binder线程池中,同时客户端线程会被挂起,这个时候如果服务端方法执行比较耗时,就会导致客户端线程长时间地阻塞在这里,而如果这个客户端线程是UI线程的话,就会导致客户端ANR。由于客户端的onServiceConnectedonServiceDisconnected方法都运行在UI线程中,所以也不可以在它们里面直接调用服务端的耗时方法。另外,由于服务端的方法本身就运行在服务端的Binder线程池中,所以服务端方法本身就可以执行大量耗时操作。
同理,当远程服务端需要调用客户端的listener中的方法时,被调用的方法也运行在Binder线程池中,只不过是客户端的线程池。所以,同样不可以在服务端中调用客户端的耗时方法。如果客户端的方法比较耗时,确保服务端中的调用运行在非UI线程中,否知将导致服务端无法响应。
另外,由于客户端中的回调方法运行在客户端的Binder线程池中,所以不能在它里面去访问UI相关的内容,如果要访问UI,使用Handler切换到UI线程。


Binder是可能意外死亡的,这往往是由于服务端进程意外停止了,这是需要重新连接服务。有两种方法,第一种方法是给Binder设置DeathRecipient监听,当Binder死亡时,会收到binderDied方法的回调。另一种方法是在onServiceDisconnected中重连远程服务。它们的区别在于:onServiceDisconnected在客户端的UI线程中被回调,而binderDied在客户端的Binder线程池中被回调。


默认情况下,远程服务任何人都可以连接,所以必须给服务加入权限验证功能。在AIDL中进行权限验证,有两种常用的方法。第一种方法,可以在onBind中进行验证,验证不通过就直接返回null,验证方式可以有多种,比如使用permission验证。第二种方法,可以在服务端的onTransact方法中进行权限验证,如果验证失败就直接返回false。具体的验证方式可以采用permission验证,还可以采用Uid和Pid来做验证,通过这两个参数可以做一些验证工作,比如验证包名。

4.5. 使用ContentProvider

(暂略)

4.6. 使用Socket

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值