本文作為這一系列的收尾總結, 詳細敘述了這個架構工具的設計思路以及一步步的優化, 在此也分享與你, 完整
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:[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
降級這塊可以點選這裡下載
如何使用
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
, 重要的事情說三遍!!
以上就是我對於移動端架構初探的心得, 期待與各位大佬進行交流.