iOS 开发:Runtime(详解六)字典转模型

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了iOS 开发:Runtime(详解六)字典转模型相关的知识,希望对你有一定的参考价值。

参考技术A 在日常开发中,将网络请求中获取的 JSON 数据转为数据模型,是我们开发中必不可少的操作。
通常我们会选用诸如 YYModel 、 JSONModel 或者 MJExtension 等第三方框架来实现这一过程。这些框架实现原理的核心就是 Runtime 和 KVC ,以及 Getter / Setter 。

实现的大体思路如下:借助 Runtime 可以动态获取 成员列表 的特性,遍历模型中所有属性,然后以获取到的属性名为 key ,在 JSON 字典中寻找对应的值 value ;再使用 KVC 或直接调用 Getter / Setter 将每一个对应 value 赋值给模型,就完成了字典转模型的目的。

从这份 JSON 中可以看出,字典中取值除了 字符串 之外,还有 数组 和 字典 。那么在将字典转换成数据模型的时候,就要考虑 模型嵌套模型 、 模型嵌套模型数组 的情况了。

经过分析,我们总共需要三个模型: XXStudentModel、XXAdressModel、XXCourseModel。

NSObject+XXModel.h、NSObject+XXModel.m 就是我们用来解决字典转模型所创建的分类,协议中的 + (NSDictionary *)modelContainerPropertyGenericClass 方法用来告诉分类特殊字段的处理规则,比如 id --> uid。

ios开发runtime学习五:KVC以及KVO,利用runtime实现字典转模型

一:KVC和KVO的学习

#import "StatusItem.h"
/*
 1:总结:KVC赋值:1:setValuesForKeysWithDictionary实现原理:遍历字典,得到所有的key,value值,再利用kvc, setVaue forkey来为value赋值 2: [item setValue:@"来自即刻笔记" forKey:@"source"],内部的底层实现,
 1.首先去模型中查找有没有setSource,找到,直接调用赋值 [self setSource:@"来自即刻笔记"]
 2.去模型中查找有没有source属性,有,直接访问属性赋值  source = value
 3.去模型中查找有没有_source属性,有,直接访问属性赋值 _source = value
 4.找不到,就会直接报错 setValue:forUndefinedKey:报找不到的错误
  当系统找不到就会调用这个方法,报错- (void)setValue:(id)value forUndefinedKey:(NSString *)key可以重写此方法更改key值
 3:KVC四个方法:利用kvc可以访问成员变量和属性,setValue,value为属性值,forKey,key为属性名,forKeyPath为键值路径,例如在model中有如下属性定义:
    @property (nonatomic, strong) BankAccount *account;
    keyPath:
    [zhangSan setValue:@150 forKeyPath:@"account.balance"];
 
 - (id)valueForKey:(NSString *)key;
 - (id)valueForKeyPath:(NSString *)keyPath;
 - (void)setValue:(id)value forKey:(NSString *)key;
 - (void)setValue:(id)value forKeyPath:(NSString *)keyPath;
 
 4:KVO:键值观察机制,用于对属性的value值的改变做监听:用法:
 @interface BankAccount : NSObject
 @property (nonatomic, assign) NSInteger balance;
 @end
 
 @interface Person : NSObject
 @property (nonatomic, strong) BankAccount *account;
 @end
 
 @implementation Person
 - (instancetype)init {
 ...
 // 注册Observer:
 [self.account addObserver:self
 forKeyPath:@"balance"
 options:NSKeyValueObservingOptionNew |
 NSKeyValueObservingOptionOld
 context:nil];
 ...
 }
 
 - (void)dealloc {
 // 不要忘了removeObserver
 [self.account removeObserver:self forKeyPath:@"balance"];
 }
 
 // 属性变化的回调方法:
 - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
 if ([keyPath isEqualToString:@"balance"]) {
 NSLog(@"Balance was %@.", change[NSKeyValueChangeOldKey]);
 NSLog(@"Balance is %@ now.", change[NSKeyValueChangeNewKey]);
 }
 }
 @end
 
 - (void)testKVO {
 Person *zhangSan = [[Person alloc] initWithName:@"ZhangSan" andBalance:20];
 // 无论是用点语法还是KVC的方法都会触发回调:
 zhangSan.account.balance = 150;
 [zhangSan setValue:@250 forKeyPath:@"account.balance"];
 }

 
 */
