mvc、mvp、mvvm、中间组件路由Router

前言

谈论他们之前,先了解一点,其他的模式基本上都是从mvc架构上根据不同的业务演化而来,实际上也可以理解为他们为mvc的子集或者或者mvc的变种,他们之间在某种程度上还是可以互通,甚至可以相互借鉴延伸出更适合自己项目的架构

另外项目在不同时期,会有其更适合的架构,若盲目尝试新架构,只会增加工作量,毕竟鱼与熊掌不可兼得,适合的才是最好的,没有最完美的架构,只有最匹配的架构(项目业务、团队规模等,都会有非常大的影响),了解了这点就有利于后面的理解了

好的架构思想在于大家的思考与碰撞,其才能擦出明亮的火花,毕竟一千个人有一千个哈姆雷特

本文会带着ios的 案例demo 进行入门讲解,方便大家理解

MVC

MVC是开发中最常见的架构之一,也是最基本的架构,很多架构都是基于MVC衍生出来的,甚至在一些语言的开发中MVC被称为设计模式中的一种

一般来说,MVC中的交互结构如下所示:

image.png

这也是比较标准的交互方式,实际上由于系统上Controller和View的默认嵌套,Controller和View很可能是嵌在一起的,所以也衍生出了很多标准的交互方式

实际工作中,仅仅使用MVC会导致Controller文件较大,因此很多人想出了很多办法来处理

例如:面向对象将View和Model最大化封装,controller中主要负责API的调用和交互处理;在View作为半个控制器,将model嵌入在里面,尽可能处理一些Model逻辑和事件,让View或者Model成为控制器的子分支,还有其他等等

这些在MVC的使用中都没有什么问题,属于正常化,没有什么对于错,唯一的缺点就是View、Model、Controller的关系出现了微妙的变化,他们之间增加了更多的耦合度,虽然Controller里面的代码减少了,但是他们之间的逻辑更加复杂了,如果不是熟悉此开发习惯或者逻辑的人,代码读取难度会直线上升,也就是出现了典型的风格适应问题,且可能是很大的风格变化

因此,如果想避免这些多变的风格,不妨使用标准的MVC,职责划分好,尝试合适的面向对象开发尽可能的对View和Model进行封装,在Controler中仅仅负责API的调用和交互的处理,合理利用分类继承等手段来减少Controller中的代码

案例演示

案例使用的是一个标准的TableView视图,里面包括的数据获取DataSource、View、Model、Controller的处理,其中DataSource负责数据的获取,例如:请求网络数据,获取本地数据

下面展示一下普通的MVC的案例代码演示:


- (void)viewDidLoad {
    [super viewDidLoad];
    [self setupTableView];
    [self getDataSource];
}

- (void)setupTableView {
    self.tableView = [[UITableView alloc] initWithFrame:self.view.frame 
        style:UITableViewStylePlain];
    self.tableView.dataSource = self;
    self.tableView.delegate = self;
    [self.tableView registerNib:[UINib nibWithNibName:@"MVCCell" bundle:nil]
        forCellReuseIdentifier:reuseIdentifier];
    [self.view addSubview:self.tableView];
}

- (void)getDataSource {
    [MVCDataSource getMVCData:^(NSArray<MVCModel *> * _Nonnull dataList) {
        self.dataList = dataList;
        [self.tableView reloadData];
    }];
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return [_dataList count];
}

- (UITableViewCell *)tableView:(UITableView *)tableView 
    cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    MVCCell *cell = [tableView dequeueReusableCellWithIdentifier:reuseIdentifier 
        forIndexPath:indexPath];
    //也有将model直接嵌入view中,但是不推荐,UI更改应当相对独立一些
    //控制器做控制器要做的事情,模型做模型的事情
    MVCModel *model = _dataList[indexPath.row];
    cell.titleLabel.text = model.name;
    cell.contentLabel.text = model.content;
    __weak typeof(self) wself = self;
    [cell setOnTapBlock:^(MVCCell * _Nonnull cell) {
        [wself onChanged];
    }];
    return cell;
}
复制代码

上面代码逻辑相信很容易看明白

