设计一个App的思路:
原则:易读 易维护 易扩展 ;技术储备;语言选择
组成:
1.应用入口(Appdelegate):存放推送 IM 支付回调等
2.功能模块:根据业务进行划分:可以灵活采用MVC MVVM MVP
3.管理模块:登陆状态信息 单例 网络监听 广告页
4.工具类:自己写的工具类
5.基类:一些定制化的内容页面 样式 空数据页面 无网络提示页面
6.分类:对系统类 自定义类增加的类别
7.宏定义文件:全局通用的宏定义 方法
8.资源文件:图片 json xml test plist
9.第三方库的封装:
10.Cocoapods:
重构需要考虑的因素:
1.明确重构的目的和重用性
2.定义重构完成的界限
3.持续渐进式重构
4.确定当前的架构状态
5.不能忽略数据的重用性
6.管理好技术债务
7.远离虚华的东西 追求实际
8.做好准备面对压力,做好面对非技术的准备
9.了解当前业务
10.时刻注意代码的质量
11.团队一致 做好准备
MVC://www.greatytc.com/p/eedbc820d40a
MVC是软件工程中的一种软件架构模式,它把软件系统分为三个基本的部分:模型Model、视图View以及控制器Controller;
数据Model: 负责封装数据、存储和处理数据运算等工作
视图View: 负责数据展示、监听用户触摸等工作
控制器Controller: 负责业务逻辑、事件响应、数据加工等工作
在iOS中,M和V之间禁止通信,必须由C控制器层来协调M和V之间的变化,C对M和V的访问是不受限的
Controller 可以直接与 Model 对话(读写调用 Model),Model 通过 Notification 和 KVO 机制与 Controller 间接通信
Controller 可以直接与 View 对话,通过 outlet,直接操作 View,outlet 直接对应到 View 中的控件,View 通过 action 向 Controller 报告事件的发生(如用户 Touch 我了)。Controller 是 View 的直接数据源(数据很可能是 Controller 从 Model 中取得并经过加工了)。Controller 是 View 的代理(delegate),以同步 View 与 Controller
MVC的缺点在于并没有区分业务逻辑和业务展示, 这对单元测试很不友好
MVP:
MVP模式是MVC模式的一个演化版本(好像所有的模式都是出自于MVC~~),MVP全称Model-View-Presenter;
MVP的 V 层是由UIViewController 和UIView 共同组成;
Model:与MVC中的model没有太大的区别。主要提供数据的存储功能,一般都是用来封装网络获取的json数据的集合
Presenter:作为model和view的中间人,从model层获取数据之后传给view,使得View和model没有耦合
view 将委托presenter 对它自己的操作,(简单来说就是presenter发命令来控制view的交互,要你隐藏就隐藏,叫你show 你就乖乖的show)
presenter拥有对 view交互的逻辑(就是上面说的意思)
presenter跟model层通信,"Present"一方面通过Service层调用接口获取数据给Model层,并将数据转化成对适应UI的数据并更新view
presenter不需要依赖UIKit
view层是单一,因为它是被动接受命令,没有主动能力
presenter 作为业务逻辑的处理者,首先要向Service层拿数据赋值给model,所以它将可以向model层通信。其次,UI的处理权移交给了它,所以它需要与view成通讯,发送命令更新UI。同时,UI的响应将触发业务逻辑的处理,所以view 层向presenter层通讯,告诉他用户做了什么操作,需要你反馈对应的数据来更新UI。这样就完成了从用户交互获得交互反馈到整个业务逻辑。
关于C端和P端的循环引用的问题, 直接用weak关键字就可以解决了
总得来说MVP的好处就是解除view与model的耦合,使得view或model有更强的复用性
只需要初始化P层, 然后调P层的接口就可以了. 至于P层内部的逻辑, 我不需要知道
V层也只专注于视图的创建
M层只专注于模型的构建(字典->模型)
优点:
对Controller进行瘦身,View和Model之间不存在耦合,同时也将业务逻辑从View中抽离,复用性更好
缺点:
由于对视图的渲染放在了Presenter中,所以视图和Presenter的交互会过于频繁。还有一点需要明白,如果Presenter过多地渲染了视图,往往会使得它与特定的视图的联系过于紧密。一旦视图需要变更,那么Presenter也需要变更了
MVVM:
在MVVM 中,view 和 view controller正式联系在一起,我们把它们视为一个组件
view 和 view controller 都不能直接引用model,而是引用视图模型(viewModel)
viewModel 是一个放置用户输入验证逻辑,视图显示逻辑,发起网络请求和其他代码的地方
使用MVVM会轻微的增加代码量,但总体上减少了代码的复杂性
MVVM模式将Presenter改名为ViewModel,基本上与MVP模式完全一致。
唯一的区别是,它采用双向绑定(data-binding) : View<->ViewModel, ViewModel作为Model中值的映射,是数据发生改变时,通知View中发生改变,以后不需要考虑View和Model之间的交互更新,只需着手界面布局逻辑即可。
①View和Model 不直接关联,而是通过ViewModel作为枢纽,沟通View和Model之间的关系。
②View中控件的值与属性进行绑定,通过KVO键值观察(这样当model的值发生变化时,View会自动发生改变)
View和Model通过ViewModel实现动态关联
MVVM 的注意事项
view 引用viewModel ,但反过来不行(即不要在viewModel中引入#import UIKit.h,任何视图本身的引用都不应该放在viewModel中)(PS:基本要求,必须满足)
viewModel 引用model,但反过来不行
MVVM 的优势:
低耦合:View 可以独立于Model变化和修改,一个 viewModel 可以绑定到不同的 View 上
可重用性:可以把一些视图逻辑放在一个 viewModel里面,让很多 view 重用这段视图逻辑
独立开发:开发人员可以专注于业务逻辑和数据的开发 viewModel,设计人员可以专注于页面设计
可测试:通常界面是比较难于测试的,而 MVVM 模式可以针对 viewModel来进行测试
MVVM 的弊端:
数据绑定使得Bug 很难被调试。你看到界面异常了,有可能是你 View 的代码有 Bug,也可能是 Model 的代码有问题。数据绑定使得一个位置的 Bug 被快速传递到别的位置,要定位原始出问题的地方就变得不那么容易了
对于过大的项目,数据绑定和数据转化需要花费更多的内存(成本)
绑定是一种响应式的通信方式。当被绑定对象某个值的变化时,绑定对象会自动感知,无需被绑定对象主动通知绑定对象。可以使用KVO和RAC实现。例如在Label中显示倒计时,是V绑定了包含定时器的VM。
组件化:(使用cocoapods进行组件化的实现)
组件化方案的几种实现:
方案一:url-block
通过在启动时注册组件提供的服务,把调用组件使用的url和组件提供的服务block对应起来,保存到内存中。在使用组件的服务时,通过url找到对应的block,然后获取服务
出现的问题:
1、需要在内存中维护url-block的表,组件多了可能会有内存问题
2、url的参数传递受到限制,只能传递常规的字符串参数,无法传递非常规参数,如UIImage、NSData等类型
3、没有区分本地调用和远程调用的情况,尤其是远程调用,会因为url参数受限,导致一些功能受限
4、组件本身依赖了中间件,且分散注册使的耦合较多
方案二:protocol-class
通过protocol定义服务接口,组件通过实现该接口来提供接口定义的服务,具体实现就是把protocol和class做一个映射,同时在内存中保存一张映射表,使用的时候,就通过protocol找到对应的class来获取需要的服务
出现的问题:
依然没有解决组件依赖中间件的问题、内存中维护映射表的问题、组件的分散调用的问题
方案三:target-action
通过给组件包装一层wrapper来给外界提供服务,然后调用者通过依赖中间件来使用服务;其中,中间件是通过runtime来调用组件的服务,是真正意义上的解耦,也是该方案最核心的地方。具体实施过程是给组件封装一层target对象来对外提供服务,不会对原来组件造成入侵;然后,通过实现中间件的category来提供服务给调用者,这样使用者只需要依赖中间件,而组件则不需要依赖中间件
方案四:使用cocoapods进行组件化的实现)
具体就是建立一个项目工程的私有化仓库,然后把各个组件的podspec上传到私有仓库,在需要用到组件时,直接从仓库里面取
1.添加Podfile文件 pod init 然后会发现你的工程目录下多了Podfile文件
2.生成xcworkspace工程 pod install
3.新建一个Lib(自己起名)文件夹,用来存放组件库(其他独立工程)然后cd到Lib下 执行 pod lib create
XXX(工程名)
4.打开新建的XXX(工程名)工程里的Example,可以看到pods里面,有个ReplaceMe的文件,意思就是要替换它,换成我们自己需要对外提供的类
5.新建一个类,比如TRUXXX,复制粘贴到ReplaceMe同级目录下,并删掉ReplaceMe.m文件
6. 之后cd到Lib/TRUXXX/Example/文件目录下,执行pod install 这个时候在Development Pods文件下会多出这两个文件,这就是本地开发的pods文件
7.而Podfile的内容其实是
pod 'TRUXXX', :path => '../'
说明他获取的是本地路径
然后删除Example for TRUXXX里面的TRUXXX类,不然运行会因为类重复报错。
至此,一个组件的本地库就创建完成了。
8. 壳工程使用本地组件库
首先cd到壳工程LZDemo目录下,修改LZDemo的Podfile文件,增加
pod 'TRUXXX', :path => 'Lib/TRUXXX'
执行 pod install
组件需要对外提供依赖关系。所以我们还得多做一步操作,那就是增加podspec文件
以TRUXXX为例,cd到TRUXXX目录下,执行
git tag 0.1.0
git push --tags
这个tag分支就是将来提供给别人依赖的版本号分支,有了它,别人使用你的组件的时候就可以根据版本号来控制了。
改好后,在上传之前,最好先本地检查一下podspec是否合法
执行下面语句
pod lib lint --verbose
如果出现passed validation,说明通过,可以提交到cocoapods上了
成功后,就可以pod search到我们提交的库了
ps:如果搜不到,不是没传成功,是我们的本地搜索库没更新,可以先删除~/Library/Caches/CocoaPods目录下的search_index.json文件或者pod repo update一下
rm~/Library/Caches/CocoaPods/search_index.json
组件间通讯
1. Protocol注册方案
通过JJProtocolManager 作为中间转化
+ (void)registerModuleProvider:(id)provider forProtocol:(Protocol*)protocol;
+ (id)moduleProviderForProtocol:(Protocol *)protocol;
有组件对外提供的procotol和组件提供的服务由中间件统一管理,每个组件提供的procotol和服务是一一对应的。
例如:
在JJLoginProvider中:load方法会应用启动的时候调用,就会在JJProtocolManager进行注册。JJLoginProvider遵守了JJLoginProvider协议,这样就可以对外根据业务需求提供一些方法。
+ (void)load
{
[JJProtocolManager registerModuleProvider:[self new] forProtocol:@protocol(JJLoginProtocol)];
}
- (UIViewController *)viewControllerWithInfo:(id)userInfo needNew:(BOOL)needNew callback:(JJModuleCallbackBlock)callback{
CLoginViewController *vc = [[CLoginViewController alloc] init];
vc.jj_moduleCallbackBlock = callback;
vc.jj_moduleUserInfo = userInfo;
return vc;
}
这样就可以在需要登录业务模块的地方,通过JJProtocolManager取出JJLoginProtocol对应的服务提供者JJLoginProvider,直接获取。如下:
id<jjwebviewvcmoduleprotocol> provider = [JJProtocolManager moduleProviderForProtocol:@protocol(JJWebviewVCModuleProtocol)];
UIViewController *vc =[provider viewControllerWithInfo:obj needNew:YES callback:^(id info) {
if (callback) {
callback(info);
}
}];
vc.hidesBottomBarWhenPushed = YES;
[self.currentNav pushViewController:vc animated:YES];</jjwebviewvcmoduleprotocol>
2. URL注册方案 OPENURL
原理:
通过url注册服务, 其他地方通过url, 获取服务;框架在维护一个url-block的表格
特点:
每个业务组件, 都需要依赖这个框架
url维护成本高 硬解码
可以在组件内部任何地方调用/注册服务, 没有必要统一组件接口服务
[MGJRouter registerURLPattern:@"mgj://detail?id=:id" toHandler:^(NSDictionary *routerParameters) {
NSNumber *id = routerParameters[@"id"];
//create view controller with id
// pushview controller
}];
首页只需调用 [MGJRouter openURL:@"mgj://detail?id=404"] 就可以打开相应的详情页。
3. Target-Action runtime调用方案
原理:
每个组件, 提供一个统一披露的接口文件
额外的维护一个中间件的分类扩展(在此处进行硬解码 通过运行时进行物理解耦)
其他地方通过target-action;的方案进行交互
特点:
统一了组件api服务
组件与框架之间无依赖关系
需要额外维护中间件类扩展
实现:
我们主要是依赖CTMediator 这个中间件工具类中主要使用如下方法:
- (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params shouldCacheTarget:(BOOL)shouldCacheTarget
方法内部使用Runtime调用 需要传三个参数
当前需要调用的类名 (字符串)
当前需要调用类的方法名 (字符串)
需要传的参数 (字典形式)
# 通过Runtime 把字符串 转换类
Class targetClass = NSClassFromString(ClassString);
id target = [[targetClass alloc] init];
# 把字符串转换成事件
SEL action = NSSelectorFromString(actionString);
# 如果当前类中有这个事件 那就执行这个事件 把需要的参数传值
if ([target respondsToSelector:action]) {
return [target performSelector:action withObject:params];
}
4.使用cocoapods进行组件化的实现)
1.每个业务组件库里面会有一个控制器的配置文件(路由配置文件),标记着每个控制器的key;
2.在App每次启动时,组件通讯的工具类里面需要解析控制器配置文件(路由配置文件),将其加载进内存;
3. 在内存中查询路由配置,找到具体的控制器并动态生成类,然后使用==消息发送机制==进行调用函数、传参数、回调,都能做到。
5. 依赖注入
组件化的好处:
业务分层、解耦,使代码变得可维护;
有效的拆分、组织日益庞大的工程代码,使工程目录变得可维护;
便于各业务功能拆分、抽离,实现真正的功能复用;
业务隔离,跨团队开发代码控制和版本风险控制的实现;
模块化对代码的封装性、合理性都有一定的要求,提升开发同学的设计能力;
在维护好各级组件的情况下,随意组合满足不同客户需求;
//www.greatytc.com/p/59c2d2c4b737创建组件化的步骤
各个组件该如何进行拆分:
1. 项目主工程:主工程就是一个空壳子工程
2. 业务组件:业务组件就是各个独立的产品业务功能模块
3. 基础工具类组件:基础工具类是各个互相独立,没有任何依赖的工具组件。它们和其它的工具组件、业务组件等没有任何依赖关系。这类组件例如有:对数组,字典进行异常保护的Safe组件,对数组功能进行扩展Array组件,对字符串进行加密处理的加密组件等等。
4. 中间件组件:中间调度者就是一个功能独立的中间件组件
5. 基础UI组件:视图组件就比较常见了,例如我们封装的导航栏组件,Modal弹框组件,PickerView组件等。
6. 业务工具组件:这类组件是为各个业务组件提供基础功能的组件。这类组件可能会依赖到其他的组件。例如:网络请求组件,图片缓存组件,jspatch组件等等
详细操作步骤:
第一步:
我们先创建一个空的iOS工程项目:MainProject,这个空项目作为我们的主工程项目,就是上面所说的壳子工程项目,然后初始化pod
第二步:
我们创建一个空工程项目:ModuleA,这个ModuleA 项目作为我们的业务A组件。然后我们初始化pod,初始化podspec文件
第三步:
我们创建一个空工程项目:ModuleB,这个ModuleB 项目作为我们的业务B组件。然后我们初始化pod,初始化podspec文件
第四步:
我们创建一个空工程项目:ComponentMiddleware,这个项目就是我们上面所说的中间调度者。然后我们初始化pod,初始化podspec文件。
第五步:
我们创建一个空工程项目: ModuleACategory,这个工程是对应业务组件A的一个分类工程。然后我们初始化pod,初始化podspec文件。
第六步:
我们创建一个空工程项目: ModuleBCategory,这个工程是对应业务组件B的一个分类工程。然后我们初始化pod,初始化podspec文件。
第七步:
我们在主工程MainProject的Podfile中引入我们的业务组件B工程ModuleB,以及引入我们的ModuleB的分类工程:ModuleBCategory。然后我们pod install。这时已将这两个组件库引入到我们的主工程中了。
#import <ModuleBCategory/ComponentScheduler+ModuleB.h>
- (void)moduleB {
UIViewController *VC = [[ComponentScheduler sharedInstance] ModuleB_viewControllerWithCallback:^(NSString *result) {
NSLog(@"resultB: --- %@", result);
}];
[self.navigationController pushViewController:VC animated:YES];
}
第八步:
上面第七步中,我们用到了ModuleBCategory 这个分类工程。这个工程我们只对外暴露了两个文件。这两文件是上面的中间调度者的分类,也就是说是中间件的分类。我们先来看下这个分类文件的.h 和.m 实现
#import "ComponentScheduler+ModuleB.h"
@implementation ComponentScheduler (ModuleB)
- (UIViewController *)ModuleB_viewControllerWithCallback:(void(^)(NSString *result))callback {
NSMutableDictionary *params = [[NSMutableDictionary alloc] init];
params[@"callback"] = callback;
return [self performTarget:@"ModuleB" action:@"viewController" params:params shouldCacheTarget:NO];
}
@end
第九步:
这个分类的作用你可以理解为我们提前约定好Target的名字和Action的名字,因为这两个名字中间件组件中会用到。
因为上面第八步中引用到中间件工程,这里我们就来看下中间件工程到底做了什么工作。还记得上面第八步中,我们调用了一个中间件提供的函数:performTarget:action:params:shouldCacheTarget吧,这个是中间件核心函数。
这个函数最终调用到苹果官方提供的函数:[target performSelector:action withObject:params];
看到 performSelector: withObject: 大家应该就比较熟悉了,iOS的消息传递机制。
[Target_ModuleB performSelector:Action_viewController withObject:params];
上面这行伪代码意思是: Target_ModuleB这个类 调用它的 Action_viewController: 方法,然后传递的参数为 params。
细心的小伙伴们就会发现,我们没有看到过哪里有这个Target_ModuleB 类啊,更没有看到Target_ModuleB 调用它的 Action_viewController: 方法啊。
是的,这个Target_ModuleB类和类的Action_viewController方法就在第十步中讲解到。
第十步:
业务组件B除了提供组件B的业务功能外,业务组件B还需要为我们提供一个Target文件
#import "Target_ModuleB.h"
#import "ModuleBViewController.h"
@implementation Target_ModuleB
- (UIViewController *)Action_viewController:(NSDictionary *)params {
ModuleBViewController *VC = [[ModuleBViewController alloc] init];
return VC;
}
@end
从上面的实现文件中,我们可以看到,Target文件的作用也很简单,就是为我们提供导航跳转的目标控制器实例对象。这里的目标控制器实例就是业务组件B的ModuleBViewController 实例。