(自以为)优雅的跨进程单例的实现思路

发表于:2016-8-05 11:06

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:未知    来源:51Testing软件测试网采编

  单例模式,也叫单子模式,是一种常用的软件设计模式。在应用这个模式时,单例对象的类必须保证只有一个实例存在。许多时候整个系统只需要拥有一个的全局对象,这样有利于我们协调系统整体的行为。
  但这种设计模式有局限:只能在一个进程内生效。但项目开发中又难免会出现开启多个进程的情况。这个时候,原本设计的单例,在整个应用的范围来看,变成了两个单例。两个进程内的单例的内部状态(变量的取值)也就无法同步了,这也是这个问题的核心(单例的行为(方法)在不同进程是一致的,内部状态会影响到行为的结果)。
  一、如何解决
  解决数据不同步问题的方法很多,简单的做法有两种:持久化或者跨进程调用。
  1、持久化
  Android可用的持久化的方式有本地文件、SharedPreference和数据库这几种。
  通过将数据持久化到本地,数据读写都通过操作持久化数据,可以实现数据的同步。
  这种方案会引入新的问题:同时写文件的问题(数据可能会乱掉),同时会增加读写本地IO的耗费。
  在以上三种持久化方式里,本地文件、SharedPreference都有可能出现同时写文件的问题。数据库还好,而且Android组件里有ContentProvider可以帮助我们简化一些操作。
  但这三种方法,都要额外做一些事情,比如数据存储格式(本地文件)、字段名的定义和维护(SharedPreference)、表的定义和维护和增删改查的实现(数据库)。光想一想就很头大。
  最开始有想过ContentProvider的方式实现,但实现起来也挺麻烦挺蛋疼的,后来就不了了之了。
  2、IPC(进程间调用)
  IPC机制很适合用于解决这个问题,这个实现方式更接近后端的RPC(远程过程调用)。Android的进程间通讯机制采用AIDL来实现。
  这种实现方式的方法步骤也不算简单:
  1、定义AIDL接口
  2、实现AIDL接口里的方法
  3、实现一个Service,在绑定的时候返回实现了AIDL接口Binder对象(被调用方)
  4、绑定Service,获得Binder对象,通过Binder对象进行方法调用(调用方)
  虽然不简单,但也不复杂。但怎么应用到现有代码里呢?
  依旧是最简单的解决思路:
  1、为每个单例的调用都封装一层(实际是两层,一层给业务,一层是AIDL,用于跨进程调用)
  2、在调用的时候,封装层里判断当前调用的执行环境,如果在单例所在的进程,则调用单例的对应方法,否则,发起一次进程间调用。
  这个解决思路里,大部分是体力活:
  1、把单例里定义的方法添加到AIDL文件里
  2、实现AIDL文件里的方法(跨进程调用的封装)
  3、添加封装层(if (在单例的进程) { 调用单例的方法; } else { 发起跨进程调用; })
  4、修改原有业务的调用代码,把它改为封装层的调用
  (我们不生产代码,我们只是代码的搬运工)
  3、完(卒)
  二、你说你要更简单的?
  让我们来审视下上面的方案的实现步骤:
  · 定义AIDL接口
  · 实现AIDL接口里的方法
  · 实现一个Service,在绑定的时候返回实现了AIDL接口Binder对象(被调用方)
  · 绑定Service,获得Binder对象,通过Binder对象进行方法调用(调用方)
  1、简化封装层
  慢着!
  既然我们都需要实现AIDL接口了,为什么不把单例的实现和AIDL接口的实现整合起来?
  · 也就是说:通过这种方式实现的单例的实例,是一个可以用于跨进程传输的对象!
  · 进一步说:我们可以在绑定的时候,把这个单例(Binder)返回,其他进程只有得到这个Binder(RPC里的Proxy),就能操作到我们这个单例了,而这个单例也就成为了我们应用程序范畴内所需要的单例。
  想到了这一点,我们的封装层就可以废掉了。80%的体力活瞬间蒸发!
  2、简化绑定处理过程
  剩下的20%的体力活就变成了:
  · 定义AIDL接口,用单例对象实现这个AIDL接口
  · 使用到这个单例的都要执行一次绑定,绑定成功后,作为单例的实例保存下来即可。
  第一点怎么都省不了了。但第二点呢?看起来是重复性很强的编码过程呢:
  · 修改Service实现,返回实现了AIDL的单例
  · onServiceConnected里,把得到的单例的代理,设为本进程的单例对象
  如果能一次性就把所有的单例都传递过来,不就能少掉多次绑定调用,同时还统一了入口和出口。
  写过AIDL的一定会跟另一个类打交道:Parcelable。Parcelable的实现需要需要我们处理数据的序列化和反序列化。在这里我们的入口和出口能实现统一,同时,Parcel对象还有两个重要的方法:writeStrongInterface和readStrongBinder,这两个方法实现了Binder对象的序列化和反序列化操作。
  因此我们可以在这里把所有的单例通过writeStrongInterface序列化,传递到另一个进程,另一个进程再进行readStrongBinder,把对应的代理给取出来,并放置到单例里。
  这样以来,我们的绑定处理过程就得到了简化。
  3、Word is cheap, show me the code
  GayHub提交地址,基本框架和使用Sample
  3.1、核心
  说完了以上那么多,其实也就两个关键点:
  · 单例对象实现AIDL接口,以支持跨进程
  · Parcelable里统一序列化(Stub)和反序列化(Proxy)单例对象
  3.2、实例-单例
  这里假定有以下几个单例:
  SingletonA(A表示是在A进程)
  SingletonA.aidl是它的AIDL接口;
  SingletonAImp.java是这个单例的实现。
  SingletonB(B表示是在B进程)
  SingletonB.aidl是它的AIDL接口;
  SingletonBImp.java是这个单例的实现。
  SingletonC(C表示是在C进程)
  SingletonC.aidl是它的AIDL接口;
  SingletonCImp.java是这个单例的实现。
  获取他们的实例的方法统一为静态方法getInstance,代码如下,这里也是单例实现中唯一需要判断所处进程的地方:
public static synchronized SingletonA getInstance() {
if (ProcessUtils.isProcessA()) {
if (INSTANCE == null) {
INSTANCE = new SingletonAImp();
}
return INSTANCE;
} else {
if (INSTANCE == null) {
/** 自发重连 */
Intent intent = new Intent(
App.getContext(), ServiceA.class);
App.getContext().bindService(intent,
new InstanceReceiver(),
Context.BIND_AUTO_CREATE);
}
return INSTANCE;
}
}
  这个getInstance跟传统的单例不一样,它可能返回为空。
  这里面有两个东西需要我们注意:
  ServiceA.class
  InstanceReceiver
21/212>
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计 发展历程

法律顾问:上海兰迪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2024
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪ICP备05003035号

沪公网安备 31010102002173号