DataSource:负责获取数据的封装,在控制器中能一步直接获取到要使用的数据

View:主要是对子视图的封装,并对外暴露赋值方式,不直接和Model进行交互

Model:主要负责数据模型的定义,或者是一些数据的处理操作,又或者是一些业务操作,又或者是通用操作

Controller:主要负责View、Model、DataSource等API的调用,即整合资源,还有就是View和Model之间的交互逻辑

MVP

相比较ios,MVP更早出现在Android之间,据说也是其中的无奈,后来ios中也流行起来,且效果更加明显,其也是基于MVC架构的一个演变,将Controller中的View和Model的逻辑放到了Present中来处理,其使用效果非常优秀,不少开发者都在使用此架构

交互逻辑如下图所示:
image.png

这里面Present持有View和Model,主要负责View和Model之间的整合逻辑,也就是扮演者原来的控制器的角色,而控制器Controller则负责present的设置等(即Controller和Present之间的交互),还有数据的获取(也可以在Present中获取), 控制器之间的业务交互等

可以看到present的出现,将控制器大部分代码都可以迁移到present当中去,一些需要在控制器中处理的逻辑(例如:控制器之间的交互),则需要在控制器中处理,这样大部分业务都跑到Present当中去了,也就是说业务相关逻辑处理,大部分都在present当中修改即可

注意:除此之外,还有一种面向业务的MVP模式,与上面不同的是,即不涉及到业务的UI等逻辑继续放到控制器controller当中,只有涉及到业务相关的View和Model等相关处理才会放到Present当中,这种情况在初期开发,对于后台和UI等变化相对巨大的情况,非常适用,也就是面向业务开发的MVP模式

而这两种方式,看了更后面的模式之后,相信加入自己的理解,会有更加合理的利用方式

案例演示

下面用案例演示第二种MVP,即面向业务的MVP模式代码案例演示:

控制器相关代码:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    [self setupPresent];
    
    [self setupTableView];
}

//实际使用中,根据情况tableView也可以独立到view层中,如果代码不多,那么没必要
- (void)setupTableView {
    self.tableView = [[UITableView alloc] initWithFrame:self.view.frame 
        style:UITableViewStylePlain];
    self.tableView.dataSource = self;
    self.tableView.delegate = self;
    [self.tableView registerNib:[UINib nibWithNibName:@"MVCCell" bundle:nil] 
        forCellReuseIdentifier:reuseIdentifier];
    
    [self.view addSubview:self.tableView];
}

- (void)setupPresent {
    self.present = [[MVPPresent alloc] init];
    self.present.tableView = self.tableView;
    [self.present getDataSource];
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return self.present && self.present.dataList ? self.present.dataList.count : 0;
}

- (UITableViewCell *)tableView:(UITableView *)tableView 
    cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    MVPCell *cell = [tableView dequeueReusableCellWithIdentifier:reuseIdentifier 
        forIndexPath:indexPath];
    //直接将赋值相关操作全部交给present,这些涉及到业务相关的内容
    self.present.cellUpdateBlock(tableView, cell, indexPath);
    return cell;
}
复制代码

present相关代码


@property (nonatomic, strong) UITableView *tableView;

@property (nonatomic, strong, readonly) NSArray *dataList;

@property (nonatomic, strong) void(^cellUpdateBlock)(UITableView *tableView, MVPCell*cell, 
    NSIndexPath *indexPath);


- (instancetype)init
{
    self = [super init];
    if (self) {
        [self getDataSource];
    }
    return self;
}

- (void)getDataSource {
    [MVPDataSource getMVPData:^(NSArray<MVCModel *> * _Nonnull dataList) {
        //除了加载数据,数据内容的处理也可以在这里面,例如内容计算、筛选,
        //当然根据业务和复杂度,可以再进一步封装划分
        self.dataList = dataList;
        [self.tableView reloadData];
    }];
}

