iOS 精品摘录

架构

2020-09-16  本文已影响0人  飞哥漂流记

设计一个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:https://www.jianshu.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

在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. 依赖注入

组件化的好处:

业务分层、解耦,使代码变得可维护;

有效的拆分、组织日益庞大的工程代码,使工程目录变得可维护;

便于各业务功能拆分、抽离,实现真正的功能复用;

业务隔离,跨团队开发代码控制和版本风险控制的实现;

模块化对代码的封装性、合理性都有一定的要求,提升开发同学的设计能力;

在维护好各级组件的情况下,随意组合满足不同客户需求;

https://www.jianshu.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 实例。

上一篇下一篇

猜你喜欢

热点阅读