第二章 IPC机制详解(1)

2/13/2017来源:iOS开发人气:1316

第二章知识点大纲

1 ipC 含义就是进程间通信或者跨进程通信

线程是CPU调度的最小单位,同时线程也是一种有限的系统资源。 进程一般指的是一个执行单元,在PC和移动设备上指一个程序或者是一个应用。

一个进程可以有很多个线程,但只有一个线程的时候即为主线程,在android里也称为UI线程。UI线程里才能去操作界面元素。很多时候,一个进程需要执行大量耗时任务,如果这些任务放在主线程里,就会造ANR(应用程序无响应)。解决这个问题就需要用到线程,需要建立子线程通过Handle去操作些耗时操作或更新UI。

2 多进程模式分析

2.1 android中的多进程模式

想要使用多进程,只需在四大组件xml文件里使用android:PRocess的属性即可。

<activity android:name=".MainActivity" android:configChanges="orientation|screenSize" android:label="@string/app_name" android:launchMode="standard" > <intent-filter> <action android:name="android.intent.action.MAIN" /> </intent-filter> </activity> <activity android:name=".SecondActivity" android:configChanges="screenLayout" android:label="@string/app_name" android:process=":remote" /> <activity android:name=".ThirdActivity" android:configChanges="screenLayout" android:label="@string/app_name" android:process="com.ryg.chapter_2.remote" />

让我们来看看这三个Activity分别对应的进程是什么

可以看到有三个进程,分别表示在xml里声明的三个Activity,一次对应MainActivity,SecondActivity,ThirdActivity SecondActivity的进程名字是com.ryg.chapter_2:remote ,:的前面部分是当前进程是在的包名。 ThirdActivity所对应的进程com.ryg.chapter_2 .remote ,这个命名方式是完整的命名方式,不会附加包名。

开头的进程是私有进程,其他应用组件是不会和它跑在同一个进程里的,而不是以:开头的则是全局进程其他应用可以通过ShareUID的方式,可以跑在同一个进程里。

注:Android的系统会给每一个应用分配一个唯一的UID,具有相同的UID才能共享数据。若两个相同的UID想要跑在同一个进程里是有要求的,必须要有相同签名可以。这种情况下,他们可以互相访问私有数据,还可以共享数据。

2.2 多进程模式的运行机制

首先举个例子好让大家理解,在上面的基础上,我们再添加一个类,并且声明一个全局变量如下图 public class UserManager{ public int sUserId = 1;}

那么我们需要在MainActivity中去修改sUserId ,把值改为2 并且打印出来,在SecondActivity将sUserId打印出来,结果

按照常理来说SecondActivity应该输出2,因为MainActivity已经把值改为2了,而事实却是相反的,因为Android中每一个进程,都会为其分配一个DVM(虚拟机),所以每一个进程都是相对独立的,而MainActivity修改的值,仅对同一进程有效。SecondActivity和MainActivity并不在同一个进程中,所以MainActivity修改的值SecondActivity会接收不到。

所以这就是多进程带来的主要影响有以下几点 1. 静态成员和单例模式完全失效 2. 线程同步机制完全失效 3. SharePreferences的可靠性降低 4. application会被多次创建

第一个影响就是上面的例子,第二个影响也一样,既让都不在同一个虚拟机中,不再同一个进程中线程又怎么去同步呢。第三个影响就是Sharepreferences本生不支持两个进程同事去写,这样有一点的几率会造成数据的丢失。第四个影响每一个应用对于一个进程,对于一个虚拟机,对于一个Application,所以开启多个进程会开启多个虚拟机开启多个Application 是一样的道理。

3 IPC的基础概念

主要包括三个,serializable接口,parcelable接口以及binder

3.1 serializable的接口

Serializable 是java提供的一个序列化的接口,就是对对象进行序列化和反序列化的操作。首先想要实现序列化的操作需要让类去实现Serialezable接口并声明一个serialVersionUID即可,事实上serialVersionUID并不是必须的,不声明serialVersionUID也可以实现序列化。

public class User implements Serializable { private static final long serialVersionUID = 519067123721295773L; public int UserId; public String username; public boolean isMale; }

就完成对象的序列化操作,几乎所有的工作都让系统去自动完成。 如何对对象进行序列化和反序列化的操作,只需采用ObjectInputStream和ObjectOutputStream

