iOS 移动端架构初探心得

本文作为这一系列的收尾总结, 详细叙述了这个架构工具的设计思路以及一步步的优化, 在此也分享与你, 完整keynote可查阅github

参考链接:

本文作为以上文章系列的总结, 如何一步一步进行思考总结, 如何开发出适合自己的通用架构设计.

设计思路

《iOS 移动端架构初探心得》

对于架构, 移动端常见的架构设计包括MVC, MVVM, MVP等, 上图简要的说明了各种常见的架构之间的交互及数据传递方式.

《iOS 移动端架构初探心得》

对于MVC, MVVM, MVP这三种架构设计模式, 相信大家一定了然于心, 相关的文章也是多如繁星, 对于这些常用架构, 每个人都肯定有每个人的理解, 但这样会导致一个问题, 就是极大的自由度导致了没有代码规范, 对于移动端或者前端及后端来说, 其本质工作就是数据层和展示层的交互, 如何将数据正确安全高效的传输到展示层.

这里的数据层从整个项目来说, 可以说是后端, 也就是服务端, 对于服务端开发的流程就是从数据库获取数据并将数据进行各种逻辑过滤作为响应返回给前端用于展示层展示, 对于java为例, 我们普通的项目就会分为controller, service, dao, pojo, vo, bo等层级设计, 就会将不同功能进行抽象, 使得代码更容易维护.

而作为展示层的前端, 也就是客户端, 其实移动端在我感觉其实也是前端的一个分支, 而前端的架构通常为组件化设计, 每一个功能view对应一个组件, 而整个页面可以通过多个组件分离进行维护, 很高效的将业务代码和视图进行分离, 使得代码更有规范及易维护.

而对于移动端, 为什么要在controller中写那么多不知所云的代码? 为什么一个控制器能超过1k行? 为什么view的逻辑回调代理要写在controller中? 为什么控制器之间的参数传递的耦合性那么强? 为什么网络请求的方法随处可见? 为什么我们不能够像前后端那样有条理的控制我们的代码? 而让其像脱缰的野马难以驾驭呢?

《iOS 移动端架构初探心得》

为了解决这些问题, 我们需要考虑一些架构设计模式, 最先想到的就是以controller为中心的抽象, 将控制器的功能抽象到该具体负责的模块, 从图上可以看到, controller维护了presenter, viewmodel, view, 而viewmodel又维护了model, 其中的model也可以说就是javabean完全的纯数据结构, viewmodelmodel的上一层, 用于操作对应的数据, 可以看到controller将代码下发至下面三层, 使得各个层级各司其职.

《iOS 移动端架构初探心得》

刚才是站在controller的视角上来看的, 对于数据的传输, 这次我们站在presenter的视角上来看, 这个设计就是将viewmodel作为传递对象通过presenter这个中间件传输至view层, 这样view层不仅可以拿到数据, 也可以对数据进行操作, 掌控性有所提高.

《iOS 移动端架构初探心得》

刚才我们站在了controllerpresenter视角上分析了架构设计的思路, 但这样各个层级的耦合会越来越大, 从而导致项目代码无法分割, 这时想到了后端controllerservice之间通过接口进行交互来降低耦合, 我们是不是也可以参考这种方案通过一个protocol文档文件来降低各个层级之间的耦合呢, 如图所示, 将除了controller之外的其他层级进行解耦. 进行高度抽象.

《iOS 移动端架构初探心得》

上面我们解决了各个层级之间的耦合, 但我们怎么解决控制器之间的耦合呢? 答案是router, 我们站在路由的视角上看, 各大控制器都是独立存在的个体, 彼此之间的交互通过路由的映射进行交互, 这样我们就能够去除各大控制器之间的耦合了. 路由这个思路最先也是在前端的架构框架中看到的, 后来就有了cocoapods私有库组件化这种集成化的解决方案, 通过路由映射能够很好的做到模块分离, 更可以做到页面降级, 所谓的页面降级就是指不仅路由可以和native进行交互, 也可以和h5进行交互, 当nativeh5是一套业务逻辑的时候, native不慎出现bug我们可以请求后端接口修改数据库将页面直接降级至h5页面而不用重新打包等待苹果审核及使用热修复工具带来了时间消耗. 能够第一时间解决问题.

《iOS 移动端架构初探心得》

解决了上述问题, 我们就只剩下页面的问题了, 对于现在的iOSer来说, 写页面几乎是日常工作的绝大部分, 但是写页面, 写业务, 当逻辑复杂的时候也会产生一系列不易维护的问题, 这时候我们就可以使用类似redux这种状态机的模式, 将业务逻辑拆分出不同复杂的状态, 当变量改变的时候, 触发不同的状态, 这样就能够有效的管理我们的页面逻辑. 推荐可以看看reactredux的思路, 对这块也会有更好的掌握.

设计思路的总结就是, 通过高度抽象进行分层, 通过接口文档进行项目层级解耦, 通过路由进行组件化及降级, 通过CDD模式贯穿subview使得所有的view都能够拿到数据及操控数据, 通过AOP切面进行hook一些特殊功能如埋点统计, 全局当前控制器等等.

设计实现

上面部分, 叙述了整个架构的设计思路, 接下来, 我们来看看如何具体实现. 以下代码取自真实项目, 对应view视角图中的设计图.

//
//  InterfaceTemplate.h
//  SQTemplate
//
//  Created by 双泉 朱 on 17/5/5.
//  Copyright © 2017年 Doubles_Z. All rights reserved.
//

#import <UIKit/UIKit.h>

@protocol HYAbroadShoppingHomeModelInterface <NSObject>

/**
 * 仅用来保持PB不为空
 */
@property(nonatomic) NSInteger status ;
/**
 * 广告位
 */
@property(nonatomic,strong) NSMutableArray * banners ;
/**
 * 四个icon
 */
@property(nonatomic,strong) NSMutableArray * shortCutIcons ;
/**
 * 促销时间戳,活动剩余时间,转换成毫秒
 */
