《Android开发艺术探索》读书笔记 (2) 第2章 IPC机制

第2章 IPC机制   http://www.nowcoder.com/discuss/3669?type=0&order=0&pos=7&page=1

        2.1 Android IPC简介
        IPC是Inter-Process Communication的缩写,含义为进程间通信或者跨进程通过是指两个进程之间进行数据交换的过程,什么是进程,什么是线程,线程是CPU调度的最小单元,同时线程是一种有限的系统资源,而进程一般指一个执行单元,在PC和移动设备上指一个程序或者一个应用,一个进程可以包含多个线程,因此进程与线程是包含与被包含的关系
      IPC不是Android中所独有的, 任何一个操作系统都需要有相应的IPC机制,Linux上可以通过命名通道、共享内存、信号量等来进行进程间通信。Android来说,它是一种基于linux内核和移动操作系统,它的进程通信方式并不能完全继承自Linux,它有自己的进程间通信方式,Android系统不仅可以使用了Binder机制来实现IPC,还可以使用Socket实现任意两个终端之间的通信
       多进程的情况分为两种,第一种情况是一个应用因为某些原因自身需要采用多进程模式来实现至于原因,可能有很多,比如有些模块由于特殊原因需要运动在单独的进程中,又比如早期android系统内存的限制16,来获取更多 内存
     2.2 Android中的多进程模式
       
       (1)首页,在Android中使用多进程只有一种方法,通过给四大组件指定 android:process 属性就可以开启多进程模式,除此之外没有其他方法,其实还有另一种非常规的多进程方法,那就是通过JNI在native层去fork一个新的进程:除了在Eclipse的DDMS视图中查看进程信息,还可以用shell来查看,命令为:adb shell ps,默认进程的进程名是包名packageName,进程名以:开头的进程属于当前应用的私有进程,其他应用的组件不可以和它跑在同一个进程中,而进程名不以:开头的进程属于全局进程,其他应用通过ShareUID方法可以和它跑在同一个进程中
       android:process=":xyz"//进程名是 packageName:xyz
android:process="aaa.bbb.ccc"//进程名是 aaa.bbb.ccc
(2)Android系统会为每个应用分配一个唯一的UID,具有相同UID的应用才能共享数据。两个应用通过ShareUID跑在同一个进程中是有要求的,需要这两个应用有相同的ShareUID并且签名相同才可以。 在这种情况下,它们可以相互访问对方的私有数据,比如data目录、组件信息等,不管它们是否跑在同一个进程中。如果它们跑在同一个进程中,还可以共享内存数据,它们看起来就像是一个应用的两个部分。

2.2.2 多进程模式的运行机制
(1) 通过例子来说,比如一个Activity开启了多进程,SecondActivity 通过指定android:process属性运行在一个独立的进程中,这里做一些改动,我们新建一个类,叫做UseManager,这个类中有一个public的静态成员变量,如下所示:
public class UserManager{
public static int sUserId = 1;
}
然后在MainActivity的onCreate中我们把这个sUserId重新赋值为2,打印出这个静态变量的值后再启动SecondActivity,在SecondActivity中我们再打印一下sUserId的值,结果SecondActivity竟然是1,上述问题出现的原因是SecondActivity运动在一个单独的进程中,android系统会为每个进程分配一个独立的虚拟机,不同的虚拟机在内存分配上有不同的地址空间,所以不同的虚拟机中访问同一个类的对象会产生多个副本
(4)使用多进程容易造成以下几个问题:
1.静态成员和单例模式完全失效;
2.线程同步机制完全失效:无论锁对象还是锁全局对象都无法保证线程同步;
3.SharedPreferences的可靠性下降:SharedPreferences不支持并发读写,底层是通过写xml;
4.Application会多次创建:当一个组件跑在一个新的进程的时候,系统要在创建新的进程的同时分配独立的虚拟机,应用会重新启动一次,也就会创建新的Application。运行在同一个进程中的组件是属于同一个虚拟机和同一个Application。
同一个应用的不同组件,如果它们运行在不同进程中,那么和它们分别属于两个应用没有本质区别
现在我们测试一下,首先在Application的onCreate方法中打印出当前进程的名字,然后连续启动三个同一个应用内但属于不同进程的Acitivty,虽然给 我们带了很多问题,但是我们就不能不用啊,实现跨进程通信的方式很多,比如通过Intent来传递数据,共享文件和SharedPreferences,基于Binder的Messager和AIDL以及socket等
2.3 IPC基础概念介绍
(1)Serializable接口是Java中为对象提供标准的序列化和反序列化操作的接口,而Parcelable接口是Android提供的序列化方式的接口。例如:
public class User implements Serializable
通过Serializable方式来实现对象的序列化,实现起来非常简单,几乎所有工作都被系统自动完成了,如何进行对象的序列化和反序列化也非常简单,只需要采用ObjectOutputStream和ObjectInputStream即可轻松实现,下面简单例子
// 序列化过程
User user = new User(0,"jake",true);
       ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("cache.txt"));
      out.writeObject(user);
      //  反序列化过程
    ObjectInputStream in = new ObjectInputStream(new FileInputstream("cache.txt"));
    User newUser =(User) in.readObject();
     in.close();
     (2) serialVersionUId 是一串long型数字,主要是用来辅助序列化和反序列化的,原则上序列化后的数据中的serialVersionUId只有和当前类的serialVersionUId相同才能够正常地被反序列化。
