玩转iOS开发:装逼技术RunTime的应用(二)

文章分享至我的个人技术博客: https://cainluo.github.io/15069332898903.html


上一章我们耍了一些RunTime的应用, 但并没有完全讲完, 现在继续接着说, 如果没有看到上一篇文章的朋友可以去玩转iOS开发:装逼技术RunTime的应用(一)看看.

转载声明:如需要转载该文章, 请联系作者, 并且注明出处, 以及不能擅自修改本文.


给Category添加属性

在此之前, 我们了解到了一个类里面对应有的是isa指针, 但实际上这个isa指针是一个Class的结构体, 里面有这个类各式各样的信息, 其中有一个methodLists里面存储着实例方法列表.

而我们给对应的类添加Category就意味着是再给methodLists添加方法, 也就是因为这样子, 所以一般我们是不能在Category里面添加属性的, 虽然我们用了@property声明, 但也只是仅仅声明了getset的方法, 并没有去实现.

而在这里, 我们就是要利用RunTime去实现这个getset:

@interface NSObject (CLObject)

@property (nonatomic, copy) NSString *categoryName;

@end
复制代码
#import "NSObject+CLObject.h"

#import <objc/runtime.h>

@implementation NSObject (CLObject)

- (void)setCategoryName:(NSString *)categoryName {
    
    objc_setAssociatedObject(self, @"categoryName", categoryName, OBJC_ASSOCIATION_COPY);
}

- (NSString *)categoryName {
    
    return objc_getAssociatedObject(self, @"categoryName");
}

@end
复制代码
#import "RunTimeCategoryController.h"
#import "NSObject+CLObject.h"

@interface RunTimeCategoryController ()

@end

@implementation RunTimeCategoryController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.title = NSStringFromClass([self class]);

    self.view.backgroundColor = [UIColor whiteColor];

    NSObject *objc = [[NSObject alloc] init];
    
    objc.categoryName = @"NSObject+CLObject";
    
    NSLog(@"%@", objc.categoryName);
}

@end
复制代码

最终效果:

2017-10-04 11:41:57.159145+0800 RunTimeExample[1431:87605] -[RunTimeCategoryController viewDidLoad] 第27行 
 NSObject+CLObject
复制代码

这里解释一下:

Category这里添加属性, 其实并不是真的添加, 只是关联上而已.

这里的objc_setAssociatedObject有四个参数:

  • id _Nonnull object: 给哪个对象关联属性
  • const void * _Nonnull key: 属性的名称, 这里我们可以用OC字符串, 也可以用静态的void.
  • id _Nullable value: 属性的数值
  • objc_AssociationPolicy policy: 保存的策略, 这里共有五种保存的策略:
    • OBJC_ASSOCIATION_ASSIGN
    • OBJC_ASSOCIATION_RETAIN_NONATOMIC
    • OBJC_ASSOCIATION_COPY_NONATOMIC
    • OBJC_ASSOCIATION_RETAIN
    • OBJC_ASSOCIATION_COPY

光看字面就知道什么意思了, 这里就不多作解释.


利用RunTime将字典转成模型

我们在开发中, 都会用到字典转模型, 方式有:

  • 手动一一对应的给模型赋值
  • 利用KVC将字典转成模型, 但这里有一些不太好的地方
    • 必须保证字典中的属性模型中的属性一一对应.
    • 如果不一样的话, 就会调用setValue:forUndefinedKey:Key找不到.
    • 解决办法:
      • 重写setValue:forUndefinedKey:方法, 把系统的方法覆盖了, 就能继续使用KVC字典转模型了.
  • 利用RunTime实现字典转模型

主要思路: 利用RunTime遍历模型中所有的属性, 根据模型的属性名去字典中找对应的Key, 然后取出对应的值给模型的属性赋值.

考虑的问题:

  • 当字典的Key模型的属性对应不上, 这里有两种情况.
    • 当字典的Key数量大于模型的属性数量时, 这个时候我们不用去管, 因为RunTime是会先遍历模型中的所有属性, 然后再根据字典里的Key一一赋值, 多出来的就不会再看了.
    • 模型里的属性数量大于字典的Key的数量时, 这时候由于属性没有对应的值就会被赋值为nil, 就会导致Crash, 这个时候我们只要加个判断就好了.
  • 模型中嵌套另一个模型(模型里的某个属性是模型).
  • 数组里的对象是装着模型(模型的属性是数组, 而数组里装的对象都是模型).

这里我们写一个工具类, 针对上面三种情况用RunTime来转换模型:

字典转模型(模型的属性数量大于字典的Key数量)

NSObject添加分类方法:

+ (instancetype)cl_modelToDictionary:(NSDictionary *)dictionary {
    
    id object = [[self alloc] init];
    
    unsigned int count = 0;
    
    // 获取成员变量列表
    Ivar *ivarList = class_copyIvarList(self, &count);
    
    for (NSInteger i = 0; i < count; i++) {
        
        // 根据角标取出对应的成员变量
        Ivar ivar = ivarList[i];
        
        // 获取对应的属性名
        NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];
        
        // 从下标1开始取对应的key, 不然的话, 就会取到带"_"的key
        NSString *key = [ivarName substringFromIndex:1];
        
        // 根据属性名在字典中查找对应的value
        id value = dictionary[key];
        
        // 判断value是否为nil, 如果不是, 就给属性赋值
        // 当属性的数量大于字典Key的数量时的判断
        if (value) {
            
            [object setValue:value
                      forKey:key];
        }
    }
    
    return object;
}
复制代码