@property(nonatomic,strong) NSString * remainingTime ;
/**
 * 倒计时的商品,客户端根据当该字段有的时候,展示“今日剁手价”图片
 */
@property(nonatomic,strong) NSMutableArray * salesGoods ;
/**
 * 全球精选商品集合
 */
@property(nonatomic,strong) NSMutableArray * selectGoods ;

@property (nonatomic,assign,getter=isLoaded) BOOL loaded;
@property (nonatomic,assign,getter=isReload) BOOL reload;

@end

@protocol HYAbroadShoppingHomeViewModelInterface <NSObject>

@optional
@property (nonatomic,strong) id<HYAbroadShoppingHomeModelInterface> model;

@optional
- (void)initializeWithModel:(id<HYAbroadShoppingHomeModelInterface>)model completion:(void(^)())completion;
/**
*立即购买
* @para goodsId 这里Android就去调用CommonUtils里面的方法即可。IOS这里自行添加相应的代码。注意这里保持一个逻辑:如果是处方药进入到商品详情页,隐形眼镜…..
*/

- (void)senderAddShoppingCartWithModel:(id<HYAbroadShoppingHomeModelInterface>)model goodsId:(GoodsID *)goodsId completion:(void(^)())completion;
/**
*获取海外购首页
*/

- (void)senderAbroadShoppingHomeWithModel:(id<HYAbroadShoppingHomeModelInterface>)model completion:(void(^)())completion;

@end

@protocol HYAbroadShoppingHomeViewInterface <NSObject>

@property (nonatomic,strong) id<HYAbroadShoppingHomeViewModelInterface> abroadshoppinghomeViewModel;
@property (nonatomic,strong) id<HYAbroadShoppingHomeViewModelInterface> abroadshoppinghomeOperator;

@end

复制代码

接口文档文件将整个页面模块分成了ModelInterface, ViewModelInterface, ViewInterface三个接口,ModelInterface接口对应了服务器返回的外层数据结构, ViewModelInterface接口对应了操作model数据的方法, 如发起请求和从数据库读取诸如此类. ViewInterface拥有两者的能力.

//
//  ControllerTemplate.m
//  SQTemplate
//
//  Created by 双泉 朱 on 17/5/5.
//  Copyright © 2017年 Doubles_Z. All rights reserved.
//

#import "HYAbroadShoppingHomeViewController.h"
#import "HYAbroadShoppingHomePresenter.h"
#import "HYAbroadShoppingHomeViewModel.h"
#import "HYAbroadShoppingHomeView.h"

@interface HYAbroadShoppingHomeViewController ()

@property (nonatomic,strong) HYAbroadShoppingHomePresenter * abroadshoppinghomePresenter;
@property (nonatomic,strong) HYAbroadShoppingHomeViewModel * abroadshoppinghomeViewModel;
@property (nonatomic,strong) HYAbroadShoppingHomeView * abroadshoppinghomeView;

@end

@implementation HYAbroadShoppingHomeViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.title = @"全球购";
    [self setupView];
}

- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
    [self adapterView];
}

- (HYAbroadShoppingHomePresenter *)abroadshoppinghomePresenter {
    
    if (!_abroadshoppinghomePresenter) {
        _abroadshoppinghomePresenter = [HYAbroadShoppingHomePresenter new];
    }
    return _abroadshoppinghomePresenter;
}

- (HYAbroadShoppingHomeViewModel *)abroadshoppinghomeViewModel {
    
    if (!_abroadshoppinghomeViewModel) {
        _abroadshoppinghomeViewModel = [HYAbroadShoppingHomeViewModel new];
    }
    return _abroadshoppinghomeViewModel;
}

- (HYAbroadShoppingHomeView *)abroadshoppinghomeView {
    
    if (!_abroadshoppinghomeView) {
        _abroadshoppinghomeView = [HYAbroadShoppingHomeView new];
        _abroadshoppinghomeView.frame = self.view.bounds;
    }
    return _abroadshoppinghomeView;
}

- (void)setupView {
    [self.view addSubview:self.abroadshoppinghomeView];
}

- (void)adapterView {
    [self.abroadshoppinghomePresenter adapterWithAbroadShoppingHomeView:self.abroadshoppinghomeView abroadshoppinghomeViewModel:self.abroadshoppinghomeViewModel];
}

@end

复制代码

经过抽象后, 我们再来看看controller的文件, 我们可以看到, 经过抽象后的控制器加上顶部的注释也只有68行, 基本是不用再用心维护上千行的控制器了.

//
//  PresenterTemplate.m
//  SQTemplate
//
//  Created by 双泉 朱 on 17/5/5.
//  Copyright © 2017年 Doubles_Z. All rights reserved.
//

#import "HYAbroadShoppingHomePresenter.h"

@interface HYAbroadShoppingHomePresenter ()

@property (nonatomic,weak) id<HYAbroadShoppingHomeViewInterface> abroadshoppinghomeView;
@property (nonatomic,weak) id<HYAbroadShoppingHomeViewModelInterface> abroadshoppinghomeViewModel;

@end

@implementation HYAbroadShoppingHomePresenter

- (void)adapterWithAbroadShoppingHomeView:(id<HYAbroadShoppingHomeViewInterface>)abroadshoppinghomeView abroadshoppinghomeViewModel:(id<HYAbroadShoppingHomeViewModelInterface>)abroadshoppinghomeViewModel {

    _abroadshoppinghomeView = abroadshoppinghomeView;
    _abroadshoppinghomeViewModel = abroadshoppinghomeViewModel;

    __weak typeof(self) _self = self;
    __weak id<HYAbroadShoppingHomeViewModelInterface> __abroadshoppinghomeViewModel = _abroadshoppinghomeViewModel;
    [_abroadshoppinghomeViewModel initializeWithModel:__abroadshoppinghomeViewModel.model completion:^{
        _self.abroadshoppinghomeView.abroadshoppinghomeViewModel = __abroadshoppinghomeViewModel;
        _self.abroadshoppinghomeView.abroadshoppinghomeOperator = _self;
    }];
}

