组件化架构之组件初始化 Component Initializer(组件初始化器)开源项目 原理分析
Component Initializer(组件初始化器)开源项目 介绍
Component Initializer(组件初始化器)介绍
原理分析
刚开始我想到的方案是通过注解来实现。首先用@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来实现此功能
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)
}
}
}
首先该依赖信息通过注解的方式来配置是最方便的。所以我们定义了一个@Component注解
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
public @interface Component {
/**
* Component的name
*
* 一般情况下该name可以不设置
* 如果该Component被其他Component所依赖,则该name必须设置
*/
String name() default "";
/**
* 依赖的Component的name列表
*/
String[] dependencies() default {};
}
我们需要一个注解处理器来解析用户配置的注解信息
@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();
}
}
该注解处理器主要的作用是:
- 解析用户配置的注解信息
- 使用解析完的注解信息生成一个IComponentInfo实现类。
IComponentInfo的作用是存储注解的信息,主要有三部分信息- 用户配置的@Component注解里的name
- 用户配置的@Component注解里的dependencies
- 用户配置的@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
参考文章:
本文地址:https://blog.csdn.net/taotao110120119/article/details/110231007
上一篇: vue实现键盘输入支付密码功能
下一篇: ps怎么设计喷墨效果的人物头像?