谈谈组件化-从源码到理解

简介: 谈谈组件化-从源码到理解

这几天一直在组件化架构方面的知识点,下面主要分析一下“得到”的组件化方案和Arouter实现组件间路由的功能。


组件化涉及到的知识点


得到的方案


最近一会在探索组件化的实现方案,得到是在每个组件的build.gradle给annotationProcessorOptions设置host参数,这个参数就是我们当前组件的Group,apt拿到这个Group名称拼接需要生成的路由表类的全路径(不同的module都会生成不同的路由表类),然后扫描当前module被注释了RouteNode的类,将path和类信息存储到生成的类,类的生成主要通过javapoet框架实现


下面是App模块的路由表


public class AppUiRouter extends BaseCompRouter {
    public AppUiRouter() {
    }
    public String getHost() {
        return "app";
    }
    public void initMap() {
        super.initMap();
        this.routeMapper.put("/main", MainActivity.class);
        this.paramsMapper.put(MainActivity.class, new HashMap() {
            {
                this.put("name", Integer.valueOf(8));
            }
        });
        this.routeMapper.put("/test", TestActivity.class);
    }
}
复制代码


这些都是在编译期间实现的,那么,运行期呢?在运行的时候,通过在Application注册这个路由表类,


UIRouter.getInstance().registerUI("app");
复制代码


这个app参数就是我们在build.gradle设置的host的值,也就是Group值,然后通过UIRouter的fetch方法,拼接apt之前生成的注册表类所在的路径,然后通过反射,将这个类拿到,存档到map集合里面