/**
*立即购买
* @para goodsId 这里Android就去调用CommonUtils里面的方法即可。IOS这里自行添加相应的代码。注意这里保持一个逻辑:如果是处方药进入到商品详情页,隐形眼镜…..
*/

- (void)senderAddShoppingCartWithModel:(id<HYAbroadShoppingHomeModelInterface>)model goodsId:(GoodsID *)goodsId completion:(void(^)())completion {

    __weak typeof(self) _self = self;
    __weak id<HYAbroadShoppingHomeViewModelInterface> __abroadshoppinghomeViewModel = _abroadshoppinghomeViewModel;
    [_abroadshoppinghomeViewModel senderAddShoppingCartWithModel:model goodsId:goodsId completion:^{
        _self.abroadshoppinghomeView.abroadshoppinghomeViewModel = __abroadshoppinghomeViewModel;
        completion();
    }];
}

/**
*获取海外购首页
*/

- (void)senderAbroadShoppingHomeWithModel:(id<HYAbroadShoppingHomeModelInterface>)model completion:(void(^)())completion {

    __weak typeof(self) _self = self;
    __weak id<HYAbroadShoppingHomeViewModelInterface> __abroadshoppinghomeViewModel = _abroadshoppinghomeViewModel;
    [_abroadshoppinghomeViewModel senderAbroadShoppingHomeWithModel:model completion:^{
        _self.abroadshoppinghomeView.abroadshoppinghomeViewModel = __abroadshoppinghomeViewModel;
        completion();
    }];
}

@end

复制代码

controller中, 我们将viewmodelview, 也就是上述的数据层和展示层传输到 presenter的中间件进行交互, 我们通过观察可以看到当请求完成后, 先进行赋值操作, 再进行自定义业务逻辑, 这样能够保证操作业务逻辑时数据是最新的.

//
//  ViewModelTemplate.m
//  SQTemplate
//
//  Created by 双泉 朱 on 17/5/5.
//  Copyright © 2017年 Doubles_Z. All rights reserved.
//

#import "HYAbroadShoppingHomeViewModel.h"
#import "HYAbroadShoppingHomeModel.h"
#import "HYAbroadShoppingSender.h"
#import "HYMallGoodsSender.h"

@implementation HYAbroadShoppingHomeViewModel

- (HYAbroadShoppingHomeModel *)model {
    
    if (!_model) {
        _model = [HYAbroadShoppingHomeModel new];
    }
    return _model;
}

- (void)initializeWithModel:(id<HYAbroadShoppingHomeModelInterface>)model completion:(void(^)())completion {
    if (!model.isLoaded) {
        [self senderAbroadShoppingHomeWithModel:model completion:completion];
    }
}

/**
*立即购买
* @para goodsId 这里Android就去调用CommonUtils里面的方法即可。IOS这里自行添加相应的代码。注意这里保持一个逻辑:如果是处方药进入到商品详情页,隐形眼镜…..
*/

- (void)senderAddShoppingCartWithModel:(id<HYAbroadShoppingHomeModelInterface>)model goodsId:(GoodsID *)goodsId completion:(void(^)())completion {
  
    NSMutableArray * editArray=[@[] mutableCopy];
    EditGoodsM_Builder * editGoodsM_Builder=[[EditGoodsM_Builder alloc]init];
    editGoodsM_Builder.goodsId = goodsId;
    editGoodsM_Builder.amount = 1;
    [editArray addObject:[editGoodsM_Builder build]];
    
    HYSenderResultModel * resultModel = [HYMallGoodsSender senderAddShoppingCart:nil token:nil  editGoods:editArray promoteIDs:nil];
    HYViewController * vc = [HYCurrentVCmanager shareInstance].getCurrentVC;
    [vc startLoading];
    [vc requestWithModel:resultModel success:^(HYResponseModel *model) {
        _model.reload = NO;
        completion();
        [vc endLoading];
    } failure:^(HYResponseModel * model) {
        [vc endLoading];
    }];
}

/**
*获取海外购首页
*/

- (void)senderAbroadShoppingHomeWithModel:(id<HYAbroadShoppingHomeModelInterface>)model completion:(void(^)())completion {
    HYSenderResultModel * resultModel = [HYAbroadShoppingSender senderAbroadShoppingHome:model status:0];
    HYViewController * vc = [HYCurrentVCmanager shareInstance].getCurrentVC;
    [vc startLoading];
    [vc requestWithModel:resultModel success:^(HYResponseModel *model) {
        _model.loaded = YES;
        completion();
        [vc endLoading];
    } failure:^(HYResponseModel * model) {
        [vc endLoading];
    }];
}


@end

复制代码

我们再来看看viewmodel层如何设计, viewmodel层持有model, 并进行数据获取, 将获取的数据赋值到model中, 由于线上真实项目使用的是TCP+ProtoBuffer, 代码显示的是我司自行封装的一套网络逻辑, 所以可能对一些同学不是很友好, 请看下面的例子:

//
//  ViewModelTemplate.m
//  SQTemplate
//
//  Created by 双泉 朱 on 17/5/5.
//  Copyright © 2017年 Doubles_Z. All rights reserved.
//

#import "ViewModelTemplate.h"
#import "ModelTemplate.h"
#import "NetWork.h"
#import "DataBase.h"

@implementation ViewModelTemplate

- (void)dynamicBindingWithFinishedCallBack:(void (^)())finishCallBack {

    [DataBase requestDataWithClass:[ModelTemplate class] finishedCallBack:^(NSDictionary *response) {
        _model = [ModelTemplate modelWithDictionary:response];
        finishCallBack();
    }];
    
    [NetWork requestDataWithType:MethodGetType URLString:@"http://localhost:3001/api/J1/getJ1List" parameter:nil finishedCallBack:^(NSDictionary * response){
        _model = [ModelTemplate modelWithDictionary:response[@"data"]];
        [DataBase cache:[ModelTemplate class] data:response[@"data"]];
        finishCallBack();
    }];
}

