浅谈IOS工程架构

一、框架模式的选择

1. MVC模式

MVC全名是Model View Controller,是模型(model)-视图(view)-控制器(controller)的缩写,一种软件设计典范,用一种业务逻辑、数据、界面显示分离的方法组织代码,将业务逻辑聚集到一个部件里面,在改进和个性化定制界面及用户交互的同时,不需要重新编写业务逻辑。MVC被独特的发展起来用于映射传统的输入、处理和输出功能在一个逻辑的图形化用户界面的结构中。

1.1 MVC 编程模式

MVC 是一种使用 MVC(Model View Controller 模型-视图-控制器)设计应用程序的模式

  • 视图(View):负责数据展示、监听用户触摸。

  • 控制器(Controller):负责业务逻辑、事件响应、数据加工。

  • 模型(Controller):负责封装数据、存储和处理数据运算。

1.2 MVC通信特点

2021051517090759.png

  • Model和View永远不能相互通信,只能通过Controller传递。

  • Controller可以直接与Model通信(读写调用Model),Model通过Notification和KVO机制与Controller间接通信。

  • Controller与View通过Target/Action,delegate和datasource三种模式进行通信。通过这三种模式,View就可以向Controller通信,Action/Target 模式来让Controller 监听View 触发的事件。View又通过Data source和delegate进行数据获取和某些通信操作。

1.3 MVC优缺点

  • 易用性:与其他几种模式相比最小的代码量,维护起来也较为容易。

  • 均衡性:厚重的ViewController、无处安放的网络逻辑与数据逻辑。

2. MVCS模式

苹果自身就采用的是这种架构思路,从名字也能看出,也是基于MVC衍生出来的一套架构。从概念上来说,它拆分的部分是Model部分,拆出来一个Store。这个Store专门负责数据存取。

从实际操作的角度上讲,它拆开的是Controller。因为Controller做了数据存储的事情,就会变得非常庞大,那么就把Controller专门负责存取数据的那部分抽离出来,交给另一个对象去做,这个对象就是Store。这么调整之后,整个结构也就变成了真正意义上的MVCS。

  • 视图(View):用户界面

  • 控制器(Controller):业务逻辑及处理

  • 模型(Model):数据存储

  • 存储器(Store):数据处理逻辑

MVCS是基于瘦Model的一种架构思路,把原本Model要做的很多事情中的其中一部分关于数据存储的代码抽象成了Store,在一定程度上降低了Controller的压力。

3.MVVM模式

MVVM是Model-View-ViewModel的简写。它本质上就是MVC 的改进版。MVVM 就是将其中的View 的状态和行为抽象化,让我们将视图 UI 和业务逻辑分开。当然这些事 ViewModel 已经帮我们做了,它可以取出 Model 的数据同时帮忙处理 View 中由于需要展示内容而涉及的业务逻辑。

20210515171017267.png

3.1 MVVM模式的组成部分

  • Model:Model是指代表内容的数据访问层,包含了本地数据以及网络数据。

  • View:View包含MVC中的View和Controller部分,View主要是视图展示,Controller则是处理页面生命周期以及页面跳转。

  • ViewModel:ViewModel是View和Model的桥接着,即管理着Model的数据逻辑,又要支持与View之间的相互通讯。

引用特点:在Controller中关联View与ViewModel(View引用ViewModel);在View中实现View与Model的单/双向绑定(KVO/RAC);ViewModel 负责Model的数据存储与逻辑处理,以及与View的交互通讯(Update/Action)。

备注:MVVM不依赖于View-Model的单/双向绑定,View-Model的单/双向绑定主要是简化了ViewModel与View之间的数据交互。

3.2 MVVM优点

  • 低耦合:View不直接与Model通讯,通过ViewModel来实现。

  • 可重用性:视图/数据逻辑相同的View可以共用一个ViewModel来实现。

结语:MVVM可以结合ReactiveCocoa,更加优雅的实现View与Model的双向绑定、View与ViewModel之间的相互通讯,具体使用可以参照博客iOS MVVM+RAC 从框架到实战MVVM-RAC-DEMO