private IComponentRouter fetch(@NonNull String host) {
        //通过host拼接apt生成的类的路径
        String path = RouteUtils.genHostUIRouterClass(host);
        if (routerInstanceCache.containsKey(path))
            return routerInstanceCache.get(path);
        try {
            Class cla = Class.forName(path);
            IComponentRouter instance = (IComponentRouter) cla.newInstance();
            routerInstanceCache.put(path, instance);
            return instance;
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
        return null;
    }
复制代码


这样,在下次发起openUri打开其他组件的Activity的时候,就可以通过openUri的方式,拿到host值,然后拿到IComponentRouter,然后拿到path,取出注册表对应的Activity.class,然后就跟平常一样startActivity打开对应的Activity,具体可以看

BaseCompRouter


当然,得到的组件化不仅仅这些,还有application的注册,因为组件模块有些需要在application中初始化,但是一个app中不允许多个application的存在,所以,得到提供了两个方案,反射的方式,将组件的application路径交给主app,由主app的application统一反射注册,另一种方案就是通过gradle插件的方式,在组件的build.gradle设置combuild参数,主要是为了向插件提供参数,如下:


combuild {
    applicationName = 'com.luojilab.share.runalone.application.ShareApplication'
    isRegisterCompoAuto = true
}
复制代码


然后module统一依赖 apply plugin: 'com.dd.comgradle'

插件中做了不少东西,具体的大家可以去看看,我大致说说,子模块生成aar,移动到componentrelease文件夹,主模块去componentrelease文件夹中compile依赖这些aar,如果组件是单独调试模块,也给模块设置了sourceSet,设置不同路径的AndroidManifest,然后注册了transform,transform主要是将combuild设置的applicationName,拿到类路径,然后通过javassist插入字节码插入到主Application的onCreate方法中去,看一看生成后是什么样的


public class AppApplication extends Application {
    public AppApplication() {
    }
    public void onCreate() {
        super.onCreate();
        UIRouter.getInstance().registerUI("app");
        Object var2 = null;
        (new ReaderAppLike()).onCreate();
        (new ShareApplike()).onCreate();
        (new KotlinApplike()).onCreate();
    }
}
复制代码


大致差不多了,我来点评一下:


得到的方案还是有点诟病的,在build.grdle中设置了moudle的名称,这个名称是要与application注册的名称是必须要一致的,这两个名称没有一个统一的来源,很容易导致集成的开发者弄错,导致找不到注册表,我建议的方案是,在组件的build.gradle设置一个ext扩展变量,为我们模块的名字,然后apt的host去拿这个扩展变量,buildTypes里面设置一个buildConfigField,指向的也是这个变量,那么在组件中注册组件的时候,就可以通过BuildConfig去拿这个变量


大致思路代码:


apply plugin: 'com.dd.comgradle'
ext.moduleName = [
        host: "share"
]
android {
   ...
        javaCompileOptions {
            annotationProcessorOptions {
                arguments = [host: moduleName.host]
            }
        }
   ...
    buildTypes {
        debug {
            buildConfigField "String", "HOST", "\"${moduleName.host}\""
        }
        release {
            buildConfigField "String", "HOST", "\"${moduleName.host}\""
        }
    }
//-------------------------------------------
子组件ShareApplike.class
    @Override
    public void onCreate() {
       //子组件包名+BuildConfig拿到host的值
        uiRouter.registerUI(com.luojilab.share.BuildConfig.HOST);
        Log.i("ShareApplike","ShareApplike-----");
        new ShareApplike().onCreate();
    }
复制代码


这样可以确保注册的组件和生成的组件是一致的


还有一个我觉得不好的就是application那个用transform插入字节码的功能,需要在build.gradle中去配置comBuild对应的application路径,对于集成者来说,配置越少,实现功能越强大是最好的方法,transform实现的功能就是对各个组件的application插入字节码,其实是完全可以抛弃使用transform,虽然用transfrom插入字节码可以避免了反射,但毕竟组件的application比较少,反射的话,也就那几个类,影响不了多大的性能,反而是注册表,如果组件注册的路由特别多,那么这个路由表就会特别大,反射会影响很大的性能,我觉得比较好的方法是,定义一个和RouteNode一样的注解叫RouteApplication,然后将组件需要执行的application都标上RouteApplication注解,apt解析拿到这些类,生成对应的moudle名称+Application的类,然后在运行阶段,openUri打开其他组件的时候,拼接路径类,然后反射,和路由表方式一样,这样,可以完全摒弃transfrom的存在,少了一些配置


还有一个就是,如果为了性能着想,还是不要用apt的方式,apt总会遇到反射,建议全用transfrom插入字节码的方式,将路由全部插入到一个路由表管理类里面,这个路由表管理类是我们自己写好的,只是里面啥都没有,都是在编译阶段通过transform插入,transform使用javassist或是asm都可以操作字节码,只不过一个好用,但耗时,一个不好用,速度快,但用谁都无关紧要,并都是在编译阶段,只要不影响运行阶段就行

还有就是apt只能对当前module的类进行扫描拿到class信息,并且是扫描不了jar包、maven、aar里面的类,所以,还是比较有局限性的,transfrom可以扫描apt解决不了地方


Arouter的方案


去年CC组件化的作者向Arouter提交了一个pr,auto-register为Arouter提供一个在编译阶段自动注册路由的功能,以前Arouter是通过反射的方式注册路由表,现在是通过transfrom插入字节码实现。


Arouter不同于“得到”组件化,Arouter的组件模块是不能单独运行的,需要开发着自行解决,Arouter只提供了路由的解决方案


Arouter主要提供了三个注解处理器


  • RouteProcessor : 处理注解的路由,作用在类上面
  • InterceptorProcessor : 路由拦截器,作用在类上面
  • AutowiredProcessor : 注入上个页面传递过来的值,作用在字段上面


配置方面,还是一样,每个组件都必须依赖注解处理器,Arouter和“得到”提供的参数作用是不一样的,得到提供的参数直接就是路由表的分组Group,而Arouter提供的module参数主要是生成收集当前module所有的分组,然后收集的分组对应各个路由表


javaCompileOptions {
            annotationProcessorOptions {
                arguments = [ moduleName : project.getName() ]
            }
复制代码


RouteProcessor中主要是扫描被Route注解的类,然后拿到当前Route注解类的path、group和被注入值Autowired字段。这些信息都存储在RouteMeta类中,主要是方便管理,这个地方说一下group这个字段,举个例子:


/**
 *  那么test就是这个group字段
 */
@Route(path = "/test/activity1")
复制代码


这个group字段是什么时候赋值给RouteMeta的呢,那就是在调用categories方法的时候,通过routeVerify方法进行校验是否符合path路径的时候赋值的,具体可以看RouteProcessor类的routeVerify方法。


然后可以看categories方法,这个方法看下groupMap这个集合,他是一个Map<String, Set<RouteMeta>>类型,主要功能还是分拣,以Group为key,将Group一样的RouteMeta放在一个set集合里面,为后面生成注册表类做基础


分拣好分组的信息之后,就会开始遍历这个groupMap集合,这个地方主要功能就是通过javapoet来创建类文件,来看下一段生成类的代码,稍微比较核心一点。


// 拼接 Arouter$$Group?<test>类(groupName)
    String groupFileName = NAME_OF_GROUP + groupName;
    //生成对应的类
    JavaFile.builder(PACKAGE_OF_GENERATE_FILE,
         TypeSpec.classBuilder(groupFileName)
            .addJavadoc(WARNING_TIPS)
            .addSuperinterface(ClassName.get(type_IRouteGroup))
            .addModifiers(PUBLIC)
            .addMethod(loadIntoMethodOfGroupBuilder.build())
            .build()).build().writeTo(mFiler);
            //将生成类存储到一个rootMap集合,这个是找到Group对应的路由表的关键
            rootMap.put(groupName, groupFileName);
            }
复制代码


在遍历循环结束后,rootMap的作用来了,首先是填充字段,拼接字段信息添加到MethodSpec.Builder中


if (MapUtils.isNotEmpty(rootMap)) {
      // Generate root meta by group name, it must be generated before root, then I can find out the class of group.
     for (Map.Entry<String, String> entry : rootMap.entrySet()) {
        loadIntoMethodOfRootBuilder.addStatement("routes.put($S, $T.class)", entry.getKey(), ClassName.get(PACKAGE_OF_GENERATE_FILE, entry.getValue()));
        }
     }
复制代码


这个地方就是我们在build.gradle中javaCompileOptions设置moduleName的原因,主要功能就是生成以当前module名字为结尾的Arouter?Root?类,然后将Group的类信息存储在这个moduleName类中


// 拼接 Arouter$$Root?<moduleName>类
    String rootFileName = NAME_OF_ROOT + SEPARATOR + moduleName;
    JavaFile.builder(PACKAGE_OF_GENERATE_FILE,
    TypeSpec.classBuilder(rootFileName)
            .addJavadoc(WARNING_TIPS)
            .addSuperinterface(ClassName.get(elements.getTypeElement(ITROUTE_ROOT)))
            .addModifiers(PUBLIC)
            //添加拼接好的字段
            .addMethod(loadIntoMethodOfRootBuilder.build())
            .build()).build().writeTo(mFiler);
复制代码


下面我贴一下生成的两个类


ARouter?Root?app.java :收集app module中所有Group对应的路由表类路径

public class ARouter$$Root$$app implements IRouteRoot {
    public ARouter$$Root$$app() {
    }
    public void loadInto(Map<String, Class<? extends IRouteGroup>> routes) {
        routes.put("service", service.class);
        routes.put("test", test.class);
        routes.put("test2", test2.class);
    }
}
复制代码

ARouter?Group?test2.java : app module中test分组的路由表

public class ARouter$$Group$$test2 implements IRouteGroup {
    public ARouter$$Group$$test2() {
    }
    public void loadInto(Map<String, RouteMeta> atlas) {
        atlas.put("/test2/activity2", RouteMeta.build(RouteType.ACTIVITY, Test2Activity.class, "/test2/activity2", "test2", new HashMap<String, Integer>() {
            {
                this.put("key1", Integer.valueOf(8));
            }
        }, -1, -2147483648));
    }
}
复制代码


Arouter生成的路由表和“得到”的方案不一样,然后我们来对比一下,“得到”的方案是给当前组件定死了这个Group分组,比如Reader组件设置的host为reader,那么,这个Reader组件中,所有生成的路由表的Group分组都是reader,好处就是提前做好了分组的概念,生成的路由表类也是根据host的名称生成出来,很直观,反观Arouter,首先生成的是一个关于module的类,这个module类中存储了当前module所有的group分组的类信息,如果当前module有很多的group,那么就会生成很多的类,不好的地方看起来不太直观,生成的类信息太多,不过都差不多,Arouter反射的对象是module,“得到”反射的对象是Group。


Group分组在Arouter并不是一个很重的概念,跟“得到”的方案不一样,每个组件都规定了group,而Arouter可以随意定义group,可能一个组件里面有很多的group。


路由表信息都生成了,接下来就是反注册了,Arouter之前的方案是采用遍历Dex文件取出类信息并将这些类信息进行反射,拿到注册表,放到一个缓存里面,后来引入auto-register之后,采用注入字节码的方式,主要逻辑来看LogisticsCenter类。


public synchronized static void init(Context context, ThreadPoolExecutor tpe) throws HandlerException {
            long startInit = System.currentTimeMillis();
            //billy.qi modified at 2017-12-06
            //load by plugin first
            loadRouterMap();
            if (registerByPlugin) {
                logger.info(TAG, "Load router map by arouter-auto-register plugin.");
            } else {
             ...
              routerMap = ClassUtils.getFileNameByPackageName(mContext, ROUTE_ROOT_PAKCAGE);
             ...
            }
    }
复制代码


loadRouterMap这个方法主要是设置是否使用自动注册的功能,默认registerByPlugin的值为false,还是采用ClassUtils的方式去反射注册表,如果想采用auto-register的方,设置registerByPlugin为true,并在build.gradle中依赖插件 apply plugin: 'com.alibaba.arouter',具体的依赖可以看arouter-api模块

auto-register的好处是什呢么?刚和作者聊了下


  • 优化了启动速度
  • 解决了加固后找不到路由的问题
AutoRegister插件从根本上解决了找不到dex文件的问题:通过编译时进行字节码扫描对应3个接口的实现类,生成注册代码到ARouter的LogisticsCenter类中,运行时无需再读取dex文件,从而避免加固的兼容性问题。
复制代码


大致意思就是,Arouter原来要遍历apk的dex来找到注册表类信息,但是,由于加固问题,会导致找不到dex文件,遍历dex文件是一个耗时的操作,在初始化应用的时候速度没有自动注册的好。


这个地方还有一个好玩的事情,我们还是来看下loadRouterMap这个方法吧,主要是来看这个注释


private static void loadRouterMap() {
        registerByPlugin = false;
        //auto generate register code by gradle plugin: arouter-auto-register
        // looks like below:
        // registerRouteRoot(new ARouter..Root..modulejava());
        // registerRouteRoot(new ARouter..Root..modulekotlin());
     }
复制代码


刚开始看的时候,我一直以为auto-register所做的插入的字节码就是插入registerRouteRoot(new ARouter..Root..modulejava()),我们在前面分析的时候就说过,注册表的Group分组是放在每个module的类信息中,如果直接将module类找到,拿出他的Group map集合,根据map集合就可以找到Route路由集合,并且,一点也不会用到反射,确实优化的不错,但看完auto-register的源码后,发现并不是插入的这段代码,而是插入register("ARouter?Root?moduleName类路径");,就是将各个module存储分组的类进行了注册,来看下regiter方法


private static void register(String className) {
        if (!TextUtils.isEmpty(className)) {
            try {
                Class<?> clazz = Class.forName(className);
                Object obj = clazz.getConstructor().newInstance();
                if (obj instanceof IRouteRoot) {
                    //
                    registerRouteRoot((IRouteRoot) obj);
                } else if (obj instanceof IProviderGroup) {
                    registerProvider((IProviderGroup) obj);
                } else if (obj instanceof IInterceptorGroup) {
                    registerInterceptor((IInterceptorGroup) obj);
                } else {
                    logger.info(TAG, "register failed, class name: " + className
                            + " should implements one of IRouteRoot/IProviderGroup/IInterceptorGroup.");
                }
            } catch (Exception e) {
                logger.error(TAG,"register class error:" + className);
            }
        }
    }
复制代码


registerRouteRoot(new ARouter..Root..modulejava())相比,多了一步反射,我很好奇,明明transform能找到存储Group分组的module类,通过插入字节码就能解决,为啥还要多做一步反射呢?摆脱反射不是能更好的优化性能吗?后来我去问了auto-register的作者,他跟我说,故事是这样的:


我提交PR后,ARouter的作者反馈说增加了首个dex的大小,要改成类名反射创建对象的方式注册(需要配置混淆规则)。
但是我测试下来没发现这个注册对首个dex的影响有多大,所以autoregister中继续保持以对象方式注册
复制代码


最终,Arouter还是采用了反射的方式

最后来说下auto-register做了啥,auto-register主要利用transform遍历所有模块的class信息,寻找class的全路径起始部分是否是com/alibaba/android/arouter/routes/,是的话,加入到一个缓存的registerList集合里面,等待被插入字节码

插入字节码部分,我们来看看吧,大致贴一点代码


@Override
        void visitInsn(int opcode) {
            //generate code before return
            if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN)) {
                extension.classList.each { name ->
                    name = name.replaceAll("/", ".")
                    mv.visitLdcInsn(name)//存储group分组的module类名
                    // generate invoke register method into LogisticsCenter.loadRouterMap()
                    mv.visitMethodInsn(Opcodes.INVOKESTATIC
                            , ScanSetting.GENERATE_TO_CLASS_NAME//com/alibaba/android/arouter/core/LogisticsCenter
                            , ScanSetting.REGISTER_METHOD_NAME//register
                            , "(Ljava/lang/String;)V"
                            , false)
                }
            }
            super.visitInsn(opcode)
        }