@end

复制代码

其中DataBase仅仅是plist的缓存, 而NetWork是封装的AFNetworking, 这样大部分同学就很熟悉了吧, 在viewmodel层可以将数数据库和网络请求这两种获取数据的方式封装在一个层级里面, 这样逻辑分明也对外界没有耦合, 而对于我们线上项目我们在底层还有一个sender层用于管理上述问题及一些其他业务逻辑, 通用的架构设计是一个思想, 需要结合实际业务逻辑进行调整, 正所谓不能脱离业务谈架构.

//
//  ViewTemplate.m
//  SQTemplate
//
//  Created by 双泉 朱 on 17/5/5.
//  Copyright © 2017年 Doubles_Z. All rights reserved.
//

#import "HYAbroadShoppingHomeView.h"
#import "HYAbroadShoppingHomeHeaderView.h"
#import "HYAbroadSelectGoodsViewCell.h"

@interface HYAbroadShoppingHomeView () <UITableViewDataSource, UITableViewDelegate>

@property (nonatomic,strong) UITableView * tableView;
@property (nonatomic,strong) HYAbroadShoppingHomeHeaderView * headerView;

@end

@implementation HYAbroadShoppingHomeView

- (void)dealloc {
    NSLog(@"%@ - execute %s",NSStringFromClass([self class]),__func__);
}

- (instancetype)initWithFrame:(CGRect)frame {
    
    self = [super initWithFrame:frame];
    if (self) {
        [self setupSubviews];
    }
    return self;
}

- (instancetype)initWithCoder:(NSCoder *)coder  {
    
    self = [super initWithCoder:coder];
    if (self) {
        [self setupSubviews];
    }
    return self;
}

- (UITableView *)tableView {
    
    if (!_tableView) {
        _tableView = [UITableView new];
        _tableView.dataSource = self;
        _tableView.delegate = self;
        _tableView.tableHeaderView = self.headerView;
        _tableView.backgroundColor = SQBGC;
        _tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
    }
    return _tableView;
}

- (HYAbroadShoppingHomeHeaderView *)headerView {
    
    if (!_headerView) {
        _headerView = [HYAbroadShoppingHomeHeaderView new];
        _headerView.hidden = YES;
    }
    return _headerView;
}

- (void)setupSubviews {
    [self addSubview:self.tableView];
}

- (void)setAbroadshoppinghomeViewModel:(id<HYAbroadShoppingHomeViewModelInterface>)abroadshoppinghomeViewModel {
    _abroadshoppinghomeViewModel = abroadshoppinghomeViewModel;
    if (abroadshoppinghomeViewModel.model.reload) {
        _headerView.hidden = !abroadshoppinghomeViewModel.model.isLoaded;
        _headerView.model = abroadshoppinghomeViewModel.model;
        CGFloat headerViewH = abroadshoppinghomeViewModel.model.remainingTime.length
        ? kscaleDeviceLength(160) + (self.width / 4) * 1.1 + 290
        : kscaleDeviceLength(160) + (self.width / 4) * 1.1 + 50;
        _headerView.frame = CGRectMake(0, 0, 0, headerViewH);
        [_tableView reloadData];
    }
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return _abroadshoppinghomeViewModel.model.selectGoods.count;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    
    HYAbroadSelectGoodsViewCell * cell = [HYAbroadSelectGoodsViewCell cellWithTableView:tableView];
    cell.good = _abroadshoppinghomeViewModel.model.selectGoods[indexPath.item];
    cell.abroadshoppinghomeOperator = _abroadshoppinghomeOperator;
    return cell;
}

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    return [HYAbroadSelectGoodsViewCell cellHeightWithGood:_abroadshoppinghomeViewModel.model.selectGoods[indexPath.item]];
}

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    [tableView deselectRowAtIndexPath:indexPath animated:YES];
}

- (void)layoutSubviews {
    [super layoutSubviews];
    _tableView.frame = self.bounds;
}

@end

复制代码

接着我们来看view,根据之前的设计图, 我们将其区分为两个模块, tableviewheaderviewtableviewcell, 其中headerview负责上面不需要复用的view, 而tableviewcell负责需要复用的部分.

这里我们看到重写set方法时根据是否需要刷新tableview, 一定限度避免了无效的刷新消耗.

还有一个知识点是将operator直接贯穿传递到各级subview这里用到的就是CDD模式的精髓, 使得所有的subview都能够获取数据, 避免了delegate, block这种冗余回调带来耦合的尴尬, 关键是丑.

//
//  HYAbroadSelectGoodsViewFrameHub.m
//  Mall
//
//  Created by 朱双泉 on 19/10/2017.
//  Copyright © 2017 _Zhizi_. All rights reserved.
//

#import "HYAbroadSelectGoodsViewFrameHub.h"
#import "NSString+SQExtension.h"

@implementation HYAbroadSelectGoodsViewFrameHub