@interface StatusItem ()
@property (nonatomic,copy)NSString *hello;
@end
@implementation StatusItem

// 模型只保存最重要的数据,导致模型的属性和字典不能一一对应

+ (instancetype)itemWithDict:(NSDictionary *)dict
{
    StatusItem *item = [[self alloc] init];
    
    // KVC:把字典中所有值给模型的属性赋值
    [item setValuesForKeysWithDictionary:dict];
    
    // 拿到每一个模型属性,去字典中取出对应的值,给模型赋值
    // 从字典中取值,不一定要全部取出来
    // MJExtension:字典转模型 runtime:可以把一个模型中所有属性遍历出来
    // MJExtension:封装了很多层
//    item.pic_urls = dict[@"pic_urls"];
//    item.created_at = dict[@"created_at"];
    
    // KVC原理:
    // 1.遍历字典中所有key,去模型中查找有没有对应的属性
    [dict enumerateKeysAndObjectsUsingBlock:^(id  _Nonnull key, id  _Nonnull value, BOOL * _Nonnull stop) {
        
        // 2.去模型中查找有没有对应属性 KVC
        // key:source value:来自即刻笔记
        // [item setValue:@"来自即刻笔记" forKey:@"source"]
        [item setValue:value forKey:key];
        
        
    }];
    
    return item;
}


// 重写系统方法? 1.想给系统方法添加额外功能 2.不想要系统方法实现
// 系统找不到就会调用这个方法,报错
- (void)setValue:(id)value forUndefinedKey:(NSString *)key
{
    
}



@end

二:利用runtime实现字典转模型

#import "ViewController.h"
#import "NSDictionary+Property.h"
#import "StatusItem.h"
#import "NSObject+Model.h"
@interface ViewController ()

@end
/*
 总结:1:项目中的文件都保存在mainBundle里,读取项目中的本地信息:[[NSBundle mainBundle] pathForResource:@"status.plist" ofType:nil]; 得到本地路径path,再看项目中的文件根节点是字典还是数组,再从中读取本地路径filer:dictionaryWithContentsOfFile读取 。若是获取网络端的路径:dictionaryWithContentsOfUrl
 */
@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
   
    
    // 获取文件全路径
    NSString *filePath = [[NSBundle mainBundle] pathForResource:@"status.plist" ofType:nil];
    
    // 文件全路径
    NSDictionary *dict = [NSDictionary dictionaryWithContentsOfFile:filePath];
    
    // 设计模型,创建属性代码 => dict
//    [dict[@"user"] createPropertyCode];
    
    // 字典转模型:KVC,MJExtension
    StatusItem *item = [StatusItem modelWithDict:dict];
    
    
}

@end
#import <Foundation/Foundation.h>
// 字典转模型
@interface NSObject (Model)

+ (instancetype)modelWithDict:(NSDictionary *)dict;