复制代码


这段代码是用asm来插入字节码,asm寻找类路径是采用斜杠的方式,但插入字节码的类,需要是点号,这段代码就是向LogisticsCenter类的loadRouterMap方法,插入一段register("存储group分组的module类名");代码

Arouter具体的分析也说完了,最后来说个总结吧


总结


涉及到的知识点

  • apt的使用
  • transfrom 的使用
目录
相关文章
|
2月前
|
前端开发 JavaScript 开发者
深入解析前端开发中的模块化与组件化实践
【10月更文挑战第5天】深入解析前端开发中的模块化与组件化实践
36 1
|
2月前
|
前端开发 JavaScript UED
探索现代Web开发中的响应式设计原则与实践
【10月更文挑战第9天】在移动互联网的浪潮中,响应式设计已成为Web开发的必备技能。本文旨在深入解析响应式设计的核心原则,并结合实战案例,展示如何运用这些原则构建灵活、高效的Web应用界面。文章不仅涵盖理论探讨,更提供具体代码示例,帮助读者从概念到实现全面掌握响应式设计。
|
4月前
|
前端开发 C# 设计模式
“深度剖析WPF开发中的设计模式应用:以MVVM为核心,手把手教你重构代码结构,实现软件工程的最佳实践与高效协作”
【8月更文挑战第31天】设计模式是在软件工程中解决常见问题的成熟方案。在WPF开发中,合理应用如MVC、MVVM及工厂模式等能显著提升代码质量和可维护性。本文通过具体案例,详细解析了这些模式的实际应用,特别是MVVM模式如何通过分离UI逻辑与业务逻辑,实现视图与模型的松耦合,从而优化代码结构并提高开发效率。通过示例代码展示了从模型定义、视图模型管理到视图展示的全过程,帮助读者更好地理解并应用这些模式。
129 0
|
7月前
|
搜索推荐 开发者
【Uniapp 专栏】探究 Uniapp 组件化开发的奥秘
【5月更文挑战第12天】Uniapp的组件化开发模式正引领移动应用开发潮流,提升开发效率并简化维护。通过将应用拆分为独立、可复用的组件,开发者能快速构建和优化功能,降低出错风险。基础组件满足基本需求,自定义组件则针对特定业务场景。Uniapp提供简洁的组件定义、通信支持及组件库管理,促进数据共享和功能协同。然而,组件设计需考虑通用性、扩展性和依赖管理。组件化开发在Uniapp中日益重要,为开发者创造更多价值,激发创新潜力。
97 4
【Uniapp 专栏】探究 Uniapp 组件化开发的奥秘
|
7月前
|
移动开发 JavaScript 前端开发
【Uniapp 专栏】解读 Uniapp 跨平台开发的底层逻辑
【5月更文挑战第12天】Uniapp是一款跨平台移动应用开发框架,基于Vue.js,通过组件化、条件编译和原生插件扩展实现跨iOS、Android、H5的代码复用。它采用分层设计,统一JavaScript环境,编译时适应不同平台需求。借助调试工具保障质量和稳定性,Uniapp为开发者提供高效开发解决方案,助力创造优质、高性能的跨平台应用。随着技术进步,Uniapp在跨平台开发领域的影响力将持续增强。
189 4
【Uniapp 专栏】解读 Uniapp 跨平台开发的底层逻辑
|
7月前
|
存储 移动开发 前端开发
【Uniapp 专栏】Uniapp 架构设计与原理探究
【5月更文挑战第12天】Uniapp是一款用于跨平台移动应用开发的框架,以其高效性和灵活性脱颖而出。它基于HTML、CSS和Vue.js构建视图层,JavaScript处理逻辑层,管理数据层,实现统一编码并支持原生插件扩展。通过抽象平台特性,开发者能专注于业务逻辑,提高开发效率。尽管存在兼容性和复杂性挑战,但深入理解其架构设计与原理将助力开发者创建高质量的跨平台应用。随着技术进步,Uniapp将继续在移动开发领域扮演重要角色。
251 1
【Uniapp 专栏】Uniapp 架构设计与原理探究
|
7月前
|
设计模式 前端开发 API
写出易维护的代码|React开发的设计模式及原则
本文对React社区里出现过的一些设计模式进行了介绍,并讲解了他们遵循的设计原则。
|
7月前
|
存储 Dart JavaScript
Flutter笔记:聊一聊依赖注入(上)
Flutter笔记:聊一聊依赖注入(上)
307 0
|
7月前
|
JavaScript 前端开发
面试题:组件化和模块化的理解
面试题:组件化和模块化的理解
70 0
|
设计模式 存储 缓存
AI问答:前端需要掌握的设计模式/vue项目使用了哪些设计模式/vue项目开发可以使用哪些设计模式(上)
AI问答:前端需要掌握的设计模式/vue项目使用了哪些设计模式/vue项目开发可以使用哪些设计模式(上)
119 0
AI问答:前端需要掌握的设计模式/vue项目使用了哪些设计模式/vue项目开发可以使用哪些设计模式(上)