iOS运行时(Runtime) 常用总结

runtime是一套底层的C语言API,包含很多强大实用的C语言数据类型和C语言函数,平时我们编写的OC代码,底层都是基于runtime实现的。它和runloop组合在一起使OC具有了面向对象功能。
本文主要介绍runtime的一些常用用法。将所用的的方法封装到一个工具里
本文主要介绍以下几个方面:
1.动态添加一个类
2.交换方法实现
3.获取类的属性列表
4.给分类动态添加属性
5.字典转模型
6.实现NSCoding的自动归档和解档
7.获取类的实例方法列表 getter,setter,对象方法等
8.获取协议列表

1.动态添加一个类
调用的方法是

  class_addMethod(<#Class  _Nullable __unsafe_unretained cls#>, <#SEL  _Nonnull name#>, <#IMP  _Nonnull imp#>, <#const char * _Nullable types#>)

class_addMethod方法有个参数,第一个参数是要添加方法的类,第二个参数是方法的SEL 可以理解为方法编号,一个SEL对应一个IMP,第三个参数则是提供方法实现的IMP,也就是implementation,是一个指针,指向方法的具体实现。第四个参数是方法的类型,一般写的是"V@:@",V表示返回值是Void,@表示是OC对象,:表示调用@selector方法。
方法实现如下:

/**
 往类上添加相应的方法与其实现
 @param fromclass 方法实现的类
 @param class 绑定方法的类
 @param methodSel 方法名
 @param methodselImp 对应方法实现的方法名
 */
+ (void)addMethodWithClass:(Class)class fromClass:(Class)fromclass method:(SEL)methodSel methodImp:(SEL)methodselImp
{
    Method method = class_getInstanceMethod(fromclass, methodSel);
    IMP methodIMP = method_getImplementation(method);
    const char *types = method_getTypeEncoding(method);
    class_addMethod(class, methodSel, methodIMP, types);
}

2.交换方法实现
SEL 和IMP的关系是一一对应的,如图


sel.png

方法交换其实也就是交换了他们的IMP,如图:


exchangeImp.png

实现代码为:

/**
 方法交换

 @param selone 方法1
 @param selTwo 方法2
 */
+ (void)exchangeMethodWithClsas:(Class)class Sel:(SEL)selone selTwo:(SEL)selTwo;
{
    Method firstMethod = class_getInstanceMethod(class, selone);
    Method secondMethod = class_getInstanceMethod(class, selTwo);
    method_exchangeImplementations(firstMethod, secondMethod);
}

3.获取类的属性列表
使用了class_copyPropertyList(Class,&count)来获取的属性列表,然后通过for循环通过property_getName()来获取每个属性的名字。当然使用property_getName()获取到的名字依然是C语言的char类型的指针,所以我们还需要将其转换成NSString类型,然后放到数组中一并返回。
代码如下:

/**
 获取类的属性列表 包括使用属性和公有属性 以及定义在延展中的属性

 @param class 类
 @return 数组 里面是字符串
 */
+ (NSArray *)getPropertyWithClass:(Class)class
{
    unsigned int count = 0;
    objc_property_t *propertyList = class_copyPropertyList(class, &count);
    NSMutableArray *mutableList = [NSMutableArray arrayWithCapacity:count];
    for (int i = 0; i < count; i++)
    {
        const char *propertyName = property_getName(propertyList[i]);
        [mutableList addObject:[NSString stringWithUTF8String:propertyName]];
    }
    free(propertyList);
    
    return [NSArray arrayWithArray:mutableList];
}

4.给分类动态添加属性
系统 NSObject 添加一个分类,我们知道在分类中是不能够添加成员属性的,虽然我们用了@property,但是仅仅会自动生成get和set方法的声明,并没有带下划线的属性和方法实现生成。但是我们可以通过runtime就可以做到给它方法的实现。
新建一个测试的分类UIImage+Test
代码如下:
.h

#import 

@interface UIImage (Test)

@property (nonatomic,strong)NSString *urlImage;

@end

.m

#import "UIImage+Test.h"
#import 
@implementation UIImage (Test)

- (NSString *)urlImage
{
    return objc_getAssociatedObject(self,  (const void*)@"urlImage");
}
- (void)setUrlImage:(NSString *)urlImage
{
    // objc_setAssociatedObject(将某个值跟某个对象关联起来,将某个值存储到某个对象中)
    
    // object:给哪个对象添加属性
    
    // key:属性名称
    
    // value:属性值
    
    // policy:保存策略
    objc_setAssociatedObject(self, (const void*)@"urlImage", urlImage, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
@end

5.字典转模型
转载至: https://juejin.im/post/593f77085c497d006ba389f0
字典转模型的方式:
一个一个的给模型属性赋值(初学者)。
字典转模型KVC实现

KVC 字典转模型弊端:必须保证,模型中的属性和字典中的key 一一对应。
如果不一致,就会调用[ setValue:forUndefinedKey:] 报key找不到的错。

分析:模型中的属性和字典的key不一一对应,系统就会调用setValue:forUndefinedKey:报错。
解决:重写对象的setValue:forUndefinedKey:,把系统的方法覆盖,就能继续使用KVC,字典转模型了。

字典转模型 Runtime 实现

思路:利用运行时,遍历模型中所有属性,根据模型的属性名,去字典中查找key,取出对应的值,给模型的属性赋值(从提醒:字典中取值,不一定要全部取出来)。

考虑情况:
1.当字典的key和模型的属性匹配不上。
2.模型中嵌套模型(模型属性是另外一个模型对象)。
3.数组中装着模型(模型的属性是一个数组,数组中是一个个模型对象)。

注解:根据上面的三种特殊情况,先是字典的key和模型的属性不对应的情况。不对应有两种,一种是字典的键值大于模型属性数量,这时候我们不需要任何处理,因为runtime是先遍历模型所有属性,再去字典中根据属性名找对应值进行赋值,多余的键值对也当然不会去看了;另外一种是模型属性数量大于字典的键值对,这时候由于属性没有对应值会被赋值为nil,就会导致crash,我们只需加一个判断即可。考虑三种情况下面一一注解;

步骤:提供一个NSObject分类,专门字典转模型,以后所有模型都可以通过这个分类实现字典转模型。

MJExtension 字典转模型实现

底层也是对 runtime 的封装,才可以把一个模型中所有属性遍历出来。(你之所以看不懂,是 MJ 封装了很多层而已_.)。

这里针对字典转模型 KVC 实现,就不做详解了,如果你 对 KVC 详解使用或是实现原理 不是很清楚的,可以参考 实用「KVC编码 & KVO监听

字典转模型 Runtime 方式实现:

说明:下面这个示例,是考虑三种情况包含在内的转换示例,具体可以看图上的注解

Runtime 字典转模型
5-1、runtime 字典转模型-->字典的 key 和模型的属性不匹配「模型属性数量大于字典键值对数」,这种情况处理如下:

// Runtime:根据模型中属性,去字典中取出对应的value给模型属性赋值

// 思路:遍历模型中所有属性->使用运行时

代码为:

+ (instancetype)modelWithDict:(NSDictionary *)dict
{
    
    // 1.创建对应的对象
    
    id objc = [[self alloc] init];
    
    
    
    // 2.利用runtime给对象中的属性赋值
    
    /**
     
     class_copyIvarList: 获取类中的所有成员变量
     
     Ivar:成员变量
     
     第一个参数:表示获取哪个类中的成员变量
     
     第二个参数:表示这个类有多少成员变量,传入一个Int变量地址,会自动给这个变量赋值
     
     返回值Ivar *:指的是一个ivar数组,会把所有成员属性放在一个数组中,通过返回的数组就能全部获取到。
     
     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)];
        
        
        
        // 处理成员变量名->字典中的key(去掉 _ ,从第一个角标开始截取)
        
        NSString *key = [ivarName substringFromIndex:1];
        
        
        
        // 根据成员属性名去字典中查找对应的value
        
        id value = dict[key];
        
        
        
        // 【如果模型属性数量大于字典键值对数理,模型属性会被赋值为nil】
        
        // 而报错 (could not set nil as the value for the key age.)
        
        if (value) {
            
            // 给模型中属性赋值
            
            [objc setValue:value forKey:key];
            
        }
        
        
        
    }
    
    
    
    return objc;
    
}

注:
这里在获取模型类中的所有属性名,是采取 class_copyIvarList 先获取成员变量(以下划线开头) ,然后再处理成员变量名->字典中的key(去掉 _ ,从第一个角标开始截取) 得到属性名。
原因:
Ivar:成员变量,以下划线开头,Property 属性
获取类里面属性 class_copyPropertyList
获取类中的所有成员变量 class_copyIvarList

{

    int _a; // 成员变量

}

@property (nonatomic, assign) NSInteger attitudes_count; // 属性

这里有成员变量,就不会漏掉属性;如果有属性,可能会漏掉成员变量;
使用runtime字典转模型获取模型属性名的时候,最好获取成员属性名Ivar因为可能会有个属性是没有setter和``getter方法的。

5-2.runtime 字典转模型-->模型中嵌套模型「模型属性是另外一个模型对象」,这种情况处理如下:

/**
 字典转模型 模型中包括模型

 @param dict 字典
 @return 结果
 */
+ (instancetype)modelWithDict2:(NSDictionary *)dict

{
    
    // 1.创建对应的对象
    
    id objc = [[self alloc] init];
    
    
    
    // 2.利用runtime给对象中的属性赋值
    
    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
        
        id value = dict[key];
        
        
        
        //--------------------------- 我是分割线 ------------------------------//
        
        //
        
        // 二级转换:如果字典中还有字典,也需要把对应的字典转换成模型
        
        // 判断下value是否是字典,并且是自定义对象才需要转换
        
        if ([value isKindOfClass:[NSDictionary class]] && ![ivarType hasPrefix:@"NS"]) {
            
            
            
            // 字典转换成模型 userDict => User模型, 转换成哪个模型
            
            // 根据字符串类名生成类对象
            
            Class modelClass = NSClassFromString(ivarType);
            
            
            
            if (modelClass) { // 有对应的模型才需要转
                
                // 把字典转模型
                
                value = [modelClass modelWithDict2:value];
                
            }
            
        }
        
        
        
        // 给模型中属性赋值
        
        if (value) {
            
            [objc setValue:value forKey:key];
            
        }
        
    }
    
    return objc;
    
}

5-3.runtime 字典转模型-->数组中装着模型「模型的属性是一个数组,数组中是字典模型对象」,这种情况处理如下:
Runtime:根据模型中属性,去字典中取出对应的value给模型属性赋值
思路:遍历模型中所有属性->使用运行时


+ (instancetype)modelWithDict3:(NSDictionary *)dict

{
    
    // 1.创建对应的对象
    
    id objc = [[self alloc] init];
    
    
    
    // 2.利用runtime给对象中的属性赋值
    
    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)];
        
        
        
        // 处理成员属性名->字典中的key(去掉 _ ,从第一个角标开始截取)
        
        NSString *key = [ivarName substringFromIndex:1];
        
        
        
        // 根据成员属性名去字典中查找对应的value
        
        id value = dict[key];
        
        
        
        
        
        //--------------------------- 我是分割线 ------------------------------//
        
        //
        
        
        
        // 三级转换:NSArray中也是字典,把数组中的字典转换成模型.
        
        // 判断值是否是数组
        
        if ([value isKindOfClass:[NSArray class]]) {
            
            // 判断对应类有没有实现字典数组转模型数组的协议
            
            // arrayContainModelClass 提供一个协议,只要遵守这个协议的类,都能把数组中的字典转模型
            
            if ([self respondsToSelector:@selector(arrayContainModelClass)]) {
                
                
                
                // 转换成id类型,就能调用任何对象的方法
                
                id idSelf = self;
                
                
                
                // 获取数组中字典对应的模型
                
                NSString *type =  [idSelf arrayContainModelClass][key];
                
                
                // 生成模型
                
                Class classModel = NSClassFromString(type);
                
                NSMutableArray *arrM = [NSMutableArray array];
                
                // 遍历字典数组,生成模型数组
                
                for (NSDictionary *dict in value) {
                    
                    // 字典转模型
                    
                    id model =  [classModel modelWithDict3:dict];
                    
                    [arrM addObject:model];
                    
                }
                
                
                
                // 把模型数组赋值给value
                
                value = arrM;
                
                
                
            }
            
        }
        
        
        
        // 如果模型属性数量大于字典键值对数理,模型属性会被赋值为nil,而报错
        
        if (value) {
            
            // 给模型中属性赋值
            
            [objc setValue:value forKey:key];
            
        }
        
    }
    
    return objc;
    
}