serialVersionUId的详细工作机制 :序列化的时候系统会把当前类的serialVersionUId写入序列化的文件中,当反序列化的时候系统会去检测文件中的serialVersionUId,看它是否和当前类的serialVersionUId一致,如果一致就说明序列化的类的版本和当前类的版本是相同的,这个时候可以成功反序列化;否则说明版本不一致无法正常反序列化。 一般来说,我们应该手动指定serialVersionUId的值。
      1.静态成员变量属于类不属于对象,所以不参与序列化过程;
       2.声明为 transient 的成员变量不参与序列化过程
   可以通过如下两个方法即可重写系统默认的序列化和反序列过程,wirteObject和readObject
(3)Parcelable接口内部包装了可序列化的数据,可以在Binder中自由传输,Parcelable主要用在内存序列化上,可以直接序列化的有Intent、Bundle、Bitmap以及List和Map等等,下面是一个实现了Parcelable接口的示例
    
public class Book implements Parcelable {
public int bookId;
public String bookName;
public Book() {
}

public Book(int bookId, String bookName) {
this.bookId = bookId;
this.bookName = bookName;
}

//“内容描述”,如果含有文件描述符返回1,否则返回0,几乎所有情况下都是返回0
public int describeContents() {
return 0;
}

//实现序列化操作,flags标识只有0和1,1表示标识当前对象需要作为返回值返回,不能立即释放资源,几乎所有情况都为0
public void writeToParcel(Parcel out, int flags) {
out.writeInt(bookId);
out.writeString(bookName);
}

//实现反序列化操作
public static final Parcelable.Creator CREATOR = new Parcelable.Creator() {
//从序列化后的对象中创建原始对象
public Book createFromParcel(Parcel in) {
return new Book(in);
}
public Book[] newArray(int size) {//创建指定长度的原始对象数组
return new Book[size];
}
};

private Book(Parcel in) {
bookId = in.readInt();
bookName = in.readString();
}

}
      既然 Parcelable和Serializable都能实现序列化并且都可用于Intent间的数据传递,那么二者该如何选取呢? Serializable是java中序列化接口,其使用起来简单但是开销很大,序列化和反序列化过程需要大量I/O操作,而Parcelable是Android中的序列化方式 ,因此更适合在Android平台上,它的不好就是使用起来稍微麻烦点,但是它的效率很高,这是Android推荐的序列化方式,因此我们首选Parcelable,Parcelable主要用在内存序列化上,通过Parcelable将对象序列化到存储设备中或者将对象序列化后通过网络传输也都可以,因此在这两种情况下建议大家使用Serializable。
 
     (4)Binder是Android中的一个类,它实现了IBinder接口。从IPC角度看,Binder是Android中一种跨进程通信的方式;Binder还可以理解为虚拟的物理设备,它的设备驱动是/dev/binder;从Framework层角度看,Binder是ServiceManager连接各种Manager和相应的ManagerService的桥梁;从Android应用层来说,Binder是客户端和服务端进行通信的媒介,当bindService的时候,服务端会返回一个包含了服务端业务调用的Binder对象,通过这个Binder对象,客户端就可以获取服务端提供的服务或者数据,这里的服务包括普通服务和基于AIDL的服务。
