2.IPC机制
线程是CPU调度的最小单元,同时线程是一种有限的系统资源。而进程一般指一个执行单元,在PC和移动设备上指一个程序或一个应用。一个进程可以包含多个线程。
任何一个操作系统都需要有相应的IPC机制,比如Windows上可以通过剪贴板、管道和邮槽等来进行进程间通信;Linux上可以通过命名管道、共享内容、信号量等来进行进程间通信。
除了Binder,Android还支持Socket,通过Socket也可以实现任意两个终端之间的通信。
在Android中一个应用中存在多个进程,首先,在Android中使用多进程只有一种方法,那就是给四大组件(Activity,Service,BroadcastReceiver,ContentProvider)在AndroidManifest中指定android:process属性,除此之外没有其他方法,也就是说我们无法给一个线程火灾一个实体类指定其运行时所在的进程。其实还有另一种非常规的多进程方法,那就是通过JNI在native层去fork一个新的进程。
进程名以":"开头的含义是指要在当前的进程名前面附加上当前的包名,这是一种简写的方法,属于当前应用的私有进程,其他应用的组件不可以和它跑在同一个进程中,而进程名不以":"开头的进程属于全局进程,其他应用通过share UID方式可以和它跑在同一个进程中。
另个应用通过share UID跑在同一个进程中是有要求的,需要这两个应用有相同的Share UID并且签名相同才可以。在这种情况下,它们可以互相访问对方的私有数据,比如data目录、组件信息等,不管它们是否跑在同一个进程中。当然如果它们跑在同一个进程中,那么除了能共享data目录、组件信息,还可以共享内存数据,或者说它们看起来就像是一个应用的两个部分。
Android为每一个应用分配了一个独立的虚拟机,或者说为每个进程都分配一个独立的虚拟机,不同的虚拟机在内存分配上有不同的地址空间,这就导致在不同的虚拟机中访问同一个类对象会产生多份副本。所有运行在不同进程中的四大组件,只要它们之间需要通过内存来共享数据,都会共享失败,这也是多进程所带来的主要影响。
一般来说,使用多进程会造成如下几方面的问题:
- 静态成员和单例模式完全失效
- 线程同步机制完全失效
- SharedPreferences的可靠性下降
- Application会多次创建
当一个组件跑在一个新的进程中的时候,由于系统要在创建新的进程同时分配独立的虚拟机,所以这个过程其实就是启动一个应用的过程。因此,相当于系统又把这个应用重新启动了一遍,既然重新启动了,那么自然会创建新的Application。这个问题其实可以这么理解,运行在同一个进程中的组件是属于同一个虚拟机和同一个Application的,同理,运行在不同进程中的组件是属于两个不同的虚拟机和Application的。
实现跨进程通信的方式很多,比如通过Intent来传递数据,共享文件和SharedPreferences,基于Binder的Messenger和AIDL以及Socket等。
静态成员变量属于类不属于对象,所以不会参与序列化过程;使用transient关键字标记的成员变量不参与序列化过程。
Parcelable也是一个接口,只要实现这个接口,一个类的对象就可以实现序列化并可以通过Intent和Binder传递。
Intent,Bundle,Bitmap等实现了Parcelable接口。
Serializable是Java中的序列化接口,其使用起来简单但是开销大,序列化和反序列化过程需要大量的I/O操作,而Parcelable是Android中的序列化方式,因此更适合用在Android平台上,它的缺点是使用其阿里稍微麻烦些,但是它的效率很高,这是Android推荐的序列化方式,因此要首选Parcelable。Parcelable主要用在内存序列化上,通过Parcelable将对象序列化到存储设备中或者将对象序列化后通过网络传输也都是可以的,但是这个过程会稍显复杂,因此在这两种情况下建议使用Serializable。
直观来说,Binder是Android中的一个类,它继承了IBinder接口。从IPC角度来说,Binder是Android中的一种跨进程通信方式,Binder还可以理解为一种虚拟的物理设备,它的设备驱动是/dev/binder,该通信方式在Linux中没有;从Android Framework角度来说,Binder是ServiceManager连接各种Manager(ActivityManager,WindowManager等等)和相应ManagerService的桥梁;从Android应用层来说,Binder是客户端和服务端进行通信的媒介,当bindService的时候,服务端会返回一个包含了服务端业务调用的Binder对象,通过这个Binder对象,客户端就可以获取服务端提供的服务或者数据,这里的服务包括普通服务和基于AIDL的服务。
生成类声明了两个在aidl中声明的方法,同时它还声明了两个整形的id分别用于标识这两个方法,这两个id用于标识在transact过程中客户端所请求的到底是哪个方法。接着,它又声明了一个内部类Stub,这个Stub就是一个Binder类,当客户端和服务端都位于同一个进程时,方法调用不会走跨进程的transact过程,而当两者位于不同进程时,方法调用需要走transact过程,这个逻辑是有Stub的内部代理类Proxy来完成。
- DESCRIPTOR,Binder的唯一标识,一般用当前Binder的类名表示。
- asInterface(android.os.IBinder obj)用于将服务端的Binder对象转换成客户端所需的AIDL接口类型的对象,这种转换过程是区分进程的,如果客户端和服务端位于同一进程,那么此方法返回的就是服务端的Stub对象本身,否则返回的是系统封装后的Stub.Proxy对象。
- asBinder此方法用于返回当前Binder对象。
- onTransact这个方法运行在服务端中的Binder线程池中,当客户端发起跨进程请求时,远程请求会通过系统底层封装后交由此方法来处理。该方法的原型为
public boolean onTransact(int code,android.os.Parcel data,android.os.Parcel reply,int flags)
。服务端通过code可以确定客户端所请求的目标方法是什么,接着从data中取出目标方法所需的参数(如果目标方法有参数的话),然后执行目标方法。当目标方法执行完毕后,就向reply中写入返回值(如果目标方法有返回值的话)。需要注意的是,如果此方法返回false,那么客户端的请求会失败。 - Proxy#getBookList,这个方法运行于客户端,当客户端远程调用此方法时,它的内部实现是这样的,首先创建该方法所需要的输入型Parcel对象
_data
,输出型Parcel对象_reply
和返回值对象List,然后把该方法的参数信息写入_data
中(如果有参数的话);接着调用transact方法来发起RPC请求,同时当前线程挂起;然后服务端的onTransact方法会被调用,直到RPC过程返回后,当前线程继续执行,并从_reply
中取出RPC过程的返回结果;最后返回_reply
中的数据。
当客户端发起远程请求时,由于当前线程会被挂起直至服务端进程返回数据,所以如果一个远程方法是很耗时的,那么不能在UI线程中发起此远程请求;由于服务端的Binder方法运行在Binder的线程池中,所以Binder方法不管是否耗时都应该采用同步的方式去实现,因为它已经运行在一个线程中了。
Binder运行在服务端进程,如果服务端进程由于某种原因异常终止,这个时候客户端到服务端的Binder连接断裂(称之为Binder死亡),会导致客户端的远程调用失败,更为关键的是,如果客户端不知道Binder连接已经断裂,那么客户端的功能就会受到影响。为了解决这个问题,Binder中提供了两个配对的方法linkToDeath和unlinkToDeath,通过linkToDeath,可以给Binder设置一个死亡代理,当Binder死亡时,客户端就会收到通知,这个时候客户端就可以重新发起连接请求从而恢复连接。
DeathRecipient是一个接口,其内部只有一个方法binderDied,当Binder死亡的时候,系统就会回调binderDied方法,这样就可以移除之前绑定的binder死亡代理并重新绑定远程服务。
SharedPreferences在底层实现上它采用的是XML文件来存储键值对。从本质上讲,SharedPreferences也属于文件的一种,但是由于系统对它的读/写有一定的缓存策略,即在内存中会有一份SharedPreferences文件的缓存,因此在多进程模式下,系统对它的读/写就变得不可靠,当面对高并发的读写访问,SharedPreferences有很大几率会丢失数据。因此,不建议在进程间通信中使用SharedPreferences。
Messenger是以串行的方式处理客户端发来的消息,如果大量的消息同时发送到服务端,服务端仍然只能一个个处理,如果有大量的并发请求,那么用Messenger就不太合适了。Messenger的作用主要是为了传递消息,需要跨进程调用服务端方法的场景Messenger就无法做到了。
AIDL文件支持的文件类型:
- 基本数据类型(int,long,char,boolean,double等)
- String和CharSequence
- List:只支持ArrayList,里面每个元素都必须能够被AIDL支持;
- Map:只支持HashMap,里面的每个元素都必须被AIDL支持,包括key和value;
- Parcelable:所有实现了Parcelable接口的对象
- AIDL:所有的AIDL接口本身也可以在AIDL文件中使用。
自定义的Parcelable对象和AIDL对象必须要显式地import进来,不管它们是否和当前的AIDL文件位于同一个包内。
AIDL中除了基本数据类型,其他类型的参数必须标上方向:in,out或者inout,in表示输入型参数,out表示输出型参数,inout表示输入输出型参数。
AIDL方法是在服务端的Binder线程池中执行的,因此当多个客户端同时连接时,会存在多个线程同时访问的情形,所以我们要在AIDL方法中处理线程同步。
Binder会把客户端传递过来的对象重新转化并生成一个新的对象。虽然我们在注册和解除注册过程中使用的是同一个客户端对象,但是通过Binder传递到服务端后,却会产生两个全新的对象。别忘了对象是不能跨进程直接传输的,对象的跨进程传输本质上都是反序列化的过程。
RemoteCallbackList是系统专门提供的用于删除跨进程listener的接口,RemoteCallbackList是一个泛型,支持管理任意的的AIDL接口。RemoteCallbackList<E extends IInterface>
,它的原理很简单,在它的内部有一个Map结构专门用来保存所有的AIDL回调,这个Map的key是IBinder类型,value是Callback类型。
虽说多次跨进程传输客户端的同一个对象会在服务端生成不同的对象,但是这些新生成的对象有一个共同点,那就是它们底层的Binder对象是同一个。当客户端解除注册的时候,只要遍历服务端所有的listener,找出那个和解除注册listener具有相同Binder对象的服务端listener并把它删除掉即可,这就是RemoteCallbackList为我们做的事。同时RemoteCallbackList还有一个很有用的功能,那就是当客户端进程终止后,它能够自动移除客户端所注册的listener。另外,RemoteCallbackList内部自动实现了线程同步的功能,所以我们使用它来注册和解除注册时,不需要做额外的线程同步工作。
使用RemoteCallbackList,有一点需要注意,我们无法像操作List一样去操作它,尽管它的名字中也带个List,但是它并不是一个List,遍历RemoteCallbackList,必须要按照下面的方式进行,其中beginBroadcast和finishBroadcast必须要配对使用,哪怕我们仅仅是要获取RemoteCallbackList中的元素个数。
int N = mRemoteCallbackList.beginBroadcast();
for(int i = 0; i < N; i++) {
IOnNewBookArrivedListener listener = mRemoteCallbackList.getBroadcastItem(i);
// TODO
}
mRemoteCallbackList.finishBroadcast();
客户端调用远程服务的方法,被调用的方法运行在服务端的Binder线程池中,同时客户端线程会被挂起,这个时候如果服务端方法执行比较耗时,就会导致客户端线程长时间的阻塞在这里,而如果这个客户端线程是UI线程的话,就会导致客户端ANR。要避免在客户端的UI线程中去访问远程方法。客户端的onServiceConnected和onServiceDisconnected方法都运行在UI线程中,所以也不可以在它们里面直接调用服务端的耗时方法。同理,当远程服务端需要调用客户端的listener中的方法时,被调用的方法也运行在Binder线程池中,只不过是客户端的Binder线程池。所以,同样不可以在服务端UI线程中调用客户端的耗时方法。
Binder是可能意外死亡的,这往往是由于服务端进程意外停止了,这时我们需要重新连接服务。有两种方法,第一种是给Binder设置DeathRecipient监听,当Binder死亡时,我们会收到binderDied方法的回调,在binderDied方法中我们可以重连远程服务,另一种方法是在onServiceDisconnected中重连远程服务。这两种方法可以随便选择一种来使用,它们的区别在于:onServiceDisconnected在客户端的UI线程中被回调,而binderDied在客户端的Binder线程池中被回调。也就是说,在binderDied中我们不能访问UI。
权限校验:
- permission checkCallingOrSelfPermission("com.solarexsoft.access_service")
- 包名校验 getPackageManager().getPackagesForUid(getCallingUid()); // Binder的onTransact方法中可以调用getCallingUid()获取到客户端所属的Uid和Pid。
创建一个自定义ContentProvider很简单,只需要继承ContentProvider类并实现六个抽象方法即可:onCreate,query,update,insert,delete和getType。getType用来返回一个Uri请求锁对应的MIME类型。这六个方法均运行在ContentProvider进程中,除了onCreate由系统回调并运行在主线程中,其他5个方法均由外界回调并运行在Binder线程池中。
ContentProvider主要以表格的形式来组织数据,并且可以包含多个表,对于每个表格来说,它们都具有行和列的层次性,行往往对应一条记录,而列对应一条记录中的一个字段,这点和数据库很类似。除了表格的形式,ContentProvider还支持文件数据,文件数据和表格数据的结构不同,因此处理这类数据时可以在ContentProvider中返回文件的句柄给外界从而让文件来访问ContentProvider中的文件信息。虽然ContentProvider的底层看起来很像一个SQLite数据库,但是ContentProvider对底层的数据存储方式没有任何要求,我们既可以使用SQLite数据库,也可以用普通的文件,甚至可以采用内存中的一个对象来进行数据的存储。
android:authorities
是ContentProvider的唯一标识,通过这个属性外部应用可以访问ContentProvider,因此必须是唯一的。ContentProvider的权限还可以细分为读权限和写权限,分别对应android:readPermission
和android:writePermission
属性。
可以使用UriMatcher的addURI方法将Uri和Uri code关联到一起。这样,当外界请求访问ContentProvider时,我们就可以根据请求的Uri来得到uri code,有了uri code我们就可以知道外界想要访问的事哪个表。
和query不同的是,update,insert,delete都会引起数据源的改变,这个时候需要通过ContentResolver的notifyChange方法来通知外界当前ContentProvider中的数据已经发生改变。要观察一个ContentProvider中的数据改变情况,可以通过ContentResolver的registerContentObserver方法来注册观察者,通过unregisterContentObserver方法来解除观察者。
SQLiteDatabase内部对数据库的操作是有同步处理的。
ContentProvider除了支持对数据源的增删改查这四个操作,还支持自定义调用,这个过程是通过ContentResolver的call方法和ContentProvider的call方法来完成的。