runtime字典转模型-->数组中装着模型 打印输出
总结:我们既然能获取到属性类型,那就可以拦截到模型的那个数组属性,进而对数组中每个模型遍历并字典转模型,但是我们不知道数组中的模型都是什么类型,我们可以声明一个方法,该方法目的不是让其调用,而是让其实现并返回模型的类型。

6.实现NSCoding的自动归档和解档
在开发项目的过程中,我们经常会用到需要对一个模型进行本地存储,这样就好对对模型惊喜解档和归档,也就是序列化。一般我们是这样写的
eg:
.h

#import 
@class TestModel;
@protocol PersonDelegate 
- (void)test;
@end

@interface Person : NSObject
@property (nonatomic,strong)NSString *name;
@property (nonatomic,assign)NSInteger age;
@property (nonatomic,assign)BOOL sex;
@property (nonatomic,strong)TestModel *model;
@end

.m


#import "Person.h"
@interface Person()

@end
@implementation Person

- (void)encodeWithCoder:(NSCoder *)aCoder
{
    [aCoder encodeObject:_name forKey:@"name"];
    [aCoder encodeInteger:_age forKey:@"age"];
    [aCoder encodeBool:_sex forKey:@"sex"];
    [aCoder encodeObject:_model forKey:@"model"];
}