- (instancetype)initWithGoodsName:(NSString *)goodsName nameGoodEvalution:(NSString *)nameGoodEvalution {
    
    self = [super init];
    if (self) {
        
        CGFloat width = DeviceWidth - 30;
        CGFloat proImageUrlButtonX = 10;
        CGFloat proImageUrlButtonY = proImageUrlButtonX;
        CGFloat proImageUrlButtonW = width - 2 * proImageUrlButtonX;
        CGFloat proImageUrlButtonH = proImageUrlButtonW;
        _proImageUrlButtonFrame = CGRectMake(proImageUrlButtonX, proImageUrlButtonY, proImageUrlButtonW, proImageUrlButtonH);
        
        CGFloat goodsSellerImageViewX = proImageUrlButtonX;
        CGFloat goodsSellerImageViewY = proImageUrlButtonY + proImageUrlButtonH + 13;
        CGFloat goodsSellerImageViewW = 20;
        CGFloat goodsSellerImageViewH = 15;
        _goodsSellerImageViewFrame = CGRectMake(goodsSellerImageViewX, goodsSellerImageViewY, goodsSellerImageViewW, goodsSellerImageViewH);
        
        CGFloat goodsNameLabelX = goodsSellerImageViewX;
        CGFloat goodsNameLabelY = proImageUrlButtonY + proImageUrlButtonH + 10;
        CGFloat goodsNameLabelW = proImageUrlButtonW;
        CGSize  goodsNameLabelSize = [goodsName getSizeWithConstraint:CGSizeMake(goodsNameLabelW, 60) font:KF03_17];
        CGFloat goodsNameLabelH = goodsNameLabelSize.height;
        _goodsNameLabelFrame = CGRectMake(goodsNameLabelX, goodsNameLabelY, goodsNameLabelW, goodsNameLabelH);
        
        CGFloat costPriceLabelY = 0.0;
        if (nameGoodEvalution.length) {
            CGFloat userEvalutionLabelX = proImageUrlButtonX + 10;
            CGFloat userEvalutionLabelY = goodsNameLabelY + goodsNameLabelH + 10;
            CGFloat userEvalutionLabelW = 55;
            CGFloat userEvalutionLabelH = 20;
            _userEvalutionLabelFrame = CGRectMake(userEvalutionLabelX, userEvalutionLabelY, userEvalutionLabelW, userEvalutionLabelH);
            
            CGFloat userEvalutionBackgroundX = proImageUrlButtonX;
            CGFloat userEvalutionBackgroundY = userEvalutionLabelY + userEvalutionLabelH / 2;
            CGFloat userEvalutionBackgroundW = proImageUrlButtonW;
            
            CGFloat nameGoodEvalutionLabelX = userEvalutionBackgroundX + 10;
            CGFloat nameGoodEvalutionLabelY = userEvalutionLabelY + userEvalutionLabelH + 10;
            CGFloat nameGoodEvalutionLabelW = userEvalutionBackgroundW - 20;
            CGSize  nameGoodEvalutionLabelSize = [nameGoodEvalution getSizeWithConstraint:CGSizeMake(nameGoodEvalutionLabelW, 40) font:KF06_12];
            
            CGFloat nameGoodEvalutionLabelH = nameGoodEvalutionLabelSize.height;
            _nameGoodEvalutionLabelFrame = CGRectMake(nameGoodEvalutionLabelX, nameGoodEvalutionLabelY, nameGoodEvalutionLabelW, nameGoodEvalutionLabelH);
            
            CGFloat userEvalutionBackgroundH = nameGoodEvalutionLabelH + 30;
            _userEvalutionBackgroundFrame = CGRectMake(userEvalutionBackgroundX, userEvalutionBackgroundY, userEvalutionBackgroundW, userEvalutionBackgroundH);
            
            costPriceLabelY = userEvalutionBackgroundY + userEvalutionBackgroundH + 10;
        } else {
            costPriceLabelY = goodsNameLabelY + goodsNameLabelH + 10;
        }
        
        CGFloat costPriceLabelX = goodsNameLabelX;
        CGFloat costPriceLabelW = 180;
        CGFloat costPriceLabelH = 30;
        _costPriceLabelFrame = CGRectMake(costPriceLabelX, costPriceLabelY, costPriceLabelW, costPriceLabelH);
        
        CGFloat buyButtonW = 80;
        CGFloat buyButtonX = goodsNameLabelX + goodsNameLabelW - buyButtonW;
        CGFloat buyButtonY = costPriceLabelY;
        CGFloat buyButtonH = costPriceLabelH;
        _buyButtonFrame = CGRectMake(buyButtonX, buyButtonY, buyButtonW, buyButtonH);
        
        _calculateHeight = CGRectGetMaxY(_buyButtonFrame) + 10;
    }
    return self;
}

@end

复制代码

当需要动态计算高度的时候, 我们可以使用framehub这种模式, 名字是自己取的, 请别见怪, 对性能有要求的同学可以将高度计算值缓存下来, 以免cpu重复大量计算导致手机的耗电.

//
//  HYAbroadSelectGoodsViewCell.m
//  Mall
//
//  Created by 朱双泉 on 12/10/2017.
//  Copyright © 2017 _Zhizi_. All rights reserved.
//

#import "HYAbroadSelectGoodsViewCell.h"
#import "HYAbroadSelectGoodsView.h"
#import "HYGoodsDetailViewController.h"
#import "HYCartNewViewController.h"
#import "UIAlertView+SQExtension.h"
#import "NSString+SQExtension.h"

@interface HYAbroadSelectGoodsViewCell ()

@property (nonatomic,strong) HYAbroadSelectGoodsView * selectGoodsView;

@end

@implementation HYAbroadSelectGoodsViewCell

- (void)dealloc {
#if DEBUG
    NSLog(@"--------");
    NSLog(@"%@ - execute %s",NSStringFromClass([self class]),__func__);
    NSLog(@"--------");
#endif
}

+ (instancetype)cellWithTableView:(UITableView *)tableView {
    
    NSString * identifier = NSStringFromClass([HYAbroadSelectGoodsViewCell class]);
    HYAbroadSelectGoodsViewCell * cell = [tableView dequeueReusableCellWithIdentifier:identifier];
    if (!cell) {
        cell = [[HYAbroadSelectGoodsViewCell alloc]initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifier];
        cell.selectionStyle = UITableViewCellSelectionStyleNone;
    }
    return cell;
}

- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
    
    self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
    if (self) {
        [self setupSubviews];
    }
    return self;
}

- (HYAbroadSelectGoodsView *)selectGoodsView {
    
    if (!_selectGoodsView) {
        _selectGoodsView = [HYAbroadSelectGoodsView new];
        _selectGoodsView.backgroundColor = [UIColor whiteColor];
        _selectGoodsView.layer.cornerRadius = 4;
        _selectGoodsView.layer.masksToBounds = YES;
    }
    return _selectGoodsView;
}

