架构设计文档
[TOC]
1、技术选型
1.1 开发语言
学习成本 | 开发效率 | 开源支持 | 稳定性 | 前景 | |
---|---|---|---|---|---|
Objective- C | 2007年发布Objective-C 2.0,拥有大量的网络学习资源。 | 语法冗余,面向对象编程,易维护,沟通成本低 | 拥有众多成熟第三方开源库 | API稳定 | 已经趋于稳定,很少变化 |
Swift | 2014年发布,开源,入门容易精通难 | 语法简洁,运行效率高,支持面向对象编程,面向协议编程,声明式编程,函数式编程,泛型编程 | 成熟第三方库明显少于Objective- C | API不会再次出现大规模变化,不向前兼容 | 新兴语言,结合了众多现代语言的优点,Apple推荐语言 |
综上所述,易维护,沟通成本低,我们选用已经成熟 Objective-C 语言作为首选开发语言。
1.2 代码仓库
这里有一篇很好的日常实践对比why-git。总结一下就是Git对比SVN
- 方便的分支切换、版本回滚
- 更好地规范化版本日志
- 更多开源免费的配套设施(如Gitlab)
- 适合实行 Code Review
- 规范化开发流程 Git-Flow
如果你还不熟悉Git的相关操作,可以在这里学习。
1.3 开源项目管理
开源库的管理决定使用CocoaPods。如果没有安装的同学可以按照如下方法安装。
# 列出gem源 gem sources -l
# 移除 ruby 源,因为要翻墙访问 gem sources --remove https://rubygems.org/
# 添加 taobao 源 gem sources -a https://ruby.taobao.org/
# 可以不更新 gem update --system
# 安装 cocoapods sudo gem install cocoapods
# 如果上述安装不能成功,因为是mac系统在10.10后不允许修改 /usr/bin 目录,需要改变安装目录。
# sudo gem install -n /usr/local/bin cocoapods
目前使用 CocoaPods 集成的一些开源仓库
仓库名称 | 用途 | 选型理由 | |
---|---|---|---|
AFNetworking | 网络请求 | AFNetworking是封装的NSURLSession的网络请求,由五个模块组成:分别由网络通信(核心),网络通讯安全策略,网络状态监听,网络通信信息序列化和反序列化,UIKit的扩展库 | |
Reachability | 网络状态判断 | Reachability类是Apple官方出的判断当前网络状况的工具类,这个库一直在随着iOS的版本在更新,目前iOS10对应的最新版本是5.0 | |
YTKNetwork | 对AFNetworking的封装 | YTKNetwork 的基本的思想是把每一个网络请求封装成对象。所以使用 YTKNetwork,你的每一个请求都需要继承 YTKRequest 类,通过覆盖父类的一些方法来构造指定的网络请求 | |
SDWebImage | 图片异步加载和缓存 | SDWebImage具有缓存支持的异步映像下载程序。并添加了像UI元素分类类UIImageView、UIButton、MKAnnotationView,可以直接为这些UI元素添加图片。 | |
Masonry | 布局框架 | 是一个轻量级的布局框架,拥有自己的描述语法,采用更优雅的链式语法封装自动布局,简洁明了并具有更高的可读性。 | |
IQKeyboardManager | 键盘管理工具 | 解决弹起键盘遮盖输入框的问题 | |
YYKit | iOS开发组件 | 以下是项目中常用到的几个组件 | |
YYCategories | 常用分类 | 为Foundation and UIKit 提供许多有用的分类 |
|
YYText | 富文本 | 强大的iOS富文本组件 | |
YYModel | 字典转模型 | 高性能的字典转模型的框架 | |
YYImage | 图片加载 | 功能强大的图像框架 | |
YYWebImage | 图片加载 | 异步图片加载框架 | |
YYCache | 缓存框架 | 高性能 iOS 缓存框架,提供内存缓存 和磁盘缓存
|
|
CHTCollectionViewWaterfallLayout | 瀑布流 | ||
UICollectionViewLeftAlignedLayout | 使collectionView左对齐 | UICollectionViewLeftAlignedLayout是第三方的左对齐布局管理类,其继承自UICollectionViewFlowLayout,使用其可以方便的进行左对齐的瀑布流界面布局。 | |
UITableView+FDTemplateLayoutCell | cell高度 | 自动计算cell高度并缓存cell高度 | |
TABAnimated | 空数据填充 | tableView骨架屏 | |
FDFullscreenPopGesture | 全屏左滑pop手势 | 提供全屏手势返回功能 | |
FMDB | SQLite数据库 | 提供线程安全的Sqlite数据库存取操作 | |
MJExtension | 字典转模型框架 | 一套字典和模型之间互相转换的超轻量级框架 | |
MJRefresh | 下拉刷新和上拉加载控件 | 用于为应用添加常用的上拉加载更多与下拉刷新效果,适用 UIScrollView、UITableView、UICollectionView、UIWebView | |
pop | 动画过渡 | 动画引擎,用于动画过渡。可以参照popping | |
DZNEmptyDataSet | 空白页 | UITableView/UICollectionView数据内容为空时展示的空白页 | |
MBProgressHUD | 蒙版 | 加载loading以及显示提示蒙版的HUD | |
SVProgressHUD | 蒙版 | 加载loading | |
JPFPSStatus | 帧数检测 | 通过FPS(Frames Per Second) 每秒传输帧数的高低来检查列表滚动的流畅度 |
|
TZImagePickerController | 图片选择器 | 总体上跟微信的照片选择器界面和功能都差不多一样 | |
PNChart | 各种图表的展示 | PNChart 是一个强大的带动画的图表库 | |
Charts | 图表 | Charts是一个轻量级的简易图表,主要为DataV大屏数据展示组件库 提供图表支持,在该场景下不考虑图表交互,仅需展示效果,因此插件不提供交互及复杂功能。插件配置项参考eCharts,具有相关经验则极易上手使用 | |
MMDrawerController | 侧边栏 | 侧边栏的 Controller,实现抽屉效果 | |
RESideMenu | 侧边栏 | QQ 侧边栏的效果 | |
JSQMessagesViewController | 推送 | 聊天对话 | |
CYLTabBarController | 低耦合集成TabBarController | CYLTabBarController 是一个自定义的TabBarController, 集成非常简单 | |
TTTAttributedLabel | 富文本的Label | TTTAttributedLabel 继承于 UILabel,所以具有 UILabel 所有的属性和方法。通过CoreText绘制富文本 | |
JVFloatLabeledTextField | 特殊效果的textField | 浮动文字的输入框 | |
SDCycleScrollView | 循环轮播 | 采用UICollectionView的重用机制和循环滚动的方式实现图片的轮播滚动 | |
iCarousel | 轮播 | iCarousel是一个类,它继承于UIView,用于简化实现各种类型的旋转木马(分页滚动视图)iPhone、iPad和Mac OS。iCarousel实现一些常见的影响如圆柱、平面式的旋转木马 | |
PDTSimpleCalendar | 日历 | PDTSimpleCalendar是一个简单的日历/日期选择器组件,基于UICollectionView。 | |
LBXScan | 二维码 | 二维码相关,ZXing、ZBar、iOS系统AVFoundation扫码封装,可自行选择 | |
FLEX | 强大的调试库 | FLEX是一个需要注入式的一种框架,从描述来看,功能非常多。主要来讲的话能够对正在运行的应用进行样式的修改和控件的读取 | |
UICKeyChainStore | 存放用户账号密码组件 | 在app开发过程中通常会涉及到敏感信息的保存,ios给我们提供了keychain来将数据保存到钥匙串中 | |
XHLaunchAd | 广告页 | 1.支持全屏/半屏广告.2.支持静态/动态广告.3.兼容iPhone和iPad.4.支持广告点击事件5.自带图片下载,缓存功能.6.支持设置未检测到广告数据,启动页停留时间7.无依赖其他第三方框架 | |
MLeaksFinder | 内存泄漏检测 | 支持自动检测代码中存在的内存泄漏问题,只在 Debug 模式下有效 | |
OpenShare | 社交分享库 | 无需引入众多第三方社交文件即可实现社交分享功能,实际大小几KB左右,降低包体积 | |
NJKWebViewProgress | Web加载进度显示 | 可对UIWebView提供真实的加载进度显示 | |
BlocksKit | 事件回调 | 针对界面事件响应行为提供便捷的Block方式回调处理,方便集中处理逻辑 | |
FXBlurView | 视图模糊库 | 提供实时的模糊效果 | |
CocoaLumberjack | 日志库 | 提供日记分级别记录,日志文件存储,日志格式化输出等 |
1.4 可持续集成方案
可持续集成选用业界成熟的Jenkins作为可持续集成工具.
Jenkins 是一个开源项目,提供了一种易于使用的持续集成系统,使开发者从繁杂的集成中解脱出来,专注于更为重要的业务逻辑
实现上。同时 Jenkins 能实施监控集成中存在的错误,提供详细的日志文件和提醒功能,还能用图表的形式形象地展示项目构建的
趋势和稳定性.
- 支持插件扩展
- 自定义执行脚本
- 自动构建
- 自动部署
- 错误反馈
2、系统架构
2.1 总体设计
图片.png(evernotecid://0C306E66-8F16-4830-9306-563B0445D83E/appyinxiangcom/29976633/ENResource/p6)
APP总体分为四大块
- 基础组件:提供网络请求,数据缓存,日志等基础服务功能,同业务逻辑分离.
- 中转器:提供组件间统一调用功能.
- 业务组件:各种不同业务组件共同组成的集合,业务组件间相互独立.
2.2 组件化设计
图片.pngRouter就像是个调度中心,各个模块通过路由调度其他模块,模块之间不需要相互引用,调度方式更加统一,更加自由,能够实现解耦的作用,同时也为之后的组件化开发提供了基础。
JLRoutes 的问题主要在于查找 URL 的实现不够高效,通过遍历而不是匹配。还有就是功能偏多。
HHRouter 的 URL 查找是基于匹配,所以会更高效,MGJRouter 也是采用的这种方法,但它跟 ViewController 绑定地过于紧密,一定程度上降低了灵活性。
于是就有了MGJRouter
。
2.2.1 MGJRouter最基本的使用
[MGJRouter registerURLPattern:@"mgj://foo/bar" toHandler:^(NSDictionary *routerParameters) {
NSLog(@"routerParameterUserInfo:%@", routerParameters[MGJRouterParameterUserInfo]);
}];
[MGJRouter openURL:@"mgj://foo/bar"];
2.2.2 当匹配到 URL 后,routerParameters 会自带几个 key
extern NSString *const MGJRouterParameterURL;
extern NSString *const MGJRouterParameterCompletion;
extern NSString *const MGJRouterParameterUserInfo;
2.2.3 处理中文也没有问题
[MGJRouter registerURLPattern:@"mgj://category/家居" toHandler:^(NSDictionary *routerParameters) {
NSLog(@"routerParameters:%@", routerParameters);
}];
[MGJRouter openURL:@"mgj://category/家居"];
2.2.4 Open时,可以传一些userinfo过去
[MGJRouter registerURLPattern:@"mgj://category/travel" toHandler:^(NSDictionary *routerParameters) {
NSLog(@"routerParameters[MGJRouterParameterUserInfo]:%@", routerParameters[MGJRouterParameterUserInfo]);
// @{@"user_id": @1900}
}];
[MGJRouter openURL:@"mgj://category/travel" withUserInfo:@{@"user_id": @1900} completion:nil];
2.2.5 如果有可变参数(包括 URL Query Parameter)会被自动解析
[MGJRouter registerURLPattern:@"mgj://search/:query" toHandler:^(NSDictionary *routerParameters) {
NSLog(@"routerParameters[query]:%@", routerParameters[@"query"]); // bicycle
NSLog(@"routerParameters[color]:%@", routerParameters[@"color"]); // red
}];
[MGJRouter openURL:@"mgj://search/bicycle?color=red"];
2.2.6 定义一个全局的 URL Pattern 作为 Fallback
[MGJRouter registerURLPattern:@"mgj://" toHandler:^(NSDictionary *routerParameters) {
NSLog(@"没有人处理该 URL,就只能 fallback 到这里了");
}];
[MGJRouter openURL:@"mgj://search/travel/china?has_travelled=0"];
2.2.7 当 Open 结束时,执行 Completion Block
[MGJRouter registerURLPattern:@"mgj://detail" toHandler:^(NSDictionary *routerParameters) {
NSLog(@"匹配到了 url, 一会会执行 Completion Block");
// 模拟 push 一个 VC
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.25 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
void (^completion)() = routerParameters[MGJRouterParameterCompletion];
if (completion) {
completion();
}
});
}];
[MGJRouter openURL:@"mgj://detail" withUserInfo:nil completion:^{
[self appendLog:@"Open 结束,我是 Completion Block"];
}];
2.2.8 生成 URL
URL 的处理一不小心,就容易散落在项目的各个角落,不容易管理。比如注册时的 pattern 是 mgj://beauty/:id,然后 open 时就是 mgj://beauty/123,这样到时候 url 有改动,处理起来就会很麻烦,不好统一管理。
所以 MGJRouter 提供了一个类方法来处理这个问题。
+ (NSString *)generateURLWithPattern:(NSString *)pattern parameters:(NSArray *)parameters;
使用方式
#define TEMPLATE_URL @"mgj://search/:keyword"
[MGJRouter registerURLPattern:TEMPLATE_URL toHandler:^(NSDictionary *routerParameters) {
NSLog(@"routerParameters[keyword]:%@", routerParameters[@"keyword"]); // Hangzhou
}];
[MGJRouter openURL:[MGJRouter generateURLWithPattern:TEMPLATE_URL parameters:@[@"Hangzhou"]]];
}
这样就可以在一个地方定义所有的 URL Pattern,使用时,用这个方法生成 URL 就行了。
2.3 基础服务设计
基础功能服务包括缓存,数据库,网络请求,下载组件等,编写基础服务时,需要遵循如下几个原则:
- 引用第三方库必须提供上层封装
- 对外提供接口尽量简明,并做好对应功能注释
- 基础服务应该是独立的,尽量减少对外耦合
2.4 业务层次设计
架构
- 界面层 (MVC、MVP、MVVM)
- 业务层
- 网络层
- 本地数据层
图片.png其中,界面层的架构模式又分为好多种,从最开始的MVC模式,演化到MVP,然后到现在的MVVM模式,在不断的演化过程中核心思想归根结底还是:降低各组件之间的耦合度,使得数据的流向更加清晰明了。
MVVM(Model、View、ViewModel)
MVVM也是对Controller进行瘦身的一种策略,将业务逻辑放到ViewModel中去处理,比如发送网络请求,加载网络数据进行字典转模型操作等等。
图片.png
简单的示例代码如下
- HomeVC类:负责创建和显示视图
// HomeVC.m
@interface HomeVC ()
@property (nonatomic, strong) UITableView *homeTableView;
@property (nonatomic, strong) HomeVM *homeVM;
@end
@implementation HomeVC
#pragma mark - <Life Cycle>
- (void)viewDidLoad {
[super viewDidLoad];
// RAC网络请求
@weakify(self);
[[self.homeVM.homeCommand execute:nil] subscribeNext:^(id _Nullable x) {
@strongify(self);
NSLog(@"currentThread = %@", [NSThread currentThread]);
//NSLog(@"x = %@", x);
[self.homeTableView reloadData];
}];
// ViewModel数据绑定到view上
[self.homeVM bindVMWithView:self.homeTableView];
// 订阅按钮事件
[self.homeVM.cellButtonEventSignal subscribeNext:^(UIButton *x) {
NSLog(@"x = %@", [x titleForState:UIControlStateNormal]);
}];
}
- (void)dealloc {
NSLog(@"%s", __func__);
}
#pragma mark - <Override>
- (void)createSubViews {
[super createSubViews];
[self.view addSubview:self.homeTableView];
}
- (void)addConstraints {
[super addConstraints];
[self.homeTableView mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.mas_equalTo(self.view.mas_safeAreaLayoutGuideTop);
make.left.right.bottom.mas_equalTo(self.view);
}];
}
#pragma mark - <getter/setter>
- (HomeVM *)homeVM {
if (_homeVM == nil) {
_homeVM = [[HomeVM alloc] init];
}
return _homeVM;
}
- (UITableView *)homeTableView {
if (_homeTableView == nil) {
_homeTableView = [[UITableView alloc] init];
_homeTableView.rowHeight = 120;
_homeTableView.tableFooterView = [[UIView alloc] init];
}
return _homeTableView;
}
@end
- HomeVM类:负责homeTableView的显示和业务逻辑处理
// HomeVM.h
@interface HomeVM : NSObject<ViewModelProtocol> {
RACCommand *_homeCommand;
}
/// 网络请求RACCommand
@property (nonatomic, strong, readonly) RACCommand *homeCommand;
/// 按钮点击信号
@property (nonatomic, strong, readonly) RACReplaySubject *cellButtonEventSignal;
@end
// HomeVM.m
static NSString *HomeTableCellID = @"HomeTableCellID";
@interface HomeVM ()<UITableViewDataSource, UITableViewDelegate>
@property (nonatomic, copy) NSArray *homeCellVMs;
@property (nonatomic, strong) HomeTableCellVM *cellVM;
@end
@implementation HomeVM {
RACReplaySubject *_cellButtonEventSignal;
}
- (RACCommand *)homeCommand {
if (_homeCommand == nil) {
_homeCommand = [[RACCommand alloc] initWithSignalBlock:^RACSignal * _Nonnull(id _Nullable input) {
// RACCommand的block
return [RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber> _Nonnull subscriber) {
// RACSignal的block
NSString *homeURL = [XMGNetworkManager urlWithHome];
NSDictionary *homeParameters = [XMGNetworkManager paramWithHome];
[XMGHttpManager POST:homeURL parameters:homeParameters progress:nil success:^(NSURLSessionDataTask *task, id responseObject) {
NSLog(@"responseObject = %@", responseObject);
int code = [[responseObject objectForKey:@"code"] intValue];
if (code == 0) {
// 推荐列表
NSDictionary *recommends = [[responseObject objectForKey:@"result"] objectForKey:@"recommends"];
NSArray *courses = [recommends objectForKey:@"courses"];
courses = [[courses.rac_sequence map:^id _Nullable(id _Nullable value) {
HomeModel *model = [HomeModel yy_modelWithJSON:value];
HomeTableCellVM *cellVM = [[HomeTableCellVM alloc] init];
cellVM.model = model;
return cellVM;
}] array];
// dic->model完成
self.homeCellVMs = courses;
[subscriber sendNext:courses];
[subscriber sendCompleted];
}
} failure:^(NSURLSessionDataTask *task, NSError *error) {
[subscriber sendError:error];
}];
/*
[LFNetManager requestWithAFURL:homeURL httpMethod:Request_Type_POST params:homeParameters success:^(id result, int code) {
if (code == 0) {
// 推荐列表
NSDictionary *recommends = [[result objectForKey:@"result"] objectForKey:@"recommends"];
NSArray *courses = [recommends objectForKey:@"courses"];
courses = [[courses.rac_sequence map:^id _Nullable(id _Nullable value) {
HomeModel *model = [HomeModel yy_modelWithJSON:value];
return model;
}] array];
// dic->model完成
[subscriber sendNext:courses];
[subscriber sendCompleted];
}
} failure:^(NSError *err) {
[subscriber sendError:err];
}];
*/
// [HomeServer loadHomeList:homeParameters success:^(id _Nonnull response, int code) {
//
// } failure:^(NSError * _Nonnull error) {
//
// }];
return nil;
}];
}];
}
return _homeCommand;
}
#pragma mark - <ViewModelProtocol>
- (void)bindVMWithView:(UIView *)view {
UITableView *tableView = (UITableView *)view;
tableView.dataSource = self;
tableView.delegate = self;
[tableView registerClass:[HomeTableCell class] forCellReuseIdentifier:HomeTableCellID];
}
#pragma mark - <UITableViewDataSource/UITableViewDelegate>
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.homeCellVMs.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
HomeTableCell *cell = [tableView dequeueReusableCellWithIdentifier:HomeTableCellID forIndexPath:indexPath];
HomeTableCellVM *cellVM = self.homeCellVMs[indexPath.row];
[cellVM bindVMWithView:cell];
[cell.button addTarget:self action:@selector(cellButtonAction:) forControlEvents:UIControlEventTouchUpInside];
return cell;
}
#pragma mark - <Event>
- (void)cellButtonAction:(UIButton *)sender {
[self.cellButtonEventSignal sendNext:sender];
//[self.cellButtonEventSignal sendCompleted];
}
#pragma mark - <getter/setter>
- (RACReplaySubject *)cellButtonEventSignal {
if (_cellButtonEventSignal == nil) {
_cellButtonEventSignal = [RACReplaySubject subject];
}
return _cellButtonEventSignal;
}
@end
- HomeTableCellVM类:负责Cell的显示和业务逻辑处理
// HomeTableCellVM.h
@interface HomeTableCellVM : NSObject<ViewModelProtocol>
@property (nonatomic, strong) HomeModel *model;
@end
// HomeTableCellVM.m
@implementation HomeTableCellVM
#pragma mark - <ViewModelProtocol>
- (void)bindVMWithView:(UIView *)view {
HomeTableCell *cell = (HomeTableCell *)view;
[cell.imgView sd_setImageWithURL:[NSURL URLWithString:self.model.courseImage]];
cell.titleLabel.text = self.model.courseName;
cell.subTitleLabel.text = self.model.grade;
[cell.button setTitle:self.model.grade forState:UIControlStateNormal];
}
@end
3、架构目录
项目.png3.1 架构优化方案及内容
暂无
3.2 文档与规范
详见 iOS代码规范
3.3、架构优化方向及内容
暂无
4、优化计划
暂无
5、优化结果验证
暂无
网友评论