二、工程结构与Pod应用

1.壳工程的搭建

工程项目一般由业务模块与基础模块构成,添加服务层主要为了实现基础组件配置与方法包装,以及提供具有业务特性的工具方法、宏定义/静态声明等,通过服务层来聚合应用的基础能力,以及降低业务模块之间的耦合。如果要实现业务模块的完全解耦,则需要通过业务模块组件化来实现。

 壳工程目录结构设计,如下图:

   BaseKit-基础层,存放着通用的基础库,应用于上层业务代码。

  • CommonLib:通用功能组件,具体功能的实现和应用,如分享、数据上报、支付等。

  • Views:通用UI组件,如BannerView、AlertView、pageControl、loadingView等。

  • Kernal:基础服务组件,提供基础服务能力,如Network、Log、dataBase、webImage等。

  • Basic:基类封装,又分为MVC+VM四个子目录(减少Basic的依赖可以通过类别扩展的方式代替)。

  • Marco:通用声明,宏定义-define、常量声明-extern。   

  • Utils:工具类集合,如Authoritys、Encrypt、NetworkAccessible等 。

  • Categorys:类别聚合,可按照类属性的不同分为不同的子目录,如View、Datas、Image等。

2. 业务/模块组件化

业务组件主要是指作为一个大的业务模块,单独分离成一个组件的形式,如电商模块、聊天模块、博客等。业务模块之间不存在耦合代码,由组件通讯中间层实现彼此的通讯。

2.1 组件管理方式

20210224000103415.png
通过xcworkspace的方式分层,A为主工程,B、C为两个业务模块
  • 目录分隔:在工程目录下,按照不同的业务组件创建不同的目录,业务模块独立,组件之间不直接调用API。

  • xcworkspace:通过xcworkspace的方式,不同的业务模块创建各自的project工程,业务project工程run成功后,将framework引入主工程中。

  • podspecs:以pod-specs的方式引入业务组件,各个业务模块都独立成一个工程,具体可以参考github组件化项目-iOS-Component-Pro

2.2 三种管理方式的特点

  • 目录分隔:目录分层比较简单,没有实现真正的代码分割,所以目录与目录直接的类是可以引用的,这样就很依赖于团队开发的规范性。

  • xcworkspacexcworkspace分层实现了不同业务组件代码的分割,作为framework的形式引入。动态编译的形式引入会影响到编译打包的效率;打包成静态framework的形式引入更新迭代成本比较高,而且不利于cocoaPod的使用。

  • podspecs:实现了不同业务组件代码的分割,又解决了xcworkspace分层影响编译效率与不利于cocoaPod使用的问题;优点 - 方便组件的版本管理与业务模块的独立测试,缺点 - 细微的代码修改也需要先升级组件再引入工程。

3. 组件分层与调用

 调用链自上而下,上层组件通过组件中心进行解耦通讯(业务组件、功能组件),底层组件通过直接#import的方式调用(UI组件、基础组件、工具/扩展)。

4. cocoaPod应用

cocoaPod应用IOS开发者应该都比较熟悉,主要是用关联第三方库与私有组件库,方便版本迭代管理。

4.1 GitHub第三方库

CocoaPods详解之-使用篇:CocoaPods详解之使用篇

4.2 GitLab私有库

CocoaPod-spec私有库配置:cocoapod-podspec私有库配置

三、组件通讯 & 页面路由

1. 组件通讯

添加中间件实现组件解耦,避免组件之间循环依赖。

1.1 URL-Block(MGJRouter)

/*********** 业务组件中注册 ***********/
[[MKRouter sharedInstance] registerHandler:^(MKRouteRequest *request) {
// 跳转至商祥页
// request.callBack(nil, @{@"productId" : request[@"productId"]});
} forRoute:MKString(@".*product/detail.*\\?(.*)\\&(.*)$")];


/*********** 模块调用 ***********/
[[MKRouter sharedInstance] handleURL:[NSURL URLWithString:@"weixin://com.apple.iphone/product/detail?productId=ID985632"] params:nil targetCallBack:^(NSError *error, NSDictionary *responseObject) {

}];
  • 数据埋点:"appData/click?params="

  • 原生商详页:"product/detail?params="

  • web商详页:"web/page?params="

