模式定义
定义一个创建对象的的接口,然后不同类型的对象提供该接口不同的实现,对象的具体实现交给接口的具体实现类去完成。
模式示例
下面以我们常见的文档处理为例,来说明工厂方法模式。
- 定义文档接口:文档支持打开操作和关闭操作。
- 创建文档:我们要创建两种类型文档,pdf格式文档和txt格式文档
- 定义一个工厂接口,提供一个创建文档的方法。
- 提供pdf文档和txt文档的工厂接口实现。
- 定义Client来检验模式的运行结果。
做一个有追求的青年
https://rainmonth.github.io/posts/A180505.html
Android Activity启动模式详解
https://rainmonth.github.io/posts/A180504.html
摘要
本文内容有点多,主要介绍View的基础知识、View的事件分发机制、从源码分析View的事件分发机制以及滑动冲出的处理这几方面的知识。
https://rainmonth.github.io/posts/A180503.html
状态栏攻略
[TOC]
IPC,即Inter-Process Communication,进程间通信(也叫跨进程通信),可以是两个应用(每个应用是一个进程),亦可以是一个应用中的两个进程。由于不同的进程有不同的内存地址分配,所以多进程在通信的时候会发生一些问题,本文从介绍多进程通信可能引发的问题开始,慢慢介绍IPC的基本概念、具体的IPC方式及其具体实现,并在最后对各种通信方式做了简单的比较。
方法一使用示例
1 | <?xml version="1.0" encoding="utf-8"?> |
然后在MainActivity中分别开启SecondActivity和ThirdActivity,这时候在Android Studio的logcat中即可看到三个如rainmonth.cn.ipcdemo有关的进程,分别是:
上面两种开启多进程方式的区别:
补充说明:
- 如果process(即主进程的私有进程)被杀死了,对主进程的存货没有影响(这里的没影响指的是不会被干掉)
- 如果主进程被杀死了,对主进程的私有进程process也没有影响(这里的没影响指的是不会被干掉)
- 如果需要在主进程被干掉的同时也干掉其私有进程,可以通过遍历当前应用存在的进程,然后手动kill,注意,主进程必须最后再kill
先给出结论,稍后论证:
为什么会出现上面的问题,Android为每一个应用(进程)都分配了一个独立的虚拟机,不同虚拟机在内存上对应不同的地址空间,这就导致不同虚拟机在访问同一个类的对象是会产生多个副本,然后问题就发生了。
上面的问题发生的原因,归根结底都是因为不同的进程间不能共享内存,多以我们可以绕过这个问题,通过其他方式来共享数据,比如:
在了解这些方式之前,先了解一些基本概念,有助于更好的理解进程间的通信方式
利用Intent传值或利用共享文件传值都涉及到对象的序列化问题,而Android中可用的序列号方式有如下两种:
Serializable接口和Parcelable接口
那为什么现在实现Serializable接口不用指定serialVersionUID了呢?
serialVersionUID是用来辅助序列化和反序列化过程的,序列化的时候系统会把当前类的serialVersionUID写入序列化的文件中,当反序列化的时候系统会检测当前文件中的serialVersionUID是否和当前类的serialVersionUID一致,如果一致这个时候可以反序列化成功,否则就说明当前类和序列化的类相比发生了某些变换,比如成员变量的数量、类型可能发生了改变,这个时候是无法正常反序列化的
Java提供的空接口,用来持久化对象,实现时最好指定serialVersionUID,有助于反序列化,如果没有指定,系统会根据当前类的结构自动生成它的hash值,并将其赋值给serialVersionUID。下面给出示例:
序列化
1 | private void serializableObject(User user) { |
反序列化
1 | private void deserializableObject(String fileName) { |
要注意三点:
手动指定serialVersionUID可以很大程度上避免反序列化失败
类的静态变量不参与序列化过程(为什么,因为它并不被该类的某一个对象所特有)
transient
修饰的变量不参与序列过程
继承给接口,实现CREATOR完成反序列化过程,实现writeToParcel完成序列化过程,具体代码实现如下:
1 | public class User2 implements Parcelable { |
注意:Parcel的writeBoolean方法已经不可见了
两种方式比较:
建议:如果是内存中的序列化操作,建议使用Parcelable,如果是要序列化存储,建议采用Serializable
如果说序列化是前面两种通信方式的基础,那么Binder就是第三种(AIDL和Messenger)的实现基础。
把这个单独拎出来是因为我在使用的时候出现了问题。步骤:
在app module下新建AIDL文件(Android Studio提供了对应模板)这时main目录下会有一个aidl文件夹,同时里面会有对应的aidl文件,如下图
三个文件的内容分别如下:
Book.java
1 | package rainmonth.cn.ipcdemo; |
Book.aidl
1 | // Book.aidl |
IBookManager.aidl
1 | // IBookManager.aidl |
运行项目,在app/build/generated下会生产对应的Java文件,如下图:
其内容如下(格式化后的内容):
1 | /* |
类的结构如上图所示,结合上面的代码和截图做以下说明:
其实AIDL(Android Interface Define Language)文件只是一套接口定义模板,我们完全可以自己实现AIDL文件为我们生成的IBookManager.java文件,依样画葫芦,要注意一下几点:
Binder是运行在服务端的,那么当服务端由于某种原因,Binder调用失败,此时如果不能通知到客户端,那么客户端就一直处于挂起状态,从而受到影响,为此,Binder提供了linkToDeath和unlinkToDeath方法来解决问题。
IBinder接口有一个内部接口DeathRecipient,该接口只有一个方法就是binderDied方法,当binder死亡的时候,系统就会回调这个方法,然后我们就可以移除之前的绑定,做些其他工作了(如重新绑定,重新发起远程调用等)
四大组件中的三大组件(Activity,Service,BroadcastReceiver)都支持在Intent中传递Bundle数据,而Bundle实现了Parcelable接口,可以很好的在不同的进程中传输。只要我们的数据是可以放入Bundle的,都可以传递(基本类型、实现Serializable接口的类对象、实现Parcelable接口的类对象以及一些其他对象。
这种通信方式最常见也最多,同时也是最简单的。
共享文件也可以完成进程通信,两个进程通过读写同一个文件来交换数据。通过序列化和反序列化我们可以很方便的将对象数据写到文件中,也可以很好的从文件中读取到对象数据。我们需要控制解决的问题应该是如何合理的安排两个进程或多个进程怎么来读写这个共享文件。
根据上面的描述,共享文件的适用于对数据同步要求不高的进程之间通信的场景,并且我们还要处理好并发读写问题。
虽然SharedPreference也是一种xml形式的文件,但是不推荐用它做共享文件来完成进程间通信。为什么呢?
SharedPreference是Android提供的轻量级数据存储方案,具有一定的缓存策略,它在内存中会有一份SharedPreference文件的缓存。涉及到内存,那么多进程(每个进程的内存空间都不一样)模式下就不可靠了,容易丢失数据,所以不建议使用SharedPreference文件来做共享文件来完成进程间通信。
底层实现是AIDL,类本身实现了Parcelable接口,通过它可以实现进程间通信(通过Message来传递数据信息),一般步骤:
服务端进程
创建一个Service来处理客户端的链接请求,同时创建一个Handler,利用这个Handler构造一个Messenger对象,然后在Service的onBind的方法中返回这个Messenger对象底层的Binder即可。
客户端进程
首先绑定服务端的Service,绑定成功后会得到服务端返回的Binder,利用这个Binder构造一个Messenger,利用这个Messenger就可以像Service发送消息了,发送的消息为Message对象。如果需要响应服务端返回的信息,首先客户端要指定装载服务端返回信息的容器Messenger(这个Messenger应该包含一个处理消息的Handler),这样服务端在接收到消息后就可以拿到这个客户端定义的容器,然后利用这个容器发送服务端要发送给客户端的消息,发送后,客户端定义好的消息处理Handler就可以处理服务端返回的消息了
注意:
- 非基本类型数据参数都必须标明参数方向(in、out或inout);
- AIDL内不能声明常量(注意与普通接口区别);
首先要明确以下几点:
- 客户端调用的服务端方法,运行在服务端的Binder线程池中,同时客户端线程会被挂起;
- 服务端调用的客户端回调方法,运行在客户端Binder线程池中,同时服务端会被挂起;
根据以上几点,我们在使用AIDL时要注意:
客户端线程被挂起时,如果这个线程是UI线程,调用服务端的方法如果是耗时操作,就会ANR;
服务端线程被挂起时,如果服务端线程是UI线程,调用了客户端的回调方法是耗时操作,就会ANR;
服务端线程被挂起时,调用客户端回调方法运行在客户端Binder线程池中,如果回调里面有UI处理,请采用Handler将其切换到UI线程中;
删除跨进程listener时要采用RemoteCallbackList,因为采用跨进程通信的基础是Binder,而Binder依靠的时序列化和反序列化过程中,这写对象虽然值相等,但却不是同一个对象,简单的使用List的remove方法是无法解除listener的注册的。
重连服务的方法:
二者不同之处在于,前者在客户端的Binder线程池中被调用,后者在客户端的UI线程中被调用。
二者都是在验证不通过的时候,返回一个空Binder对象
ContentProvider设计的目的就是用于应用之间共享数据,因而它本身就是为进程间通信而生的。由于系统做了比较好的封装,我们只要熟悉ContentProvider的使用即可。使用ContentProvider要注意的问题:
"content://"+ authority + path
),不同的path对应的是不同的表使用一般步骤:
ServerSocket serverSocket = new ServerSocket(8688)
Socket client = setverSocket.accept()
得到客户端请求信息Socket = new Socket("localhost", 8688)
,并采用一定的超时重连机制避免针对每个AIDL文件新建一个与之对应的Service(Service是四大组件之一,是系统的资源),而是自定义每个AIDL中对应接口方法的实现(继承相应的Stub类)通过BinderPool的queryBinder方法根据相应的标识符来查询对应的Binder。
BinderPool主要做以下工作:
- 链接远程服务
- 提供queryBinder方法
- 提供服务重连解决方案
1 | private synchronized void connectBinderPoolService() { |
注意采用CountDownLatch可以将并行服务串行化。
1 | public IBinder queryBinder(int binderCode) throws RemoteException { |
具体实现实在继承IBinderPool的BinderPoolImpl中,根据不同的binderCode,返回不同的Binder对象。
1 | private ServiceConnection mBinderPoolConnection = new ServiceConnection() { |
就是利用前面提到的linkToDeath
和unlinkToDeath
方法,通过Binder的死亡代理来实现。
名称 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
Bundle | 简单易用 | 只能传输Bundle支持的类型 | 四大组件间的进程通信 |
文件共享 | 简单易用 | 处理高并发能力弱,不及时 | 无并发情形,数据交换实时性要求不高 |
AIDL | 功能强大,支持一对多并发通信,支持实时通信 | 使用复杂,要处理好线程同步 | 一对多通信且要求RPC(远程过程调用) |
Messenger | 功能一般,支持一对多串行通信,支持实时通信 | 不能处理高并发,由于是通过Message传递数据的,所以只支持传递Bundle支持的数据类型,不支持RPC | 低并发的一对多即时通信,无RPC需求 |
ContentProvider | 支持一对多的并发数据共享 | 受约束的AIDL,只支持数据的CRUD操作 | 一对多进程间数据共享 |
Socket | 功能强大,可以通过网络传输字节流,支持一对多并发实时通信 | 实现略显麻烦,不支持直接的RPC | 网络数据交换 |