Spa框架 -- Android架构优化利器

1 背景

在组件化的模式设计里,模块之间基于接口编程,模块内不对实现类进行硬编码。由于一旦代码里涉及具体的实现类,就违反了可拔插的原则,当须要替换一种实现,就须要修改代码。为了实如今模块装配的时候能不在程序里动态指明,这就须要一种服务发现机制。 SPI就是这样的一个机制:为某个接口寻找服务实现的机制。有点相似IOC的思想,就是将装配的控制权移到程序以外,在模块化设计中这个机制尤为重要。html

2 业界技术方案

2.1 常规模块依赖方式

组件开发过程当中,若是想要在模块B中实例化模块A中的类或使用模块A中的方法,常规的方式是让模块B依赖模块A,以下图所示:java

                             

可是组件化开发过程当中,每每并不但愿某些模块之间有直接的依赖关系,由于若是依赖关系创建了,模块之间的耦合性就高了。git

2.2 Java的SPI机制

有没有什么办法能够解决这个问题呢?可使用Java提供的SPI机制!github

SPI(Service Provider Interface)是JDK内置的一种服务发现机制。它的应用仍是很是普遍的,尤为在服务端开发技术栈中,编程

  • JDBC 中经过 SPI 的方式加载不一样的驱动实现windows

  • SLF4J中经过 SPI 的方式加载不一样提供商的日志实现类缓存

  • Gradle源码中有大量的服务是基于 SPI机制来作服务实现扩展的安全

  • SpringFactoriesLoader 是 Spring 中十分重要的一个扩展机制之一,算是SPI的一个变种,原理基本一致服务器

2.3 SPI是如何解决上述问题的呢?

Java的SPI机制使用流程如上图所示,markdown

  • 首先须要一个Base模块提供IA接口,模块A和模块B都依赖Base模块
  • 模块A中的类A实现IA接口
  • SourceSet下建立resources/META-INF/services父目录,
  • 在父目录下建立以IA接口的全限定名为文件名的文本文件,文件内容是IA实现类的全限定名列表,以回车换行分割
  • 最后模块B就能够经过ServiceLoader.load(IA.class)方法来建立模块A中类A对象了

 2.4 Java原生SPI机制的不足

若是你熟悉SPI机制那么你会发现,不管是在JDBC, SLF4J,Gradle仍是Spring中,一个模块每每只是提供个别关键接口做为一个服务的切入点让外部模块发现,这一般是足够的。

但若是有大量的服务须要被发现,那么就要在resources/META-INF/services目录写不少的接口文件,而后使用ServiceLoader去加载。

  • 问题一:写法太繁琐,接口多了不易维护, 能不能简化?
  • 问题二:resources/META-INF/services下配置的接口文件是个配置文件, ServiceLoader经过文件流读出接口实现类的全限定名,再经过反射实例化出具体的实现类对象, 性能较低,并且服务越多性能越低。
  • 问题三:  它只提供了服务发现的能力。ServiceLoader只负责把服务实例化出来。没有对实例化的对象作任何管理。

3. 简单易用的SPI机制 -- spa       

3.1 spa服务发现机制                   

github连接: github.com/luqinx/sp

spa(Service Pool for Android)将待实例化的类当作一个一个的服务, 是基于Java SPI思想基础之上建立出来的全新的SPI机制,可是他不只仅只有服务发现能力,还有服务生命周期管理,服务优先级管理,服务拦截管理等等Java SPI以外的能力,它生于Android,但不只仅只试用于Android端,理论上Jvm环境下都适用。

                          

spa的基本思想如上图所示:

  1. spa使用注解替代了繁琐的services文件配置, 使用方式获得了极大的简化
  2. spa在编译阶段经过字节码生成为被@Service注解标记的接口实现类建立工厂方法,实现类对象经过工厂方法建立,不存在文件流操做也不须要反射实例化对象,提升了性能。
  3. 无需读配置,无需缓存映射表,所以spa甚至作到了无需手动初始化。

其余相似的框架广泛须要在运行时读配置文件(io),缓存配置映射表和反射建立对象,这些都会对性能形成必定影响。而spa并不须要,spa在编译阶段生成工厂类来替代配置文件和和缓存映射,这很容易理解。不使用反射,spa是如何建立服务对象的呢?

其实很简单,模块隔离只是一个模式设计, 是为了在开发过程当中不让不相关的模块相互之间存在引用关系,从而下降模块之间的耦合性。模块编译后代码最终会变成字节码/jar/aar/dex, 而字节码/jar/aar/dex里并无模块的概念。通俗的说A类中建立B类对象时,类A并不关心也并不知道类B是写在哪一个模块的。

这也是为何spa是使用字节码生成而不是apt代码生成的缘由。

到这里, spa已解决了跨模块无直接依赖状况下实例化对象的问题。

                                     就这?就这?就这?就这?就这?表情包图片- 求表情网,斗图今后不求人!

看到这你可能发现,主流的路由组件ARouter也有相似的能力,为何不直接使用ARouter呢?只是由于spa性能更好一点? 