- (instancetype)initWithCoder:(NSCoder *)aDecoder
{
    self = [super init];
    if (self)
    {
        self.name  = [aDecoder decodeObjectForKey:@"name"];
        self.age   = [aDecoder decodeIntegerForKey:@"age"];
        self.sex   = [aDecoder decodeBoolForKey:@"sex"];
        self.model = [aDecoder decodeObjectForKey:@"model"];
    }
    
    return self;
    
}
@end

如果这里有100个属性,或者更多,那么再手写就太麻烦了。
如果使用runtime,可以很好的解决这个问题。
代码如下:

- (void)encodeWithCoder:(NSCoder *)encoder

{
    
    unsigned int count = 0;
    
    Ivar *ivars = class_copyIvarList([self class], &count);
    
    
    
    for (int i = 0; i

也可定义两个宏 这样就可以在各处调用了
eg:

#define encodeRuntime(A) \
- (void)encodeWithCoder:(NSCoder *)aCoder\
{\
unsigned int count = 0;\
Ivar *ivars = class_copyIvarList([self class], &count);\
for (int i = 0; i

在.m方法里直接调用就可以了

#import "Person.h"
#import 
@interface Person()
@end
@implementation Person

encodeRuntime(self)

initCoderRuntime(self)

@end

7.获取类的实例方法列表 getter,setter,对象方法等,不能获取类方法

+ (NSArray *)getInstaceMethodListWithClass:(Class)class
{
    unsigned int count = 0;
    Method *methodList = class_copyMethodList(class, &count);
    NSMutableArray *mutableList = [NSMutableArray arrayWithCapacity:count];
    for (unsigned int i = 0; i

8.获取协议列表

+ (NSArray *)getProtocolListWithClass:(Class)class
{
    unsigned int count = 0;
    __unsafe_unretained Protocol **protocolList = class_copyProtocolList(class, &count);
    NSMutableArray *mutableList = [NSMutableArray arrayWithCapacity:count];
    for (unsigned int i = 0; i

Demo地址

你可能感兴趣的:(iOS运行时(Runtime) 常用总结)