Android组件化利器-SPI

关键词:SPI、ServiceLoader、AutoService、组件化、APT

对于APT(注解处理器),之前写过一篇,使用自定义注解自己实现ButterKnife功能。最近接手的新项目中用了一个新的东西,就是SPI,拿来研究下。

SPI, service provider interface,使用接口、配置文件和策略模式,实现了解耦、模块化和组件化。

ServiceLoader的使用

  1. 在公共模块或者接口模块中定义一个接口类; com.aya.demo.common.ILogInterface.java

    java 复制代码
    public interface ILogInterface{
        void logD(String msg)
    }
  2. 在实际业务模块中,写一个接口的实现类; com.aya.demo.LogImpl.java

    typescript 复制代码
    public class LogImpl implements ILogInterface{
        @Override
        public void logD(String msg){
            Log.d("AYA", msg);
        }
    }
  3. 在该业务模块中,在src/main/resources文件夹下创建/META-INF/services目录,并新建一个文件,文件名是接口的全路径,文件的内容是接口实现类的全路径; 文件路径如下: src/main/resources/META-INF/services/com.aya.demo.common.ILogInterface 文件内容是:

    com.aya.demo.LogImpl
    
  4. 写一个serverloader工具类。

    SPIUtils.java

    swift 复制代码
     /**
         * 如果有多个实现,取第一个
         */
        public static <T> T getSpiImpl(Class<T> t) {
            try {
                ServiceLoader<T> loader = ServiceLoader.load(t, CommonHolder.ctx.getClassLoader());
                Iterator<T> iter = loader.iterator();
                if (iter.hasNext()) {
                    return iter.next();
                }
            } catch (Exception e) {
                //
            }
            return null;
        }
    
        /**
         * 返回所有实现
         */
        public static <T> List<T> getSpiImpls(Class<T> t) {
            List<T> impls = new ArrayList<>();
            try {
                ServiceLoader<T> loader = ServiceLoader.load(t, CommonHolder.ctx.getClassLoader());
                Iterator<T> iter = loader.iterator();
                while (iter.hasNext()) {
                    impls.add(iter.next());
                }
            } catch (Exception e) {
                //
            }
            return impls;
        }
  5. 使用接口类

    ini 复制代码
    ILogInterface iLog =  SPIUtils.getSpiImpl(ILogInterface.class);
    
    iLog.d("测试下");

使用SPI可以很好的实现解耦合组件化隔离,但是这样做有一个缺点,就是每次新建一个实现类,就需要在/META-INF/services目录下创建一个文件或者修改已有的文件,不能动态添加,有点麻烦而且容易写错。针对这种麻烦的事情,程序员们肯定会想办法处理掉的,所以Google又给我们提供了AutoServcie工具。

AutoService的使用

  1. 首先需要引入依赖

    注意:这个依赖的引入是有讲究的,接口类所在的模块要全部引入

    arduino 复制代码
    // 依赖 autoService 库
    implementation 'com.google.auto.service:auto-service:1.0'
    annotationProcessor 'com.google.auto.service:auto-service:1.0'

    而实现类所在的模块只需要引入注解处理器即可:

    arduino 复制代码
    // 依赖 autoService 库
    annotationProcessor 'com.google.auto.service:auto-service:1.0'

    注意 :如果实现类使用的是kotlin编写的,则在引入依赖时需要使用 kapt,即

    arduino 复制代码
    apply plugin: 'kotlin-android'
    apply plugin: 'kotlin-kapt'
    
    dependencies {
        kapt 'com.google.auto.service:auto-service:1.0'
    }
  2. 原先的接口类不用动,只需修改实现类 只需要在具体的实现类上面加上一个@AutoService注解,参数则为接口的class类。

    com.aya.demo.LogImpl.java

    typescript 复制代码
    @AutoService(ILogInterface.class)
    public class LogImpl implements ILogInterface{
        @Override
        public void logD(String msg){
            Log.d("AYA", msg);
        }
    }
  3. 写一个serverloader工具类SPIUtils.java,同上。

