Android组件化核心,全面掌握

发布时间:2026/8/27 19:02:35
Android组件化核心,全面掌握 前端开发经常遇到一个词路由在Android APP开发中路由还经常和组件化开发强关联在一起那么到底什么是路由一个路由框架到底应该具备什么功能实现原理是什么样的路由是否是APP的强需求呢与组件化到底什么关系本文就简单分析下如上几个问题。路由的概念路由这个词本身应该是互联网协议中的一个词维基百科对此的解释如下路由routing就是通过互联的网络把信息从源地址传输到目的地址的活动。路由发生在OSI网络参考模型中的第三层即网络层。个人理解在前端开发中路由就是通过一串字符串映射到对应业务的能力。APP的路由框首先能够搜集各组件的路由scheme并生成路由表然后能够根据外部输入字符串在路由表中匹配到对应的页面或者服务进行跳转或者调用并提供会获取返回值等示意如下image.png所以一个基本路由框架要具备如下能力APP路由的扫描及注册逻辑路由跳转target页面能力路由调用target服务能力APP中在进行页面路由的时候经常需要判断是否登录等一些额外鉴权逻辑所以还需要提供拦截逻辑等比如登陆。三方路由框架是否是APP强需求答案不是系统原生提供路由能力但功能较少稍微大规模的APP都采用三方路由框架。Android系统本身提供页面跳转能力如startActivity对于工具类APP或单机类APP这种方式已经完全够用完全不需要专门的路由框架那为什么很多APP还是采用路由框架呢这跟APP性质及路由框架的优点都有关。比如淘宝、京东、美团等这些大型APP无论是从APP功能还是从其研发团队的规模上来说都很庞大不同的业务之间也经常是不同的团队在维护采用组件化的开发方式最终集成到一个APK中。多团队之间经常会涉及业务间的交互比如从电影票业务跳转到美食业务但是两个业务是两个独立的研发团队代码实现上是完全隔离的那如何进行通信呢首先想到的是代码上引入但是这样会打破了低耦合的初衷可能还会引入各种问题。例如部分业务是外包团队来做这就牵扯到代码安全问题所以还是希望通过一种类似黑盒的方式调用目标业务这就需要中转路由支持所以国内很多APP都是用了路由框架的。其次我们各种跳转的规则并不想跟具体的实现类扯上关系比如跳转商详的时候不希望知道是哪个Activity来实现只需要一个字符串映射过去即可这对于H5、或者后端开发来处理跳转的时候就非常标准。原生路由的限制功能单一扩展灵活性差不易协同传统的路由基本上就限定在startActivity、或者startService来路由跳转或者启动服务。拿startActivity来说传统的路由有什么缺点startActivity有两种用法一种是显示的一种是隐式的显示调用如下!--1 导入依赖-- import com.snail.activityforresultexample.test.SecondActivity; public class MainActivity extends AppCompatActivity { void jumpSecondActivityUseClassName(){ !--显示的引用Activity类-- Intent intent new Intent(MainActivity.this, SecondActivity.class); startActivity(intent); }显示调用的缺点很明显那就是必须要强依赖目标Activity的类实现有些场景尤其是大型APP组件化开发时候有些业务逻辑出于安全考虑并不想被源码或aar依赖这时显式依赖的方式就无法走通。再来看看隐式调用方法。第一步manifest中配置activity的intent-filter至少要配置一个action?xml version1.0 encodingutf-8? manifest xmlns:androidhttp://schemas.android.com/apk/res/android packagecom.snail.activityforresultexample application ... activity android:name.test.SecondActivity intent-filter !--隐式调用必须配置android.intent.category.DEFAULT-- category android:nameandroid.intent.category.DEFAULT/ !--至少配置一个action才能通过隐式调用-- action android:namecom.snail.activityforresultexample.SecondActivity / !--可选-- !-- data android:mimeTypevideo/mpeg android:schemehttp ... /-- /intent-filter /activity /application /manifest第二步调用void jumpSecondActivityUseFilter() { Intent intent new Intent(); intent.setAction(com.snail.activityforresultexample.SecondActivity); startActivity(intent); }如果牵扯到数据传递写法上会更复杂一些隐式调用的缺点有如下几点首先manifest中定义复杂相对应的会导致暴露的协议变的复杂不易维护扩展。其次不同Activity都要不同的action配置每次增减修改Activity都会很麻烦对比开发者非常不友好增加了协作难度。最后Activity的export属性并不建议都设置成True这是降低风险的一种方式一般都是收归到一个ActivityDeeplinkActivitiy统一处理跳转这种场景下DeeplinkActivitiy就兼具路由功能隐式调用的场景下新Activitiy的增减势必每次都要调整路由表这会导致开发效率降低风险增加。可以看到系统原生的路由框架并没太多考虑团队协同的开发模式多限定在一个模块内部多个业务间直接相互引用基本都要代码级依赖对于代码及业务隔离很不友好。如不考虑之前Dex方法树超限制可以认为三方路由框架完全是为了团队协同而创建的。APP三方路由框架需具备的能力目前市面上大部分的路由框架都能搞定上述问题简单整理下现在三方路由的能力可归纳如下路由表生成能力业务组件**[UI业务及服务]**自动扫描及注册逻辑需要扩展性好无需入侵原有代码逻辑scheme与业务映射逻辑 无需依赖具体实现做到代码隔离基础路由跳转能力 页面跳转能力的支持服务类组件的支持 如去某个服务组件获取一些配置等[扩展]路由拦截逻辑比如登陆统一鉴权可定制的降级逻辑找不到组件时的兜底可以看下一个典型的Arouter用法第一步对新增页面添加Router Scheme 声明Route(path /test/activity2) public class Test2Activity extends AppCompatActivity { ... }build阶段会根据注解搜集路由scheme生成路由表。第二步使用ARouter.getInstance() .build(/test/activity2) .navigation(this);如上在ARouter框架下仅需要字符串scheme无需依赖任何Test2Activity就可实现路由跳转。APP路由框架的实现路由框架实现的核心是建立scheme和组件**[Activity或者其他服务]**的映射关系也就是路由表并能根据路由表路由到对应组件的能力。其实分两部分第一部分路由表的生成第二部分路由表的查询路由表的自动生成生成路由表的方式有很多最简单的就是维护一个公共文件或者类里面映射好每个实现组件跟schemeimage.png不过这种做法缺点很明显每次增删修改都要都要修改这个表对于协同非常不友好不符合解决协同问题的初衷。不过最终的路由表倒是都是这条路就是将所有的Scheme搜集到一个对象中只是实现方式的差别目前几乎所有的三方路由框架都是借助注解APT[Annotation Processing Tool]工具AOPAspect-Oriented Programming面向切面编程来实现的基本流程如下image.png其中牵扯的技术有注解、APT(Annotation Processing Tool)、AOPAspect-Oriented Programming面向切面编程。APT常用的有JavaPoet主要是遍历所有类找到被注解的Java类然后聚合生成路由表由于组件可能有很多路由表可能也有也有多个之后这些生成的辅助类会跟源码一并被编译成class文件之后利用AOP技术【如ASM或者JavaAssist】扫描这些生成的class聚合路由表并填充到之前的占位方法中完成自动注册的逻辑。JavaPoet如何搜集并生成路由表集合以ARouter框架为例先定义Router框架需要的注解如Target({ElementType.TYPE}) Retention(RetentionPolicy.CLASS) public interface Route { /** * Path of route */ String path();该注解用于标注需要路由的组件用法如下Route(path /test/activity1, name 测试用 Activity) public class Test1Activity extends BaseActivity { Autowired int age 10;之后利用APT扫描所有被注解的类生成路由表实现参考如下Override public boolean process(Set? extends TypeElement annotations, RoundEnvironment roundEnv) { if (CollectionUtils.isNotEmpty(annotations)) { !--获取所有被Route.class注解标注的集合-- Set? extends Element routeElements roundEnv.getElementsAnnotatedWith(Route.class); !--解析并生成表-- this.parseRoutes(routeElements); ... return false; } !--生成中间路由表Java类-- private void parseRoutes(Set? extends Element routeElements) throws IOException { ... // Generate groups 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);产物如下包含路由表及局部注册入口。image.png自动注册ASM搜集上述路由表并聚合插入Init代码区为了能够插入到Init代码区首先需要预留一个位置一般定义一个空函数以待后续填充public class RouterInitializer { public static void init(boolean debug, Class webActivityClass, IRouterInterceptor... interceptors) { ... loadRouterTables(); } //自动注册代码 public static void loadRouterTables() { } }首先利用AOP工具遍历上述APT中间产物聚合路由表并注册到预留初始化位置遍历的过程牵扯是gradle transform的过程搜集目标聚合路由表/**扫描jar*/ fun scanJar(jarFile: File, dest: File?) { val file JarFile(jarFile) var enumeration file.entries() while (enumeration.hasMoreElements()) { val jarEntry enumeration.nextElement() if (jarEntry.name.endsWith(XXRouterTable.class)) { val inputStream file.getInputStream(jarEntry) val classReader ClassReader(inputStream) if (Arrays.toString(classReader.interfaces) .contains(IHTRouterTBCollect) ) { tableList.add( Pair( classReader.className, dest?.absolutePath ) ) } inputStream.close() } else if (jarEntry.name.endsWith(HTRouterInitializer.class)) { registerInitClass dest } } file.close() }对目标Class注入路由表初始化代码fun asmInsertMethod(originFile: File?) { val optJar File(originFile?.parent, originFile?.name .opt) if (optJar.exists()) optJar.delete() val jarFile JarFile(originFile) val enumeration jarFile.entries() val jarOutputStream JarOutputStream(FileOutputStream(optJar)) while (enumeration.hasMoreElements()) { val jarEntry enumeration.nextElement() val entryName jarEntry.getName() val zipEntry ZipEntry(entryName) val inputStream jarFile.getInputStream(jarEntry) //插桩class if (entryName.endsWith(RouterInitializer.class)) { //class文件处理 jarOutputStream.putNextEntry(zipEntry) val classReader ClassReader(IOUtils.toByteArray(inputStream)) val classWriter ClassWriter(classReader, ClassWriter.COMPUTE_MAXS) val cv RegisterClassVisitor(Opcodes.ASM5, classWriter,tableList) classReader.accept(cv, EXPAND_FRAMES) val code classWriter.toByteArray() jarOutputStream.write(code) } else { jarOutputStream.putNextEntry(zipEntry) jarOutputStream.write(IOUtils.toByteArray(inputStream)) } jarOutputStream.closeEntry() } //结束 jarOutputStream.close() jarFile.close() if (originFile?.exists() true) { Files.delete(originFile.toPath()) } optJar.renameTo(originFile) }最终RouterInitializer.class的 loadRouterTables会被修改成如下填充好的代码public static void loadRouterTables() { !---- register(com.alibaba.android.arouter.routes.ARouter$$Root$$modulejava); register(com.alibaba.android.arouter.routes.ARouter$$Root$$modulekotlin); register(com.alibaba.android.arouter.routes.ARouter$$Root$$arouterapi); register(com.alibaba.android.arouter.routes.ARouter$$Interceptors$$modulejava); ... }如此就完成了路由表的搜集与注册大概的流程就是如此。当然对于支持服务、Fragment等略有不同但大体类似。Router框架对服务类组件的支持通过路由的方式获取服务属于APP路由比较独特的能力比如有个用户中心的组件我们可以通过路由的方式去查询用户是否处于登陆状态这种就不是狭义上的页面路由的概念通过一串字符串如何查到对应的组件并调用其方法呢这种的实现方式也有多种每种实现方式都有自己的优劣。一种是可以将服务抽象成接口沉到底层上层实现通过路由方式映射对象一种是将实现方法直接通过路由方式映射先看第一种这种事Arouter的实现方式它的优点是所有对外暴露的服务都暴露接口类【沉到底层】这对于外部的调用方也就是服务使用方非常友好示例如下先定义抽象服务并沉到底层image.pngpublic interface HelloService extends IProvider { void sayHello(String name); }实现服务并通过Router注解标记Route(path /yourservicegroupname/hello) public class HelloServiceImpl implements HelloService { Context mContext; Override public void sayHello(String name) { Toast.makeText(mContext, Hello name, Toast.LENGTH_SHORT).show(); }使用利用Router加scheme获取服务实例并映射成抽象类,然后直接调用方法。((HelloService) ARouter.getInstance().build(/yourservicegroupname/hello).navigation()).sayHello(mike);这种实现方式对于使用方其实是很方便的尤其是一个服务有多个可操作方法的时候但是缺点是扩展性如果想要扩展方法就要改动底层库。再看第二种将实现方法直接通过路由方式映射服务的调用都要落到方法上参考页面路由也可以支持方法路由两者并列关系所以组要增加一个方法路由表实现原理与Page路由类似,跟上面的Arouter对比不用定义抽象层直接定义实现即可定义Method的Routerpublic class HelloService { !--参数 name-- MethodRouter(url {arouter://sayhello}) public void sayHello(String name) { Toast.makeText(mContext, Hello name, Toast.LENGTH_SHORT).show(); }使用即可RouterCall.callMethod(arouter://sayhello?namehello);上述的缺点就是对于外部调用有些复杂尤其是处理参数的时候需要严格按照协议来处理优点是没有抽象层如果需要扩展服务方法不需要改动底层。上述两种方式各有优劣不过如果从做服务组件的初衷出发第一种比较好对于调用方比较友好。另外对于CallBack的支持Arouter的处理方式可能也会更方便一些可以比较方便的交给服务方定义。如果是第二种服务直接通过路由映射的方式处理起来就比较麻烦尤其是Callback中的参数可能要统一封装成JSON并维护解析的协议这样处理起来可能不是很好。路由表的匹配路由表的匹配比较简单就是在全局Map中根据String输入匹配到目标组件然后依赖反射等常用操作定位到目标。组件化与路由的关系组件化是一种开发集成模式更像一种开发规范更多是为团队协同开发带来方便。组件化最终落地是一个个独立的业务及功能组件这些组件之间可能是不同的团队处于不同的目的在各自维护甚至是需要代码隔离如果牵扯到组件间的调用与通信就不可避免的借助路由因为实现隔离的只能采用通用字符串scheme进行通信这就是路由的功能范畴。组件化需要路由支撑的根本原因组件间代码实现的隔离总结路由不是一个APP的必备功能但是大型跨团队的APP基本都需要路由框架的基本能力路由自动注册、路由表搜集、服务及UI界面路由及拦截等核心功能组件化与路由的关系组件化的代码隔离导致路由框架成为必须最后分享给大家一份最近小编有幸在字节跳动高工手里扒到这份“组件化实战学习手册”这位大佬主导开发过多个采用组件化架构的APP项目。现在我只想吹爆这位大佬熬夜半个月把积累的经验和踩的坑都进行了总结整理成了长达220页的学习手册手册中收录了大厂热门的组件化项目实战从新手到架构师这份学习手册足以。第一章 Android组件化初识组件化和模块化的区别组件化和插件化的区别组件化开发的优势业务逻辑层组件化开发要遇到的问题从组件化实战来解决问题Android组件化基础……第二章 Android组件化初探目录组件化演示案例概述模块化和组件化组件化Demo组件application和library动态切换……WanAndroid APP 组件化项目实战附demo版本更新效果图主要功能项目目录结构主要开源框架……第三章 架构演化大厂篇从智行 Android 项目看组件化架构实践组件化调整的原因和目标组件化架构调整的整体规划组件化架构调整中遇到的一些问题得到 AppAndroid彻底组件化demo发布JIMU使用指南组件化拆分的感悟……微信 App微信Android模块化架构重构实践微信Android架构历史为何再次重构微信……蘑菇街 App蘑菇街 App 的组件化实现方式组件生命周期管理壳工程遇到的问题持续集成周边设施……爱奇艺 App基于Service的IPC通信组件化跨进程通信的核心诉求Andromeda架构分析……美团 App外卖客户端容器化架构的演进美团外卖容器化架构全景图外卖跨端容器建设外卖页面容器建设外卖容器化架构的衡量指标外卖容器化架构的监控运维外卖容器化架构的发布能力……美团Android组件化方案及组件消息总线modular-event实战美团Android消息总线的演进之路用LiveDataBus替代RxBus、EventBusWMRouter美团外卖Android开源路由框架美团猫眼android模块化实战-可能是最详细的模块化实战携程App支付宝 App…由于篇幅限制这里只能展示部分内容朋友们如果需要这份完整版的PDF资料微信扫描下方二维码【免费获取】。