iOS开发 - 抛开表面看本质之iOS常用架构(MVC,MVP,MVVM)

Posted CodingFire

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了iOS开发 - 抛开表面看本质之iOS常用架构(MVC,MVP,MVVM)相关的知识,希望对你有一定的参考价值。

前言

既然是看本质,那我们今天要说的内容肯定不是常规的大家在网上都能搜到的内容,所以,我们今天就来说说别人没有写过的东西。具体来给大家讲讲什么是ios架构,什么是我们常说的MVC,MVP,MVVM。

在开始之前,想吐个槽。现在这面试动不动就问架构,有几个人是真正把架构玩明白的?我们按照网上别人写的博客说一遍,又融入了自己的几分理解?我们要明白的一点是:架构服务于人,而不是人服务于架构。

让很多人来讲架构,MVC,MVP,MVVM,说的都千篇一律,网上查到的内容也大相径庭,可能很多人讲完之后也没有明白这些架构具体要怎么写才能符合它的名字。其实很多人已经在使用这些架构了,只是自己不知道而已,这就是因为没有对架构有一个清晰的认知。

今天,我们打破这一现象,不讲概念,不画图,我们就来聊聊,什么是iOS下常用的MVC,MVP,MVVM,和什么情况下用这些架构。

1.MVC

博主不是个按套路出牌的人,你所熟知的model,view,controller博主一概不讲,博主要讲的就是:龙头凤尾猪肚子。

model和view通不通信,我们也不管,管那么多干嘛?我们要明白,代码服务于人,我们要根据具体的需求和代码表现来选择适合的传值和交互方式,MVC只是因为大多数的代码都写在了viewDidload下,或者说在viewController内,这才导致了我们所说的龙头凤尾猪肚子。

那么什么情况下会用MVC呢?简而言之,什么情况下都会用,这不等于废话吗?当然不是,现在,请你忘记所谓的架构和便捷方式,现在,你是一个新手了,你只知道AFNetworking请求数据,UIViewController创建页面,UIView创建视图,其他的东西,你一概不知道,这时候,一个MVC的基础架构就已经存在了,啥都不管,一股脑都在UIViewController里面调用和实现,各种全局参数和实例方法满天飞。数据在UIViewController里请求,拿到数据后直接去改变UIView上的参数,我们俗称:刷新UI。

非常好,这时候,你已经实现了一个很重的MVC架构的页面,按照此模式,你已经可以在MVC上一骑绝尘了。那么现在,还有人不知道什么是MVC框架么?还有人不会用MVC框架吗?

2.MVP

这龙头凤尾猪肚子的形容成为现实,领导和新来的开发看着我们写的代码陷入了沉思,内心大喊着MMP,如果这时候眼神能杀人,你已经死了无数次,脾气不好的可能直接就怼你了,脾气好的可能会质疑一下,然后建议你改一改代码方式,这个算是比较友好了。

那么MVC框架真的一无是处吗?

当然不是,假设你要写一个关于我们的界面,写一个只有一个联系方式的页面,你不用MVC框架还留着过年吗?甚至于任何一个简单的固定数据的页面,这时候谁要去用MVP,以至于MVVM框架,我只能说:请便!

永远记住一句话:框架服务于人,人服务于业务,所以一切框架最终都要服务于业务,我们的目的很简单,解耦,解耦,还是解耦,这样的简单页面,MVC足以应付,且几乎不存在解耦的问题,如果有,也可以通过简单的方式来解决,这可能会涉及MVP,因为一旦数据做中转,那可能就是MVP的范畴,但是别忘了,MVP变种于MVC,但MVC并不过时。

所以问题来了,什么是MVP?

我们可以简单理解为:MVP就是将庞大的Controller中的业务逻辑抽离到一个单独的类中的一种实现方式,这里面自然包括了UIVIew的布局,当然,MVP绝不仅限于此。所以,我们来思考下另一个问题:P层可不可以存在多个?可不可以?可不可以?快速给出答案,懵不懵?我们所看的固定式解说从没提到过这个问题,网上搜了一下,也没找到,起码没有很容易找到这个问题。

