原文链接:项目大了人员多了,架构怎么设计更合理?
03 章节 项目大了人员多了,架构怎么设计更合理?
随着业务需求量和团队的规模达到一定量级后,任何一个 APP 都需要考虑架构设计的合理性。
先思考,再动手。
戴铭老师在文中先抛出一种组件化划分模块粒度的原则:SOLID 原则
然后介绍了他心目中的好架构:CTMediator。
最后分享了在CTMediator的基础上扩展的案例:案例。
这个案例在中介者架构的基础上,增加了对中间件、状态机、观察者、工厂模式的支持。同时支持了链式调用。有兴趣的朋友可以去阅读老师的源码。
我个人对于项目架构的理解比较简单,首先是代码模块的解偶和拆分,其次是模块间的通信。
戴铭老师的原文中,重点介绍的第二部分并推荐使用CTMediator的方式。
那么接下来,我先简单介绍一下代码模块的拆分,然后回归代码介绍一下CTMediator。
1 代码模块拆分
我接触到的代码模块的拆分有三种方式:静态库、动态库和远程私有库。
- 静态库 .a和.framework
- 动态库 .dylib和.framework
区别:
1 静态库链接时,静态库会被完整的复制到可执行文件中,被多次使用就有多份冗余拷贝
2 系统动态库链接时不复制,程序运行时由系统动态加载到内存,供系统调用。系统只加载一次,多个程序共用,节省内存。 - 远程私有库 pod
1 静态库创建:
一共4种: DEBUG(真机,模拟),RELEASE(真机,模拟)
1 创建静态库
2 添加头文件,在Subpath处修改头文件路径
3 静态库分为真机和模拟器: lipo -info xxxx.a
- 真机结构是armv7 arm64
- 模拟器结构是x86_64
4 将真机和模拟器进行合并
lipo -create xxxx1.a xxxx2.a -output test.a
最后使用合并后的test.a文件。
5 将静态库的include-头文件,和.a文件(合并后的test.a文件)拖入真实项目中
注意1: 链接文件,如果在静态库中有分类文件。
-ObjC 链接所有OC文件
-all_load 链接所有文件
-force_load 链接.a路径
注意2:如果静态库中有xib文件,需要另行处理。
2 动态库创建:
1 创建动态库
2 头文件处理
3 将.framework直接拖入工程中, 但是直接运行会报错,因为苹果审核机制导致动态库失去了动态性,所以找不到映射关系,需要手动导入
4 手动copy
5 动态库真机和模拟器合并(脚本合并)
需要先分别生成动态库的真机和模拟器环境的.framework(各运行一遍)
然后再加入脚本,再运行一遍(真机和模拟环境随便选)
脚本代码大家自行百度一下。^ ^
6 动态库编译成静态库,这种方式就不用在项目中添加映射(copy file 第4步)
3 远程私有库:
这种方式是我们常用的方式,网上的资料也非常多,我这里把大概的步骤整理一下。具体遇到的问题还需要大家自己解决。最后的成果就是把项目中独立的业务模块抽离成pod的形式,在主项目中通过pod install进行安装。
1 建立本地私有库 pod lib create 'name'
2 在码云创建于本地私有库同名的私有项目
3 把本地私有库的文件提交到远程
3.1 在本地私有库目录下,提交文件 git add.
3.2 初始化 git commit -m '提交文件'
3.3 关联到远程 git remote add origin xxx(网址)
3.4 提交到远程 git push origin master -f 强制提交
3.5 提交标签 git tag 0.1.0(这里与spec文件中的version相同) -> git push
4 配置本地私有库的spec文件
填写homepage、source等
PS: 填写完成之后记得提交到项目远程
5 把spec文件提交到远程索引库(自己创建的库,里面只存放.spec文件)
5.1 查看本地repo:pod repo
5.2 把自己创建的索引库提交:pod repo add NAME xxx (xxx 表示项目地址,NAME是你的库名)
5.3 把本地私有库的spec文件提交到远程私有库:
pod repo push NAME Utils.spec —allow-warnings (有时验证不通过需要忽略警告)
5.4 或者手动将spec文件放入自己创建的远程索引库文件夹中
PS: pod lib lint 验证spec文件(这里经常会出错,需要对应处理)
2 模块间通信
1 路由
之前项目中一直使用的是 URL scheme + Router 的形式,根据不同的url路径,跳转不同的viewController控制器。这个负责跳转的模块被封装成一个类,目标控制器最终被当作block的参数返回。
这是非常灵活的一种跳转模式,但是没过多久就遇到了问题:
1 随着业务的发展,这个负责跳转的类中可能有几十个判断,每一个可点击的区域都有可能跳转到任意模块。最终导致这个类中代码冗余,难以维护,最重要是很low。
针对这个问题,可以将url和控制器的对应关系保存在一张plist表中。每次调用方法时通过获取控制器的名称,通过NSClassFromString(_:)方法获取。
2 此时又遇到第二个问题,就是项目中也许有的控制器是通过xib文件或者sb创建的。那么通过init方法创建控制器时无法正确获得控制器。
这个问题的解决其实非常容易,就是需要重写所有通过xib文件创建的控制器的init方法。保证在通过init方法创建时,分别调用他们本来的创建方式。
3 每当业务更新时,需要手动去修改url和控制器对应的plist文件,效率很低。
这个时候,你需要给项目的路由模块添加一个注册的机制,即通过开放的API修改本地的plist文件,达到可以动态添加和修改plist文件的目的。
最终的路由就保持上面的样子,不知道大家还有什么更好的优化方案。
当然,路由模式是有先天缺陷的,因为注册流程是必须的,但是又完全没有必要。其次是注册的内容常驻内存。最后是不符合组件化中“中间件为openUrl服务”的原则。
所以戴铭老师在文章中提到的“他认为好的架构”是CTMediator这种中介者模式。
2 中介者
我们先看一下下面的代码:
Person *person = [Person new];
// 1
[person speak:@"hi"];
// 2
[person performSelector:@selector(speak:) withObject:@"hi"];
// 3
NSString *string = @"hi";
NSMethodSignature *sign = [person methodSignatureForSelector:@selector(speak:)];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sign];
[invocation setTarget:person];
[invocation setSelector:@selector(speak:)];
[invocation setArgument:&string atIndex:2];
[invocation invoke];
这是3种不同的方法调用的方式。第三种就是中介者模式的核心原理。
为什么在setArgument:atIndex:时后者参数要传入 2?
void dynamicMethodIMP(id self, SEL _cmd, NSString *msg)
{
// implementation ....
}
因为动态创建方法的第0个位置是self指针,第1个位置是sel选择器,第2个位置是传递参数,所以index是2。
在上面的基础上,CTMediator对传入的target和action加入容错处理:
- (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params shouldCacheTarget:(BOOL)shouldCacheTarget
{
NSString *swiftModuleName = params[kCTMediatorParamsKeySwiftTargetModuleName];
// generate target
NSString *targetClassString = nil;
if (swiftModuleName.length > 0) {
targetClassString = [NSString stringWithFormat:@"%@.Target_%@", swiftModuleName, targetName];
} else {
targetClassString = [NSString stringWithFormat:@"Target_%@", targetName];
}
NSObject *target = self.cachedTarget[targetClassString];
if (target == nil) {
Class targetClass = NSClassFromString(targetClassString);
target = [[targetClass alloc] init];
}
// generate action
NSString *actionString = [NSString stringWithFormat:@"Action_%@:", actionName];
SEL action = NSSelectorFromString(actionString);
if (target == nil) {
// 这里是处理无响应请求的地方之一,这个demo做得比较简单,如果没有可以响应的target,就直接return了。实际开发过程中是可以事先给一个固定的target专门用于在这个时候顶上,然后处理这种请求的
[self NoTargetActionResponseWithTargetString:targetClassString selectorString:actionString originParams:params];
return nil;
}
if (shouldCacheTarget) {
self.cachedTarget[targetClassString] = target;
}
if ([target respondsToSelector:action]) {
return [self safePerformAction:action target:target params:params];
} else {
// 这里是处理无响应请求的地方,如果无响应,则尝试调用对应target的notFound方法统一处理
SEL action = NSSelectorFromString(@"notFound:");
if ([target respondsToSelector:action]) {
return [self safePerformAction:action target:target params:params];
} else {
// 这里也是处理无响应请求的地方,在notFound都没有的时候,这个demo是直接return了。实际开发过程中,可以用前面提到的固定的target顶上的。
[self NoTargetActionResponseWithTargetString:targetClassString selectorString:actionString originParams:params];
[self.cachedTarget removeObjectForKey:targetClassString];
return nil;
}
}
}
最终调用一个弹窗显示的代码:
[self performTarget:kCTMediatorTargetA
action:kCTMediatorActionShowAlert
params:paramsToSend
shouldCacheTarget:NO];
理解原理之后,才能刚方便我们的使用,由于我并没有在真是项目中使用过这种模式。所以只能给大家提供几片文章参考:
iOS应用架构谈 组件化方案
在现有工程中实施基于CTMediator的组件化方案
CTMediator的Swift应用