跨模块实例化对象是spa的核心能力,但远远不是所有,好比spa在何时建立服务对象,一个服务被建立后何时会被gc回收?

3.2 spa服务的生命周期

能够对比一下ARouter, 使用过ARouter的同窗应该知道,ARouter经过实现IProvider接口定义一个服务,这个服务建立后是全局生命周期的,且全局惟一至关于一个单例。

这在一些场景下是很是有用的,好比我须要一个全局存储服务StorageService,提供save, get, delete等存储相关能力,我能够任什么时候候、任意模块下经过StorageService接口获取到的这个单例服务并使用它,很是方便。

但ARouter只能建立全局生命周期的服务,这是不够的,好比多个业务模块都须要一个品类Fragment并命名为CategoryFragment, 为了模块解耦,我须要将CategoryFragment当作一个服务 ,且须要根据不一样的品类id建立多个CategoryFragment。ARouter的服务是没办法建立多个的,且由于是单例Fragment没法被回收会形成内存泄漏。

spa能够经过@Service注解的scope字段定义一个服务的生命周期,经常使用的生命周期就是上述的两种,

StorageService伪代码以下:

// 全局生命周期,全局惟一
@Service(scope = Spa.Global)
public class StorageServiceImpl implements StorageService {
    ...
}
复制代码

CategoryFragment伪代码以下:

// 默认生命周期,每次都会建立一个新的CategoryFragment,
@Service
public class CategoryFragment implements CategoryService {
    ...
}
复制代码

获取服务的伪代码以下

....

StorageService storageService = Spa.getService(StorageService.class); 

CategoryService categoryFragment = Spa.getService(CategoryService.class);

....
复制代码

除了以上两种经常使用生命周期,还有被弱引用、软引用持有的生命周期和自定义生命周期管理。

3.3 spa服务优先级管理

当一个服务有多个实现类,那Spa.getService(xx.class)会获取哪个呢?所以Spa引入服务优先级管理。

3.3.1  为何须要优先级管理?

当你的想写一个基础服务,而这个服务须要在不一样的环境条件下执行不一样的行为,有什么好的办法呢?(不一样环境能够指不一样的编译环境buildType, 也能够是不一样的运行环境(Java or Android, windows or Mac), 也能够是不一样的业务场景, 甚至能够是不一样的项目等等)

举个栗子卡通图片(第1页) - 要无忧健康图库

个人项目中内部环境和生产环境的代码是严格分离的,内部环境相关的代码好比日志,调试工具,分析工具,数据Mock等是绝对不会打到生成环境的安装包中的。这保证了生成环境的安全性和性能。

以日志输出为例, 

public interface LogService implements IService {
    void e(String tag, String message);
    ....
}
复制代码

内部环境中, 须要将日志输出到控制台,方便发现问题, 由于内部环境代码和生产环境是隔离的,能够将优先级设置高一点。则当它存在时会优先被实例化

@Service(scope = Spa.global, priority = 100)
public class AlphaLogService implements LogService {
    
    void e(String tag, String message) {
        ...
        Log.e(tag, message);
    }

    ....
}
复制代码

线上生产环境,则不能输出到控制台, 而是根据必定策略上传到日志平台,由于代码环境隔离生产环境并不存在AlphaLogService,因此虽然ProductLogService是低优先级,但它依然会被实例化

@Service(scope = Spa.global, priority = 10)
public class ProductLogService implements LogService {
    void e(String tag, String message) {        ...
        RemoteLog.e(tag, message);
    }

    ....
}
复制代码

有人可能会以为: 简单的if-else就能解决的问题你为何要搞得这么复杂???

是的, if-else能解决不一样环境使用不一样的日志输出,可是if-else很难在环境隔离的前提下拿到不一样环境的LogService的实现类

3.3.2 spa也能够同时获取多个服务实现

ServiceLoader的load方法返回的ServiceLoader对象是一个Iterator迭代器,迭代器内容就是服务接口的实现列表。Spa有实现这一个功能吗?直接上代码

假设拦截器服务接口Interceptor,有A, B, C三个拦截器实现

// 服务接口
public interface Interceptor extends IService{
    void intercept();
    String interceptorName();
}

// 拦截器A
@Service(priority = 10)
public class AInterceptor implements Interceptor {

    void intercept() {
        System.out.println("interceptor A is running...");
    }    

    public String interceptorName() {
        return "A";
    }
}

// 拦截器B
@Service(priority = 30)
public class BInterceptor implements Interceptor {

    void intercept() {
        System.out.println("interceptor B is running...")
    }

    public String interceptorName() {
        return "B";
    }
}

// 拦截器C
@Service(priority = 20)
public class CInterceptor implements Interceptor {

    void intercept() {
        System.out.println("interceptor C is running...")
    }

    public String interceptorName() {
        return "C";
    }
}
复制代码

Spa使用CombineService来组合多个服务接口实现,CombineService一样也是一个Iterator迭代器,迭代器的返回顺序将按服务器优先级值大小依次返回

CombineService<Interceptor> as = Spa.getCombineService(Interceptor.class);
for (Interceptor interceptor: interceptors) {    
    System.out.print(interceptor.interceptorName());
}