这样,就可以像之前一样使用接口了。

AutoService原理

AutoService的原理就是借助自定义注解,在编译期使用注解处理器扫描注解,并自动生成/resources/META-INF/... 文件。

AutoService工具关键类是 AutoServiceProcessor.java 关键代码如下,加了一些注释:

scss 复制代码
public class AutoServiceProcessor extends AbstractProcessor {

    ...
    //Multimap:key可以重复
    private final Multimap<String, String> providers = HashMultimap.create();
    ...
        
      /**
       * <ol>
       *  <li> For each class annotated with {@link AutoService}<ul>
       *      <li> Verify the {@link AutoService} interface value is correct
       *      <li> Categorize the class by its service interface
       *      </ul>
       *
       *  <li> For each {@link AutoService} interface <ul>
       *       <li> Create a file named {@code META-INF/services/<interface>}
       *       <li> For each {@link AutoService} annotated class for this interface <ul>
       *           <li> Create an entry in the file
       *           </ul>
       *       </ul>
       * </ol>
       */
      @Override
      public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
      //该方法数注解处理器的入口
        try {
          processImpl(annotations, roundEnv);
        } catch (RuntimeException e) {
          // We don't allow exceptions of any kind to propagate to the compiler
          fatalError(getStackTraceAsString(e));
        }
        return false;
      }
    
      private void processImpl(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        if (roundEnv.processingOver()) {
            //如果已经处理结束,则去生成注册文件
            generateConfigFiles();
        } else {
            //处理注解
            processAnnotations(annotations, roundEnv);
        }
      }
    
        //处理注解
      private void processAnnotations(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        //获取所有添加AutoService注解的类
        Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(AutoService.class);
    
        log(annotations.toString());
        log(elements.toString());
    
        for (Element e : elements) {
          // TODO(gak): check for error trees?
          TypeElement providerImplementer = MoreElements.asType(e);
          //获取AutoService注解value,即声明实现类时注解括号里指定的接口
          AnnotationMirror annotationMirror = getAnnotationMirror(e, AutoService.class).get();
          //获取value的集合
          Set<DeclaredType> providerInterfaces = getValueFieldOfClasses(annotationMirror);
          if (providerInterfaces.isEmpty()) {
            //如果集合为空,也就是没有指定value,报错,不处理
            error(MISSING_SERVICES_ERROR, e, annotationMirror);
            continue;
          }
          //遍历所有value,获取value的完整类名
          for (DeclaredType providerInterface : providerInterfaces) {
            TypeElement providerType = MoreTypes.asTypeElement(providerInterface);
    
            log("provider interface: " + providerType.getQualifiedName());
            log("provider implementer: " + providerImplementer.getQualifiedName());
    
            //判断是否为接口的实现类,是,则存入Multimap类型的providers缓存中,其中key为接口的全路径,value为实现类的全路径;否则报错
            if (checkImplementer(providerImplementer, providerType, annotationMirror)) {
              providers.put(getBinaryName(providerType), getBinaryName(providerImplementer));
            } else {
              String message = "ServiceProviders must implement their service provider interface. "
                  + providerImplementer.getQualifiedName() + " does not implement "
                  + providerType.getQualifiedName();
              error(message, e, annotationMirror);
            }
          }
        }
      }
    
        //生成spi文件
      private void generateConfigFiles() {
        Filer filer = processingEnv.getFiler();
        //遍历providers的key值
        for (String providerInterface : providers.keySet()) {
          String resourceFile = "META-INF/services/" + providerInterface;
          log("Working on resource file: " + resourceFile);
          try {
            SortedSet<String> allServices = Sets.newTreeSet();
            try {
              // would like to be able to print the full path
              // before we attempt to get the resource in case the behavior
              // of filer.getResource does change to match the spec, but there's
              // no good way to resolve CLASS_OUTPUT without first getting a resource.
              //"META-INF/services/**"文件夹下已经存在SPI文件
              FileObject existingFile = filer.getResource(StandardLocation.CLASS_OUTPUT, "",
                  resourceFile);
              log("Looking for existing resource file at " + existingFile.toUri());
              // 该SPI文件中的service集合
              Set<String> oldServices = ServicesFiles.readServiceFile(existingFile.openInputStream());
              log("Existing service entries: " + oldServices);
              //将spi文件中的service写入缓存
              allServices.addAll(oldServices);
            } catch (IOException e) {
              // According to the javadoc, Filer.getResource throws an exception
              // if the file doesn't already exist.  In practice this doesn't
              // appear to be the case.  Filer.getResource will happily return a
              // FileObject that refers to a non-existent file but will throw
              // IOException if you try to open an input stream for it.
              log("Resource file did not already exist.");
            }
    
            //根据providers缓存创建service集合
            Set<String> newServices = new HashSet<>(providers.get(providerInterface));
            //如果新建的都已经存在,则直接返回不处理
            if (allServices.containsAll(newServices)) {
              log("No new service entries being added.");
              return;
            }
    
            //将新建的service也加入缓存
            allServices.addAll(newServices);
            log("New service file contents: " + allServices);
            //将所有的service写入文件
            FileObject fileObject = filer.createResource(StandardLocation.CLASS_OUTPUT, "",
                resourceFile);
            try (OutputStream out = fileObject.openOutputStream()) {
              ServicesFiles.writeServiceFile(allServices, out);
            }
            log("Wrote to: " + fileObject.toUri());
          } catch (IOException e) {
            fatalError("Unable to create " + resourceFile + ", " + e);
            return;
          }
        }
      }
      
      ...
}