查看一下转换后的结果:

1

这里我们是采用class_copyIvarList的方式获取所有成员变量的, 而成员变量都是使用"_"符号进行命名的, 所以我们要处理一下, 当然也可以使用class_copyPropertyList, 命名就不用处理, 其他都差不多.

为什么使用class_copyIvarList呢? 在前面我们都看到过给分类添加属性的时候, 只是简单的声明了setget, 并没有实现

如果我们使用class_copyPropertyList, 可能就会漏掉了成员变量或者是没有实现setget方法的属性, 而使用class_copyIvarList就不会有这个问题.

字典转模型(模型中嵌套模型)

其实模型中嵌套模型这样子的例子并不少见, 几乎都是这样子的, 借助刚刚的例子, 再加以改进一下, 我们就可以实现解析模型嵌套模型的场景:

+ (instancetype)cl_modelToDictionary2:(NSDictionary *)dictionary {
    
    id object = [[self alloc] init];
    
    unsigned int count = 0;
    
    // 获取成员变量列表
    Ivar *ivarList = class_copyIvarList(self, &count);
    
    for (NSInteger i = 0; i < count; i++) {
        
        // 根据角标取出对应的成员变量
        Ivar ivar = ivarList[i];
        
        // 获取对应的属性名
        NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];
        
        // 获取成员变量的类型
        NSString *ivarType = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];

        // 从下标1开始取对应的key, 不然的话, 就会取到带"_"的key
        NSString *key = [ivarName substringFromIndex:1];
        
        // 根据属性名在字典中查找对应的value
        id value = dictionary[key];
        
        if ([value isKindOfClass:[NSDictionary class]] && ![ivarType hasPrefix:@"NS"]) {
            
            // 替换成员变量的类型
            ivarType = [ivarType stringByReplacingOccurrencesOfString:@"\"" withString:@""];
            ivarType = [ivarType stringByReplacingOccurrencesOfString:@"@" withString:@""];

            Class modelClass = NSClassFromString(ivarType);
            
            // 有对应的模型才需要转
            if (modelClass) {
                
                // 把字典转模型
                value = [modelClass cl_modelToDictionary2:value];
            }
        }
        
        // 判断value是否为nil, 如果不是, 就给属性赋值
        // 当属性的数量大于字典Key的数量时的判断
        if (value) {
            
            [object setValue:value
                      forKey:key];
        }
    }
    
    return object;
}
复制代码

查看一下转换后的结果:

2

数组中嵌套字典

在上面, 我们解决了前面两个问题, 最后一个问题就是在数组里嵌套模型, 这里我们需要先声明一个代理:

@protocol ModelDelegate <NSObject>

@optional

// 提供一些用来转换模型的协议, 只要遵守了这个协议, 就可以把数组中的字典转成模型
+ (NSDictionary *)cl_arrayToModelClass;

@end
复制代码
+ (instancetype)cl_modelToDictionary3:(NSDictionary *)dictionary {

    id object = [[self alloc] init];
    
    unsigned int count = 0;
    
    // 获取成员变量列表
    Ivar *ivarList = class_copyIvarList(self, &count);
    
    for (NSInteger i = 0; i < count; i++) {
        
        // 根据角标取出对应的成员变量
        Ivar ivar = ivarList[i];
        
        // 获取对应的属性名
        NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];
        
        // 从下标1开始取对应的key, 不然的话, 就会取到带"_"的key
        NSString *key = [ivarName substringFromIndex:1];
        
        // 根据属性名在字典中查找对应的value
        id value = dictionary[key];
        
        // 判断是否是数组类型
        if ([value isKindOfClass:[NSArray class]]) {
            
            // 判断能否响应代理方法
            if ([self respondsToSelector:@selector(cl_arrayToModelClass)]) {
                
                // 转换一下self
                id allSelf = self;
                
                // 获取数组中字典对应的模型
                NSString *classType = [allSelf cl_arrayToModelClass][key];
                
                // 生成对应的模型
                Class classModel = NSClassFromString(classType);
                
                NSMutableArray *modelArray = [NSMutableArray array];
                
                // 遍历字典里的数组
                for (NSDictionary *dictionary in value) {
                    
                    // 字典转模型
                    id model = [classModel cl_modelToDictionary3:dictionary];
                    
                    [modelArray addObject:model];
                }
                
                value = modelArray;
            }
        }
        
        // 判断value是否为nil, 如果不是, 就给属性赋值
        // 当属性的数量大于字典Key的数量时的判断
        if (value) {
            
            [object setValue:value
                      forKey:key];
        }
    }
    
    return object;
}
复制代码

然后在对应的模型解析时, 我们需要实现代理方法:

+ (NSDictionary *)cl_arrayToModelClass {
 
 // 我这里对应的类型是RunTimeDataList   
    return @{@"data" : @"RunTimeDataList"};
}
复制代码

查看一下转换后的结果:

3


工程地址

项目地址: https://github.com/CainRun/iOS-Project-Example/tree/master/RunTime/Five


最后

码字很费脑, 看官赏点饭钱可好

微信

支付宝