美文网首页iOS分享世界
戴铭(iOS开发课)读书笔记:04章节-架构设计

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

作者: YYYYYY25 | 来源:发表于2019-03-30 18:51 被阅读92次

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


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

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

    先思考,再动手。

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


    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文件)拖入真实项目中

    image.png

    注意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应用

    相关文章

      网友评论

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

        本文链接:https://www.haomeiwen.com/subject/howjbqtx.html