- (void (^)(UITableView *, MVPCell *, NSIndexPath *))cellUpdateBlock {
    __weak typeof(self) wself = self;
    _cellUpdateBlock = ^(UITableView *tableView, MVPCell *cell, NSIndexPath *indexPath) {
        __strong typeof(self) sself = wself;
        MVPModel *model = sself.dataList[indexPath.row];
        cell.titleLabel.text = model.name;
        cell.contentLabel.text = model.content;
        
//        cell.clickDelegate = sself;
        //这里也可以实现点击的代理等相关操作业务相关问题,毕竟视图这里都可以访问的到
        [cell setOnTapBlock:^(MVCCell * _Nonnull cell) {
            [wself onChangeItem:model cell:cell indexPath:indexPath];
        }];
    };
    return _cellUpdateBlock;
}

- (void)onChangeItem:(MVPModel *)model cell:(MVCCell *)cell 
    indexPath:(NSIndexPath *)indexPath {
    model.content = [NSString stringWithFormat:@"我是更改过的内容:%ld", indexPath.row];
    //如果想要model改动的同时,自动更新View,则需要对view添加对model的监听,这种时候,mvp代码又会变得复杂
    cell.contentLabel.text = model.content; 
    [self.tableView reloadData];
}
复制代码

通过上面可以看到,一部分不变的View逻辑直接放到了controller当中,而涉及到网络数据和相关View部分的逻辑均交给Present代为处理,此操作将基本视图、业务逻辑视图、业务逻辑完全分离,当视图变动,只需要更改视图,或者根据情况present中更改赋值方式即可,或者网路请求数据格式调整,更改完网络请求数据,根据情况直接到present中调整即可,目的明确,使用灵活,非常适合前期开发,甚至调整之后,可以直接一直用于整个项目

MVVM

MVVM也是在某次开发大会中被发掘出来,其使用方式之新奇,更是令不少人神往,其也是MVC衍生出来的一种架构模式,也非常优秀,受到不少人的大爱,使用过程推荐使用RAC相关架构(由于RAC架构的侵入性较强,且对新人需要一定门槛,个人不是非常推荐),实际使用中其实没必要使用RAC相关架构也可以正常使用

后面也介绍其使用过程中的优缺点,以及与一些架构的相似之处,相信理解后,MVVM可能真的是被神话的架构,并非吊打一切,只是一个阶段的产物罢了

其交互逻辑图如下所示

image.png

看上图,可以看到ViewModel主要负责View和Model的整合和绑定,当Model发生改变时,View也发生改变,当View响应时,Model也可以随之发生改变,而一个Controller也是可以由一个或者多个ViewModel组成,每一个ViewModel给控制器提供一套相对完善的View处理,这也是ViewModel设计的初衷

注意:看到了上面的结构,你也许会想到,这个结构怎么和MVP这么像?没错,MVP的Present经过分离调整,就是另外一个MVVM,很多人很早已经将MVP的Present分发出来了,但是却不叫MVVM,甚至瞧不起MVVM,也因此有说MVVM是MVP的另外一个变种,如下图所示

image.png

而我看感觉,MVP和MVVM是完全不会冲突的一个架构,MVP将Controler的部分逻辑分发出来,ViewModel开发出了数据和视图绑定的概念,配合观察者模式,更适合应用于模型和数据固定的交互关系,在这种固定模式下使用ViewModel可以让人更好的理解其中的关系,其更像一个整体,而Present更像是一个整合器

因此在合适的使用使用合适的结构,方为上上之策,你觉得是灵活使用好,还是统一架构好呢?

代码案例

代码案例使用起来和MVP很像,这里就介绍Controller和ViewModel,可以看一下里面的逻辑,其中观察者模式中使用了KVOController来配合,而不是使用庞大的RAC(ReativeCocoa)框架,如下所示

Controller代码

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.viewModelList = [NSMutableArray array];
    
    [self setupTableView];
    
    [self getDataSource];
}

- (void)setupTableView {
    self.tableView = [[UITableView alloc] initWithFrame:self.view.frame 
        style:UITableViewStylePlain];
    self.tableView.dataSource = self;
    self.tableView.delegate = self;
    [self.tableView registerNib:[UINib nibWithNibName:@"MVCCell" bundle:nil] 
        forCellReuseIdentifier:reuseIdentifier];
    
    [self.view addSubview:self.tableView];
}