@end
#import "NSObject+Model.h"
#import <objc/message.h>
/*
 
总结:MJExtension字典转模型的底层核心实现:runtime实现字典转模型。
 
 1:因为模型model都继承NSObject,所以可以给系统类写分类进行拓展,子类继承NSObject,也就是继承了扩展的方法。所以模型转字典的方法考虑给NSObject写一个分类进行方法的拓展
 2:在分类中若是对象方法,self指的是调用该方法的对象,类方法中self指的是调用该方法的类。方法的设计:类方法简单粗暴,直接用类去调用,字典转模型方法获得所转换的模型,不用创建对象,并且会将调用方法的类作为参数传进方法中(对象方法也如此)
 
 3:原理:runtime:根据模型中属性,去字典中取出对应的value给模型属性赋值
 1:先获取模型中所有成员变量 key
 参数意义:
 // 获取哪个类的成员变量
 // count:成员变量个数 int *类型
 unsigned int count = 0;
 // 获取成员变量数组
 Ivar *ivarList = class_copyIvarList(self, &count);
 
 注意:1:int *count ,此count的类型为int *类型,当作为参数的时候,需要传入一个int *类型的指针,指针里存放的都是内存地址,也就是将地址作为参数传递,当方法执行完毕后,系统会拿到*count 进行赋值
 int a = 2;
 int b = 3;
 int c = 4;
 int arr[] = {a,b,c};
 int *p = arr;
 p[0];
 NSLog(@"%d %d",p[0],p[1]);
 
     2: 获取类里面属性
        class_copyPropertyList(<#__unsafe_unretained Class cls#>, <#unsigned int *outCount#>)
 
        获取类里面所有方法
        class_copyMethodList(<#__unsafe_unretained Class cls#>, <#unsigned int *outCount#>)// 本质:创建谁的对象
 
     3:获取模型中所有成员变量 key,Ivar:成员变量 以下划线开头,相当于一个数组
 // 获取哪个类的成员变量
 // count:成员变量个数
 unsigned int count = 0;
 // 获取成员变量数组
 Ivar *ivarList = class_copyIvarList(self, &count);
 
 4:具体实现:获取成员变量名字:ivar_getName(ivar),属于c语言字符串,所以要进行UTF8编码  获取成员变量类型:ivar_getTypeEncoding(ivar)
 // 获取成员变量名字
 NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];
 // 获取成员变量类型
 NSString *ivarType = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];
 
 注意:1:字符串替换:stringByReplacingOccurrencesOfString  2:字符串截取:substringFromIndex:包括该index   substringToIndex:不包括该index 3:前后缀:hasPrefix前缀,hasSuffix:后缀 4:是否包含某个字符串:containString 5:字符串转换为Class:NSClassFromString: // 获取类
 Class modelClass = NSClassFromString(ivarType);
 
 value = [modelClass modelWithDict:value];

  6:一般某个方法接受传递进来的参数的时候,要判断参数是否为空,为nil或是为空值,给某个值赋值的时候,也要判断该值是否存在:
 // 给模型中属性赋值
 if (value) {
 [objc setValue:value forKey:key];
 }


 
 */




@implementation NSObject (Model)


// Ivar:成员变量 以下划线开头
// Property:属性
+ (instancetype)modelWithDict:(NSDictionary *)dict
{
    id objc = [[self alloc] init];
    
    // runtime:根据模型中属性,去字典中取出对应的value给模型属性赋值
    // 1.获取模型中所有成员变量 key
    // 获取哪个类的成员变量
    // count:成员变量个数
    unsigned int count = 0;
    // 获取成员变量数组
    Ivar *ivarList = class_copyIvarList(self, &count);
    
    // 遍历所有成员变量
    for (int i = 0; i < count; i++) {
        // 获取成员变量
        Ivar ivar = ivarList[i];
        
        // 获取成员变量名字
        NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];
        // 获取成员变量类型
        NSString *ivarType = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];
        // @\"User\" -> User
        ivarType = [ivarType stringByReplacingOccurrencesOfString:@"\"" withString:@""];
        ivarType = [ivarType stringByReplacingOccurrencesOfString:@"@" withString:@""];
        // 获取key
        NSString *key = [ivarName substringFromIndex:1];
        
        // 去字典中查找对应value
        // key:user  value:NSDictionary
        
        id value = dict[key];
        
        // 二级转换:判断下value是否是字典,如果是,字典转换层对应的模型
        // 并且是自定义对象才需要转换
        if ([value isKindOfClass:[NSDictionary class]] && ![ivarType hasPrefix:@"NS"]) {
            // 字典转换成模型 userDict => User模型
            // 转换成哪个模型

            // 获取类
            Class modelClass = NSClassFromString(ivarType);
            
            value = [modelClass modelWithDict:value];
        }
        
        // 给模型中属性赋值
        if (value) {
            [objc setValue:value forKey:key];
        }
    }
        
    return objc;
}


void test(int *count){
    *count = 3;
}

@end

 

以上是关于iOS 开发:Runtime(详解六)字典转模型的主要内容,如果未能解决你的问题,请参考以下文章

详解 Objective-C 中的 Runtime(下)

iOS开发UI篇—字典转模型

iOS开发之使用Runtime给Model类赋值

iOS开发UI篇—字典转模型

iOS 模块分解—「Runtime面试工作」看我就 🐒 了 ^_^.

ios开发网络学习二:URL转码以及字典转模型框架MJExtension的使用