一直以来想写一篇文章,可是没找到合适的主题,前段时间一直在看 Flutter 的一些东西,原本有意向想写关于 Flutter 的一些总结,可是看的有些零零散散,而且没有实际应用过,因此也就搁置了。正好最近一段时间除主业务之余,一直在作咱们 甘草医生 用户端的重构,恰好有一些对于 iOS 架构方面的见解与感悟,在这里与你们分享。 万事开头难!其实在开始重构以前,我是很纠结的,一直很难开始。我也曾翻阅过不少资料,想找到一个合适的符合咱们本身目前业务的架构,最后作了种种的比对与测试,选择了 MVVM + 组件化 + AOP
的模式来重构。可能有人会疑问,你为何选择这样的架构模式?使用这些模式有什么好处?这些抽象的模式概念具体应该怎么在实际项目中运用?OK,那咱们就带着这些疑问一步步往下看。html
咱们先来了解一下在 iOS 中经常使用的一些架构模式ios
MVC 关于 MVC(Model-View-Controller)这个设计模式我相信稍有些编程经验的人都了解至少据说过,做为应用最为普遍的架构模式,你们应该都是耳熟能详了,可是不一样的人对 MVC 的理解是不一样的。在 iOS 中,Cocoa Touch 框架使用的就是 MVC ,以下 git
MVVMgithub
MVVM (Model-View-ViewModel),其实也是基于 MVC 的。上面咱们说的 MVC 臃肿的问题,在 MVVM 的架构模式中获得了解决,咱们一些经常使用的网络请求、数据存储等都交给它处理,这样就能够分离出 ViewController 里面的一些代码使其“减肥”。 编程
其余的一些架构模式swift
还有一些其余的架构模式,好比 MVP(Model-View-Presenter)、VIPER(View-Interactor-Presenter-Entity-Routing)、MVCS(Model-View-Controller-Store)等,其实都是基于 MVC 思想派生出来的一些架构模式,基本都是为了给 Controller 减负而生的,因此仍是那句话,万变不离 MVC !设计模式
了解到每一个架构模式的优缺点以后,这里,我决定用 MVVM 的架构模式来重构咱们的 APP。那么说到 MVVM ,咱们就确定是要提到 RAC ,也就是 ReactiveCocoa,它是一个响应式编程的框架,可使每层交互起来更加方便清晰。固然, RAC 确定不是实现数据绑定的惟一方案,在 iOS 中好比 KVO、Notification、Delegate、Block等均可以实现,只不过是 RAC 的实现更加优雅一些,因此咱们常常会采用 RAC 来实现数据的绑定。关于 RAC ,下面一张图很清晰的解释了它的思想,也就是 FRP(Function Reactive Programming)函数响应式编程 浏览器
//一、导入代理
<UITextFieldDelegate>
//二、设置代理
self.phoneTextField.delegate = self;
//三、实现代理
- (void)textFieldDidChange:(UITextField *)textField {
if (textField == self.phoneTextField) {
if (textField.text.length > 11) {
textField.text = [textField.text substringToIndex:11];
}
}
}
复制代码
那么若是使用 RAC ,以下bash
@weakify(self);
[[self.phoneTextField.rac_textSignal map:^id _Nullable(NSString * _Nullable value) {
return value.length > 11 ? [value substringToIndex:11] : value;
}] subscribeNext:^(NSString *x) {
@strongify(self);
self.phoneTextField.text = x;
}];
复制代码
能够看出代码变得更加清晰了,咱们只须要实现对 phoneTextField
信号的监听,就能够实现了。咱们再来看一个例子,好比在咱们用户端的登陆界面,以下图 微信
//一、导入代理
<UITextFieldDelegate>
//二、设置代理
self.phoneTextField.delegate = self;
self.passwordTextField.delegate = self;
//三、实现代理
- (void)textFieldDidChange:(UITextField *)textField {
if (self.phoneTextField.text.length == 11 && [self.passwordTextField isNotBlank]) {
self.loginButton.enabled = YES;
} else {
self.loginButton.enabled = NO;
}
}
复制代码
而使用 RAC 则以下
@weakify(self);
[[[RACSignal combineLatest:@[self.phoneTextField.rac_textSignal,
self.passwordTextField.rac_textSignal]] map:^id _Nullable(RACTuple * _Nullable value) {
RACTupleUnpack(NSString *phone, NSString *password) = value;
return @([password isNotBlank] && phone.length == 11);
}] subscribeNext:^(NSNumber *x) {
@strongify(self);
if (x.boolValue) {
self.loginButton.enabled = YES;
} else{
self.loginButton.enabled = NO;
}
}];
复制代码
咱们将 self.phoneTextField.rac_textSignal
和 self.passwordTextField.rac_textSignal
这两个信号合并成一个信号而且监听,实现必定的逻辑,简单明了。固然, RAC 的好处远远不止这些,这里只是冰山一角,有兴趣的能够去本身用一用这个库,体验更多的功能,这里也就很少赘述了。
组件化这个概念相信你们都据说过,使用组件化的好处就是使咱们项目更好的解耦,下降各个分层之间的耦合度,使项目始终保持着 高聚合,低耦合
的特色。举个简单的例子,在 iOS 中页面之间的跳转,两个开发人员负责开发两个页面,小 A 负责开发的 AViewController 已经开发完毕,而后须要点击按钮跳到小 B 负责的 BViewController,而且须要传一个值,以下
//一、导入BViewController
#import "BViewController"
//二、跳转
BViewController *bViewController = [[BViewController alloc]init];
bViewController.uid = @"123";
[self.navigationController pushViewController:bViewController animated:YES];
复制代码
这时候小 A 已经准备去写其余业务了,可是一问才发现小 B 并无开始写 BViewController,还须要一段时间才能写,那么小 A 就郁闷了,要么就等着小 B 写完我再去作其余的,要么就先注释我这段代码,等到小 B 写完我再解注释。形成这种状况的缘由就是由于两个页面之间牢牢地耦合在一块儿了,在开发人员少或者独立开发的状况下咱们常用这种方式进行页面间的跳转和传值,页面基本都是一我的负责,因此感受不到问题,试想一下在几十人开发的工做组中,划分很细的状况下,你本身的脱节是否是给别人带去了没必要要的麻烦。我相信这是全部人都不想发生的,那么咱们就须要对页面进行组件化解耦,这里我所使用的组件化方案是 target-action
方式,使用的是 Casa Taloyum 的 CTMediator,其主要的思想就是经过一个中间者来提供服务,经过 runtime
来调用组件服务,好比之前的依赖关系以下
//一、导入Mediator
#import "CTMediator+BViewControllerActions.h"
//二、跳转
UIViewController *viewController = [[CTMediator sharedInstance] gc_bViewController:@{@"uid": @"123"}];
[self.navigationController pushViewController:viewController animated:YES complete:nil];
复制代码
这样小 A 就不用管小 B 是否是写完没,也不须要导入小 B 的页面,就能够跳转到小 B 的页面,实现了页面间的解耦。能达到这一目的的功臣就是咱们的中间者,咱们来看看它作了什么,咱们仍是以咱们登陆页面为例,咱们从登录跳转到注册页面的代码以下
//一、导入Mediator
#import "CTMediator+RegistViewControllerActions.h"
//二、跳转
UIViewController *viewController = [[CTMediator sharedInstance] gc_registViewController];
[self.navigationController pushViewController:viewController animated:YES complete:nil];
复制代码
其中 CTMediator+RegistViewControllerActions.h
中的代码以下
//
// CTMediator+RegistViewControllerActions.m
// GCUser
//
// Created by HenryCheng on 2019/4/15.
// Copyright © 2019 HenryCheng. All rights reserved.
//
#import "CTMediator+RegistViewControllerActions.h"
NSString *const gc_targetRegistVC = @"RegistViewController";
NSString *const gc_actionRegistVC = @"registViewController";
@implementation CTMediator (RegistViewControllerActions)
- (UIViewController *)gc_registViewController {
UIViewController *viewController = [self performTarget:gc_targetRegistVC
action:gc_actionRegistVC
params:@{@"title": @"注册"}
shouldCacheTarget:NO
];
if ([viewController isKindOfClass:[UIViewController class]]) {
return viewController;
} else {
return [[UIViewController alloc] init];
}
}
@end
复制代码
其中重要的就是 performTarget:action:params:shouldCacheTarget:
这个方法,内部的实现方式以下
- (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params shouldCacheTarget:(BOOL)shouldCacheTarget {
NSString *swiftModuleName = params[kCTMediatorParamsKeySwiftTargetModuleName];
// generate target
NSString *targetClassString = nil;
if (swiftModuleName.length > 0) {
targetClassString = [NSString stringWithFormat:@"%@.Target_%@", swiftModuleName, targetName];
} else {
targetClassString = [NSString stringWithFormat:@"Target_%@", targetName];
}
NSObject *target = self.cachedTarget[targetClassString];
if (target == nil) {
Class targetClass = NSClassFromString(targetClassString);
target = [[targetClass alloc] init];
}
// generate action
NSString *actionString = [NSString stringWithFormat:@"Action_%@:", actionName];
SEL action = NSSelectorFromString(actionString);
if (target == nil) {
// 这里是处理无响应请求的地方之一,这个demo作得比较简单,若是没有能够响应的target,就直接return了。实际开发过程当中是能够事先给一个固定的target专门用于在这个时候顶上,而后处理这种请求的
[self NoTargetActionResponseWithTargetString:targetClassString selectorString:actionString originParams:params];
return nil;
}
if (shouldCacheTarget) {
self.cachedTarget[targetClassString] = target;
}
if ([target respondsToSelector:action]) {
return [self safePerformAction:action target:target params:params];
} else {
// 这里是处理无响应请求的地方,若是无响应,则尝试调用对应target的notFound方法统一处理
SEL action = NSSelectorFromString(@"notFound:");
if ([target respondsToSelector:action]) {
return [self safePerformAction:action target:target params:params];
} else {
// 这里也是处理无响应请求的地方,在notFound都没有的时候,这个demo是直接return了。实际开发过程当中,能够用前面提到的固定的target顶上的。
[self NoTargetActionResponseWithTargetString:targetClassString selectorString:actionString originParams:params];
[self.cachedTarget removeObjectForKey:targetClassString];
return nil;
}
}
}
复制代码
能够看到若是有响应则调用 safePerformAction:target: params:
这个方法,以下
- (id)safePerformAction:(SEL)action target:(NSObject *)target params:(NSDictionary *)params {
NSMethodSignature* methodSig = [target methodSignatureForSelector:action];
if(methodSig == nil) {
return nil;
}
const char* retType = [methodSig methodReturnType];
if (strcmp(retType, @encode(void)) == 0) {
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
[invocation setArgument:¶ms atIndex:2];
[invocation setSelector:action];
[invocation setTarget:target];
[invocation invoke];
return nil;
}
if (strcmp(retType, @encode(NSInteger)) == 0) {
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
[invocation setArgument:¶ms atIndex:2];
[invocation setSelector:action];
[invocation setTarget:target];
[invocation invoke];
NSInteger result = 0;
[invocation getReturnValue:&result];
return @(result);
}
if (strcmp(retType, @encode(BOOL)) == 0) {
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
[invocation setArgument:¶ms atIndex:2];
[invocation setSelector:action];
[invocation setTarget:target];
[invocation invoke];
BOOL result = 0;
[invocation getReturnValue:&result];
return @(result);
}
if (strcmp(retType, @encode(CGFloat)) == 0) {
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
[invocation setArgument:¶ms atIndex:2];
[invocation setSelector:action];
[invocation setTarget:target];
[invocation invoke];
CGFloat result = 0;
[invocation getReturnValue:&result];
return @(result);
}
if (strcmp(retType, @encode(NSUInteger)) == 0) {
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
[invocation setArgument:¶ms atIndex:2];
[invocation setSelector:action];
[invocation setTarget:target];
[invocation invoke];
NSUInteger result = 0;
[invocation getReturnValue:&result];
return @(result);
}
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
return [target performSelector:action withObject:params];
#pragma clang diagnostic pop
}
复制代码
经过这两个方法咱们就能够看到整个 CTMediator 实现的思路了,为何 AViewController 不引用 BViewController 还能向其进行跳转传值,原来都是因为 runtime
在中间起做用。 固然,虽然中间者这个方案能很好地实现各页面之间的解耦,可是也有它的缺点。咱们能够看到咱们在 CTMediator+RegistViewControllerActions.h
中定义的 gc_targetRegistVC
和 gc_actionRegistVC
这两个常量,分别对应 ‘target’ 和 ‘action’,这里面须要注意的是必定要细心,若是这儿写错,会引起未知的错误,可是编译器并不会提示,对应的 Target_...
必定要和这里的 target
一致,不然就会引起错误。这种方案的实施对开发人员的细心程度是有很大要求的,由于若是有错误,在编译中没法发现的。 组件化的方案的实施还有不少其余的方案,好比 url-block
、protocol-class
方式,有兴趣的能够看看蘑菇街的 MGJRouter,还有就是阿里的 BeeHive ,它是基于 Spring 的 Service 理念,使用 Protocol
的方式进行模块间的解耦。
先看一个案例,小 C 最近愁眉苦脸,你发现了他状态不对劲,因而就发生了下面的对话
你:“小 C,你这是怎么啦,是否是工做上有什么不顺心的?”
小 C:“是啊,最近接到一个需求,让我很头疼!”
你:“接到需求不是很正常,作就是了啊!”
小 C:“你不知道,这个需求是统计每一个页面的浏览状况,就是用户到了这个页面我就要统计一下,
运营产品他们要看 PV,因而我就在基类里面的 `viewDidLoad` 方法加了一下,这样很简单就解决了”
小 C:“但是他们又说还要我作每一个页面按钮的点击统计,你说这 APP 几百个页面,这么多按钮,我怎么加啊,
就算我加了,个人代码也会由于这些与业务无关的代码而变得混乱,万一哪天不统计再让我删了,那我不是要命了啊!愁死我了!”
你:“那你这使用 AOP 就能够了啊”
小 C :“A...OP???”
复制代码
AOP(Aspect-oriented programming),面向切面编程,是计算机科学中的一种程序设计思想,旨在将横切关注点与业务主体进行进一步分离,以提升程序代码的模块化程度。在 iOS 中有一个应用很是多的轻量级的 AOP 库 Aspects ,它容许你能在任何一个类和实例的方法中插入新的代码。看到这里,你可能就已经知道小 C 的问题该如何解决了,下面是使用 Aspects 实现页面统计的代码
//
// GCViewControllerIntercepter.m
// GCUser
//
// Created by HenryCheng on 2019/4/25.
// Copyright © 2019 HenryCheng. All rights reserved.
//
#import "GCViewControllerIntercepter.h"
#import <Aspects/Aspects.h>
@implementation GCViewControllerIntercepter
+ (void)load {
[GCViewControllerIntercepter sharedInstance];
}
+ (GCViewControllerIntercepter *)sharedInstance {
static GCViewControllerIntercepter *_sharedClient = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_sharedClient = [[GCViewControllerIntercepter alloc] init];
});
return _sharedClient;
}
- (instancetype)init {
if (self == [super init]) {
[UIViewController aspect_hookSelector:@selector(viewDidLoad)
withOptions:AspectPositionAfter
usingBlock:^(id<AspectInfo> aspectInfo, UITouch *touch, UIEvent *event) {
if ([aspectInfo.instance isKindOfClass:[UIViewController class]]) {
// 页面统计的代码
}
} error:NULL];
[UIControl aspect_hookSelector:@selector(beginTrackingWithTouch:withEvent:)
withOptions:AspectPositionAfter
usingBlock:^(id<AspectInfo> aspectInfo, UITouch *touch, UIEvent *event) {
if ([aspectInfo.instance isKindOfClass:[UIButton class]]) {
// 按钮统计的代码
}
} error:NULL];
}
return self;
}
@end
复制代码
咱们能够看到,经过新建 GCViewControllerIntercepter
这个类就实现了页面的统计和按钮点击统计功能,你只须要实现就行,连导入都不用,若是哪天你不须要这些统计的代码了,你直接从项目中移除这个类就能够了。是否是很简单!这就是 AOP 的一个使用实例,经过 + (void)load
这个方法(+ load
做为 Objective-C 中的一个方法,与其它方法有很大的不一样。它只是一个在整个文件被加载到运行时,在 main 函数调用以前被 ObjC 运行时调用的钩子方法),实现了 GCViewControllerIntercepter
这个类被调用,而后经过 Aspects 实现对 UIViewController 和 UIControl 的 hook。这样在每一个页面被加载、每一个按钮被点击以前这边就能够捕捉到。 还有就是有人提到过去基类,也就是抛弃厚重的 base ,直接使用 AOP ,这样的话好比我想写个新 demo 就不用引入各类父类了,直接 hook 拿来用就行了。这种方法我的以为没有到大工程的时候仍是用继承来实现比较好。若是工程量比较大便于各个开发人员调试,可使用这种方法。 固然 AOP 的做用也不只如此,这里就说这么一个咱们经常使用的 hook 的例子,有兴趣能够下去好好了解下。
一、
AspectOptions
有四个值,分别是AspectPositionAfter
、AspectPositionInstead
、AspectPositionBefore
和AspectOptionAutomaticRemoval
,这样你能够决定你 hook 的位置二、对于
+ (void)load
还有+ (void)initialize
这两个方法不是太了解的童鞋能够看看大左 Draveness 的 你真的了解 load 方法么? 和 懒惰的 initialize 方法 这两篇文章,了解这两个方法相信对你会颇有帮助
了解了上面的内容,接下来咱们看看在实际项目中的应用
项目的目录结构
Model
、View
、ViewController
和 ViewModel
这几个类联系比较紧密,因此建议这几个类的项目结构保持一致,以下图Login
模块里面去寻找。并且建议目录不要过深,通常三层就够了,过深的话查找起来比较麻烦。 Category 的使用
可能你们已经看到了,个人项目目录里面有一项是 AppDelegate+Config
这一项,这其实就是 AppDelegate
的一个 Category
。在 iOS 开发中 Category
随处可见,如何应用那就是看本身的需求状况了,这里我用 AppDelegate+Config
这个类来处理 AppDelegate
里面的一些配置,减小 AppDelegate
的代码,让项目更加清晰,使用了之后咱们能够看到 AppDelegate
目录的代码片断
#import "AppDelegate.h"
#import "AppDelegate+Config.h"
#import "GCPushManager.h"
@interface AppDelegate ()
@end
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[self configTabbar];
[self registWeChat];
[self configNetWork];
[self configJPushWithLaunchOptions:launchOptions];
[self configKeyboard];
[self configBaiduMobStat];
[self configShareSDKWithLaunchOptions:launchOptions];
return YES;
}
// between iOS 4.2 - iOS 9.0
- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(nullable NSString *)sourceApplication annotation:(id)annotation {
[self handleOpenURL:url];
return NO;
}
// after iOS 9.0
- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey, id> *)options {
[self handleOpenURL:url];
return NO;
}
复制代码
这样代码看起来很清晰,相信你们都有过一打开 AppDelegate
这个类看到一大堆代码,东找西找很不规范的经历。因为是项目重构初期,AppDelegate
和 AppDelegate+Config
使用的比较多,暂时先放在这里,后期再将其移动到合适的位置。
CocoaPods 的使用
相信这个东西你们都用过,为何要强调一下 CocoaPods 的使用,由于在我整理以前项目时发现,有的地方(好比微信支付、支付宝支付)就是直接将 lib 直接拖进工程,有的还须要各类配置,这样若是升级或者移除的时候就很麻烦。使用 CocoaPods 管理的话那么升级或者移除就很方便,因此建议仍是能使用 CocoaPods 安装的就直接使用其安装,最好不要直接在项目中添加第三方。 还有一种状况就是有时候第三方知足不了咱们的需求,须要修改一下,因此有些就不集成在 CocoaPods 里面了(万一一不当心 update 之后修改的内容被覆盖)。这里我想说的是,对于这种状况你仍然可使用 CocoaPods,那么怎么解决须要修改代码的问题?没错,就是 Category !
MVVM的运用
具体项目的实现咱们仍是以登陆为例,在 ViewModel 中
- (void)initialize {
[super initialize];
RAC(self, isLoginEnable) = [[RACSignal combineLatest:@[
RACObserve(self, phone),
RACObserve(self, password)
]] map:^id _Nullable(RACTuple * _Nullable value) {
RACTupleUnpack(NSString *phone, NSString *password) = value;
return @([phone isNotBlank] && [password isNotBlank] && phone.length == 11); }];
RAC(self.loginRequest, params) = [[RACSignal combineLatest:@[
RACObserve(self, phone),
RACObserve(self, password)
]] map:^id _Nullable(RACTuple * _Nullable value) {
RACTupleUnpack(NSString *phone, NSString *password) = value;
return @{@"phone": GC_NO_BLANK(phone),
@"pwd": GC_NO_BLANK(password)
}; }];
}
- (RACCommand *)loginCommand {
if (!_loginCommand) {
@weakify(self);
_loginCommand = [[RACCommand alloc] initWithSignalBlock:^RACSignal * _Nonnull(id _Nullable input) {
@strongify(self);
return [self.loginRequest requestSignal] ;
}];
}
return _loginCommand;
}
复制代码
这里咱们作了网络的请求以及一些数据的绑定,在 ViewController 中
- (void)gc_bindViewModel {
[super gc_bindViewModel];
RAC(self.viewModel, phone) = self.loginView.phoneTextField.rac_textSignal;
RAC(self.viewModel, password) = self.loginView.passwordTextField.rac_textSignal;
RAC(self.loginView.loginButton, enabled) = RACObserve(self.viewModel, isLoginEnable);
@weakify(self);
[[[self.loginView.loginButton rac_signalForControlEvents:UIControlEventTouchUpInside] throttle:0.25] subscribeNext:^(__kindof UIControl * _Nullable x) {
@strongify(self);
[self.viewModel.loginCommand execute:nil];
}];
[[self.viewModel.loginCommand.executing skip:1] subscribeNext:^(NSNumber * _Nullable x) {
if (x.boolValue) {
[GCHUDManager show];
} else {
[GCHUDManager dismiss];
}
}];
// 登陆命令监听
[self.viewModel.loginCommand.executionSignals.switchToLatest subscribeNext:^(NSDictionary *x) {
@strongify(self);
UserModel *userModel = [UserModel modelWithDictionary:x];
[[GCCacheManager sharedManager] updateDataWithDictionary:x key:GCUserInfoStoreKey()];
[GCPushManager gc_setAlias:x[@"phone"]];
if (userModel.is_agree.intValue == 0) {
// 未赞成甘草协议
} else if (userModel.is_agree.intValue == 1 && userModel.pwd_status.intValue == 0) {
// 赞成协议可是没改过密码
} else {
// 登陆
}
} error:^(NSError * _Nullable error) {
}];
}
复制代码
能够看到 ViewController 将 View 和 ViewModel 进行了绑定,而且当登陆按钮点击的时候监测登陆信号的变化,根据其信号执行的开始和结束来控制 HUD 的显示和消失,而后再根据信号的返回结果来处理相关的登陆配置和跳转(极光推送的登陆、根据状态执行跳转逻辑等)。这里网络的请求都是在 ViewModel 中进行的,ViewController 只负责处理ViewModel、View 和 Model 之间的关系。
DRY(Don't repeat yourself),能封装起来的类必定要封装起来,到时候使用也简单,千万不要为了一时之快而各类 ctrl + c
和 ctrl + v
,这样会使你的代码混乱不堪,这其实也是项目臃肿的一个缘由。在重构的过程当中就封装了不少的类,管理起来很方便
其实最开始的时候一直都有重构的想法,可是迟迟没有动手。其中一个缘由就是不知道该如何动手,不知道该使用什么工具,该使用哪一种方案。等到真正开始的时候发现其实没有想象中的那么难,因此当你有想法的时候你就去作,在作的过程当中你能够慢慢体会。 在重构以前,我又从新读了一下代码规范,也就是 禅与 Objective-C 编程艺术 这本书,并在重构的过程当中严格执行,好比 loginButton
就毫不会写成 loginBtn
,相信我,按着规范来,你会体会到其中的意义的。 在作一个 APP 以前,在咱们新建工程的时候,就应该已经肯定你的架构模式,而且在之后的业务处理中,严格的按着这种设计模式执行下去。若是在前面需求量很少的时候你还能按着最初的设计模式执行下去,在业务忽然增多的时候,为了偷懒省事,直接各类代码混乱的糅合在一块儿,各类 ctrl + c
和ctrl + v
,致使架构的混乱引发蝴蝶效应,那么这个架构在后期若是再想从新规范起来将会是个费时费力的过程。因此,在最初设计的时候咱们就应该肯定架构方案,以及严格的执行下去。 还有就是平时的一些技术积累以及知识存储。知其然知其因此然,研究技术背后的底层原理,会对你有很大的帮助。好比说我要说来讲说 ViewController 的生命周期,可能你们都会随口说出 viewDidLoad
、viewWillAppear
等,我要问说说 View 的生命周期,可能就会有少数人茫然了。这些都是很基本的东西,可能你平时用不到,可是仍是须要你去了解他,注意细节。不少人可能会常常有这样的困惑,好比我想写一个图片浏览器,可是我不知道该如何写?写完了性能如何?别人是怎么写的?这个就是须要平时的积累了,好比关于 UIText
相关的的你就得想到 YYText,数据存储方面的你不只要知道老的 fmdb ,微信开源的 wcdb 有没有去了解下呢?好比我就平时没事喜欢在 GitHub 上看一些 star 比较高的开源库,看看别人是怎么实现的,想一想在个人项目中怎么使用。举个例子,最近阿里开源的 协程 框架 coobjc ,就在项目中使用,用来判断用户是否登陆
- (void)judgeLoginBlock:(void(^)(GCLoginStatus status))block {
co_launch(^{
NSDictionary *dic = await([self co_loginRequest]);
if (co_getError()) {
block(GCLoginStatusError);
} else if (dic) {
if ([dic[@"status"] intValue] == 1) {
block(GCLoginStatusLogin);
} else if ([dic[@"status"] intValue] == -99) {
block(GCLoginStatusUnLogin);
} else {
block(GCLoginStatusError);
}
} else {
block(GCLoginStatusError);
}
});
}
复制代码
一眼看去逻辑就很简单明了,比 Block 嵌套 Block 这种方式优雅的多。 如今只是重构的开始,如今已经完成的登陆的重构就 LoginViewController
而言,与以前相比就已经有很大的改变了(以前将近 800 行代码,重构后只有 200 行),可能整体上各个模块代码加起来都差很少,可是为 ViewController 减负后更加清晰明了了。后面重构完成后会出一个代码量、包大小、性能等的对比,到时候再与你们分享!
Reference
三、iOS 如何实现Aspect Oriented Programming
五、BeeHive
七、coobjc