- (void)getDataSource {
    [MVVMDataSource getMVVMData:^(NSArray<MVVMModel *> * _Nonnull dataList) {
        [dataList enumerateObjectsUsingBlock:^(MVVMModel * _Nonnull obj, 
            NSUInteger idx, BOOL * _Nonnull stop) {
            MVVMViewModel *viewModel = [MVVMViewModel new];
            viewModel.coreModel = obj;
            [self.viewModelList addObject:viewModel];
        }];
        [self.tableView reloadData];
    }];
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return [self.viewModelList count];
}

- (UITableViewCell *)tableView:(UITableView *)tableView 
    cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    MVVMCell *cell = [tableView dequeueReusableCellWithIdentifier:reuseIdentifier 
        forIndexPath:indexPath];
    
    //实际上一般是一个view对应一个viewModel,复用里面一般用的少,这里只是演示使用
    MVVMViewModel *viewModel = self.viewModelList[indexPath.row];
    viewModel.coreView = cell;
    
    viewModel.delegate = self;
    
    [viewModel update];
    
    return cell;
}

//长按跳转,这个是viewModel和controller之间的交互
- (void)navigateMVVMController {
    LSMVVMController *testNewVC = [LSMVVMController new];
    [self presentViewController:testNewVC animated:YES completion:nil];
    
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), 
        dispatch_get_main_queue(), ^{
        [testNewVC dismissViewControllerAnimated:YES completion:nil];
    });
}
复制代码

ViewModel代码

- (instancetype)init
{
    self = [super init];
    if (self) {
        _kvoController = [FBKVOController controllerWithObserver:self];
    }
    return self;
}

- (void)update {
//此模型里面的内容,可能会跨越多个界面,因此使用view与与其绑定
//可以使得使用同一个模型的view在model更改后同时响应更改
    [_kvoController observe:self.coreModel keyPath:@"name" 
        options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionInitial 
            block:^(id  _Nullable observer, MVVMModel *_Nonnull object, 
                NSDictionary<NSString *,id> * _Nonnull change) {
        self.coreView.titleLabel.text = object.name; // change[@"new"];
    }];
    //注意KVOControlelr并不会与self造成循环引用,注意block的持有者并不是self,而是_FBKVOInfo
    [_kvoController observe:self.coreModel keyPath:@"content"
        options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionInitial 
            block:^(id  _Nullable observer, MVVMModel *_Nonnull object, 
            NSDictionary<NSString *,id> * _Nonnull change) {
        self.coreView.contentLabel.text = object.content; // change[@"new"];;
    }];
    __weak typeof(self) wself = self;
    [self.coreView setOnTapBlock:^(MVCCell * _Nonnull cell) {
        [wself onClickToUpdate];
    }];
    
    [self.coreView setOnLongPressBlock:^(MVCCell * _Nonnull cell) {
        if ([wself.delegate respondsToSelector:@selector(navigationController)] ) {
            [wself.delegate navigateMVVMController];
        }
    }];
}

//更改不一定是在当前界面
- (void)onClickToUpdate {
    self.coreModel.name = @"更改的标题";
    self.coreModel.content = @"更改过的内容";
}
复制代码

通过上面代码,可以看到MVVM的优势,非常适用于一个模型作用域多个UI界面的场景,当模型改变,其他涉及到的UI都会发生改变,这也是观察者模式的优势,其更加合理的利用此优势,来完成响应的功能

在我看来他更像是一个设计模式应用合理运用的一个开始

中间路由router

中间路由router也是由MVC衍生出来的一个非常优秀的架构,他算是一个面向协议编程的一个典型架构,其思想与其他架构完全不冲突,可以与MVP、MVVM等结合使用,其交互逻辑图如下所示

image.png

上图我故意没有在Context多个端全部放置Present,而是夹杂着一个ViewModel,表示着其结构有着无线的可能性,并不是局限于一种模式,实际工作中一般使用同一种架构

这种架构中可以看到,context作为交互的中间路由,其可以与Controller、所有Present、所有ViewModel进行交互,因此Present与Present之间的交互也可以通过Context进行桥接,可以通过创建协议,使用面向协议方式进行交互,这样可以使得交互更加清晰