那么到底MVP中的P层可不可以有多个呢?我们假设可以吧,这样我们就可以给每一个呈现在屏幕上的UIView一个单独的P层来管理这一个UIView,这样是不是P层的压力也小了很多呢?这时候有一个新的问题,布局的时候怎么保证不同的P层内UIView的位置不出现问题呢?我们想想现在的布局方式:

1)绝对布局,frame的y和height写成固定的,这样就可以解决多个P层的问题(我们要先明确一点,P层内是包含了所管理的UIView的布局和所有的需要判断业务逻辑的,可能还包含了网络请求的Model的调用和数据的绑定,也就是UI刷新,这一点很重要);

2)相对布局,加约束,这样的话貌似就不是很好处理了,基于这种方式,我们考虑把UIView的创建和P层的创建写在同一级,这样好像可以解决了,就是不知道会不会有人说这违背了MVP的原则,大家给给意见,个人认为可以,只要具体的实现和逻辑是放在P层内的,我觉得这种方式反而可以对P层瘦身。

基于以上两条,大家对P层可以有多个的问题怎么看?网上搜的时候,看到有android的文章讲解多个P层的情况,那么我认为iOS也是可以的。

接下来我们要说的通信问题就成了多个P层之间存在的另一个问题,那么下一个问题就是,关于P层的通信问题,他们如何实现通信?

在搜到的相关内容中,都是说P层和model,和view进行双向通信,这里也有个明显的问题,没有实现双向绑定,如何实现双向通信?其实这个表述有误,关于P层的通信,虽然可以实现双向,但却并不是双向绑定的,不能做到model改变之后自动改变view的能力,这属于双向绑定的范畴。

model和view的通信:model通过在P层的刷新,最终返回到P层,P层再通过view暴露出来的接口(接口的概念来源于Java,iOS本没有接口这一概念,起码这么多年,博主没见过这种说法,多是从外界引入的概念,我们理解为暴露出来的方法即可),去刷新UIView。

view和model的通信:UIView在用户的操作下,完成了某一个状态,比如点赞,评论,这时候需要通知model获取数据,UIView在P层封装着,而点赞操作在P层下的UIView下的某一层,这里就是反向传值的运用,传到哪?传到P层,P层再去调用数据,数据再回到P层,P层再刷新UIView的UI,关于反向传值,我们常见的有三种方法,一是通过协议代理,二是通过传值block,三是通过Notification,除此之外貌似没有其他有效的方法了,当然,你可以选择观察者的KVO,但是在这种层级结构下似乎不是很友好,关于这个问题,我们在MVVM中再讨论。

以上是关于传值的路径问题,MVP没有我们想象的那么难,其实你早已运用到自己的项目中,P层不局限于某一种命名方式,某一种结构形式,你要按照这个思想,你可以任意操作,只有新手才会完全按照某种制式去写代码,老司机们,你们觉得呢?

3.MVVM

说起来MVVM,通常的说法是:在MVP的基础上做双向绑定,即为MVVM,VM层即是P层的另一种叫法,不过是做了双向绑定。

双向绑定即是数据的变化在回调中直接刷新UI,UI刷新之后直接去拿数据并进行UI刷新。总之,UI的刷新离不开数据的变化,数据的变化离不开UI刷新,这样的过程可以通过回调自动完成,而不需要再调用繁复的方法,此为双向数据绑定。

MVVM,博主还是推荐大家使用RAC来做双想绑定,还有其他的方式,大家可以自行选择,关于RAC的具体使用,大家自己去查看API即可,不是什么太难的东西。

MVP我们提到了KVO,但却没有推荐,原因是没有对其进行友好的封装,而在MVVM中,实现这种机制的条件就是观察者模式,所谓的监听,这才是实现双向绑定的本质。所以RAC全称是RACObserve,大家明白这个问题的根本就行,后期自己学习。

4.总结

总之,写代码呢,一定要思路清晰,将不同的模块分区管理,做到高聚合,低耦合,做到这点,就无关乎框架什么事情了,管你是用什么架构,我们的目的只有一个,那就是实现可移植,在此基础上,三种框架你可以随心所欲的去切换使用,没有最好的框架,只有最好的程序猿

