组件化架构之组件初始化 Component Initializer(组件初始化器)开源项目 原理分析

2022-07-27,,,,

Component Initializer(组件初始化器)开源项目 介绍

Component Initializer(组件初始化器)介绍

原理分析

  • 问题1:该框架是如何实现Component类的自动注册的

刚开始我想到的方案是通过注解来实现。首先用@Component注解标记Component类,然后我们用注解处理器就可以拿到Component类的信息,然后利用注解处理器自动生成ComponentInitializer.java类,将收集到的Component类的初始化代码new Component(),添加到ComponentInitializer类中,这样就可以完成自动注册的功能。

看着方案很完美,实际做的时候发现一个问题,就是对于多module的形式,每个module都是独立编译的,都会独立编译成一个aar文件,这也就是为什么我们如果用annotationProcessor的时候需要在每个用到的module上都添加annotationProcessor。这样就会导致我们通过annotationProcessor只能拿到当前module中的Component类的信息。无法拿到用户在各个组件module中配置的所有的Component类的信息。

所以通过annotationProcessor来实现自动注册是行不通的。

那么有没有其他方案呢?

后来我在想,这个问题做框架的是都需要考虑的,那么其他框架是怎么解决的呢。Arouter是如何解决这个问题的?从Arouter的介绍中我发现它是通过AutoRegister来解决的,然后通过AutoRegister的实现原理,我发现它是通过Transform API的技术方案来解决的。

Transform API是从Gradle 1.5.0版本之后提供的,它允许第三方在打包Dex文件之前的编译过程中修改.class字节码文件,这样在这个过程中,我们就可以搜集到所有的Component类,然后通过修改ComponentInitializer.class字节码文件,就可以把注册的代码添加到ComponentInitializer.class字节码文件里。

除了Transform API的方案,其实还有其他两种技术方案。(以下来自AutoRegister作者的分析)

  • ARouter的解决方案。
    AutoRegister在ARouter里是一个可选方案,ARouter本身还有一个解决方案:每个module都生成自己的java类,这些类的包名都是’com.alibaba.android.arouter.routes’,然后在运行时通过读取每个dex文件中的这个包下的所有类通过反射来完成映射表的注册,详见ClassUtils.java源码

  • ActivityRouter的解决方案(demo中有2个组件名为’app’和’sdk’):

    • 在主app module中有一个@Modules({“app”, “sdk”})注解用来标记当前app内有多少组件,根据这个注解生成一个RouterInit类
    • 在RouterInit类的init方法中生成调用同一个包内的RouterMapping_app.map
    • 每个module生成的类(RouterMapping_app.java 和 RouterMapping_sdk.java)都放在com.github.mzule.activityrouter.router包内(在不同的aar中,但包名相同)
    • 在RouterMapping_sdk类的map()方法中根据扫描到的当前module内所有路由注解,生成了调用Routers.map(…)方法来注册路由的代码
    • 在Routers的所有api接口中最终都会触发RouterInit.init()方法,从而实现所有路由的映射表注册

    这种方式用一个RouterInit类组合了所有module中的路由映射表类,运行时效率比扫描所有dex文件的方式要高,但需要额外在主工程代码中维护一个组件名称列表注解: @Modules({“app”, “sdk”})

我们最终选择的是利用AutoRegister的能力来实现自动注册。为了方便用户使用我自定义了一个Gradle plugin来实现此功能

  • 自定义Gradle plugin

class Plugin implements org.gradle.api.Plugin<Project> {

    @Override
    void apply(Project project) {
        /**
         * 注册transform接口
         */
        def isApp = project.plugins.hasPlugin(AppPlugin)
        if (isApp) {
            println 'project(' + project.name + ') apply com.jst.component.initializer plugin'
            def android = project.extensions.getByType(AppExtension)
            def transformImpl = new RegisterTransform(project)

            AutoRegisterConfig config = new AutoRegisterConfig()
            Map<String, Object> map = new HashMap<>()
            map.put('scanInterface','com.jst.compinit.IComponentInfo')
            map.put('codeInsertToClassName','com.jst.compinit.ComponentInitializer')
            map.put('registerMethodName','register')

            config.registerInfo.add(map)
            config.project = project
            config.convertConfig()
            transformImpl.config = config
            android.registerTransform(transformImpl)
        }
    }
}
  • 问题2:该框架是如何实现配置组件的初始化依赖的