通过上面的可以看到他们之间很多都是双向箭头,那么持有关系是怎样的呢,哪里应该是weak,哪里必须strong

MVC中都是Controler直接持有View和Model

MVP中都是Controller直接持有Present,Present持有View和Model

MVVM中都是Controller直接持有ViewModel,ViewModel持有View和Model

而中间路由router明显复杂一些,为了避免Context不是创建完毕就马上释放,Controller强持有Context,Context引用Controller以保持与Controller的联系,且避免引用循环;而Context与Present、ViewModel之间的关系也是类似,为了保证他们都随着Controller的释放而释放,且不会创建完毕就立即释放,Context应当强持有所有Present和ViewModel,而Present、ViewModel则弱引用Context以保证联系

通过上面逻辑,就得以保证他们之间没有循环引用关系,Controller里面的组件会随着Controller的释放而集体释放,详细可以参考Router的代码

代码案例

代码的实现模块跟上面介绍的略有不同,故意多添加了一个模块,如果前面的你都看了,相信这种结构也会很容易看明白,其结构如下图所示,相信你的实际项目也许会用得到

image.png

context实现

@class LSRouteHeaderModel, LSRouterRowModel;

@protocol LSRouteHeadViewDelegate <NSObject>

- (void)onUpdateWithHeadModel:(LSRouteHeaderModel *)headModel;

@end

@protocol LSRouteTableViewDelegate <NSObject>

- (void)onUpdateWithRowModel:(LSRouterRowModel *)rowModel;

@end



@class LSRouterController, LSRouterPresent, LSRouteHeadPresent;

@interface LSRouterContext : NSObject

@property (nonatomic, weak) LSRouterController *routerController;

@property (nonatomic, strong) LSRouterPresent *present;

@property (nonatomic, strong) LSRouteHeadPresent *headPresent;

@end
复制代码

Controller实现

@property (nonatomic, strong) LSRouterContext *context;

- (void)setup {
    self.context = [[LSRouterContext alloc] init];
    self.context.routerController = self;
    
    //初始化头部视图相关
    self.context.headPresent = [[LSRouteHeadPresent alloc] init];
    self.context.headPresent.context = self.context;
    
    UIView *headView = [self.context.headPresent loadHeadViewWithFrame:
        CGRectMake(0, 0, self.view.frame.size.width, 50)];
    [self.view addSubview:headView];
    
    //加载数据
    [self.context.headPresent loadDataSouce];
    
    //初始化表格视图
    self.context.present = [[LSRouterPresent alloc] init];
    self.context.present.context = self.context;
    
    UIView *homeView = [self.context.present loadTableView];
    homeView.frame = 
        CGRectMake(0, 50, self.view.frame.size.width, self.view.frame.size.height - 50);
    [self.view addSubview:homeView];
    
    //加载数据
    [self.context.present loadDataSource];
    
    //设置代理关系,有利于交互,这一部分代理关系的设置
    self.context.present.delegate = self.context.headPresent;
    self.context.headPresent.delegate = self.context.present;
}
复制代码

TableView的Present实现

@interface LSRouterPresent : NSObject<LSRouteHeadViewDelegate>

@property (nonatomic, weak) id<LSRouteTableViewDelegate> delegate;

@property (nonatomic, weak) LSRouterContext *context;

@property (nonatomic, strong) UITableView *tableView;

@property (nonatomic, strong) NSArray *rowList;

- (UITableView *)loadTableView;

- (void)loadDataSource;

@end

@implementation LSRouterPresent

- (UITableView *)loadTableView {
    self.tableView = [[UITableView alloc] initWithFrame:CGRectZero 
        style:UITableViewStylePlain];
    self.tableView.dataSource = self;
    self.tableView.delegate = self;
    [self.tableView registerNib:[UINib nibWithNibName:@"MVCCell" bundle:nil] 
        forCellReuseIdentifier:cellIdentifier];
    return self.tableView;
}