1.2 Target-Action(CTMediator)

/*********** 公共实现 ***********/
#import "MKMediator+Feature.h"

static NSString * const kTargeFeature = @"Feature";

static NSString * const kActionViewControllerForFeature = @"viewControllerForFeature";

@implementation MKMediator (Feature)

- (UIViewController *)mediator_ViewControllerForFeature:(MKFeatureActionModel *)params {
    return  [self call:kTargeFeature action:kActionViewControllerForFeature parameters:params];
}

@end

/*********** 业务组件中实现 ***********/
#import "MKMediator+Feature.h"
#import "MKTarget_Feature.h"
#import "AudioVideoPraticeVC.h"

@implementation MKTarget_Feature

- (UIViewController *)action_viewControllerForFeature:(MKActionModel *)params {
    AudioVideoPraticeVC* audioVC = [[AudioVideoPraticeVC alloc] init];
    return audioVC;
}

@end

/*********** 模块调用 ***********/
MKActionModel* acitonModel = [[MKActionModel alloc] init];
actionModel.keyValues = @{};
actionModel.complete = ^(id result) {

};
UIViewController* audioVC = [[MKMediator shared] mediator_ViewControllerForFeature:acitonModel];
  • 内部->数据埋点:直接调用对应组件的API

  • 外部->原生录音页"appScheme://nativePage?pageId=&args="(通过pageId映射到对应组件的API)

  • 打开hybrid容器页:通过url正则匹配到页面的相关配置,再调用容器组件的API实现页面的跳转和渲染

1.3 Protocol-Class(BeeHive)

面向接口编程,通过protocol定义协议接口与属性(注意:Protocol的属性只是一个声明,并没有实际用途,需要实现协议的类本身定义了该属性)。

/* 定义协议方法与属性 */
@protocol MKOpenURLProtocol <NSObject>

@property (nonatomic, assign) BOOL isPresent;

- (BOOL)openURLWithURLString:(NSString *)URLString params:(NSDictionary *)params;

@end

 MKOpenURLExecutor声明了MKOpenURLProtocol,并实现其协议方法。

/* 执行者-MKOpenURLExecutor.h */
@interface MKOpenURLExecutor : NSObject <MKOpenURLProtocol>

@end

/* 实现者-MKOpenURLExecutor.m */
@implementation MKOpenURLExecutor
@synthesize isPresent;

- (BOOL)openURLWithURLString:(NSString *)URLString params:(NSDictionary *)params {
    // do something
    return YES;
}

@end

上面讲的这种声明协议再到类的实现,虽然是应用了接口编程的方式,但是业务组件之间还是会有直接调用的关系,所以需要有个中间件实现protocol与service的一一匹配。

/* 中间件-MKServiceManager.h */
@interface MKServiceManager : NSObject

+ (instancetype)sharedInstance;

- (void)registerService:(id)service protocol:(Protocol *)protocol;
- (id)serviceWithProtocol:(Protocol *)protocol;

@end

/* 中间件-MKServiceManager.m */
@interface MKServiceManager ()

@property (nonatomic, strong) NSMutableDictionary* serviceSet;

@end

@implementation MKServiceManager

+ (instancetype)sharedInstance {
    static MKProtocolManager* instance = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        instance = [[self alloc] init];
        instance.serviceSet = [NSMutableDictionary dictionary];
    });
    return instance;
}

- (void)registerService:(Class)service protocol:(Protocol *)protocol {
    if (service && protocol) {
        [_serviceSet setObject:service forKey:NSStringFromProtocol(protocol)];
    }
}

- (Class)serviceWithProtocol:(Protocol *)protocol {
    if (protocol) {
      return [_serviceSet objectForKey:NSStringFromProtocol(protocol)];
    }
    return nil;
}

@end

