本文作为这一系列的收尾总结, 详细叙述了这个架构工具的设计思路以及一步步的优化, 在此也分享与你, 完整
keynote
可查阅github
参考链接:
- Hybird 搭建零耦合架构从MVC开始
- Hybird 搭建后端Koa.js并过度到MVVM
- Hybird 搭建前端Vue.js并升级至MVP
- Hybird 搭建路由Router实现组件化
- Hybird 搭建客户端实时降级架构
- iOS 执行.py脚本生成解耦架构
- iOS 执行.py脚本生成UI层结构
- iOS 移动端面向文档开发
- iOS 移动端生成工具开发
本文作为以上文章系列的总结, 如何一步一步进行思考总结, 如何开发出适合自己的通用架构设计.
设计思路
对于架构, 移动端常见的架构设计包括MVC
, MVVM
, MVP
等, 上图简要的说明了各种常见的架构之间的交互及数据传递方式.
对于MVC
, MVVM
, MVP
这三种架构设计模式, 相信大家一定了然于心, 相关的文章也是多如繁星, 对于这些常用架构, 每个人都肯定有每个人的理解, 但这样会导致一个问题, 就是极大的自由度导致了没有代码规范, 对于移动端或者前端及后端来说, 其本质工作就是数据层和展示层的交互, 如何将数据正确安全高效的传输到展示层.
这里的数据层从整个项目来说, 可以说是后端, 也就是服务端, 对于服务端开发的流程就是从数据库获取数据并将数据进行各种逻辑过滤作为响应返回给前端用于展示层展示, 对于java为例, 我们普通的项目就会分为controller
, service
, dao
, pojo
, vo
, bo
等层级设计, 就会将不同功能进行抽象, 使得代码更容易维护.
而作为展示层的前端, 也就是客户端, 其实移动端在我感觉其实也是前端的一个分支, 而前端的架构通常为组件化设计, 每一个功能view
对应一个组件, 而整个页面可以通过多个组件分离进行维护, 很高效的将业务代码和视图进行分离, 使得代码更有规范及易维护.
而对于移动端, 为什么要在controller
中写那么多不知所云的代码? 为什么一个控制器能超过1k行? 为什么view
的逻辑回调代理要写在controller
中? 为什么控制器之间的参数传递的耦合性那么强? 为什么网络请求的方法随处可见? 为什么我们不能够像前后端那样有条理的控制我们的代码? 而让其像脱缰的野马难以驾驭呢?
为了解决这些问题, 我们需要考虑一些架构设计模式, 最先想到的就是以controller
为中心的抽象, 将控制器的功能抽象到该具体负责的模块, 从图上可以看到, controller
维护了presenter
, viewmodel
, view
, 而viewmodel
又维护了model
, 其中的model
也可以说就是javabean
完全的纯数据结构, viewmodel
是model
的上一层, 用于操作对应的数据, 可以看到controller
将代码下发至下面三层, 使得各个层级各司其职.
刚才是站在controller
的视角上来看的, 对于数据的传输, 这次我们站在presenter
的视角上来看, 这个设计就是将viewmodel
作为传递对象通过presenter
这个中间件传输至view
层, 这样view层不仅可以拿到数据, 也可以对数据进行操作, 掌控性有所提高.
刚才我们站在了controller
和presenter
视角上分析了架构设计的思路, 但这样各个层级的耦合会越来越大, 从而导致项目代码无法分割, 这时想到了后端controller
和service
之间通过接口进行交互来降低耦合, 我们是不是也可以参考这种方案通过一个protocol
文档文件来降低各个层级之间的耦合呢, 如图所示, 将除了controller
之外的其他层级进行解耦. 进行高度抽象.
上面我们解决了各个层级之间的耦合, 但我们怎么解决控制器之间的耦合呢? 答案是router
, 我们站在路由的视角上看, 各大控制器都是独立存在的个体, 彼此之间的交互通过路由的映射进行交互, 这样我们就能够去除各大控制器之间的耦合了. 路由这个思路最先也是在前端的架构框架中看到的, 后来就有了cocoapods
私有库组件化这种集成化的解决方案, 通过路由映射能够很好的做到模块分离, 更可以做到页面降级, 所谓的页面降级就是指不仅路由可以和native
进行交互, 也可以和h5
进行交互, 当native
和h5
是一套业务逻辑的时候, native
不慎出现bug
我们可以请求后端接口修改数据库将页面直接降级至h5
页面而不用重新打包等待苹果审核及使用热修复工具带来了时间消耗. 能够第一时间解决问题.
解决了上述问题, 我们就只剩下页面的问题了, 对于现在的iOSer来说, 写页面几乎是日常工作的绝大部分, 但是写页面, 写业务, 当逻辑复杂的时候也会产生一系列不易维护的问题, 这时候我们就可以使用类似redux
这种状态机的模式, 将业务逻辑拆分出不同复杂的状态, 当变量改变的时候, 触发不同的状态, 这样就能够有效的管理我们的页面逻辑. 推荐可以看看react
和redux
的思路, 对这块也会有更好的掌握.
设计思路的总结就是, 通过高度抽象进行分层, 通过接口文档进行项目层级解耦, 通过路由进行组件化及降级, 通过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
中, 我们将viewmodel
和view
, 也就是上述的数据层和展示层传输到 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
,根据之前的设计图, 我们将其区分为两个模块, tableview
的headerview
和tableviewcell
, 其中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
类是一个单例, 需要有添加和删除参数的接口, 以及可以区分是native
和h5
的设计, 以及之前讲到的降级, 不用看一些三方库的设计多么酷炫, 究其本质还是对于"\(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
模式进行面向切片编程, 避免需要继承一个基类而带来的强耦合.
生成工具
所谓工欲善其事必先利其器, 上面的架构设计虽好, 但要让其他同学模仿写法实在是太麻烦了, 也会导致抵触情绪, 但为了我们之前代码规范的目标, 架构设计的执行也是志在必行, 这时我们就需要进行代码自动生成的工作.
所谓的代码生成究其本质就是字符串替换, 就是将可变的字符串替换模板中的标记, 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
和Android
两端的代码, 保持两端逻辑相同, 但由于表格的设计与公司业务及个人习惯相关, 这部分代码不予公开, 请谅解.可以看到生成的文件通过文件夹的形式存在, 只需要将文件夹导入项目中即可立即获得之前所设计的架构.
最后我将
架构demo
以及工具
放在了github上, 关于上面router
降级这块可以点击这里下载如何使用
git clone
|download
后打开SQTemplate
工作空间, 就能够看到两个项目.
SQBuilder
是生成工具的项目.配置生成工具的接口文档字段后, 点击
Run
即可生成代码, 显示在桌面.
SQTemplate
是模板生成后在项目中使用的demo
.可以看到上述所有的架构设计模式的简单引用, 点击秒速五厘米的图片, 可以通过路由跳转到下一页面.
图片下面的数据是通过
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:
🌟 项目源码 请点这里🌟 >>> 喜欢的朋友请点喜欢 >>> 下载源码的同学请送下小星星 >>> 有闲钱的壕们可以进行打赏 >>> 小弟会尽快推出更好的文章和大家分享 >>> 你的激励就是我的动力!!