博客写的好不好,你说了算,有问题,有质疑,欢迎评论区留言讨论。

iOS开发之数据存储

一、iOS应用数据存储的常用方式

- XML属性列表(plist)归档
- Preference(偏好设置) 本质还是通过“plist”来存储数据, 但是使用更简单(无需关注文件、文件夹路径和名称)
- NSKeyedArchiver归档(NSCoding) 把任何对象, 直接保存为文件的方式。
- SQLite3 当非常大量的数据存储时使用
- Core Data 就是对SQLite的封装   

二、获取路径

    - 获取bundle路径

      NSString *path = [NSBundle mainBundle].bundlePath;

    -获取沙盒路径

      NSString *home = NSHomeDirectory();

三、沙盒路径目录结构

    -Document          保存应用运行时生成的需要持久化的数据
    -library
        Caches         保存应用运行时生成的需要持久化的数据
        Preferences    保存应用的所有偏好设置
    -temp              保存应用运行时所需的临时数据

四、利用沙盒根目录拼接”Documents”字符串

  • 方式一:(拼接字符串)

    NSString *home =NSHomeDirectory();
    NSString *documents = [home stringByAppendingString:@“/Documents"];
    
  • 方式二:(作为路径的一部分)

    NSString *home =NSHomeDirectory();
    NSString*documents =[homestringByAppendingPathComponent:@"Documents"];
    
  • 方式三:()

    参数1:目标文件,参数二:作用域, 参数三:是否展开波浪线
    
    [NSSearchPathForDirectoriesInDomains(<#NSSearchPathDirectory directory#>, <#NSSearchPathDomainMask domainMask#>, <#BOOL expandTilde#>)]
    
    NSString * documents = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
    

五、获取caches文件夹路径

//获取caches文件夹

NSString * cachesPath = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, NO) lastObject];

六、获取偏好设置文件夹路径

  NSTemporaryDirectory()

七、plist数据的存储与读取

  • //数据存储

        NSArray * names = [NSArray arrayWithObjects:@"Bob",@"王宝", @"球童",@"Jack",nil];
    
      // 1.获取路径
     NSString * path = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
    
      // 2.拼接文件名
    
    NSString * filePath = [path stringByAppendingPathComponent:@"names.plist"];
    
    // 3. 存储
    // atomically 是否允许原子写入
    [names writeToFile:filePath atomically:YES];
    
    NSLog(@"%@",filePath);
    

  • 读取数据

    //1.获取数据路径
    NSString * path = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
    
    //2.拼接文件路径
    
    NSString * filePath = [path stringByAppendingPathComponent:@"names.plist"];
    
    //3.读取数据
    
    NSArray * names = [NSArray arrayWithContentsOfFile:filePath];
    
    NSLog(@"%@",names);
    

八、plist数据的存储与读取

  • //偏好设置存储

    //1.获取NSUserDefaults对象
    NSUserDefaults * userDefaults = [NSUserDefaults standardUserDefaults];
    
    //2.存储数据
    [userDefaults setObject:@"Jack" forKey:@"name"];
    
    [userDefaults setInteger:23 forKey:@"age"];
    
    [userDefaults setBool:YES forKey:@"sex"];
    
    //3.立即存储
    [userDefaults synchronize];
    
    NSLog(@"%@",NSHomeDirectory());
    

  • //读取偏好设置

    //1.获取NSUserDefaults对象
    NSUserDefaults * userDefaults = [NSUserDefaults standardUserDefaults];
    
    //2.读取数据
    NSString * name = [userDefaults objectForKey:@"name"];
    
    NSInteger age = [userDefaults integerForKey:@"age"];
    
    BOOL sex = [userDefaults boolForKey:@"sex"];
    
    NSLog(@"name =%@  age = %ld sex = %d",name,age,sex);
    

九、归档与反归档

“归档”:是一种可以把任何对象,直接保存为文件的方式。

- 如果对象是NSString、NSDictionary、NSArray、NSData、NSNumber等类型,可以直接用NSKeyedArchiver进行归档和恢复
- 不是所有的对象都可以直接用这种方法进行归档,只有遵守了NSCoding协议的对象才可以
- NSCoding协议有2个方法:
- encodeWithCoder:

     每次归档对象时,都会调用这个方法。一般在这个方法里面指定如何归档对象中的每个实例变量,可以使用encodeObject:forKey:方法归档实例变量

- initWithCoder:

 每次从文件中恢复(解码)对象时,都会调用这个方法。一般在这个方法里面指定如何解码文件中的数据为对象的实例变量,可以使用decodeObject:forKey方法解码实例变量

CZPerson.h
[x] 注意:改方法要遵守NSCodiing协议

#import <Foundation/Foundation.h>

@interface CZPerson : NSObject<NSCoding
@property (nonatomic,copy) NSString * name;
@property (nonatomic,copy) NSString * phone;
@end

CZPerson.m
[x] 注意:实现NSCodiing协议的方法*

#import "CZPerson.h"

@implementation CZPerson
//确定要存储对象的哪些属性
- (void)encodeWithCoder:(NSCoder *)aCoder

    [aCoder encodeObject:self.name forKey:@"name"];
    [aCoder encodeObject:self.phone forKey:@"phone"];

//确定要读取对象的哪些属性

- (id)initWithCoder:(NSCoder *)aDecoder

    if(self = [super init])
    
        self.name = [aDecoder decodeObjectForKey:@"name"];
        self.phone = [aDecoder decodeObjectForKey:@"phone"];
    
    return self;

@end

ViewController.m

#import "ViewController.h"
#import "CZPerson.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad 
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    //反归档 (反序列化)

    //1.获取路径
    NSString * path = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];

    //2.拼接文件名

    NSString * filePath = [path stringByAppendingPathComponent:@"person"];

    //3.通过反归档读取数据

    CZPerson * p = [NSKeyedUnarchiver unarchiveObjectWithFile:filePath];
    NSLog(@"name =%@  phone = %@",p.name,p.phone);