在Android开发中,Binder主要用在Service中,包括AIDL和Messenger,其中普通Service中的Binder不涉及进程间通信,较为简单;而Messenger的底层其实是AIDL,正是Binder的核心工作机制。为了分析AIDL示例,我们新建一个工程源码下载地址:https://github.com/singwhatiwanna/android-art-res 第二章
       定义了Book.java表示一个图书信息的类,它实现了Parcelable接口,Book.aidl是Book类在AIDL中的声明,IBookManager.aidl是我们定义的一个接口,下面我们先看一下系统为IBookManager.aidl产生的Binder类,在gen目录下的com.ryg.chapter_2.aidl包中有一个IBookManager.java的类,现在我们来分析一下,它继承了IInterface接口,所有可以在Binder中传输的接口都需要继承IInterface接口,我们在IBookManager.aidl里面声明的方法也会在IBookManager.java里面,同时还有两个整形的id分别用于标识这两个方法,接着,它声明了一个内部类Stub,这个Stub就是一个Binder类,当客户端和服务端都位于同一个进程时,方法调用不会走跨进程的transact过程,当两者位于不同进程时,方法调用需要走transact过程 ,这个逻辑由stub的内部代理Proxy来完成,核心就是内部类Stub和Stub的内部代理类Proxy,下面介绍两个类的每个方法的含义。
     aidl工具根据aidl文件自动生成的java接口的解析:首先,它声明了几个接口方法,同时还声明了几个整型的id用于标识这些方法,id用于标识在transact过程中客户端所请求的到底是哪个方法;接着,它声明了一个内部类Stub,这个Stub就是一个Binder类,当客户端和服务端都位于同一个进程时,方法调用不会走跨进程的transact过程,而当两者位于不同进程时,方法调用需要走transact过程,这个逻辑由Stub内部的代理类Proxy来完成。
     
1. asInterface(android.os.IBinder obj) :用于将服务端的Binder对象转换成客户端所需的AIDL接口类型的对象,这种转换过程是区分进程的, 如果客户端和服务端是在同一个进程中,那么这个方法返回的是服务端的Stub对象本身,否则返回的是系统封装的Stub.Proxy对象。
2. asBinder :返回当前Binder对象。
3. onTransact :这个方法运行在 服务端中的Binder线程池 中,当客户端发起跨进程请求时,远程请求会通过系统底层封装后交由此方法来处理。
这个方法的原型是 public Boolean onTransact(int code, Parcelable data, Parcelable reply, int flags)
服务端通过 code 可以知道客户端请求的目标方法,接着从 data 中取出所需的参数,然后执行目标方法,执行完毕之后,将结果写入到 reply 中。如果此方法返回false,说明客户端的请求失败,利用这个特性可以做权限验证(即验证是否有权限调用该服务)。