- (void)setupSubviews {
    self.contentView.backgroundColor = SQBGC;
    [self.contentView addSubview:self.selectGoodsView];
}

- (void)setGood:(AbroadGoods *)good {
    _good = good;
    __weak typeof(self) _self = self;
    [_selectGoodsView.proImageUrlButton sd_setBackgroundImageWithURL:[NSURL URLWithString:good.proImageUrl] forState:0 placeholderImage:[UIImage imageNamed:@"placeholder_200"]];
    [_selectGoodsView.proImageUrlButton whenTapped:^{
        [[HYCurrentVCmanager shareInstance].getCurrentVC hyPushDetail:_self.good.targetUrl];
    }];
    [_selectGoodsView.goodsSellerImageView sd_setImageWithURL:[NSURL URLWithString:good.goodsSellerImage]];
    _selectGoodsView.goodsNameLabel.text = [NSString stringWithFormat:@" %@", [good.goodsName trim]];
    _selectGoodsView.nameGoodEvalutionLabel.text = [good.nameGoodEvalution trim];
    _selectGoodsView.costPriceLabel.text = good.ecPrice;
    [_selectGoodsView.buyButton whenTapped:^{
        if (_self.good.goodsType == GoodsTypePrescriptionAllow ||
            _self.good.goodsType == GoodsTypePrescriptionForbid ||
            _self.good.goodsType == GoodsTypeGlasses) {
            [[HYCurrentVCmanager shareInstance].getCurrentVC HYPushViewController:[HYGoodsDetailViewController new] animated:YES];
        } else {
            [_self.abroadshoppinghomeOperator senderAddShoppingCartWithModel:nil goodsId:_self.good.goodsId completion:^{
                [UIAlertView showAlertViewWithTitle:@"添加成功!" message:@"商品已加入购物车" cancelButtonTitle:@"再逛逛" otherButtonTitles:@[@"去购物车"] clickAtIndex:^(NSInteger buttonIndex) {
                    if (buttonIndex == 1) {
                        [[HYCurrentVCmanager shareInstance].getCurrentVC HYPushViewController:[HYCartNewViewController new] animated:YES];
                    }
                }];
            }];
        }
    }];
    [_selectGoodsView setNeedsLayout];
    [self setNeedsLayout];
}

- (void)layoutSubviews {
    [super layoutSubviews];
    
    CGFloat selectGoodsViewX = 15;
    CGFloat selectGoodsViewY = 0;
    CGFloat selectGoodsViewW = self.width - 2 * selectGoodsViewX;
    CGFloat selectGoodsViewH = [HYAbroadSelectGoodsView viewHeightWithGoodsName:[NSString stringWithFormat:@" %@", [_good.goodsName trim]] nameGoodEvalution:[_good.nameGoodEvalution trim]];
    _selectGoodsView.frame = CGRectMake(selectGoodsViewX, selectGoodsViewY, selectGoodsViewW, selectGoodsViewH);
}

+ (CGFloat)cellHeightWithGood:(AbroadGoods *)good {
    return [HYAbroadSelectGoodsView viewHeightWithGoodsName:[NSString stringWithFormat:@" %@", [good.goodsName trim]] nameGoodEvalution:[good.nameGoodEvalution trim]] + 10;
}

@end

复制代码

可以看到, 推荐将tableviewcell的高度及获取封装在内, 避免和tableview进行耦合, 这里注意的是以下代码:

[_self.abroadshoppinghomeOperator senderAddShoppingCartWithModel:nil goodsId:_self.good.goodsId completion:^{
                [UIAlertView showAlertViewWithTitle:@"添加成功!" message:@"商品已加入购物车" cancelButtonTitle:@"再逛逛" otherButtonTitles:@[@"去购物车"] clickAtIndex:^(NSInteger buttonIndex) {
                    if (buttonIndex == 1) {
                        [[HYCurrentVCmanager shareInstance].getCurrentVC HYPushViewController:[HYCartNewViewController new] animated:YES];
                    }
                }];
            }];
复制代码

直接在subview中获取了请求的逻辑, 当调用opeator的方法时会通过presenter中间件传递给viewmodel进行请求, 当请求成功后进行赋值操作刷新tableview, 最后回调自定义操作弹出了alert框, 由于都是主线程操作, 也不会有线程安全的问题.

//
//  Router.swift
//  RouterPatterm
//
//  Created by 双泉 朱 on 17/4/12.
//  Copyright © 2017年 Doubles_Z. All rights reserved.
//

import UIKit

class Router {
    static let shareRouter = Router()
    var params: [String : Any]?
    var routers: [String : Any]?
    fileprivate let map = ["J1" : "Controller"]
    
    func guardRouters(finishedCallback : @escaping () -> ()) {
        
        Http.requestData(.get, URLString: "http://localhost:3001/api/J1/getRouters") { (response) in
            guard let result = response as? [String : Any] else { return }
            guard let data:[String : Any] = result["data"] as? [String : Any] else { return }
            guard let routers:[String : Any] = data["routers"] as? [String : Any] else { return }
            self.routers = routers
            finishedCallback()
        }
    }
}

extension Router {
    
    func addParam(key: String, value: Any) {
        params?[key] = value
    }
    
    func clearParams() {
        params?.removeAll()
    }
    
    func push(_ path: String) {
        
        guardRouters {
            guard let state = self.routers?[path] as? String else { return }
            
            if state == "app" {
                guard let nativeController = NSClassFromString("RouterPatterm.\(self.map[path]!)") as? UIViewController.Type else { return }
                currentController?.navigationController?.pushViewController(nativeController.init(), animated: true)
            }
            
            if state == "web" {
                
                let host = "http://localhost:3000/"
                var query = ""
                let ref = "client=app"
                
                guard let params = self.params else { return }
                for (key, value) in params {
                    query += "\(key)=\(value)&"
                }
                
                self.clearParams()
                
                let webViewController = WebViewController("\(host)\(path)?\(query)\(ref)")
                currentController?.navigationController?.pushViewController(webViewController, animated: true)
            }
        }
    }
}

