关于Android组件化深度分析蘑菇街
Posted 初一十五啊
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了关于Android组件化深度分析蘑菇街相关的知识,希望对你有一定的参考价值。
前言
昨天把之前十二个模块的内容补充了一下,形成了完整版的。
今天继续更之前对组件化的介绍
后续等组件化更新完,更新:
- Shadow插件化
- ASM其他
- Systrace使用与解析
- 字节码相关
关注公众号:初一十五a
解锁 《Android十三大板块文档》,让学习更贴近未来实战。已形成PDF版
内容如下:
1.2022最新Android11位大厂面试专题,128道附答案
2.音视频大合集,从初中高到面试应有尽有
3.Android车载应用大合集,从零开始一起学
4.性能优化大合集,告别优化烦恼
5.Framework大合集,从里到外分析的明明白白
6.Flutter大合集,进阶Flutter高级工程师
7.compose大合集,拥抱新技术
8.Jetpack大合集,全家桶一次吃个够
9.架构大合集,轻松应对工作需求
10.Android基础篇大合集,根基稳固高楼平地起
11.Flutter番外篇:Flutter面试+Flutter项目实战+Flutter电子书
12.高级Android组件化强化实战
13.十二模块之补充部分:其他Android十一大知识体系
整理不易,关注一下吧。开始进入正题,ღ( ´・ᴗ・` ) 🤔
从蘑菇街APP看组件化架构实践
- 提到了组件间通信的实现:URL 统跳、协议下沉(需要有一个公共的地方来容纳这些 public protocl)、隐式Intent
- 提到了组件生命周期管理:监听系统通知丶ModuleManager 中手动遍历调用
- 提到了组件版本管理与持续集成经验
一丶蘑菇街APP组件化之路
在组件化之前,蘑菇街 App 的代码都是在一个工程里开发的,在人比较少,业务发展不是很快的时候,这样是比较合适的,能一定程度地保证开发效率。
慢慢地代码量多了起来,开发人员也多了起来,业务发展也快了起来,这时单一工程开发模式就会显露出一些弊端:
- 耦合比较严重(因为没有明确的约束,「组件」间引用的现象会比较多);
- 容易出现冲突(尤其是使用 Xib,还有就是 Xcode Project,虽说有脚本可以改善);
- 业务方的开发效率不够高(只关心自己的组件,却要编译整个项目,与其他不相干的代码糅合在一起)。
为了解决这些问题,就采取了「组件化」策略。它能带来这些好处:
- 加快编译速度(不用编译主客那一大坨代码了);
- 自由选择开发姿势(MVC / MVVM / FRP);
- 方便 QA 有针对性地测试;
- 提高业务开发效率。
先来看下,组件化之后的一个大概架构:
「组件化」顾名思义就是把一个大的 App 拆成一个个小的组件,相互之间不直接引用。那如何做呢?
1.1.实现方式
组件间通信
以 ios 为例,由于之前就是采用的 URL 跳转模式,理论上页面之间的跳转只需 open 一个 URL 即可。所以对于一个组件来说,只要定义「支持哪些 URL」即可,比如详情页,大概可以这么做的:
[MGJRouter registerURLPattern:@"mgj://detail?id=:id" toHandler:^(NSDictionary
*routerParameters)
NSNumber *id = routerParameters[@"id"];
// create view controller with id
// push view controller
首页只需调用 [MGJRouter openURL:@"mgj://detail?id=404"]
就可以打开相应的详情页。
那问题又来了,我怎么知道有哪些可用的 URL?为此,我们做了一个后台专门来管理。
然后可以把这些短链生成不同平台所需的文件,iOS 平台生成 .h,m 文件,android 平台生成 .java 文件,并注入到项目中。这样开发人员只需在项目中打开该文件就知道所有的可用 URL 了。
目前还有一块没有做,就是参数这块,虽然描述了短链,但真想要生成完整的 URL,还需要知道如何传参数,这个正在开发中。
还有一种情况会稍微麻烦点,就是「组件A」要调用「组件B」的某个方法,比如在商品详情页要展示购物车的商品数量,就涉及到向购物车组件拿数据。
类似这种同步调用,iOS 之前采用了比较简单的方案,还是依托于 MGJRouter
,不过添加了新的方法 -
(id)objectForURL
: ,注册时也使用新的方法进行注册:
[MGJRouter registerURLPattern:@"mgj://cart/ordercount" toObjectHandler:^id(NSDictionary
*routerParamters)
// do some calculation
return @42;
使用时 NSNumber *orderCount = [MGJRouter objectForURL:@"mgj://cart/ordercount"]
这样就拿到了购物车里的商品数。
稍微复杂但更具通用性的方法是使用「协议」 <-> 「类」绑定的方式,还是以购物车为例,购物车组件可以提供这么个 Protocol:
@protocol MGJCart <NSObject>
+ (NSInteger)orderCount;
@end
可以看到通过协议可以直接指定返回的数据类型。然后在购物车组件内再新建个类实现这个协议,假设这个类名为MGJCartImpl
,接着就可以把它与协议关联起来 [ModuleManager registerClass:MGJCartImpl forProtocol:@protocol(MGJCart)]
,对于使用方来说,要拿到这个 MGJCartImpl
,需要调用 [ModuleManager classForProtocol:@protocol(MGJCart)]
。拿到之后再调用 + (NSInteger)orderCount
就可以了。
那么,这个协议放在哪里比较合适呢?如果跟组件放在一起,使用时还是要先引入组件,如果有多个这样的组件就会比较麻烦了。所以我们把这些公共的协议统一放到了 PublicProtocolDomain.h
下,到时只依赖这一个文件就可以了。
Android 也是采用类似的方式。
1.2.组件生命周期管理
理想中的组件可以很方便地集成到主客中,并且有跟 AppDelegate
一致的回调方法。这也是 ModuleManager
做的事情。
先来看看现在的入口方法:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary
*)launchOptions
[MGJApp startApp];
[[ModuleManager sharedInstance] loadModuleFromPlist:[[NSBundle mainBundle]
pathForResource:@"modules" ofType:@"plist"]];
NSArray *modules = [[ModuleManager sharedInstance] allModules];
for (id<ModuleProtocol> module in modules)
if ([module respondsToSelector:_cmd])
[module application:application didFinishLaunchingWithOptions:launchOptions];
[self trackLaunchTime];
return YES;
其中 [MGJApp startApp]
主要负责一些 SDK 的初始化。 [self trackLaunchTime]
是我们打的一个点,用来监测从 main 方法开始到入口方法调用结束花了多长时间。其他的都由 ModuleManager
搞定,loadModuleFromPlist:pathForResource
: 方法会读取 bundle 里的一个 plist 文件,这个文件的内容大概是这样的:
每个 Module
都实现了 ModuleProtocol
,其中有一个 -(BOOL)applicaiton:didFinishLaunchingWithOptions
:方法,如果实现了的话,就会被调用。
还有一个问题就是,系统的一些事件会有通知,比如 applicationDidBecomeActive
会有对应的
UIApplicationDidBecomeActiveNotification
,组件如果要做响应的话,只需监听这个系统通知即可。但也有一些事件是没有通知的,比如 -application:didRegisterUserNotificationSettings
: ,这时组件如果也要做点事情,怎么办?
一个简单的解决方法是在 AppDelegate
的各个方法里,手动调一遍组件的对应的方法,如果有就执行。
- (void)application:(UIApplication *)application
didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken
NSArray *modules = [[ModuleManager sharedInstance] allModules];
for (id<ModuleProtocol> module in modules)
if ([module respondsToSelector:_cmd])
[module application:application
didRegisterForRemoteNotificationsWithDeviceToken:deviceToken];
1.3.壳工程
既然已经拆出去了,那拆出去的组件总得有个载体,这个载体就是壳工程,壳工程主要包含一些基础组件和业务SDK,这也是主工程包含的一些内容,所以如果在壳工程可以正常运行的话,到了主工程也没什么问题。不过这里存在版本同步问题,之后会说到。
1.4.遇到的问题
组件拆分
由于之前的代码都是在一个工程下的,所以要单独拿出来作为一个组件就会遇到不少问题。首先是组件的划分,当时在定义组件粒度时也花了些时间讨论,究竟是粒度粗点好,还是细点好。粗点的话比较有利于拆分,细点的话灵活度比较高。最终还是选择粗一点的粒度,先拆出来再说。
假如要把详情页迁出来,就会发现它依赖了一些其他部分的代码,那最快的方式就是直接把代码拷过来,改个名使用。比较简单暴力。说起来比较简单,做的时候也是挺有挑战的,因为正常的业务并不会因为「组件化」而停止,所以开发同学们需要同时兼顾正常的业务和组件的拆分。
版本管理
我们的组件包括第三方库都是通过 Cocoapods 来管理的,其中组件使用了私有库。之所以选择 Cocoapods,一个是因为它比较方便,还有就是用户基数比较大,且社区也比较活跃(活跃到了会时不时地触发 Github 的 rate limit,导致长时间 clone 不下来··· ),当然也有其他的管理方式,比如 submodule / subtree,在开发人员比较多的情况下,方便、灵活的方案容易占上风,虽然它也有自己的问题。主要有版本同步和更新/编译慢的问题。
假如基础组件做了个 API 接口升级,这个升级会对原有的接口做改动,自然就会升一个中位的版本号,比如原先是1.6.19,那么现在就变成 1.7.0 了。而我们在 Podfile 里都是用 ~指定的,这样就会出现主工程的 pod 版本升上去了,但是壳工程没有同步到,然后群里就会各种反馈编译不过,而且这个编译不过的长尾有时能拖上两三天。
然后就想了个办法,如果不在壳工程里指定基础库的版本,只在主工程里指定呢,理论上应该可行,只要不出现某个基础库要同时维护多个版本的情况。但实践中发现,壳工程有时会莫名其妙地升不上去,在 podfile 里指定最新的版本又可以升上去,所以此路不通。
还有一个问题是 pod update
时间过长,经常会在 Analyzing Dependency
上卡 10 多分钟,非常影响效率。后来排查下来是跟组件的 Podspec 有关,配置了 subspec,且依赖比较多。
然后就是 pod update 之后的编译,由于是源码编译,所以这块的时间花费也不少,接下去会考虑 framework 的方式。
1.5.持续集成
在刚开始,持续集成还不是很完善,业务方升级组件,直接把 podspec 扔到 private repo 里就完事了。这样最简单,但也经常会带来编译通不过的问题。而且这种随意的版本升级也不太能保证质量。于是我们就搭建了一套持续集成系统,大概如此:
每个组件升级之前都需要先通过编译,然后再决定是否升级。这套体系看起来不复杂,但在实施过程中经常会遇到后端的并发问题,导致业务方要么集成失败,要么要等不少时间。而且也没有一个地方可以呈现当前版本的组件版本信息。还有就是业务方对于这种命令行的升级方式接受度也不是很高。
基于此,在经过了几轮讨论之后,有了新版的持续集成平台,升级操作通过网页端来完成。
大致思路是,业务方如果要升级组件,假设现在的版本是 0.1.7,添加了一些 feature 之后,壳工程测试通过,想集成到主工程里看看效果,或者其他组件也想引用这个最新的,就可以在后台手动把版本升到 0.1.8-rc.1,这样的话,原先依赖 ~> 0.1.7
的组件,不会升到 0.1.8,同时想要测试这个组件的话,只要手动把版本调到 0.1.8-rc.1 就可以了。这个过程不会触发 CI 的编译检查。
当测试通过后,就可以把尾部的 -rc.n
去掉,然后点击「集成」,就会走 CI 编译检查,通过的话,会在主工程的podfile 里写上固定的版本号 0.1.8。也就是说,podfile 里所有的组件版本号都是固定的。
1.6.周边设施
基础组件及组件的文档 / Demo / 单元测试
无线基础的职能是为集团提供解决方案,只是在蘑菇街 App 里能 work 是远远不够的,所以就需要提供入口,知道有哪些可用组件,并且如何使用,就像这样(目前还未实现)
这就要求组件的负责人需要及时地更新 README / CHANGELOG / API,并且当发生 API 变更时,能够快速通知到使用方。
公共UI组件
组件化之后还有一个问题就是资源的重复性,以前在一个工程里的时候,资源都可以很方便地拿到,现在独立出去了,也不知道哪些是公用的,哪些是独有的,索性都放到自己的组件里,这样就会导致包变大。还有一个问题是每个组件可能是不同的产品经理在跟,而他们很可能只关注于自己关心的页面长什么样,而忽略了整体的样式。公共 UI组件就是用来解决这些问题的,这些组件甚至可以跨 App 使用。(目前还未实现)
1.7.小结
「组件化」是 App 膨胀到一定体积后的解决方案,能一定程度上解决问题,在提高开发效率的过程中,踩坑是难免的,希望这篇文章能够带来些帮助。
二丶蘑菇街APP的组件化之路后续
2.1.通用的调用实现
将「URL 调用」和「组件间调用」通过 runtime 达到统一,通过 prefix 的方式来避免安全上的一些漏洞。看起来确实会舒服些,也比较灵活。
2.2. 通过 Category 来统一组件对外暴露的接口
支持 openURL
: 但最终还是走的 target-action,跟内部调用无差别。 这也是我们目前有待提升的点,想知道某个组件支持哪些 URL 或 哪些 Protocol 不够方便,URL 的参数传递也是个问题,将来 URL 发生变动的话,调整起来也比较麻烦。后续会在这块再加强下。
当初决定使用 openURL
: 来做页面间的跳转,而不是方法调用,主要是考虑到我们的大部分场景都可以通过这种方式解决,因此就这么定了。
openURL
: 更像 Android 里的 「隐式 Intent」,不关心谁来处理这个 URL,由系统(MGJRouter)来决定。而方法调用更像「显式 Intent」或者 RPC,明确地知道应该由谁来处理。前者的好处是可以更少地关心业务逻辑,后者的好处是可以很方便地完成参数传递。
2.3.更明确的表述
openURL
只是页面间的调用方式- 组件间的调用通过 protocol 来实现
每个组件都有一个 Entry
,这个 Entry
,主要做了三件事
- 注册这个组件关心的 URL
- 注册这个组件能够被调用的方法/属性
- 在 App 生命周期的不同阶段做不同的响应
2.4. 注册这个组件关心的 URL
[MGJRouter registerURLPattern:@"mgj://detail?id=:id" toHandler:^(NSDictionary
*routerParameters)
NSNumber *id = routerParameters[@"id"];
// create view controller with id
// push view controller
;
URL 的注册会有对应的 block,拿到这个 URL 后,想怎么折腾就怎么折腾。
2.5.注册这个组件能够被调用的方法/属性
当有一些场景不适合用 URL 的方式时,就可以通过注册 protocol 来实现
[ModuleManager registerClass:ClassA forProtocol:ProtocolA] 的结果就是在 MM 内部维护的 dict 里新加了一个映射关系。
[ModuleManager classForProtocol:ProtocolA]
的返回结果就是之前在 MM 内部 dict 里 protocol 对应的class,使用方不需要关心这个 class 是个什么东东,反正实现了 ProtocolA
协议,拿来用就行。
这里需要有一个公共的地方来容纳这些 public protocl,也就是图中的 PublicProtocl.h
2.6.在 App 生命周期的不同阶段做不同的响应
上一篇文章中有提到,这里简单说下, ModuleEntry
,实现某个特定的协议(该协议继承自UIApplicationDelegate
),然后实现对应的方法即可
关注公众号:初一十五a
解锁 《Android十三大板块文档》,让学习更贴近未来实战。已形成PDF版
内容如下:
1.2022最新Android11位大厂面试专题,128道附答案
2.音视频大合集,从初中高到面试应有尽有
3.Android车载应用大合集,从零开始一起学
4.性能优化大合集,告别优化烦恼
5.Framework大合集,从里到外分析的明明白白
6.Flutter大合集,进阶Flutter高级工程师
7.compose大合集,拥抱新技术
8.Jetpack大合集,全家桶一次吃个够
9.架构大合集,轻松应对工作需求
10.Android基础篇大合集,根基稳固高楼平地起
11.Flutter番外篇:Flutter面试+Flutter项目实战+Flutter电子书
12.高级Android组件化强化实战
13.十二模块之补充部分:其他Android十一大知识体系
整理不易,关注一下吧。ღ( ´・ᴗ・` ) 🤔
以上是关于关于Android组件化深度分析蘑菇街的主要内容,如果未能解决你的问题,请参考以下文章
阿里否认蒋凡调任阿里大文娱集团;蘑菇街回应裁员 14%;Android Studio 3.6.3 稳定版发布| 极客头条