日常扯淡
上一篇是第一次写纯阅读类文章, 没有贴出的任何的代码块, 也没有写任何关于技术实现, 但反响出乎意料的好, 不仅获得了掘金的年度征文人气奖, 也得到了iOS大神南峰子的肯定并转载到了知识小集, 对此只有感恩和加倍的努力.
是的, 上一篇所提出的三个问题, 的确是每个开发者都该认真思考的, 但光有价值观, 没有方法论可不行, 有好多读者私信我想要知道前方的路在哪, 想必这也是困扰你许久的症结.
所以这一篇, 我们来谈谈如何进行快速的学习并使知识为我所用, 想要学习真正的技术, 光看技术博客可不行, 这一点在上一篇文章也有所涉及, 如何有效的学习技术, 必然的途径就是看官方文档和阅读优秀的三方源码.
源码区别
首先我们要了解官方源码和三方源码的区别, 到底看官方源码还是优秀的三方源码对成长的效率高, 这里要理解一个概念, 官方源码, 顾名思义对于iOS来说, 就是系统级别的代码, 就是官方会一直维护支持的, 而三方源码就是优秀的开发者开源出来的代码, 可能很优秀, 但并不能保证长时间的有效更新, 打个比方当你的iPhoneXs屏幕破碎了, 你可以到苹果天才吧进行维修, 或者到一些小作坊进行维修, 这里可以类比优秀的三方源码就是手艺高超的手艺人.
知道了区别, 我的建议是先看官方的源码, 这里不仅仅是iOS, 因为所有的三方源码都是大部分都是参照官方源码进行设计的, 也不乏优秀的三方源码被苹果爸爸相中集成到官方源码中, 但这毕竟是少数, 而且我对于三方源码一直是持有保留意见的.
难易程度
怎么选源码对学习的效率也有讲究么? 是的, 而且大有关系, 如果你是iOS开发, 想必你肯定用过AFNetWorking, SDWebImage, 这种牛逼哄哄的框架, 但如果你基础不是很扎实, 就算官方给出了架构文档, 你是不是感觉也无从下手? 就算看了个大概, 也不知道为什么要这样设计, 当然这两个框架网上有太多的文章来进行源码解读, 我也不会作出狂贴代码这种那么Low的事情, 因为所有的选择都是在特定的上下文之下才能观其端倪, 这也就是场景的重要性, 简单来说, 就是要输出倒逼输入, 在场景中想问题, 逆向思考, 你会得出不一样的结论.
还有很重要的一点是, 那你所看的源码需要和那你的能力所匹配, 比如说, 你并不懂C++, 而要看runtime源码你就会感觉很蛋疼, 如果你不懂汇编, 看消息发送那段就算有注释都自觉天书.
所以我的建议是可以看一些比你能力强一点点的, 稍微努努力能够够着的源码, 如果你感觉什么源码都入手困难的话, 可能你的语言基础太过薄弱, 还是乘早转行吧, 因为现在的环境下, 看不了源码几乎没有什么生存空间了.
一些趣事
入手之前, 我们先要了解一个概念, 并不是只有1000star以上的源码才有阅读的必要, 阅读和使用是两个不同的概念, 使用在项目中我们需要的是稳定性, 所以没有大量验证的代码是不能集成到项目中去的, 而阅读就不一样, 阅读是用来学习思想的, 理解了别人写的代码, 自己写一个, 就会觉得所谓的三方库也就不过尔尔.
这里挑选一个库ZIKViper, 可能大家并不了解这个库, 也跟别提用过这个库了, 在github上也就不到200个star, 作者也好像不怎么维护这个库了, 但这都不是重点, 重点是这我们能够从这个源码中学习到一些关于Viper架构优秀的设计思路, 以及将这些经验运用到自己的项目中去, 这才是我们看源码应该有的角度.
为什么选这个源码, 首先最近拟定要进行架构升级, 我之前也写过一篇关于架构的文章, 但现在回看还是非常幼稚和脆弱的, 但是这也证明了我的成长, 当你感觉过去的代码你看不下去了, 恭喜你, 你进步了.
有缘的是, 在我写之前的架构的时候还和ZIKViper的作者有一次交流, 我将交流整理成文章放在了网上可以点击查阅: iOS 关于组件化Router设计的争辩, 希望对你有所帮助.
了解概念
既然选择了一个源码, 在入手之前, 我们需要了解这个库到底是用来做什么的, 即场景的重要性. 可以看下相关库的README.md.
VIPER简介
VIPER的全称是View-Interactor-Presenter-Entity-Router。示意图如下:
相比MVX架构,VIPER多出了两个东西:Interactor(交互器)和Router(路由)。
各部分职责如下:
View 提供完整的视图,负责视图的组合、布局、更新 向Presenter提供更新视图的接口 将View相关的事件发送给Presenter Presenter 接收并处理来自View的事件 向Interactor请求调用业务逻辑 向Interactor提供View中的数据 接收并处理来自Interactor的数据回调事件 通知View进行更新操作 通过Router跳转到其他View Router 提供View之间的跳转功能,减少了模块间的耦合 初始化VIPER的各个模块 Interactor 维护主要的业务逻辑功能,向Presenter提供现有的业务用例 维护、获取、更新Entity 当有业务相关的事件发生时,处理事件,并通知Presenter Entity 和Model一样的数据模型 和MVX的区别 VIPER把MVC中的Controller进一步拆分成了Presenter、Router和Interactor。和MVP中负责业务逻辑的Presenter不同,VIPER的Presenter的主要工作是在View和Interactor之间传递事件,并管理一些View的展示逻辑,主要的业务逻辑实现代码都放在了Interactor里。Interactor的设计里提出了"用例"的概念,也就是把每一个会出现的业务流程封装好,这样可测试性会大大提高。而Router则进一步解决了不同模块之间的耦合。所以,VIPER和上面几个MVX相比,多总结出了几个需要维护的东西:
View事件管理 数据事件管理 事件和业务的转化 总结每个业务用例 模块内分层隔离 模块间通信 而这里面,还可以进一步细分一些职责。VIPER实际上已经把Controller的概念淡化了,这拆分出来的几个部分,都有很明确的单一职责,有些部分之间是完全隔绝的,在开发时就应该清晰地区分它们各自的职责,而不是将它们视为一个Controller。
优点 VIPER的特色就是职责明确,粒度细,隔离关系明确,这样能带来很多优点:
可测试性好。UI测试和业务逻辑测试可以各自单独进行。 易于迭代。各部分遵循单一职责,可以很明确地知道新的代码应该放在哪里。 隔离程度高,耦合程度低。一个模块的代码不容易影响到另一个模块。 易于团队合作。各部分分工明确,团队合作时易于统一代码风格,可以快速接手别人的代码。
好了, 以上都是抄的, 都是Zuikyo的文字, 我仅仅是粘贴使用, 想必他也不会介意的吧hhh
准备入手
了解了源码的概念, 我们就该开始着手阅读了, 首先将库克隆岛本地, pod install什么的常规操作我就不说了.
首先我们要看的是主文件
#import <UIKit/UIKit.h>
#import "SQViperView.h"
#import "SQViperViewEventHandler.h"
#import "SQViperPresenter.h"
#import "SQViperInteractor.h"
#import "SQViperWireframe.h"
#import "SQViperRouter.h"
#import "SQViperViewPrivate.h"
#import "SQViperPresenterPrivate.h"
#import "SQViperInteractorPrivate.h"
#import "SQViperWireframePrivate.h"
#import "NSObject+SQViperAssembly.h"
#import "UIViewController+SQViperRouter.h"
FOUNDATION_EXPORT double SQViperVersionNumber;
FOUNDATION_EXPORT const unsigned char SQViperVersionString[];
复制代码
这里你会发现和ZIK的前缀不同, 没关系, 我就是抄的, 在学习别人源码的过程中, 最有效的方法就是把别人的代码从新敲上一遍, 你会有不同的感悟, 在此声明我一直都是原创的簇拥者, 此篇文章所表达的意思是如何学习源码, 这里仅仅是代码举例.
你是不是也发现了优秀的代码都是极为的相似, 优秀是有范式, 有迹可循的.
重点分析
我这里不会长篇的贴代码进行解读, 想必看到这里, 你一定是有一定经验阅读源码的人, 所以文末放出地址, 自己去看就是了.
这里我们重点关注的是为什么要这样设计, 这样设计的好处在哪里.
看了主头文件, 我们可以发现所有协议后有加上Private字样. 以SQViperView举例.
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@protocol SQViperViewEventHandler;
@protocol SQViperView <NSObject>
- (nullable UIViewController *)routeSource;
@property (nonatomic, readonly, strong) id<SQViperViewEventHandler> eventHandler;
@optional
@property (nonatomic, readonly, strong) id viewDataSource;
@end
NS_ASSUME_NONNULL_END
复制代码
#import "SQViperView.h"
NS_ASSUME_NONNULL_BEGIN
@protocol SQViperViewEventHandler;
@protocol SQViperViewPrivate <SQViperView>
- (id<SQViperViewEventHandler>)eventHandler;
- (void)setEventHandler:(id<SQViperViewEventHandler>)eventHandler;
@optional
- (id)viewDataSource;
- (void)setViewDataSource:(id)viewDataSource;
- (void)setRouteSource:(UIViewController *)routeSource;
@end
NS_ASSUME_NONNULL_END
复制代码
这里很清楚的就能够看到, 这是表示权限问题, 也就是只读和读写的问题, 这个在架构设计上尤为重要, 也是工作经验的体现.
#import "NSObject+SQViperAssembly.h"
@implementation NSObject (SQViperAssembly)
+ (void)assembleViperForView:(id<SQViperViewPrivate>)view presenter:(id<SQViperPresenterPrivate>)presenter interactor:(id<SQViperInteractorPrivate>)interactor wireframe:(id<SQViperWireframePrivate>)wireframe router:(id<SQViperRouter>)router {
NSParameterAssert([view conformsToProtocol:@protocol(SQViperView)]);
NSParameterAssert([presenter conformsToProtocol:@protocol(SQViperPresenter)]);
NSParameterAssert([interactor conformsToProtocol:@protocol(SQViperInteractor)]);
NSParameterAssert([wireframe conformsToProtocol:@protocol(SQViperWireframe)]);
NSParameterAssert([router conformsToProtocol:@protocol(SQViperRouter)]);
NSAssert3(interactor.eventHandler == nil, @"Interactor (%@)'s eventHandler (%@) already exists when assemble viper for new eventHandler", interactor, interactor.eventHandler, presenter);
NSAssert3(interactor.dataSource == nil, @"Interactor (%@)'s dataSource (%@) already exists when assemble viper for new dataSource", interactor, interactor.dataSource, presenter);
interactor.eventHandler = presenter;
interactor.dataSource = presenter;
NSAssert3(wireframe.view == nil, @"Wireframe (%@)'s view (%@) already exists when assemble viper for new view", wireframe, wireframe.view, view);
wireframe.view = view;
wireframe.router = router;
NSAssert3(presenter.interactor == nil, @"Presenter (%@)'s interactor already exists when assemble viper for new interactor", presenter, presenter.interactor, interactor);
NSAssert3(presenter.view == nil, @"Presenter (%@)'s view already exists when assemble viper for new view", presenter, presenter.view, view);
NSAssert3(presenter.wireframe == nil, @"Presenter (%@)'s wireframe (%@) already exists assemble viper for new router", presenter, presenter.wireframe, self);
presenter.interactor = interactor;
presenter.view = view;
presenter.wireframe = wireframe;
if ([view respondsToSelector:@selector(viewDataSource)] &&
[view respondsToSelector:@selector(setViewDataSource:)]) {
NSAssert3(view.viewDataSource == nil, @"View (%@)'s viewDataSource (%@) already exists when assemble viper for new viewDataSource", view, view.viewDataSource, presenter);
view.viewDataSource = presenter;
}
NSAssert3(view.eventHandler == nil, @"View (%@)'s eventHandler (%@) already exists when assemble viper for new eventHandler", view, view.eventHandler, presenter);
view.eventHandler = presenter;
}
@end
复制代码
这个分类很好的将架构的所有逻辑结构进行了组装, 堪称是这个源码的精髓所在.
其他的就没有什么值得称道的东西了, 毕竟这是一份简单的源码. 我们的关注点在思想上.
这里要说的是, 根据不同的源码了解分析出其关键点, 并了解设计思路.
动手实践
所谓实践才是检验真理的唯一标准, 所以一般优秀的三方源码都会有demo作为参考, 所以想要学习的同学, 可以自行查阅.
我这里对比之前我写的容量增肌项目, 对比写了一个Viper版本的作为对照, 也会把碰到的问题分享给大家.
#import "SQTrainingCapacityViewController.h"
#import "SQTrainingCapacityCell.h"
#import "SQTrainingCapacityHeaderView.h"
#import "SQTrainingCapacityFooterView.h"
#import "SQTrainingCapacityCellPresenter.h"
#import "SQTrainingCapacityDataSource.h"
#import "SQTrainingCapacityViewEventHandler.h"
@interface SQTrainingCapacityViewController ()
@property (nonatomic, strong) NSMutableArray *dataSource;
@property (nonatomic, weak) SQTrainingCapacityFooterView * footerView;
@end
@implementation SQTrainingCapacityViewController
- (void)setupData {
_dataSource = [NSMutableArray array];
}
- (void)fetchDataSource {
NSArray *dataSource = [(id<SQTrainingCapacityDataSource>)self.viewDataSource fetchDataSourceFromDB];
[self.dataSource removeAllObjects];
[self.dataSource addObjectsFromArray:dataSource];
}
- (void)setupUI {
[self setupRightBarButtonItem];
[self setupTableView];
}
- (void)setupRightBarButtonItem {
[self setRightBarButtonItem:(UIBarButtonSystemItemAdd) target:self action:@selector(addTraningAction)];
}
- (void)setRightBarButtonItem:(UIBarButtonSystemItem)item target:(nullable id)target action:(nullable SEL)action {
self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc]initWithBarButtonSystemItem:item target:target action:action];
}
- (void)setupTableView {
self.tableView.tableHeaderView = [SQTrainingCapacityHeaderView headerView];
SQTrainingCapacityFooterView * footerView = [SQTrainingCapacityFooterView footerView];
footerView.totalCapacityLabel.text = [(id<SQTrainingCapacityDataSource>)self.viewDataSource totalCapacity];
self.tableView.tableFooterView = footerView;
self.footerView = footerView;
[self.tableView registerNib:[UINib nibWithNibName:@"SQTrainingCapacityCell" bundle:[NSBundle bundleForClass:self.class]] forCellReuseIdentifier:@"TrainingCapacity"];
[self setupDataSource:self.dataSource loadCell:^UITableViewCell *(UITableView * _Nonnull tableView, NSIndexPath * _Nonnull indexPath) {
return [tableView dequeueReusableCellWithIdentifier:@"TrainingCapacity" forIndexPath:indexPath];
} loadCellHeight:^CGFloat(id _Nonnull model) {
return 160;
} bind:^(UITableViewCell * _Nonnull cell, id _Nonnull model) {
SQTrainingCapacityCell * c = (SQTrainingCapacityCell *)cell;
SQTrainingCapacityCellPresenter * p = (SQTrainingCapacityCellPresenter * )model;
[p bindToCell:c];
}];
}
- (void)addTraningAction {
[(id<SQTrainingCapacityViewEventHandler>)self.eventHandler didTouchNavigationBarAddButton];
}
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
[(id<SQTrainingCapacityViewEventHandler>)self.eventHandler handleCommitEditingAtIndexPath:indexPath];
}
@end
复制代码
这里仅仅贴出控制器的代码, 我们可以看到, 在Viper架构下, 获取数据和操作逻辑都通过协议进行传递, 没有任何冗余的代码, 分层清晰明了.
#import "SQTrainingCapacityPresenter.h"
#import "SQTrainingCapacityViewProtocol.h"
#import "SQTrainingCapacityInteractorInput.h"
#import "SQTrainingCapacityWireframeInput.h"
#import "SQTrainingCapacityNotification.h"
@interface SQTrainingCapacityPresenter ()
@property (nonatomic, weak) id<SQTrainingCapacityViewProtocol> view;
@property (nonatomic, strong) id<SQTrainingCapacityInteractorInput> interactor;
@property (nonatomic, strong) id<SQTrainingCapacityWireframeInput> wireframe;
@end
@implementation SQTrainingCapacityPresenter
- (void)handleViewReady {
NSAssert(self.wireframe, @"Router should be initlized when view is ready.");
NSAssert([self.view conformsToProtocol:@protocol(SQViperView)], @"Presenter should be attach to a view");
NSAssert([self.interactor conformsToProtocol:@protocol(SQViperInteractor)], @"Interactor should be initlized when view is ready.");
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillShow:) name:UIKeyboardWillShowNotification object:nil];
[[NSNotificationCenter defaultCenter]addObserver:self selector:@selector(keyboardWillHide:) name:UIKeyboardWillHideNotification object:nil];
[self.interactor loadDataSourceWithTitle:self.view.title type:self.view.type];
[self.view fetchDataSource];
}
- (void)handleViewWillAppear:(BOOL)animated {
NSLog(@"%s", __func__);
}
- (void)handleViewDidAppear:(BOOL)animated {
NSLog(@"%s", __func__);
}
- (void)handleViewWillDisappear:(BOOL)animated {
NSLog(@"%s", __func__);
}
- (void)handleViewDidDisappear:(BOOL)animated {
NSLog(@"%s", __func__);
}
- (void)handleViewRemoved {
NSLog(@"%s", __func__);
[self.interactor storeDataSourceWithTitle:self.view.title type:self.view.type dataSource:self.fetchDataSourceFromDB];
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (void)handleCommitEditingAtIndexPath:(NSIndexPath *)indexPath {
NSMutableArray *tempDataSource = self.fetchDataSourceFromDB.mutableCopy;
[tempDataSource removeObjectAtIndex:indexPath.row];
[self.interactor storeDataSourceWithTitle:self.view.title type:self.view.type dataSource:tempDataSource];
[self.view fetchDataSource];
[self.view.tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade];
self.view.footerView.totalCapacityLabel.text = self.totalCapacity;
}
- (void)didTouchNavigationBarAddButton {
[self.interactor addTrainingAction];
[self.view fetchDataSource];
[self.view.tableView insertRowsAtIndexPaths:@[[NSIndexPath indexPathForRow:self.fetchDataSourceFromDB.count - 1 inSection:0]] withRowAnimation:(UITableViewRowAnimationLeft)];
}
- (void)didTouchNavigationBarDoneButton {
[self.view.tableView endEditing:YES];
[[NSNotificationCenter defaultCenter]postNotificationName:SQTrainingCapacityBindToModelNotification object:nil];
self.view.footerView.totalCapacityLabel.text = self.totalCapacity;
[self.view.tableView reloadData];
}
- (void)keyboardWillShow:(NSNotification *)sender {
[self.view setRightBarButtonItem:(UIBarButtonSystemItemDone) target:self action:@selector(didTouchNavigationBarDoneButton)];
}
- (void)keyboardWillHide:(NSNotification *)sender {
[self.view setRightBarButtonItem:(UIBarButtonSystemItemAdd) target:self action:@selector(didTouchNavigationBarAddButton)];
}
- (NSString *)totalCapacity {
return self.interactor.totalCapacity;
}
- (NSArray *)fetchDataSourceFromDB {
return self.interactor.fetchDataSource;
}
@end
复制代码
协调整个架构的presenter, 逻辑一下子也清晰了很多, 和之前的进行对比, 你会更好的了解这个架构.
如何实践, 文档是讲不清楚的, 需要大家阅读源码和动手实践
写在最后
这篇分析了如何阅读源码的全过程, 简单来说就是, 确定场景, 了解概念, 重点分析, 动手实践, 这些东西说说简单, 实际操作中还是有很多的精妙之处的
以上.