Android组件化工具-SPI

前言

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

ServiceLoader的使用

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

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

    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

     /**
         * 如果有多个实现,取第一个
         */
        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. 使用接口类

    ILogInterface iLog =  SPIUtils.getSpiImpl(ILogInterface.class);
    
    iLog.d("测试下");
    
    
    

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

AutoService的使用

  1. 首先需要引入依赖

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

    // 依赖 autoService 库
    implementation 'com.google.auto.service:auto-service:1.0'
    annotationProcessor 'com.google.auto.service:auto-service:1.0'
    
    

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

    // 依赖 autoService 库
    annotationProcessor 'com.google.auto.service:auto-service:1.0'
    
    

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

    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

    @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 关键代码如下,加了一些注释:

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

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,都是程序员的智慧结晶!佩服这些大牛。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,542评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,596评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,021评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,682评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,792评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,985评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,107评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,845评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,299评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,612评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,747评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,441评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,072评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,828评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,069评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,545评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,658评论 2 350

推荐阅读更多精彩内容