- (void)loadDataSource {
    //这里为了方便展示结构,row使用的是同一串数据
    [LSRouterDataSource getRouterData:^(NSArray<LSRouterRowModel *> * _Nonnull rowList) {
        NSMutableArray *dataList = [NSMutableArray array];
        [rowList enumerateObjectsUsingBlock:^(LSRouterRowModel * _Nonnull obj, 
            NSUInteger idx, BOOL * _Nonnull stop) {
            LSRouteCellViewModel *viewModel = [LSRouteCellViewModel new];
            viewModel.coreModel = obj;
            [dataList addObject:viewModel];
        }];
        self.rowList = dataList;
        
        [self.tableView reloadData];
    }];
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return self.rowList ? self.rowList.count : 0;
}

- (UITableViewCell *)tableView:(UITableView *)tableView 
    cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    LSRouterCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier 
        forIndexPath:indexPath];
    
    //实际上一般是一个view对应一个viewModel,复用里面一般用的少,这里只是演示使用
    LSRouteCellViewModel *viewModel = self.rowList[indexPath.row];
    viewModel.coreView = cell;
    viewModel.delegate = self;
    [viewModel update];
    
    return cell;
}

- (void)onTapUpdate:(LSRouterRowModel *)rowModel {
    if ([self.delegate respondsToSelector:@selector(onUpdateWithRowModel:)]) {
        [self.delegate onUpdateWithRowModel:rowModel];
    }
}

//更新视图
- (void)onUpdateWithHeadModel:(LSRouteHeaderModel *)headModel {
    [self.rowList enumerateObjectsUsingBlock:^(LSRouteCellViewModel * _Nonnull obj, 
        NSUInteger idx, BOOL * _Nonnull stop) {
        obj.coreModel.content = headModel.content;
    }];
}
复制代码

顶部视图HeadView的present实现

@interface LSRouteHeadPresent : NSObject<LSRouteTableViewDelegate>

@property (nonatomic, weak) id<LSRouteHeadViewDelegate> delegate;

@property (nonatomic, weak) LSRouterContext *context;

@property (nonatomic, strong) LSRouteHeadView *coreView;

@property (nonatomic, strong) LSRouteHeaderModel *coreModel;

- (UIView *)loadHeadViewWithFrame:(CGRect)frame;

- (void)loadDataSouce;

@end

@implementation LSRouteHeadPresent

- (UIView *)loadHeadViewWithFrame:(CGRect)frame {
    self.coreView = [[LSRouteHeadView alloc] initWithFrame:frame];
    __weak typeof(self) wself = self;
    [self.coreView setOnUpdateBlock:^(LSRouteHeadView * _Nonnull coreView) {
        [wself onClickToUpdateAllCell];
    }];
    return self.coreView;
}

- (void)loadDataSouce {
    self.coreModel = [[LSRouteHeaderModel alloc] init];
    self.coreModel.name = @"我是测试的标题内容";
    self.coreModel.content = @"我是测试的副标题内容,这部分可以更新或者使用";
    
    self.coreView.lblHead.text = self.coreModel.name;
}

- (void)onClickToUpdateAllCell {
    if ([self.delegate respondsToSelector:@selector(onUpdateWithHeadModel:)]) {
        [self.delegate onUpdateWithHeadModel:self.coreModel];
    }
    
}

- (void)onUpdateWithRowModel:(LSRouterRowModel *)rowModel {
    self.coreView.lblHead.text = rowModel.name;
    NSLog(@"LSRouterRowModel-content:%@", rowModel.content);
}
复制代码

上面仅仅是部分中间路由router的部分代码,详情可以查看代码,相信你结合图可以看的更清晰,并且会有自己的理解

最后

看了上面的中间路由Router有什么感想,如果多个控制器中间还有交互呢,可以试着将Controller当做Present再试试看,会清晰很多,从局部看总体,再从总体看局部,相信会是另一种感悟

没有最好的架构,只有更好的架构,一百个人有一百个哈姆雷特,可是让每一个哈姆雷特都能理解其他哈姆雷特,这就是能力,需要我们更多的积累知识

最后,本文不是终结,而是开始,大家一起努力,让架构们碰撞出爱情的火花吧!

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享