注册组件A的接口服务,在组件B中获取组件A的接口服务,再执行相应的能力。(通过APP构造函数 / load方法实现协议的动态注册,省去了手动注册这一步。具体实现可以参考BeeHive源码,或者博客:iOS模块启动项自注册实现

/* 业务组件A的协议与接口对象的一一匹配 */
+ (void)registerProtocol {
    [[MKServiceManager sharedInstance] registerService:[MKOpenURLExecutor class] protocol:@protocol(MKOpenURLProtocol)];
}

/* 其他业务组件中调用业务组件A的协议接口 */
+ (void)openURL:(NSString *)URLString params:(id)params {
    id <MKOpenURLProtocol> service = [[MKServiceManager sharedInstance] serviceWithProtocol:@protocol(MKOpenURLProtocol)];
    [service openURLWithURLString:URLString params:params];
}
  • 内部->数据埋点:直接调用对应组件的API

  • 外部->原生录音页"appScheme://nativePage?pageId=&args="(通过pageId映射到对应组件的API)

  • 打开hybrid容器页:通过url正则匹配到页面的相关配置,再调用容器组件的API实现页面的跳转和渲染

1.4 三种通讯方式的特点

  • URL-Block:能解决组件间的依赖,路由配置灵活,但需要去注册&维护路由表,方法调用不够直观,API参数为硬编码,大量的正则匹配也会带来一定的性能开销。

  • Target-Action:统一了组件api服务,接口实现不依赖中间件,但需要额外维护中间件类扩展,方法调用不够直观,API参数为硬编码。

  • Protocol-Class:面向接口编程,方法调用比较直观,但组件方需要声明一个对外的协议和用于协议实现的类,并进行一对一绑定。

1.5 通讯方式的选型建议

  • 组件间的页面路由采用URL-Block实现,通过页面URI(scheme://host/path?query)命中路由规则,保持原生与前端页面跳转协议的一致性。

  • 组件间的方法调用采用Protocol-Class实现,调用链路相对于其他两种更加直观,与依赖倒置原则更为贴切。

2. 页面路由(推送、scheme、通用链接、JSAPI-openURL)

2.1 页面路由配置

20210824195502505.png

2.2 具体实现

通过url正则匹配到相关的路由配置,生成对应类型的ViewController(URL Scheme和Universal Link的页面链接需要url编码)。Native页面可以通过URL-Block的方式生成;或者通过页面key映射ViewController类名,用runtime的方式生成。页面的通用参数用UIViewController类别添加。

  • native:通过页面配置信息与url参数,生成对应的原生页面,最终实现页面的跳转与渲染。

  • web:直接通过url,生成WebVC,实现页面跳转和H5资源加载。

  • weex:获取wx的资源链接及页面配置参数,传递给wx容器实现资源加载和页面渲染。

  • react naive:获取rn的资源链接及页面配置参数,传递给rn容器实现资源加载和页面渲染。

  • flutter:配置相关的routeName及页面配置参数,传递给Flutter容器实现flutter端页面的渲染。

2.3 页面协议

 1)http链接的形式(url匹配路由配置)

"https://domain/nt/home_page"    // native页
"https://domain/wb/order_detail" // web页
"https://domain/wx/star_info"    // weex页
"https://domain/rn/order_list"   // RN页
"https://domain/ft/mine_detail"  // flutter页

2)appscheme的形式

// 原生页(命中本地路由 | 获取home_page路由配置)
"appscheme://native/page/home_page"

// web页(获取order_detail的路由配置 | url跳转)
"appscheme://web/page/order_detail"
"appscheme://web/page?url=https%3A%2F%2Fm.domain.com%2Forder_detail.html"

"appscheme://weex/page/star_info" // weex页(获取star_info路由配置)
"appscheme://rn/page/order_list"  // RN页(获取order_list路由配置)
"appscheme://ft/page/mine_detail" // flutter页(获取mine_detail路由配置)

备注:在云端页面路由配置较多的情况下,频繁正则匹配会有一定的性能开销,native与web页可以不采用云端下发的路由匹配机制,通过已注册的路由实现页面的生成与跳转(native由url-path命中规则)。

  • 7
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值