首先该依赖信息通过注解的方式来配置是最方便的。所以我们定义了一个@Component注解

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
public @interface Component {

    /**
     * Component的name
     *
     * 一般情况下该name可以不设置
     * 如果该Component被其他Component所依赖,则该name必须设置
     */
    String name() default "";

    /**
     * 依赖的Component的name列表
     */
    String[] dependencies() default {};
}
  • 自定义annotationProcessor

我们需要一个注解处理器来解析用户配置的注解信息

@SupportedAnnotationTypes({"com.jst.compinit.annotation.Component"})
@AutoService(Processor.class)
public class ComponentProcessor extends AbstractProcessor {
    private Elements elementUtils;
    Filer filer;
    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        elementUtils = processingEnv.getElementUtils();
        filer = processingEnv.getFiler();
    }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {

        for (Element element : roundEnvironment.getElementsAnnotatedWith(Component.class)) {

            //接下来的代码所做的事情:被注解类所在的包名下,以 被注解类的类名$$IComponentInfoImpl 生成java文件


            //取出注解对象
            Component component = element.getAnnotation(Component.class);
            //获取包名
            PackageElement packageElement = elementUtils.getPackageOf(element);
            String packageName = packageElement.getQualifiedName().toString();
            //获取被注解类的类名
            String annotatedClassName = ((TypeElement)element).getSimpleName().toString();
            //生成的类的类名
            String generatedClassName = annotatedClassName + "$$IComponentInfoImpl";

            String literal;
            if (component.dependencies().length == 0) {
                literal = "{}";
            }else {
                literal = "{\"" + String.join("\",\"", component.dependencies()) + "\"}";
            }

            ArrayTypeName stringArray = ArrayTypeName.of(String.class);
            ClassName annotatedClass = ClassName.get((TypeElement)element);
            ClassName iComponentEntityClass = ClassName.get("com.jst.compinit", "IComponentInfo");
            ClassName iComponentClass = ClassName.get("com.jst.compinit", "IComponent");

            //getName方法
            MethodSpec getNameMethod = MethodSpec.methodBuilder("getName")
                    .addModifiers(Modifier.PUBLIC)
                    .returns(String.class)
                    .addStatement("return $S",component.name())
                    .build();

            //getDependencies方法
            MethodSpec getDependenciesMethod = MethodSpec.methodBuilder("getDependencies")
                    .addModifiers(Modifier.PUBLIC)
                    .returns(stringArray)
                    .addStatement("return new $T$L",stringArray,literal)
                    .build();

            //getComponent方法
            MethodSpec getComponentMethod = MethodSpec.methodBuilder("getComponent")
                    .addModifiers(Modifier.PUBLIC)
                    .returns(iComponentClass)
                    .addStatement("return new $T()",annotatedClass)
                    .build();

            //类
            TypeSpec generatedClass = TypeSpec.classBuilder(generatedClassName)
                    .addModifiers(Modifier.PUBLIC)
                    .addSuperinterface(iComponentEntityClass)
                    .addMethod(getNameMethod)
                    .addMethod(getDependenciesMethod)
                    .addMethod(getComponentMethod)
                    .build();

            try {
                JavaFile.builder(packageName, generatedClass).build().writeTo(filer);
            } catch (IOException e) {
                e.printStackTrace();
            }

        }

        return true;
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }
}

该注解处理器主要的作用是:

  1. 解析用户配置的注解信息
  2. 使用解析完的注解信息生成一个IComponentInfo实现类。
    IComponentInfo的作用是存储注解的信息,主要有三部分信息
    1. 用户配置的@Component注解里的name
    2. 用户配置的@Component注解里的dependencies
    3. 用户配置的@Component注解所注解的类

生成java类的能力需要用到javapoet

IComponentInfo类定义:

/**
 * 用户的Component的相关信息
 */
public interface IComponentInfo {
    /**
     * 用户配置的@Component注解里的name
     */
    String getName();
    /**
     * 用户配置的@Component注解里的dependencies
     */
    String[] getDependencies();
    /**
     * 用户配置的@Component注解所注解的类
     */
    IComponent getComponent();
}

注解处理器会在被注解类所在的包名下,以 “被注解类的类名$$IComponentInfoImpl” 作为类名生成java文件
我们来看看注解处理器生成的IComponentInfo实现类的示例:

