戴铭(iOS开发课)读书笔记:04章节-架构设计

原文链接:项目大了人员多了,架构怎么设计更合理?


03 章节 项目大了人员多了,架构怎么设计更合理?

随着业务需求量和团队的规模达到一定量级后,任何一个 APP 都需要考虑架构设计的合理性。

先思考,再动手。

戴铭老师在文中先抛出一种组件化划分模块粒度的原则:SOLID 原则


戴铭(iOS开发课)读书笔记:04章节-架构设计_第1张图片
SOLID 原则

然后介绍了他心目中的好架构:CTMediator。
最后分享了在CTMediator的基础上扩展的案例:案例。

戴铭(iOS开发课)读书笔记:04章节-架构设计_第2张图片
戴铭老师手绘图

这个案例在中介者架构的基础上,增加了对中间件、状态机、观察者、工厂模式的支持。同时支持了链式调用。有兴趣的朋友可以去阅读老师的源码。


我个人对于项目架构的理解比较简单,首先是代码模块的解偶和拆分,其次是模块间的通信
戴铭老师的原文中,重点介绍的第二部分并推荐使用CTMediator的方式。
那么接下来,我先简单介绍一下代码模块的拆分,然后回归代码介绍一下CTMediator。

1 代码模块拆分

我接触到的代码模块的拆分有三种方式:静态库、动态库和远程私有库。

  • 静态库 .a和.framework
  • 动态库 .dylib和.framework
    区别:
    1 静态库链接时,静态库会被完整的复制到可执行文件中,被多次使用就有多份冗余拷贝
    2 系统动态库链接时不复制,程序运行时由系统动态加载到内存,供系统调用。系统只加载一次,多个程序共用,节省内存。
  • 远程私有库 pod
1 静态库创建:

一共4种: DEBUG(真机,模拟),RELEASE(真机,模拟)

1 创建静态库

戴铭(iOS开发课)读书笔记:04章节-架构设计_第3张图片

2 添加头文件,在Subpath处修改头文件路径

戴铭(iOS开发课)读书笔记:04章节-架构设计_第4张图片

3 静态库分为真机和模拟器: lipo -info xxxx.a

  • 真机结构是armv7 arm64
  • 模拟器结构是x86_64
戴铭(iOS开发课)读书笔记:04章节-架构设计_第5张图片

4 将真机和模拟器进行合并

lipo -create xxxx1.a xxxx2.a -output test.a

最后使用合并后的test.a文件。


戴铭(iOS开发课)读书笔记:04章节-架构设计_第6张图片

5 将静态库的include-头文件,和.a文件(合并后的test.a文件)拖入真实项目中

戴铭(iOS开发课)读书笔记:04章节-架构设计_第7张图片
image.png

注意1: 链接文件,如果在静态库中有分类文件。

戴铭(iOS开发课)读书笔记:04章节-架构设计_第8张图片

-ObjC 链接所有OC文件
-all_load 链接所有文件
-force_load 链接.a路径

注意2:如果静态库中有xib文件,需要另行处理。

2 动态库创建:

1 创建动态库

戴铭(iOS开发课)读书笔记:04章节-架构设计_第9张图片

2 头文件处理

戴铭(iOS开发课)读书笔记:04章节-架构设计_第10张图片

3 将.framework直接拖入工程中, 但是直接运行会报错,因为苹果审核机制导致动态库失去了动态性,所以找不到映射关系,需要手动导入

戴铭(iOS开发课)读书笔记:04章节-架构设计_第11张图片

4 手动copy

戴铭(iOS开发课)读书笔记:04章节-架构设计_第12张图片
戴铭(iOS开发课)读书笔记:04章节-架构设计_第13张图片

5 动态库真机和模拟器合并(脚本合并)
需要先分别生成动态库的真机和模拟器环境的.framework(各运行一遍)
然后再加入脚本,再运行一遍(真机和模拟环境随便选)
脚本代码大家自行百度一下。^ ^

戴铭(iOS开发课)读书笔记:04章节-架构设计_第14张图片

6 动态库编译成静态库,这种方式就不用在项目中添加映射(copy file 第4步)

戴铭(iOS开发课)读书笔记:04章节-架构设计_第15张图片
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应用

你可能感兴趣的:(戴铭(iOS开发课)读书笔记:04章节-架构设计)