//序列化的过程 String CHAPTER_2_PATH = Environment.getExternalStorageDirectory().getPath() + "/singwhatiwanna/chapter_2/"; String CACHE_FILE_PATH = CHAPTER_2_PATH + "usercache"; 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); } //反序列化过程 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); } }

这就是一个简单的使用,将对象输出到txt中就是序列化,将txt的数据读取出来就是反序列化。


android中Intent 是可以来传递一个序列化的对象

好了 之前说serialVersionUID 没有也可以序列化,但到底有什么用呢? 1. serialVersionUID就像是标识符,当你序列化的时候serialVersionUID的值也会被写入,在反序列化的时候 系统会通过对比类里的serialVersionUID与文件中的serialVersionUID 是否一致来进行反序列化,但一致的时候反序列化成功不一致则不成功。 2. 当你没声明serialVersionUID的时候,在反序列化的时候 修改了类那么反序列化会不成功 因为当前类与序列化时候的类不一样导致了反序列化的失败。(因为不声明serialVersionUID,系统会自动声明serialVersionUID,因为你进行类修改所以会导致 前后两次serialVersionUID的值不一样,所以反序列化失败) 3. 但是如果你手动声明了serialVersionUID,当你修改了序列化对象,对其进行增删改属性之后再进行反序列化,此时反序列化会是成功的,也是无论修改这样serialVersionUID的值都是一样的,系统会最大程度的去回复数据。

3.2 Parcelable的接口

Parcelable也是序列化的一种方式,使用Parcelable只需去实现Parcelable接口即可

public class User implements Parcelable { public int userId; public String userName; public boolean isMale; public User() { } public User(int userId, String userName, boolean isMale) { this.userId = userId; this.userName = userName; this.isMale = isMale; } public int describeContents() { return 0; } public void writeToParcel(Parcel out, int flags) { out.writeInt(userId); out.writeString(userName); out.writeInt(isMale ? 1 : 0); } public static final Parcelable.Creator<User> CREATOR = new Parcelable.Creator<User>() { public User createFromParcel(Parcel in) { return new User(in); } public User[] newArray(int size) { return new User[size]; } }; private User(Parcel in) { userId = in.readInt(); userName = in.readString(); isMale = in.readInt() == 1; } }

其实下列方法都会自动生成,不需要手打 既让Serializable 和Parcelable都可以序列化,那就说说两者之间的区别,Serializable 在序列化和反序列化的时候,需要进行大量 I/O操作,很耗时。Parcelable是android的序列化方式,他很高效,但是使用起来很麻烦。 Parcelable的性能比Serializable好,在内存开销方面较小,所以在内存间数据传输时推荐使用Parcelable,如activity间传输数据,而Serializable可将数据持久化方便保存,所以在需要保存或网络传输数据时选择Serializable,因为android不同版本Parcelable可能不同,所以不推荐使用Parcelable进行数据持久化

3.2 Binder

Binder就是android的一个类,它实现了IBinder的接口。从IPC角度上来说,就是Binder是Android的一种跨进程通信的方式。从Android Framework的角度来分析就是Binder 是ServiceManager用来连接各种Manager的(ActivityManager WindowManager)桥梁。从Android应用层的角度来分析就是客户端和服务端进行通信的媒介,当bindService的时候,服务端会调用一个Binder对象,通过这个对象,客户端就可以获取到一些数据。

在Android的开发中,Binder主要用在Service中,包括AIDL和Messenger。普通的Service中的Binder不涉及到进程通信,所以适用AIDL来分析Binder的工作机制。

先演示一下AIDL的建立和通过AIDL生成JAVA文件去看看Binder的机制

其中Book.java是自定义类,用途就是个Bean。如果需要使用自定义类 必须要建立一个同名的aidl文件。 系统可能会找不到我们创建的类,那么就是要一下声明

sourceSets { main { manifest.srcFile ='src/main/AndroidManifest.xml' java.srcDirs = ['src/main/java', 'src/main/aidl'] resources.srcDirs = ['src/main/java', 'src/main/aidl'] aidl.srcDirs = ['src/main/aidl'] res.srcDirs = ['src/main/res'] assets.srcDirs = ['src/main/assets'] } }

接下来 我们来看看生成的JAVA文件 去看看Binder的机制,先看看生成的java文件,代码确实比较多,我就截取一个方法为例子,我直接在代码上进行解释