复制代码

由于线上真实项目的路由涉及公司业务, 这里就通过我的一个小demo进行讲解, router的本质就是一个映射, 首先router类是一个单例, 需要有添加和删除参数的接口, 以及可以区分是nativeh5的设计, 以及之前讲到的降级, 不用看一些三方库的设计多么酷炫, 究其本质还是对于"\(host)\(path)?\(query)\(ref)"进行逻辑拆分, 使用路由的好处是当使用cocoapod私有库组件化的时候, 完全避免了多控制器之间的耦合.

#import <objc/runtime.h>
@interface MySafeDictionary : NSObject
@end
static NSLock *kMySafeLock = nil;
static IMP kMySafeOriginalIMP = NULL;
static IMP kMySafeSwizzledIMP = NULL;
@implementation MySafeDictionary
+ (void)swizzlling {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        kMySafeLock = [[NSLock alloc] init];
    });
    
    [kMySafeLock lock];
    
    do {
        if (kMySafeOriginalIMP || kMySafeSwizzledIMP) break;
        
        Class originalClass = NSClassFromString(@"__NSDictionaryM");
        if (!originalClass) break;
        
        Class swizzledClass = [self class];
        SEL originalSelector = @selector(setObject:forKey:);
        SEL swizzledSelector = @selector(safe_setObject:forKey:);
        Method originalMethod = class_getInstanceMethod(originalClass, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(swizzledClass, swizzledSelector);
        if (!originalMethod || !swizzledMethod) break;
        
        IMP originalIMP = method_getImplementation(originalMethod);
        IMP swizzledIMP = method_getImplementation(swizzledMethod);
        const char *originalType = method_getTypeEncoding(originalMethod);
        const char *swizzledType = method_getTypeEncoding(swizzledMethod);
        
        kMySafeOriginalIMP = originalIMP;
        kMySafeSwizzledIMP = swizzledIMP;
        
        class_replaceMethod(originalClass,swizzledSelector,originalIMP,originalType);
        class_replaceMethod(originalClass,originalSelector,swizzledIMP,swizzledType);
    } while (NO);
    
    [kMySafeLock unlock];
}
+ (void)restore {
    [kMySafeLock lock];
    
    do {
        if (!kMySafeOriginalIMP || !kMySafeSwizzledIMP) break;
        
        Class originalClass = NSClassFromString(@"__NSDictionaryM");
        if (!originalClass) break;
        
        Method originalMethod = NULL;
        Method swizzledMethod = NULL;
        unsigned int outCount = 0;
        Method *methodList = class_copyMethodList(originalClass, &outCount);
        for (unsigned int idx=0; idx < outCount; idx++) {
            Method aMethod = methodList[idx];
            IMP aIMP = method_getImplementation(aMethod);
            if (aIMP == kMySafeSwizzledIMP) {
                originalMethod = aMethod;
            }
            else if (aIMP == kMySafeOriginalIMP) {
                swizzledMethod = aMethod;
            }
        }
        // 尽可能使用exchange,因为它是atomic的
        if (originalMethod && swizzledMethod) {
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
        else if (originalMethod) {
            method_setImplementation(originalMethod, kMySafeOriginalIMP);
        }
        else if (swizzledMethod) {
            method_setImplementation(swizzledMethod, kMySafeSwizzledIMP);
        }
        kMySafeOriginalIMP = NULL;
        kMySafeSwizzledIMP = NULL;
    } while (NO);
    
    [kMySafeLock unlock];
}
- (void)safe_setObject:(id)anObject forKey:(id<NSCopying>)aKey {
    if (anObject && aKey) {
        [self safe_setObject:anObject forKey:aKey];
    }
    else if (aKey) {
        [(NSMutableDictionary *)self removeObjectForKey:aKey];
    }
}
@end
复制代码

对于AOP这种, 我截取了一位大佬博客中的代码, 可以看到的是, 当多线程的时候, 我们只需要在hook的时候进行加锁和解锁保持线程安全就可以了, 当然也可以使用Aspects这个库来简化hook操作,毕竟AOP这块是要看业务逻辑的, 并不能一概而论.

//
//  UIViewController+hook.m
//  SQTemplate
//
//  Created by 朱双泉 on 23/11/2017.
//  Copyright © 2017 Doubles_Z. All rights reserved.
//

#import "UIViewController+hook.h"
#import "CurrentViewController.h"
#import <Aspects.h>

@implementation UIViewController (hook)

+ (void)load {
    
    [UIViewController aspect_hookSelector:@selector(viewWillAppear:) withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> aspectInfo, BOOL animated) {
        kCurrentViewController = aspectInfo.instance;
    } error:NULL];
    
}
    
@end

复制代码

最常用的AOP就是hook生命周期来获取当前控制器, 通过一个全局变量可以全方位获取, 以上代码就是通过AOP模式进行面向切片编程, 避免需要继承一个基类而带来的强耦合.

生成工具

《iOS 移动端架构初探心得》

所谓工欲善其事必先利其器, 上面的架构设计虽好, 但要让其他同学模仿写法实在是太麻烦了, 也会导致抵触情绪, 但为了我们之前代码规范的目标, 架构设计的执行也是志在必行, 这时我们就需要进行代码自动生成的工作.

所谓的代码生成究其本质就是字符串替换, 就是将可变的字符串替换模板中的标记, ES6中的模板字符串${变量}也是这个道理.

我们来看一下模板:

//
//  InterfaceTemplate.h
//  SQTemplate
//
//  Created by 双泉 朱 on 17/5/5.
//  Copyright © 2017年 Doubles_Z. All rights reserved.
//

#import <UIKit/UIKit.h>

@protocol <#Root#><#Unit#>ModelInterface <NSObject>

<#ModelInterface#>
@end

@protocol <#Root#><#Unit#>ViewModelInterface <NSObject>