public class Module1Component$$IComponentInfoImpl implements IComponentInfo {
  public String getName() {
    return "Module1Component";
  }

  public String[] getDependencies() {
    return new String[]{"Module2Component","Module3Component"};
  }

  public IComponent getComponent() {
    return new Module1Component();
  }
}

生成的类在什么位置呢,我们使用AndroidStudio的Android视图模式可以很方便的看到编译过程中自动生成的代码

然后我们自定义的Gradle plugin会将所有的IComponentInfo接口的实现类收集起来,然后注册到ComponentInitializer类中

  • 生成组件的依赖关系图

组件的依赖关系可以抽象成有向无环图,我们要保证被依赖的组件先被初始化,实际上就是要生成有向无环图的拓扑排序。

/**
 * 拓扑排序的工具类
 */
public class TopSortUtil {
    public static List<IComponentInfo> topSort(List<IComponentInfo> componentInfoList){
        //将图初始化出来
        List<Vertex> vertexList = new ArrayList<>();
        for (IComponentInfo componentInfo : componentInfoList) {
            Vertex vertex = new Vertex();
            vertex.componentInfo = componentInfo;
            vertexList.add(vertex);
        }
            //初始化邻接表
        for (Vertex vertex : vertexList) {
            String[] dependencies = vertex.componentInfo.getDependencies();
            for (String dependency : dependencies) {
                for (Vertex vertex1 : vertexList) {
                    if (dependency.equals(vertex1.componentInfo.getName())) {
                        vertex1.adjList.add(vertex);
                        vertex.indegree++;
                    }
                }
            }
        }

        List<IComponentInfo> sortedList = new ArrayList<>();
        int counter = 0;

        /*
        拓扑排序大致步骤:
        1.初始化一个入度为零顶点的队列
        2.出队一个顶点  (该顶点为拓扑排序的下一个顶点,可以将该顶点保存在list里面)
        3.删除该顶点及该顶点的边
        (这里说的删除顶点及边是直观上理解,对应到的代码逻辑就是,
        删除顶点:实际上在上一步出队时已经做过删除了;
        删除边:将该顶点的邻接表里的顶点入度-1)
        4.将入度-1后入度等于0的顶点添加到队列中
        5.重复 2-4 步骤

        循环退出条件:队列为空

        有环判断条件:队列里出队的顶点的计数 小于 顶点的总数
        */


        //初始化一个入度为零顶点的队列
        Queue<Vertex> queue = new LinkedList<>();
        for (Vertex vertex : vertexList) {
            if (vertex.indegree == 0) {
                queue.add(vertex);
            }
        }

        while (!queue.isEmpty()) {
            Vertex vertex = queue.remove();
            counter++;
            sortedList.add(vertex.componentInfo);
            for (Vertex vertex1 : vertex.adjList) {
                if (--vertex1.indegree == 0) {
                    queue.add(vertex1);
                }
            }
        }

        //判断是否有环
        if (counter < vertexList.size()){
            //构成环的List
            List<IComponentInfo> hasLoopList = new ArrayList<>();

            for (IComponentInfo componentInfo : componentInfoList) {
                if (!sortedList.contains(componentInfo)) {
                    hasLoopList.add(componentInfo);
                }
            }

            StringBuilder sb = new StringBuilder();
            sb.append("有一个dependency循环依赖存在于下列component中:");
            for (IComponentInfo componentInfo : hasLoopList) {
                sb.append(componentInfo.getComponent().getClass().getName())
                        .append(",");
            }
            sb.deleteCharAt(sb.lastIndexOf(","));
            throw new IllegalArgumentException(sb.toString());
        }

        return sortedList;
    }
}

该算法是有向无环图拓扑排序的标准算法。
其中还增加了对循环依赖的检测和通过抛异常来提示给用户。循环依赖相当于该有向图中存在了环。

最后

到此,基本上该框架的原理都讲完了,欢迎大家评论,提问题。

Github地址

https://github.com/ShuangtaoJia/ComponentInitializer

欢迎大家 Star , Fork

参考文章:

  1. AutoRegister
  2. Arouter

本文地址:https://blog.csdn.net/taotao110120119/article/details/110231007

《组件化架构之组件初始化 Component Initializer(组件初始化器)开源项目 原理分析.doc》

下载本文的Word格式文档,以方便收藏与打印。