4.Proxy#[Method]:代理类中的接口方法,这些方法运行在客户端,当客户端远程调用此方法时,它的内部实现是:首先创建该方法所需要的参数,然后把方法的参数信息写入到_data中,接着调用transact方法来发起RPC请求,同时当前线程挂起;然后服务端的onTransact方法会被调用,直到RPC过程返回后,当前线程继续执行,并从_reply中取出RPC过程的返回结果,最后返回_reply中的数据。

        5、需要注意两点:首先,当客户端发起远程请求时,由于当前线程会被挂起直至服务器进程返回数据,所以如果一个远程方法很耗时的,那么不能在UI线程中发此远程请求,其次,由于服务器的Binder方法运动在Binder的线程池中,所以Binder方法不管是否耗时都应该采用同步的方式去实现,因为它已经运动在一个线程中了,下面给出Binder的工作机制图:

     《Android开发艺术探索》读书笔记 (2) 第2章 IPC机制_第1张图片

    如果搞清楚了自动生成的接口文件的结构和作用之后,其实是可以不用通过AIDL而直接实现Binder的,主席写的示例代码

    下面我们自己来写,首先写服务端的代码:

	private Binder mBinder = new IBookManager.Stub() {

        @Override
        public List getBookList() throws RemoteException {
            SystemClock.sleep(5000);
            return mBookList;
        }

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

(6)Binder的两个重要方法linkToDeathunlinkToDeath
Binder运行在服务端,如果由于某种原因服务端异常终止了的话会导致客户端的远程调用失败,所以Binder提供了两个配对的方法linkToDeathunlinkToDeath,通过linkToDeath方法可以给Binder设置一个死亡代理,当Binder死亡的时候客户端就会收到通知,然后就可以重新发起连接请求从而恢复连接了。
如何给Binder设置死亡代理呢?
1.声明一个DeathRecipient对象,DeathRecipient是一个接口,其内部只有一个方法bindeDied,实现这个方法就可以在Binder死亡的时候收到通知了。

private IBinder.DeathRecipient mDeathRecipient = new IBinder.DeathRecipient() {
@Override
public void binderDied() {
if (mRemoteBookManager == null) return;
mRemoteBookManager.asBinder().unlinkToDeath(mDeathRecipient, 0);
mRemoteBookManager = null;
// TODO:这里重新绑定远程Service
}
};

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

mRemoteBookManager.asBinder().linkToDeath(mDeathRecipient, 0);
      其中linkToDeath的第二个参数是个标记位,我们直接设为0即可,上面两步已经给Binder设置了死亡代理了,当Binder死亡的时候我们就可以收到通知了,另外,通过Binder的方法isBinderAlive也可以判断Binder是否死亡,这个是在客户端中连接成功了过后绑定的。、
     

2.4 Android中的IPC方式(其他进程通信的方式)

        (1) 使用Bundle :Bundle实现了Parcelable接口,Activity、Service和Receiver都支持在Intent中传递Bundle数据。
        (2) 使用文件共享 :这种方式简单,适合在对数据同步要求不高的进程之间进行通信,并且要妥善处理并发读写的问题。
SharedPreferences 是一个特例,虽然它也是文件的一种,但是由于系统对它的读写有一定的缓存策略,即在内存中会有一份SharedPreferences文件的缓存,因此在多进程模式下,系统对它的读写就变得不可靠,当面对高并发读写访问的时候,有很大几率会丢失数据,因此,不建议在进程间通信中使用SharedPreferences,下面使用文件的形式来进行文件共享。
 // 写入文件
private void persistToFile() {
        new Thread(new Runnable() {

            @Override
            public void run() {
                User user = new User(1, "hello world", false);
                File dir = new File(MyConstants.CHAPTER_2_PATH);
                if (!dir.exists()) {
                    dir.mkdirs();
                }
                File cachedFile = new File(MyConstants.CACHE_FILE_PATH);
                ObjectOutputStream objectOutputStream = null;
                try {
                    objectOutputStream = new ObjectOutputStream(
                            new FileOutputStream(cachedFile));
                    objectOutputStream.writeObject(user);
                    Log.d(TAG, "persist user:" + user);
                } catch (IOException e) {
                    e.printStackTrace();
                } finally {
                    MyUtils.close(objectOutputStream);
                }
            }
        }).start();
    }
// 从文件恢复
private void recoverFromFile() {
        new Thread(new Runnable() {

            @Override
            public void run() {
                User user = null;
                File cachedFile = new File(MyConstants.CACHE_FILE_PATH);
                if (cachedFile.exists()) {
                    ObjectInputStream objectInputStream = null;
                    try {
                        objectInputStream = new ObjectInputStream(
                                new FileInputStream(cachedFile));
                        user = (User) objectInputStream.readObject();
                        Log.d(TAG, "recover user:" + user);
                    } catch (IOException e) {
                        e.printStackTrace();
                    } catch (ClassNotFoundException e) {
                        e.printStackTrace();
                    } finally {
                        MyUtils.close(objectInputStream);
                    }
                }
            }
        }).start();
    }
(3) 使用Messenger :把数据放到Messager里面进行传递, Messenger 是一种轻量级的IPC方案,它的底层实现就是AIDL。Messenger是以串行的方式处理请求的,即服务端只能一个个处理,不存在并发执行的情形,详细的示例见原书,其实Messager的使用方法很简单,它只是对AIDL做了封装,Messager的构造函数。
public Messenger(Handler target) {
        mTarget = target.getIMessenger();
    }
public Messenger(IBinder target) {
        mTarget = IMessenger.Stub.asInterface(target);
    }
     实现一个Messenger分为如下几个步骤:
      1、服务端进程
           首先,我们需要在服务端创建一个Service来处理客户端的连接请求,同时创建一个Handler,并通过它来创建一个Message对象,然后在Service的onBind中返回这个Messager对象底层的Binder即可。
     2、客户端进程
 (4)使用AIDL(Android interface defination language)
     Messenger只能传递消息,无法实现方法的调用,而且只能一个一个处理,不能并发,AIDL可以实现方法调用,并发处理
    大致流程:首先在服务端创建一个Service来监听客户端连接,和AIDL接口,暴露给客户端的接口,最后在Service实现一个AIDL接口,首先建一个Service和一个AIDL接口,接着创建一个类继承自AIDL接口中的Stub类并实现Stub类中的抽象方法,在Service的onBind方法中返回这个类的对象,然后客户端就可以绑定服务端Service,建立连接后就可以访问远程服务端的方法了
       1.AIDL支持的数据类型:基本数据类型、StringCharSequenceArrayListHashMapParcelable以及AIDL
2.某些类即使和AIDL文件在同一个包中也要显式import进来,另外一个需要注意的地方,如果AIDL文件中用到了自定义的Parcelable对象,那么必须新建一个和它同名的AIDL文件,并在其中声明它为Parcelable类型,上面的IBookManager.aidl中,我们用到了Book这个类,所以,我们必须要创建Book.aidl,然后在里面添加如下内容
               package com.ryg.chapter_2.aidl;
               parcelable Book;
3.AIDL中除了基本数据类型,其他类型的参数都要标上方向:inout或者inout;in表示输入参数,out表示输出参数,inout表示输入输出参数,
4.AIDL接口中支持方法,不支持声明静态变量;
5.为了方便AIDL的开发,建议把所有和AIDL相关的类和文件全部放入同一个包中,这样做的好处是,当客户端是另一个应用的时候,可以直接把整个包复制到客户端工程中。
        比如我们希望服务端有新书的时候,主动去通知客户端,而不是客户端主动去获取,这是一种观察者模式,很常见的,这里面无法使用AIDL普通接口,需要新建一个IOnNewBookArrivedListener.aidl文件,从程序上来说就是调用所有IOnNewBookArrivedListener对象中的onNewBookArrived方法,我们在onDestory里面需要解除远程的Listener,会发现无法解除,因为不是同一个对象,这就需要用一RemoteCallBakList接口,
6.RemoteCallbackList是系统专门提供的用于删除跨进程Listener的接口。RemoteCallbackList是一个泛型,支持管理任意的AIDL接口,因为所有的AIDL接口都继承自IInterface接口。RemoteCallBackList很有用的功能,当客户端进程终止后,能够自动移除客户端的listenr,而且是线程同步的操作非常方便。专门用来放接口的
      特别注意一点:客户端调用远程服务的方法,被调用的方法运动在服务端的Binder线程池中,同时客户端线程会被挂起,这个时候如果服务端方法执行比较耗时,就会导致客户端线程长时间地阻赛,出现ANR,要避免在客户端的UI线程中去访问远程方法,由于客户端的onServiceConnected和onServiceDisconnected方法都运动在UI线程,所以不能在这里面调用服务端的方法,另外服务端的方法本身就运行在服务端的Binder线程池中,所以服务端方法本身可以执行大量耗时的操作,不要在服务端方法开启线程去进行异步任务,这时候就需要客户端开启线程去调用服务端的方法。同理,服务端在调用客户端的时候也不能耗时,由于客户端的IOnNewBookArrivedListener中的onNewBookArrived方法运行在客户端的Binder线程池中,所以不能在它里面去访问UI相关的内容,
      为了程序的健状性,要设置Binder意外死亡监听,第一种是设置DeathRecipient,另外一种是onServiceDisConnected,区别OnServiceDisConnected是在UI线程,DeathRecipient是binder线程池
      我们使用AIDL进行权限验证的两种方法:
    第一是在onBinder里面验证返回null
   第二种是在服务端的onTransact方法中进行权限验证,可以验证调用者的包名还有permission等
  (5)使用ContentProvide
       1.ContentProvider主要以表格的形式来组织数据,并且可以包含多个表;
2.ContentProvider还支持文件数据,比如图片、视频等,系统提供的MediaStore就是文件类型的ContentProvider;
3.ContentProvider对底层的数据存储方式没有任何要求,可以是SQLite、文件,甚至是内存中的一个对象都行;
4.要观察ContentProvider中的数据变化情况,可以通过ContentResolverregisterContentObserver方法来注册观察者;
        5、ContentProvider进程中,除了onCreate由系统回调并运动在主线程里外,其他五个方法均外界回调并运行在Binder线程池中
    (6)使用Socket
Socket是网络通信中“套接字”的概念,分为流式套接字和用户数据包套接字两种,分别对应网络的传输控制层的TCP和UDP协议。
    

2.5 Binder连接池

      (1)当项目规模很大的时候,创建很多个Service是不对的做法,因为service是系统资源,太多的service会使得应用看起来很重,所以最好是将所有的AIDL放在同一个Service中去管理。整个工作机制是:每个业务模块创建自己的AIDL接口并实现此接口,这个时候不同业务模块之间是不能有耦合的,所有实现细节我们要单独开来,然后向服务端提供自己的唯一标识和其对应的Binder对象;对于服务端来说,只需要一个Service,服务端提供一个queryBinder接口,这个接口能够根据业务模块的特征来返回相应的Binder对象给它们,不同的业务模块拿到所需的Binder对象后就可以进行远程方法调用了。
     Binder连接池的主要作用就是将每个业务模块的Binder请求统一转发到远程Service去执行,从而避免了重复创建Service的过程。
    (2)作者实现的Binder连接池BinderPool的实现源码,建议在AIDL开发工作中引入BinderPool机制。

2.6 选用合适的IPC方式

img

OK,本章结束,谢谢阅读。


你可能感兴趣的:(android)