public interface IBookManager extends android.os.IInterface { /** Stub是运行在服务端中的 */ public static abstract class Stub extends android.os.Binder implements com.example.gyh.myapplication.IBookManager { //这个属性就是Binder的唯一标识,也就是包名 private static final java.lang.String DESCRIPTOR = "com.example.gyh.myapplication.IBookManager"; /** Construct the stub at attach it to the interface. */ public Stub() { this.attachInterface(this, DESCRIPTOR); } //该方法就是为了将服务端的Binder转化为用户端的AIDL接口类型的对象,这种转化过程是区分进程的,若是同一个进程则就返回服务端本生的Stub对象,不是则转化成Stub.Proxy对象。 public static com.example.gyh.myapplication.IBookManager asInterface(android.os.IBinder obj) { if ((obj==null)) { return null; } android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR); if (((iin!=null)&&(iin instanceof com.example.gyh.myapplication.IBookManager))) { return ((com.example.gyh.myapplication.IBookManager)iin); } return new com.example.gyh.myapplication.IBookManager.Stub.Proxy(obj); } //该方法就是返回Binder对象 @Override public android.os.IBinder asBinder() { return this; } //该方法是运行在服务端的Binder线程池中的,当客户端发起跨进程请求的时候,远程请求会通过系统底层的封装后交由改方法进行处理。现在来解释一下里面的参数代表着什么意思 code表示客户端请求的方式是什么,data保存着方法的参数,reply是要返回的值,flags为false的时候,客户端请求失败,这样利用这参数做权限验证。 @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_addBook: { data.enforceInterface(DESCRIPTOR); com.example.gyh.myapplication.Book _arg0; if ((0!=data.readInt())) { _arg0 = com.example.gyh.myapplication.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.example.gyh.myapplication.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 void addBook(com.example.gyh.myapplication.Book book) throws android.os.RemoteException { //首先要创立三个对象,_data,_reply,_result , 然后如果有参数的话把参数写到_data中,接着调用transact方法去发起远程过程调用(RPC),同时将现场挂起(就是暂停的意思);然后服务端的ontransact调用,直到RPC过程返回后,该线程继续。并从_reply中取出返回值,并且赋予_result去返回。 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); } //transact回去调用stub中的ontransact方法,找到对应的方法也就是ontransact里的addBook方法,取出返回的_reply,从中取出客户端需要的返回值 mRemote.transact(Stub.TRANSACTION_addBook, _data, _reply, 0); _reply.readException(); } finally { _reply.recycle(); _data.recycle(); } } } static final int TRANSACTION_addBook = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0); } public void addBook(com.example.gyh.myapplication.Book book) throws android.os.RemoteException; }

通过上面的分析,我们可以知道客户端发起远程请求时,当前线程会被挂起直到服务端进程返回结果,这是一个耗时操作,所以不能再UI线程中发起。其实服务端的Binder是运行在Binder线程池中的,所以不管Binder是否耗时都应该是同步的。下图解释 那么我再来总的概括一下,当我们和服务器建立连接之后,通过服务器返回的Binder,将Binder转化为客户端的AIDL接口对象,因为是跨进程所以返回的是Stub.Proxy对象,通过这个对象去调用相应的方法如addBook,addBook方法中则会通过tranasct方法去调用Stub中onTranasct方法中对应的addBook,并通过_reply返回相应的数据给客户端。这样一来整个跨进程通信就结束了。


Binder的死亡代理

Binder的两个很重要的方法linkToDeath和unlinkToDeath,当我们服务端的进程由于某种原因断裂,那么对导致我们远程调用失败。但更为重要的是我们不知道Binder是否断裂,那么客户端的功能就会收到影响,所以Binder给我们配置了两个方法,通过linkToDeath我们可以设置一个死亡代理,当Binder死亡的时候,可以给客户端发来消息,从而我们可以重新开启服务。然后如何去设置这个代理呢?我们先来看看demo

首先声明一个DeathRecipent对象,DeathRecipent是一个接口,其内部只有一个方法binderDied,当我们实现该方法的时候,当Binder死亡的时候系统就会回调改方法,然后我们可以移除之前绑带的Binder去重新开去新的Binder

linkToDeath方法的第二个参数是标记位,我们直接可以设置为0,这样我们就设置好了死亡代理。另外我们可以通过Binder的isBinderAlive的方法去判断Binder是否死亡。