这些读书笔记是根据《Android开发艺术探索》和《Android群英传》这两本书,然后将书上的一些知识点做一下记录。方便学习和理解,如果有存在侵犯版权的地方,还麻烦告知。个人强烈建议购买这两本书。真心不错。
本节是和《Android开发艺术探索》中的第2章 “IPC机制” 有关系,建议先买书查看这一章。
[]IPC简介
IPC:进程间通信或者跨进程通信,是指两个进程之间进行数据交换的过程。
线程:是CPU调度的最小单元,
进程:在移动设备上是指一个应用,
一个进程中可以包含多个线程,因此线程和进程是包含和被包含的关系。最简单的情况下,一个进程中可以只有一个线程,即主线程。在android中主线程也叫UI线程,在UI线程里才能操作界面元素。
应用无响应(ANR):如果把一些耗时的任务放到主线程中去执行会造成界面无法响应,解决方法:把一些耗时的任务放到线程中执行。
使用IPC的场景:
(1)应用因为某些原因自身需要采用多进程模式来实现,至于原因有很多,比如有些模块由于特殊原因需要运行在单独的进程中,又或者为了加大一个应用可使用的内存所以需要通过多进程来获取多份内存空间。
(2)当前应用需要向其它应用获取数据,由于是两个应用,所以必须采用跨进程的方式来获取所需数据。甚至我们通过系统提供的ContentProvider去查询数据时,也是一种进程间通信,只不过通信细节被系统内部屏蔽了。
[]Android的多进程模式
()开启多进程模式
开启多进程的方式:
(1)给四大组件(Activity、Service、ContentProvider、BroadcastReceiver)在AndroidManifest.xml中指定android:process属性,也就是说无法给一个线程或者一个实体类指定其运行时所在的进程。
(2)通过JNI在native层去fork一个新的进程,这种属于特殊情况,也不是常用的创建多进程的方式。
<application
......
>
<activity android:name=".MainActivity">
......
</activity>
<activity
android:name=".SecondActivity"
android:process=":remote" />
<activity
android:name=".ThirdActivity"
android:process="com.hhb.text.remote" />
</application>
SecondActivity和ThirdActivity指定不同的process属性值,所以它们运行在不同的进程中,假如应用的包名是com.lfy.text。那么MainActivity运行在”com.lfy.text”进程里(没有指定process属性,那么运行在默认的进程中,默认进程的进程名是包名),SecondActivity运行在”com.lfy.text:remote”进程里,ThirdActivity运行在”com.hhb.text.remote”进程里。
android:process值中的”:”是指在当前的进程名前面附加上当前的包名,其次,进程名以”:”开头的进程属于当前应用的私有进程,其他应用的组件不可以和它跑在同一进程中,而进程名不以”:”开头的的进程属于全局进程,其他应用通过ShareUID方式可以和它跑在同一个进程中。
Android系统会为每个应用分配一个唯一的UID,具有相同的UID应用才能共享数据,需要说明的是,两个应用通过ShareUID跑在同一个进程中是有条件的,需要两个应用有相同的ShareUID并且签名相同才可以,在这种情况下,它们可以互相访问对方的私有数据,比如data目录,组件信息等,不管它们是否跑在同一个进程中,当然如果它们跑在同一个进程中,那么除了能共享data目录,组件信息,还可以共享内存数据,或者说它们看起来就像是一个应用的两个部分。
()多进程模式的运行机制
Android为每一个应用分配了一个独立的虚拟机,或者说为每个进程都分配一个独立的虚拟机,不同的虚拟机在内存分配上有不同的地址空间,这就导致在不同的虚拟机中访问同一个对象会产生多分副本。
所有运行在不同进程中的四大组件,只要它们之间需要通过内存来共享数据,都会共享失败,这也是多进程所带来的主要影响。
一般来说,使用多进程会造成有以下几个方面的问题
(1)静态成员和单例模式完全失效
(2)线程同步机制完全失效
(3)SharedPreferences的可靠性下降
(4)Application会多次创建
第一个问题,每个进程都分配一个独立的虚拟机,不同的虚拟机中访问同一个对象会产生多分副本,
第二个问题,既然都不是一块内存了,那么不管是锁对象还是锁全局类都无法保证线程同步,因为不同进程锁的不是同一个对象。
第三个问题,因为SharedPreferences不支持两个进程同时去执行写操作,否则会导致一定几率的数据丢失,这是因为SharedPreferences底层是通过读/写xml文件来实现的,并发写显然是可能出问题,甚至并发读/写都有可能出问题。
第四个问题,当一个组件跑到一个新的进程中的时候,由于系统要在创建新的进程同时分配独立的虚拟机,所以这个进程其实就是启动一个应用的过程,因此,相当于系统又把这个应用重新启动了一遍,既然重新启动了,那么自然会创建新的Application。其实这个问题可以理解为:运行在同一进程中的组件是属于同一个虚拟机和同一个Application的,同理,运行在不同进程的组件是属于两个不同虚拟机和Application的。可以在Application的onCreate()来验证下。
[]IPC基础概念介绍
()Serializable接口
Serializable是java提供的一个序列化接口,它是一个空接口,为对象提供标准的序列化和反序列化操作。
public class People implements Serializable {
private static final long serialVersionUID = -3186293159658835879L;
private int userId;
private String userName;
private boolean isMale;
}
想让一个对象实现序列化,只需要这个类实现Serializable接口并声明一个serialVersionUID即可,甚至这个serialVersionUID也不是必需的。serialVersionUID可以手动指定,也可以通过Android Studio来生成。
serialVersionUID是用来辅助序列化和反序列化过程的。原则上序列化后的数据中的serialVersionUID只有和当前类的serialVersionUID相同才能够正常地被反序列化。
serialVersionUID的工作机制是:序列化时系统会把当前类的serialVersionUID写入序列化的文件中(也可能是其他中介),当反序列化的时候系统会去检测文件中的serialVersionUID,看它是否和当前类的serialVersionUID一致,如果一致就说明序列化的类的版本和当前类的版本是相同的,这时可以成功反序列化,否则就说明当前类和序列化的类相比发生了某些变换,无法正常反序列化。
如果没有指定serialVersionUID,反序列化时当前类有所变化,比如增加或者删除某些成员变量,那么系统就会重新计算当前类的hash值并把它赋值给serialVersionUID,这时当前类的serialVersionUID和序列化的数据中的serialVersionUID不一致,于是反序列化失败。程序就会出现crash。如果类的结构发生非常规性的改变,比如修改了类名,修改了成员变量的类型,这时尽管serialVersionUID验证通过了,但是反序列化过程还是会失败,因为类结构有了毁灭性的改变,根本无法从老版本的数据中还原出一个新的类结构的对象。
对象的序列化(将对象写到文件)和反序列化(将对象从文件中读取出来),只需要采用ObjectOutputStream和ObjectInputStream来实现。
静态成员变量属于类不属于对象,所以不会参与序列化过程。其次用transient关键字标记的成员变量不参与序列化过程。
通过实现如下两个方法即可重写系统默认的序列化和反序列化过程
//ObjectOutputStream类
public final void writeObject(Object obj) throws IOException {
......
}
//ObjectInputStream类
public final Object readObject()throws IOException, ClassNotFoundException
{
......
}
()Parcelable接口
Parcelable也是一个接口,只要实现这个接口,一个类的对象就可以实现序列化和反序列化。
public class User implements Parcelable {
private int userId;
private String userName;
private boolean isMale;
private Book book;
public User(int userId, String userName, boolean isMale, Book book) {
this.userId = userId;
this.userName = userName;
this.isMale = isMale;
this.book = book;
}
/**
* 内容描述
*
* @return
*/
@Override
public int describeContents() {
return 0;
}
/**
* 序列化过程 写的过程
*
* @param dest
* @param flags
*/
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(userId);
dest.writeString(userName);
dest.writeInt(isMale ? 1 : 0);
dest.writeParcelable(book, 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];
}
};
/**
* 由于book是另一个可序列化对象,所以它的反序列化过程需要传递当前线程的上下文类加载器,
* 否则会报无法找到类的错误
* @param in
*/
private User(Parcel in) {
this.userId = in.readInt();
this.userName = in.readString();
this.isMale = in.readInt() == 1;
this.book = in.readParcelable(Thread.currentThread().getContextClassLoader());
}
}
序列化由writeToParcel()来完成,最终通过Parcel对象的一系列write…()来完成,反序列化由CREATOR来完成,其内部标明如何创建序列化对象和数组。并通过Parcel对象的一系列read…()来完成。
createFromParcel(Parcel in):从序列化后的对象中创建原始对象
newArray(int size):创建指定长度的原始对象数组
User(Parcel in):从序列化后的对象中创建原始对象
writeToParcel(Parcel dest, int flags):将当前对象写入序列化结构中,其中flags标识有两种值:0或者1。为1时标识当前对象需要作为返回值返回,不能立即释放资源。几乎所有情况都返回0。
describeContents():返回当前对象的内容描述,如果含有文件描述符,返回1。否则返回0,几乎所有情况都返回0。
系统提供了许多实现Parcelable接口的类,它们都是可以直接序列化的。比如:Intent,Bundle,BitMap等。
Serializable和Parcelable的区别:Serializable是Java中的序列化接口,使用简单但是开销大,序列化和反序列化过程需要大量的I/O操作,主要用在将对象序列化到存储设备中或者将对象序列化后通过网络传输。Parcelable是Android的序列化接口,使用稍微麻烦,但是效率很高。主要用在内存序列化。
()Binder
Binder是实现了IBinder接口的一个类。Binder是Android中的一种跨进程通信方式。Binder主要用于Service中,Binder是客户端和服务端进行通信的媒介,当bindService()时,服务端会返回一个包含服务端业务调用的Binder对象,通过这个Binder对象,客户端就可以获取服务端提供的服务(普通服务和基于AIDL的服务)或者数据。
Messenger的底层是AIDL来实现的,AIDL的底层是Binder来实现的。所以选择新建一个AIDL来分析Binder原理。
新建Book.java、Book.aidl、IBookManager.aidl。
//Book.java
public class Book implements Parcelable {
public int bookId;
public String bookName;
public Book(int bookId, String bookName) {
this.bookId = bookId;
this.bookName = bookName;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(bookId);
dest.writeString(bookName);
}
public static final Creator<Book> CREATOR
= new Creator<Book>() {
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();
}
}
// Book.aidl
package com.hhb.text.aidl;
parcelable Book;
// IBookManager.aidl
package com.hhb.text.aidl;
import com.hhb.text.aidl.Book;
interface IBookManager {
List<Book> getBookList();
void addBook(in Book book);
}
Book.aidl是Book.java在AIDl的声明,IBookManager.aidl是定义的接口。
注:虽然Book.aidl和IBookManager.aidl在相同的包下,但是在IBookManager.aidl仍然还是要导入Book.aidl。
系统在build\generated\source\aidl\debug目录下生成IBookManager.Java类。
IBookManager继承IInterface接口的接口,所以可以在Binder中传输的接口都要继承IInterface接口。
IBookManager首先声明了两个方法getBookList()和addBook()。其次声明了一个内部静态抽象类Stub。 Stub继承Binder并实现IBookManager,所以Stub就是Binder类。 Stub中声明了两个整型的id分别用于标识getBookList()和addBook(),这两个id用于标识在onTransact()中客户端请求的到底是哪个方法。当客户端和服务端都位于同一个进程时,方法调用不会走跨进程的onTransact()。当两者位于不同进程时,方法调用需要走onTransact(),这个逻辑由Stub的内部代理类Proxy来完成。
DESCRIPTOR
Binder的唯一标识,一般用当前Binder的类名来表示。
asInterface(android.os.IBinder obj)
用于将服务端的Binder对象转换成客户端所需的AIDL接口类型的对象,如果客户端和服务端都位于同一个进程,那么返回的就是服务端的Stub对象本身。否则返回的是系统封装后的Stub.Proxy对象。
asBinder()
返回当前Binder对象
onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags)
这个方法运行在服务端的Binder线程池中,当客户端发起跨进程请求时,远程请求会通过系统底层封装后交由此方法来处理。它的内部实现:服务端通过code可以确定客户端所请求的目标方法是什么,接着从data中取出目标方法所需的参数,然后执行目标方法。当目标方法执行完毕后,就向reply中写入返回值。如果返回false,那么客户端的请求会失败,因此可以利用这个特性来做权限验证,不让任何一个进程都能远程调用我们的服务。
Proxy#getBookList() Proxy#addBook(Book book)
这个方法运行在客户端,当客户端远程调用此方法时,它的内部实现:首先创建该方法所需要的输入型Parcel对象_data、输出型Parcel对象_reply和返回值对象_result。然后把该方法的参数信息写入_data中;接着调用transact()来发起PRC(远程过程调用)请求,同时当前线程挂起。然后服务端的onTransact()会被调用,直到PRC过程(远程过程调用)返回后,当前线程继续执行,并从_reply中取出PRC过程(远程过程调用)的返回结果,最后返回_reply中的数据。
注:
()当客户端发起远程请求时,由于当前线程会被挂起直至服务端进程返回数据,所以如果一个远程方法时很耗时的,那么不能再UI线程中发起此远程请求。
()由于服务端的onTransact()运行在Binder线程池中,所以Binder方法不管是否耗时都应该采用同步的方式去实现,因为它已经运行在一个线程中。
Binder运行在服务端进程中,如果服务端进程由于某种原因异常终止,这时客户端到服务端的Binder连接断裂(或者称为Binder死亡),会导致远程调用失败。
解决方案:Binder提供了配对的linkToDeath()和unlinkToDeath(),通过linkToDeath()给Binder设置一个死亡代理(DeathRecipient对象)。当Binder死亡时会回调binderDied(),这时可以移除之前绑定的Binder代理并重新绑定远程服务。
DeathRecipient mDeathRecipient=new DeathRecipient(){
@Override
public void binderDied() {
//绑定之前绑定的Binder
Binder对象.unlinkToDeath(mDeathRecipient,0);
//重新绑定远程Service
......
}
};
//设置死亡代理
Binder对象.unlinkToDeath(DeathRecipient recipient,0);
[]Android的IPC方式
()使用Bundle
三大组件(Activity、Service、BroadcastReceiver)都是支持在Intent传递Bundle数据,由于Bundle实现了Parcelable接口,所以它可以方便地在不同的进程间传输,但是,传输的数据必须能够被序列化,比如基本类型、实现Parcelable接口对象、实现Serializable接口对象以及一些Android支持的特殊对象,具体的可以看Bundle类支持的类型。Bundle不支持的类型无法通过它在进程间传递数据。
除了直接传递数据这种场景,还有一种特殊的使用场景,比如A进程正在计算,计算完成后它要启动B进程的一个组件并把计算结果传递个B进程,但是计算结果不支持放入Bundle中,因此无法通过Intent来传输。解决方案:通过Intent启动进程B的Service组件,让Service在后台计算,计算完毕后再启动B进程中真正要启动的目标组件,由于Service也运行在B进程中,所以目标组件就可以直接获取计算结果。这种方式的核心思想在于将原本需要在A进程的计算任务转移到B进程的后台Service中去执行。
()使用文件共享
两个进程通过读/写同一个文件来交换数据,比如A进程把数据写入文件,B进程通过读取这个文件来获取数据。由于Android系统基于Linux,使得其并发读/写文件可以没有限制地进行。甚至两个线程同时对同一个文件进行写操作都是允许的,尽管这可能出现问题,通过文件交换数据很好使用,除了可以交换一些文本信息外,还可以序列化一个对象到文件系统中的同时从另一个进程中恢复这个对象。
文件共享方式对文件格式没有具体要求,可以是文本文件,也可以是XML文件,只要读/写双方约定数据格式即可。通过文件共享的方式也是有局限性的,比如并发读/写的问题,如果并发读/写,那么读出的内容就有可能不是最新的,如果是并发写的话就更加严重,因此要尽量避免并发写的这种情况发生或者考虑使用线程同步来限制多个线程的写操作。
文件共享方式适合在对数据同步要求不高的进程之间进程通信,并且要妥善处理并发读/写的问题。
SharedPreferences是Android中提供的轻量级存储方案,通过键值对的方式来存储数据,在底层实现上它采用XML文件来存储键值对,每个应用的SharedPreferences文件都在当前包所在的data/data/package name/shared_prefs目录下,从本质来说,SharedPreferences也属于文件的一种,但是由于系统对它的读/写有一定的缓存策略,即在内存中会有一份SharedPreferences文件的缓存。因此在多进程模式下,系统对它的读/写就变得不可靠,当面对高并发的读/写访问,SharedPreferences有很大的几率会丢失数据,因此,不建议在进程间通信中使用SharedPreferences。