Android进程间通信
进程间通信几种方式:
文件
SharedPreferences
BroadcastReceiver
ContentProvider
AIDL
序列化
Serializable 和 Parcelable
(1)序列化 ID 的问题
(2)静态变量序列化
(3)Transient 关键字
(4)序列化存储规则
静态成员变量属于类,而不是对象,所以不会参与序列化;使用transient关键字标记的成员变量不参与序列化过程。
Serializable 序列化时会调用ObjectOutputStream.writeObject
ObjectInputStream.readObject()引用了大量反射机制,
自己实现序列化与反序列化过程需要重写writeObject()和readObject()
(5)多次writeObject问题
第二次写入对象时文件只增加了 5 字节,并且两个对象是相等的,这是为什么呢?
解答:Java 序列化机制为了节省磁盘空间,具有特定的存储规则,当写入文件的为同一对象时,并不会再将对象的内容进行存储,而只是再次存储一份引用,上面增加的 5 字节的存储空间就是新增引用和一些控制信息的空间。反序列化时,恢复引用关系,使得清单 3 中的 t1 和 t2 指向唯一的对象,二者相等,输出 true。该存储规则极大的节省了存储空间。
1、SharedPreferences
取SharedPreferences实际上是在ContextImpl这个类中完成的。
1、context.getSharedPreferences(pref_name, mode)的流程:
A 在sSharedPrefs这个map(同步的)中以pref_name为键取SharedPreferencesImp对象sp。如果sp不为空并且对应的pref文件未被异常修改,就返回这个对象。否则进入B。
B 如果sp为空,重新生成一个SharedPreferenceImp对象并且加入到sSharedPrefs这个map中。
C 同步的:从pref文件中解析出map对象并用之替换SharedPreferenceImp对象中原有的存放pref键值对的mMap成员对象。如果pref文件解析异常导致map为null,就保持原有对象而不替换。 如果备份的pref文件(…pref_name.xml.bak)存在,就使用备份文件。
D 返回SharedPreferenceImp对象sp。
注意:sSharedPrefs在程序中是静态的:private static final HashMap sSharedPrefs = new HashMap(); 如果退出了程序但Context没有被清掉,那么下次进入程序仍然可能取到同一个对象。
2、从SharedPreference中取值getString(String key, String defValue):
从SharedPreferencesImp对象的mMap成员对象中根据key取出相应的对象v。如果取得的对象v为空,返回默认对象defValue;否则,返回对象v。
3、commit过程:
A 在内存中提交,即用要提交的map去刷新已有的mMap对象。如果map对象中某个键的值指向editer对象自身,就代表要移除这个键值对。
B 将步骤A返回的MemoryCommitResult对象加入到写入本地的队列中,写入本地文件。在写入文件前,如果同名文件已经存在,则会原文件重命名为备份文件名,如果写入成功,才删除bak备份文件。
C 通知SharedPreferences的监听状态改变了。返回提交是否内存成功的状态。
4、EditorImpl内部类:
内部有一个Map成员对象mModified,用来保存将要提交的pref键值。
apply方法与commit方法的区别:前者先提交到内存中,再异步写到文件,并且不需要返回写入成功与否的状态;后者同步写入内存和文件。
5、MemoryCommitResult内部类:
用来存放Editor提交到内存的返回状态,包括是否有键值改变、将要写入文件中的map对象,写入文件成功与否等。
总结一下:要想及时并安全清除SharedPreferences一定要使用Editor去clear并commit,不要直接暴力地删除其xml文件。
6、commit()和apply()
writtenToDiskLatch是一个CountDownLatch,如果它的值大于0,那么commit在await上阻塞,在writtenToDiskLatch变为0时,才能继续往下走执行后面的notifyListeners。
writtenToDiskLatch初始值为1,在setDiskWriteResult执行完后,计数减1,await才会从阻塞中被唤醒继续往下执行。在writeToFile中,在真正完成写入后的地方会调用setDiskWriteResult。
再来看apply的实现,在另起一个线程把map中的数据写入到磁盘后,postWriteRunnable会被执行,它会去执行另一个Runnable – awaitCommit。awaitCommit中同样执行的是mcr.writtenToDiskLatch.await();,但要注意现在是在另一个线程中被执行的。
可见,writtenToDiskLatch保证了无论是commit还是apply,都必须在前一个的writeToFile完成后,才能开始新一个的commit或apply操作。
CountDownLatch
在实时系统中的使用场景
让我们尝试罗列出在java实时系统中CountDownLatch都有哪些使用场景。我所罗列的都是我所能想到的。如果你有别的可能的使用方法,请在留言里列出来,这样会帮助到大家。
- 实现最大的并行性:有时我们想同时启动多个线程,实现最大程度的并行性。例如,我们想测试一个单例类。如果我们创建一个初始计数为1的CountDownLatch,并让所有线程都在这个锁上等待,那么我们可以很轻松地完成测试。我们只需调用 一次countDown()方法就可以让所有的等待线程同时恢复执行。
- 开始执行前等待n个线程完成各自任务:例如应用程序启动类要确保在处理用户请求前,所有N个外部系统已经启动和运行了。
- 死锁检测:一个非常方便的使用场景是,你可以使用n个线程访问共享资源,在每次测试阶段的线程数目是不同的,并尝试产生死锁。
HashMap
SparseArray
ArrayMap
2、BroadcastReceiver
它和Binder机制不一样的地方在于,广播的发送者和接收者事先是不需要知道对方的存在的
BroadcastReceiver(注册过程,发送接收)
(1)注册方式 静态注册,动态注册
(2)广播分类 粘性广播,有序广播,无序广播
(3)前台广播(10s超时)、后台广播(60s超时)(intent.FLAG_RECEIVER_FOREGROUND)
(4)并行队列 串行队列(BroadcastQueue)
1) 为intent添加FLAG_EXCLUDE_STOPPED_PACKAGES标记;
2) 处理和package相关的广播;
3) 处理其他一些系统广播;
4) 判断当前是否有权力发出广播;
5) 如果要发出sticky广播,那么要更新一下系统中的sticky广播列表;
6) 查询和intent匹配的静态receivers;
7) 查询和intent匹配的动态receivers;
8) 尝试向并行receivers递送广播;
9) 整合(剩下的)并行receivers,以及静态receivers,形成一个串行receivers表;
10) 尝试逐个向串行receivers递送广播。
RegistReceiver
SendBroadcast
图:客户进程中的mReceivers表
mReceivers
receives_list
该表的key项是我们比较熟悉的Context,也就是说可以是Activity、Service或Application。而value项则是另一张“子哈希表”。这是个“表中表”的形式。言下之意就是,每个Context(比如一个activity),是可以注册多个receiver的,这个很好理解。mReceivers里的“子哈希表”的key值为BroadcastReceiver,value项为ReceiverDispatcher,示意图如下:
到这里其实开始走两个分支,
1,如果是动态注册的receiver,其实可以知道,运行进程是活的,接收对象是存在的,deliverToRegisteredReceiverLocked(r, (BroadcastFilter)target, false),
2,如果是静态注册的receiver,运行进程不确定是否存活,接收对象不存在,如果进程未拉起,mService.startProcessLocked(),启动接收进程然后继续scheduleBroadcastsLocked()循环,下次进入processCurBroadcastLocked(r, app)。既如果目标进程未启动,这里是会拉起来的。如果进程已启动,则processCurBroadcastLocked(r, app)分发广播。
4、AIDL
AIDL(Messenger) in out inout定向tag
支持数据类型如下:
- Java 的原生类型
- String 和CharSequence
- List 和 Map ,List和Map 对象的元素必须是AIDL支持的数据类型; 以上三种类型都不需要导入(import)
- AIDL 自动生成的接口 需要导入(import)
- 实现android.os.Parcelable 接口的类. 需要导入(import)。
AIDL中in,out和inout的区别
(1) in:只能在客户端设置值,传入服务端,服务端获取客户端设置的值
out:用于在服务端设置值,服务端设置这个值后,客户端也可以得到这个由服务端设置的值,客户端如果有设置初始值,到了服务端会得不到这个值
inout:服务端可以得到客户端设置的值,客户端也可以得到服务端设置的值
基本数据类型参数只能是in类型
IInterface IBinder Binder Bundle Parcel Parcelable
IBinder是远程对象的基本接口,是为高性能而设计的轻量级远程调用机制的核心部分。但它不仅用于远程调用,也用于进程内调用。这个接口定义了与远程对象交互的协议。不要直接实现这个接口,而应该从Binder派生。
IBinder的主要API是transact(),与它对应另一方法是Binder.onTransact()。第一个方法使你可以向远端的IBinder对象发送发出调用,第二个方法使你自己的远程对象能够响应接收到的调用。IBinder的API都是同步执行的,比如transact()直到对方的Binder.onTransact()方法调用完成后才返回。调用发生在进程内时无疑是这样的,而在进程间时,在IPC的帮助下,也是同样的效果。
通过transact()发送的数据是Parcel,Parcel是一种一般的缓冲区,除了有数据外还带有一些描述它内容的元数据。元数据用于管理IBinder对象的引用,这样就能在缓冲区从一个进程移动到另一个进程时保存这些引用。这样就保证了当一个IBinder被写入到Parcel并发送到另一个进程中,如果另一个进程把同一个IBinder的引用回发到原来的进程,那么这个原来的进程就能接收到发出的那个IBinder的引用。这种机制使IBinder和Binder像唯一标志符那样在进程间管理。
系统为每个进程维护一个存放交互线程的线程池。这些交互线程用于派送所有从另外进程发来的IPC调用。例如:当一个IPC从进程A发到进程B,A中那个发出调用的线程(这个应该不在线程池中)就阻塞在transact()中了。进程B中的交互线程池中的一个线程接收了这个调用,它调用Binder.onTransact(),完成后用一个Parcel来做为结果返回。然后进程A中的那个等待的线程在收到返回的Parcel后得以继续执行。实际上,另一个进程看起来就像是当前进程的一个线程,但不是当前进程创建的。
Binder机制还支持进程间的递归调用。例如,进程A执行自己的IBinder的transact()调用进程B的Binder,而进程B在其Binder.onTransact()中又用transact()向进程A发起调用,那么进程A在等待它发出的调用返回的同时,还会用Binder.onTransact()响应进程B的transact()。总之Binder造成的结果就是让我们感觉到跨进程的调用与进程内的调用没什么区别。
当操作远程对象时,你经常需要查看它们是否有效,有三种方法可以使用:
1 isBindAlive()。
2 如果目标进程不存在,那么调用pingBinder()时返回false。
3 可以用linkToDeath()方法向IBinder注册一个IBinder.DeathRecipient,在IBinder代表的进程退出时被调用。
要实现IBinder来支持远程调用,应从Binder类派生一个类。Binder实现了IBinder接口。但是一般不需要直接实现此类,而是跟据你的需要由开发包中的工具生成,这个工具叫AIDL。你通过AIDL语言定义远程对象的方法,然后用AIDL工具生成Binder的派生类,然后就可使用之。然而,可是,但是,当然,你也可以直接从Binder类派生以实现自定义的RPC调用,或只是实例化一个原始的Binder对象直接作为进程间共享的令牌来使用。
1、客户端发出绑定请求,服务端返回一个Binder对象,该对象能处理跨进程请求,而客户端拿到的是Binder对象的引用,Binder的实体是在服务端的。客户端执行asInterface()方法,如果客户端和服务端处于同一进程,则直接返回服务端的Stub对象本身,如果处于不同进程,则返回的是Stub.proxy代理类对象。
2、客户端发送远程请求(addPerson或者getPersonList),此时客户端线程挂起,Binder拿到数据后,对数据进行处理如在不同进程,会把数据写入Parcel,调用Transact方法。
3、触发onTransact方法,该方法运行在Binder线程池,方法中会调用到服务端实现的接口方法,当数据处理完毕后,返回reply值,经过Binder返回客户端,此时客户端线程被唤醒。
am_proxy.jpg
AMS基本架构