// 输出 BCA
复制代码

Spa.getCombineService(Interceptor.class)返回的对象也同时是一个Interceptor代理, 当代理对象的intercept()方法执行时,将按优先级顺序依次执行每一个服务实现的intercept()方法

Interceptor interceptor = Spa.getCombineService(Interceptor.class);
interceptor.intercept();

// 输出
// interceptor B is running...
// interceptor C is running...
// interceptor A is running...
复制代码

CombineService默认策略是按优先级大小来决定多服务的执行顺序,上面示例中,多个Interceptor服务对象的intercept()方法的调用大致流程以下图:

ComineService也能够经过实现CombineStrategy接口来支持自定义执行策略。

1. 定义自定义多服务执行策略

public class InterceptorStrategy implements CombineStrategy {
    @Override    
    public boolean filter(Class serviceClass, Method method, Object[] args) {    
        return Interceptor.class.isAssignableFrom(serviceClass); // 选择策略对应的接口
    }

    @Override
    public Object invoke(final List<ServiceProxy> proxies, Class serviceClass, final Method method, final Object[] args) {
        // 自定义调用过程
    }

}
复制代码

2. 使用自定义对服务执行策略

Interceptor interceptor = Spa.getCombineService(Interceptor.class, InterceptorStrategy);
interceptor.intercept()
复制代码

自定义多服务执行策略是很是有用的,在spa的内部就有多处应用

  • 场景1: SpRouter中路由拦截策略,业务能够经过实现RouteInterceptor接口,调用onContinue或onInterrapt()来决定是继续路由仍是拦截路由,实现类是RouteCombineStrategy。每一个路由框架通常都有路由拦截能力,方式大同小异,这里就再也不赘述,感兴趣能够自行查看实现方式。
  • 场景2: 自定义生命周期的类型检查策略,实现类是CustomCombineStrategy,感兴趣能够自行查看。
  • 场景3: spa服务拦截的拦截策略,业务能够经过实现IServiceInterceptor接口,调用onContinue或onInterrapt()来决定是继续执行方法仍是拦截掉方法不执行,又或者是换一个方法执行等等。服务拦截是spa不可或缺的一部分。

3.4 spa服务拦截

spa定义的服务默认支持服务拦截能力,经过拦截能力能够实现服务的AOP操做,想要拦截spa的服务也很简单只须要实现IServiceInterceptor接口, 且服务拦截器也被当作服务,因此须要使用@Service注解标记

@Service
public class MinPriorityServiceInterceptor implements IServiceInterceptor {    
    @Override    
    public void intercept(Class<? extends IService> originClass, IService source, Method method, Object[] args, IServiceInterceptorCallback callback) {
        logger.log(source.toString() + ": " + method.getName());
        if (method.getReturnType() == int.class) {
            callback.onInterrupt(100); // 拦截方法,并返回100
        } else {
            callback.onContinue(method, args); // 不拦截,继续执行
        }
    }
}
复制代码

能够将@Service的注解参数disableIntercept设置为true来禁用服务拦截。

3.5 服务别名

上面的介绍都是经过类来查找服务,Spa同时还支持给类设置别名,而后经过别名来查找服务。别名用@Service注解的path(为何不是alias? 历史缘由)参数标识。

@Service(path = "firstAlias", scope = Spa.Scope.Global)
public class MyAliasService implements IService {
	....	
}

// 使用
MyAliasService byPath = Spa.getService("firstAlias");
// spa经过接口/抽象类查找它的实现类/子类对应的服务用getService(IXxx.class)
// spa经过指定具体的服务实现类建立服务对象使用getFixedService(Xxx.class)
MyAliasService byClass = Spa.getFixedService(MyAliasService.class); 
assert byPath == byClass; 
复制代码

Spa的核心是经过类来查找服务,别名查找也是在类查找的基础上作了一层映射: spa在编译阶段生成了PathServicesInstance类,它维护着一张别名(path)到服务类的映射表。





4. 总结

Spa是目前最完备的SPI开源方案,虽然大厂们开源的如ARouter, DRouter, WMRouter等路由方案都有相似的SPI能力,但它们的立足点都是解决Android框架下的路由问题,不是纯粹的SPI方案。而Spa的目标是跨模块建立服务和管理服务,它不关心上层的服务具体是什么,因此Spa的服务管理能力更强大,且更容易扩展。

若是想要ARouter同样的路由能力可使用spa下的SpRouter, SpRouter就是基于Spa实现的一套路由方案。



5. 思考

想象一下,若是把项目中页面、弹窗、功能都抽象成一个个的服务,而后将这些服务以资源的形式(好比URL)暴露出来给内部和外部访问(好比给混合端(H5, Flutter)访问,经过接口访问,经过推送访问,经过adb访问等等), 当这些服务达到必定规模,整个App是否是变得更加灵活、更加动态化,这就是我当前应用的服务化的框架。

参考文档:

rgb-24bit.github.io/blog/2019/j…

www.cnblogs.com/jalja365/p/…

相关文章
相关标签/搜索