组件化通信方案Arouter WMRouter之外的选择
主流通信基本原理(这里不讲解路由)2个
- Arouter利用注解存储实现类的path,使用的时候做下反射就可以。
- WMRouter虽然说参考SPI,但实现和SPI没啥关系。利用注解在编译期存储实现类的Class。
甚少直接采用SPI,而且两家为何没有选择正宗的SPI呢,原因很简单,无法通过Key来查找服务,只能通过一个接口获取服务列表。没有key,谁知道提供给我的是什么服务,谁还敢用。
但SPI的简单的吸引力又很足。因为没有注解,没有生成其他类,没有初始化,没有注解处理器,只有一个配置文件,其简洁性秒杀所有Router方案,而且Java的JNDI采用了SPI,是可以落地的。但天生有缺点,尴尬的是在组件化中几乎没有落地。
组件化是否有其他方案呢,是否可以利用SPI的简洁性思想呢。
答案是肯定的,其他方案有2个。一个是我自己实现的,一个是我同事实现的。
- 基于spi,保证单实现。第一个方案基于一个前提,在公共服务lib 暴露实现方的服务接口,接口唯一。在这个前提下,那么可以采用改造过的SPI,需要自己实现ServiceLoader
- 基于指令分发。定义一个统一接口,组件module要在清单文件注册定义实现类,然后将实现类注册到map容器。实现类中是基于指令方式再分发不同的处理逻辑。
先不说具体实现,这个两个方案何以能替换Arouter,WMRouter.有什么理由去替换呢。
从我个人角度而言,理由有下面几个。
5. 编译期注解一般需要单独module,这个就增加了gradle编译时间。会生成中间类,难于调试。
6. 比之Arouter,WMRouter使用更加方便。对实现方简化逻辑,屏蔽中间类。
第一个方案:基于SPI的单实现。
spi是Java提供的一套用来被第三方实现或者扩展的API,比较方便。可移植到组件化中,但要解决一个问题,就是spi获得的实现是多个,因为是自己项目中,极少两个个不同业务的module暴露的服务基于同个接口。所以需要改造,原理就是移除掉原来的迭代即可,自定义的serviceLoader如下:
public class ServiceLoader {
private static final String PREFIX = "META-INF/services/";
/**
* 存储services的class 对象,应该使用LruCache算法,默认存100个
*/
private LruCache<String, Class> providers = new LruCache<>(100);
private ClassLoader mClassLoader;
private volatile static ServiceLoader instance = null;
private ServiceLoader() {
mClassLoader = Thread.currentThread().getContextClassLoader();
}
public static ServiceLoader getInstance() {
if (instance == null) {
synchronized (ServiceLoader.class) {
instance = new ServiceLoader();
}
}
return instance;
}
public <T >T load(Class<T> obj) {
String fullName = PREFIX + obj.getName();
URL url = mClassLoader.getResource(fullName);
Class<T> classInstance;
if ((classInstance = providers.get(obj.getName())) == null) {
classInstance = parse(url);
providers.put(obj.getName(), classInstance);
}
return createInstance(classInstance);
}
private <T >T createInstance(Class<T> obj) {
T instance = null;
try {
instance = obj.newInstance();
} catch (Exception e) {
Log.e("ServiceLoader","instance falied:" + e.getMessage());
}
return instance;
}
/**
* 一个接口是否只有一个实例?
*/
private Class parse(URL url) {
Class obj = null;
InputStream inputStream = null;
BufferedReader bufferedReader = null;
try {
long start = System.currentTimeMillis();
inputStream = url.openStream();
bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
String line = bufferedReader.readLine();
long middle = System.currentTimeMillis();
Log.i("ServiceLoader", "IO 读写耗时:" + (middle - start));
if (!TextUtils.isEmpty(line)) {
obj = Class.forName(line.trim());
}
Log.i("ServiceLoader", "反射耗时:" + (System.currentTimeMillis() - middle));
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (inputStream != null) {
inputStream.close();
}
if (bufferedReader != null) {
bufferedReader.close();
}
} catch (Exception e) {
}
}
return obj;
}
}
用法:
ModuleA1Service moduleA1Service = ServiceLoader.getInstance()
.load(ModuleA1Service.class);
Log.i(TAG,"获取服务需要时间:" + (System.currentTimeMillis() - start));
if (moduleA1Service != null) {
moduleA1Service.modulea();
Log.i(TAG, "获取服务成功");
}else {
Log.i(TAG, "获取服务失败");
}
ModuleA1Service 的注册和原生的SPI一致,不细说,如果有需要,可以找我要demo。就是这么简单,没有生成中间类,没有注解相关的,没有路由表,门槛极低。但也不尽然是优点,有个缺点就是要做一次io读写,需要300ms左右(在我的三星j5手机上)的时间,但有缓存,只有第一次会消耗一点性能。有个解决方案,就是在application的时候,把服务定义的文件从apk包里拿出来存到另外一个目录,那么后面就可以用nio去做读写,性能提高会非常可观。有人会问,啥不直接用nio,因为apk相当于压缩包,调用File file = new File(url.toURI()) 会抛出 URI is not hierarchical 的错误。别看serviceloader简单,有个非常重要的知识点,也是关键之处,为何用Thread.currentThread().getContextClassLoader()返回的classloader,而不是用**.class.getClassLoader()返回的classloader,这里涉及到破坏双亲委托的概念,这里不展开,下篇文章单独论述这个问题。我们再看第二种解决方案。
基于指令分发的实现
因为这个方案是我同事写的,我贴出他的分析链接 艾瑞Android组件化框架使用说明
新出的方案
因为基于spi的需要读写io,在没有用nio做读写的情况下,牺牲性能,总让人不舒服。所以有了一个折中方案。方案前提是module之间是强依赖,采用手动注册,原理也简单,一看遍知。
public class ServiceManager {
private static HashMap<Class, Class> service = new HashMap<>();
public static void registService(Class key, Class value) {
if (key != null) {
service.put(key, value);
}
}
public static <T> T getService(Class<T> key) {
Class value = service.get(key);
try {
return (T) value.newInstance();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
}
return null;
}
}
总结
我原本是是一个注解爱好者,最近还写过一个IOC的框架,也是基于注解。但是我最近反思一个问题,编译器注解的利用,虽然简化了过程,但对于用注解的人而言,实际上比较尴尬,追本溯源比较繁琐,不下载他的源码,你是不知道细节的。当然注解很高级,在运行期描述元数据是最适合的场景,只是个人的想法。