写SPI的File工具类ServicesFiles.java

java 复制代码
final class ServicesFiles {
  public static final String SERVICES_PATH = "META-INF/services";

  private ServicesFiles() { }
    
    ...
    
  /**
   * Writes the set of service class names to a service file.
   *
   * @param output not {@code null}. Not closed after use.
   * @param services a not {@code null Collection} of service class names.
   * @throws IOException
   */
  static void writeServiceFile(Collection<String> services, OutputStream output)
      throws IOException {
    BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(output, UTF_8));
    for (String service : services) {
      writer.write(service);
      writer.newLine();
    }
    writer.flush();
  }
}

看了源码后,可以发现,AutoServiceProcessor的主要功能就是将加了AutoService注解的类写到SPI文件中去,而SPI文件的名称根据注解时value的内容来定的,即接口的全路径,SPI文件的内容是实现类的全路径,每个实现类占一行。

使用AutoService时需要注意:

  1. minSdkVersion最低为26
  2. 接口的实现类必须是public的

总结一下吧,SPI可以更好的实现组件化,结合AUtoService和ServiceLoader来实现,很方便。当然也可以自己开发一个sdk,把SPIUtils放进去,就可以更方便的使用了。

APT的应用,除了ButterKnife、AUtoService,还有阿里的ARouter,都是程序员的智慧结晶!佩服这些大牛。

相关推荐
SRC_BLUE_171 小时前
SQLI LABS | Less-39 GET-Stacked Query Injection-Intiger Based
android·网络安全·adb·less
无尽的大道5 小时前
Android打包流程图
android
镭封6 小时前
android studio 配置过程
android·ide·android studio
夜雨星辰4876 小时前
Android Studio 学习——整体框架和概念
android·学习·android studio
邹阿涛涛涛涛涛涛6 小时前
月之暗面招 Android 开发,大家快来投简历呀
android·人工智能·aigc
IAM四十二6 小时前
Jetpack Compose State 你用对了吗?
android·android jetpack·composer
奶茶喵喵叫7 小时前
Android开发中的隐藏控件技巧
android
Winston Wood9 小时前
Android中Activity启动的模式
android
众乐认证9 小时前
Android Auto 不再用于旧手机
android·google·智能手机·android auto
三杯温开水9 小时前
新的服务器Centos7.6 安卓基础的环境配置(新服务器可直接粘贴使用配置)
android·运维·服务器