@optional
@property (nonatomic,strong) id<<#Root#><#Unit#>ModelInterface> model;

@optional
- (void)initializeWithModel:(id<<#Root#><#Unit#>ModelInterface>)model <#InitializeInterface#>completion:(void(^)())completion;
<#ViewModelInterface#>
@end

@protocol <#Root#><#Unit#>ViewInterface <NSObject>

@property (nonatomic,weak) id<<#Root#><#Unit#>ViewModelInterface> <#unit#>ViewModel;
@property (nonatomic,weak) id<<#Root#><#Unit#>ViewModelInterface> <#unit#>Operator;

@end

复制代码

并进行读写操作:

//
//  SQFileParser.m
//  SQBuilder
//
//  Created by 朱双泉 on 17/08/2017.
//  Copyright © 2017 Castie!. All rights reserved.
//

#import "SQFileParser.h"

@implementation SQFileParser

+ (NSDictionary *)parser_plist_r {
    
    NSBundle * bundle = [NSBundle bundleWithPath:[[NSBundle mainBundle] pathForResource:@"builder.bundle" ofType:nil]];
    NSDictionary * config = [NSDictionary dictionaryWithContentsOfFile:[bundle pathForResource:@"config/config.plist" ofType:nil]];
    NSMutableDictionary * plist = [NSDictionary dictionaryWithContentsOfFile:[bundle pathForResource:[NSString stringWithFormat:@"config/%@.plist",config[@"builderSource"]] ofType:nil]].mutableCopy;
    [config enumerateKeysAndObjectsUsingBlock:^(id  _Nonnull key, id  _Nonnull obj, BOOL * _Nonnull stop) {
        [plist setObject:obj forKey:key];
    }];
    return plist;
}

+ (void)parser_rw:(NSString *)path code:(NSString *)code filename:(NSString *)filename header:(NSString *)header parameter:(NSMutableArray *)parameter {

    NSString * arch = [[filename componentsSeparatedByString:@"."]firstObject];
    NSString * suffix = [[filename componentsSeparatedByString:@"."]lastObject];
    NSString * filename_r = [NSString stringWithFormat:@"%@Template.%@", arch,suffix];
    NSString * filename_w = [NSString stringWithFormat:@"%@/%@%@.%@", path,header,arch,suffix];
    NSString * template =  [SQFileParser parser_r:filename_r code:

];
[[SQFileParser replaceThougth:template parameter:parameter] writeToFile:filename_w atomically:YES encoding:NSUTF8StringEncoding error:nil];
}

+ (NSString *)parser_r:(NSString *)filename code:(NSString *)code {

NSBundle * bundle = [NSBundle bundleWithPath:[[NSBundle mainBundle] pathForResource:@"builder.bundle" ofType:nil]];
return [NSMutableString stringWithContentsOfFile:[bundle pathForResource:[NSString stringWithFormat:@"template/%@/%@", code, filename] ofType:nil] encoding:NSUTF8StringEncoding error:nil];
}

static NSString * code;

+ (NSString *)replaceThougth:(NSString *)templete parameter:(NSMutableArray *)parameter {

__block NSString * temp = templete;
[[parameter firstObject] enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull key, NSString * _Nonnull obj, BOOL * _Nonnull stop) {
temp = [templete stringByReplacingOccurrencesOfString:key withString:obj];
}];
[parameter removeObjectAtIndex:0];
if (parameter.count) {
[SQFileParser replaceThougth:temp parameter:parameter];
} else {
code = temp;
}
return code;
}

@end

复制代码

关于生成工具的开发之前有一篇详细论述了, 这里就不过多赘述了. 点击跳转

《iOS 移动端架构初探心得》

我司已经通过读取表格进行生成iOSAndroid两端的代码, 保持两端逻辑相同, 但由于表格的设计与公司业务及个人习惯相关, 这部分代码不予公开, 请谅解.

《iOS 移动端架构初探心得》

可以看到生成的文件通过文件夹的形式存在, 只需要将文件夹导入项目中即可立即获得之前所设计的架构.

《iOS 移动端架构初探心得》

最后我将架构demo以及工具放在了github上, 关于上面router降级这块可以点击这里下载

如何使用

coderZsq.project.oc

《iOS 移动端架构初探心得》

git clone | download 后打开SQTemplate工作空间, 就能够看到两个项目.

SQBuilder是生成工具的项目.

《iOS 移动端架构初探心得》

配置生成工具的接口文档字段后, 点击Run即可生成代码, 显示在桌面.

SQTemplate是模板生成后在项目中使用的demo.

《iOS 移动端架构初探心得》

可以看到上述所有的架构设计模式的简单引用, 点击秒速五厘米的图片, 可以通过路由跳转到下一页面.

图片下面的数据是通过koa服务返回的数据, 下载coderZsq.target.swift中的RouterPattern中的/server/RouterPattern, cd进去后执行npm start, 即可开启服务. 当然你需要Node, 环境和webpack的全局环境.

如果你需要查看了解Router部分, 可以通过coderZsq.target.swift这个项目进行学习交流.

  • app/RouterPattern 直接双击打开项目即可
  • web/RouterPattern cd 进去 npm run dev
  • server/RouterPattern cd 进去 npm start

具体可以查看Hybird 搭建客户端实时降级架构系列.

注意!!! 使用git上传时会通过.gitignore忽略上传文件, 所以pull下来记得pod install | npm install, 记得pod install | npm install记得pod install | npm install, 重要的事情说三遍!!

以上就是我对于移动端架构初探的心得, 期待与各位大佬进行交流.

About:

《iOS 移动端架构初探心得》

🌟 项目源码 请点这里🌟 >>> 喜欢的朋友请点喜欢 >>> 下载源码的同学请送下小星星 >>> 有闲钱的壕们可以进行打赏 >>> 小弟会尽快推出更好的文章和大家分享 >>> 你的激励就是我的动力!!

    原文作者:算法小白
    原文地址: https://juejin.im/post/5a183f38f265da432528fefc
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