//    [self test01];

//归档 (序列化)
- (void) test01

    //创建person对象
    CZPerson * person = [[CZPerson alloc] init];

    //赋值
    person.name = @"JackMeng";
    person.phone = @"186000001";

    //获取路径
    NSString * path = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];

    //拼接文件名
    NSString * filePath = [path stringByAppendingPathComponent:@"person"];

    // 归档

    [NSKeyedArchiver archiveRootObject:person toFile:filePath];

    NSLog(@"%@",NSHomeDirectory());

- (void)didReceiveMemoryWarning 
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.


@end

ViewController.h

#import "ViewController.h"
#import "CZPerson.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad 
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    //反归档 (反序列化)

    //1.获取路径
    NSString * path = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];

    //2.拼接文件名

    NSString * filePath = [path stringByAppendingPathComponent:@"person"];

    //3.通过反归档读取数据

    CZPerson * p = [NSKeyedUnarchiver unarchiveObjectWithFile:filePath];
    NSLog(@"name =%@  phone = %@",p.name,p.phone);
//    [self test01];

//归档 (序列化)
- (void) test01

    //创建person对象
    CZPerson * person = [[CZPerson alloc] init];

    //赋值
    person.name = @"JackMeng";
    person.phone = @"186000001";



    //获取路径
    NSString * path = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];

    //拼接文件名
    NSString * filePath = [path stringByAppendingPathComponent:@"person"];

    // 归档

   [NSKeyedArchiver archiveRootObject:person toFile:filePath];

    NSLog(@"%@",NSHomeDirectory());

@end

以上是关于iOS开发 - 抛开表面看本质之iOS常用架构(MVC,MVP,MVVM)的主要内容,如果未能解决你的问题,请参考以下文章

iOS开发架构学习记录

iOS开发之数据存储

iOS开发之数据存储

iOS开发面试拿offer攻略之block篇

李洪强iOS开发之应用程序的本质与简单执行过程

李洪强iOS开发之 - 项目架构