本文作为这一系列的收尾总结, 详细叙述了这个架构工具的设计思路以及一步步的优化, 在此也分享与你, 完整
可查阅github
- keynote
本文作为以上文章系列的总结, 如何一步一步进行思考总结, 如何开发出适合自己的通用架构设计.
对于架构, 移动端常见的架构设计包括
,
- MVC
,
- MVVM
等, 上图简要的说明了各种常见的架构之间的交互及数据传递方式.
- MVP
对于
,
- MVC
,
- MVVM
这三种架构设计模式, 相信大家一定了然于心, 相关的文章也是多如繁星, 对于这些常用架构, 每个人都肯定有每个人的理解, 但这样会导致一个问题, 就是极大的自由度导致了没有代码规范, 对于移动端或者前端及后端来说, 其本质工作就是数据层和展示层的交互, 如何将数据正确安全高效的传输到展示层.
- MVP
这里的数据层从整个项目来说, 可以说是后端, 也就是服务端, 对于服务端开发的流程就是从数据库获取数据并将数据进行各种逻辑过滤作为响应返回给前端用于展示层展示, 对于java为例, 我们普通的项目就会分为
,
- controller
,
- service
,
- dao
,
- pojo
,
- vo
等层级设计, 就会将不同功能进行抽象, 使得代码更容易维护.
- bo
而作为展示层的前端, 也就是客户端, 其实移动端在我感觉其实也是前端的一个分支, 而前端的架构通常为组件化设计, 每一个功能
对应一个组件, 而整个页面可以通过多个组件分离进行维护, 很高效的将业务代码和视图进行分离, 使得代码更有规范及易维护.
- view
而对于移动端, 为什么要在
中写那么多不知所云的代码? 为什么一个控制器能超过1k行? 为什么
- controller
的逻辑回调代理要写在
- 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:[code lowercaseString]];
- [[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
coderZsq.project.oc
|
- git clone
后打开
- download
工作空间, 就能够看到两个项目.
- SQTemplate
是生成工具的项目.
- SQBuilder
配置生成工具的接口文档字段后, 点击
即可生成代码, 显示在桌面.
- Run
是模板生成后在项目中使用的
- SQTemplate
.
- demo
可以看到上述所有的架构设计模式的简单引用, 点击秒速五厘米的图片, 可以通过路由跳转到下一页面.
图片下面的数据是通过
服务返回的数据, 下载coderZsq.target.swift中的
- koa
中的
- RouterPattern
,
- /server/RouterPattern
进去后执行
- cd
, 即可开启服务. 当然你需要
- npm start
, 环境和
- Node
的全局环境.
- webpack
如果你需要查看了解
部分, 可以通过coderZsq.target.swift这个项目进行学习交流.
- Router
直接双击打开项目即可
- 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
以上就是我对于移动端架构初探的心得, 期待与各位大佬进行交流.
来源: https://juejin.im/post/5a183f38f265da432528fefc