OC Runtime实践

引言

关于什么是OC Runtime这里不再赘述,可以参考Objective-C Runtime简述这篇文章。
这里讨论Objective-C Runtime在实际开发中是如何使用的,并举栗子加以说明。要使用runtime,要先引入头文件#import <objc/runtime.h>

实践

1.动态的添加成员变量和方法,修改成员变量值

 //1.修改实例变量值
void changeInstanceVariableValue() {
    Person *person = [Person new];
    person.name = @"Tom";
    NSLog(@"name==%@", person.name);
    unsigned int count = 0;
    Ivar *ivars = class_copyIvarList([person class], &count);
    for (int i = 0; i < count; i++) {
        Ivar ivar = ivars[i];
        const char *varName = ivar_getName(ivar);
        NSString *name = [NSString stringWithUTF8String:varName];
        if ([name isEqualToString:@"_name"]) {
            object_setIvar(person, ivar, @"Jerry");
            break;
        }
    }
    NSLog(@"修改后 name==%@", person.name);
}

//c形式的函数,必须有两个指定参数(id self,SEL _cmd),参考oc runtime 消息(message)
void run(id self, SEL _cmd) {
    NSLog(@"%@ is running!", self);
}

//2.添加方法
//"v@:" 表示函数类型,v代表无返回值void,如果是i则代表int;@代表 id sel; : 代表 SEL _cmd;
void addMethod() {
    class_addMethod([Person class], @selector(run), (IMP)run, "v@:");
    Person *person = [Person new];
    person.name = @"Tom";
    //    [p run];
    [person performSelector:@selector(run)];
}

2.交换方法的实现,拦截并替换方法

OC Method Siwzzling的实现原理就是方法交换。利用方法交换,可以替换系统方法,在系统方法上增加额外的功能,比如打印一些必要的参数。

//3.交换方法实现
void exchangeMethodImplementation() {
    Method eatMethod = class_getInstanceMethod([Person class], @selector(eat));
    Method sleepMethod = class_getInstanceMethod([Person class], @selector(sleep));
    method_exchangeImplementations(eatMethod, sleepMethod);
    Person *person = [Person new];
    person.name = @"Tom";
    [person eat];
    [person sleep];
}

3.实现分类也可以增加属性

利用Associated Object可以实现分类添加属性。可以参考之前的博客:Objective-C Runtime之Associated Objects

4.实现NSCoding的自动归档和自动解档

自定义模型要实现归档和解档需要实现NSCoding协议的- (instancetype)initWithCoder:(NSCoder *)aDecoder- (void)encodeWithCoder:(NSCoder *)aCoder方法。
比如:Person类有名字、年龄、身高和体重4个属性:

@interface Person : NSObject
@property (copy, nonatomic) NSString *name;  ///< 名字
@property (assign, nonatomic) NSInteger age;  ///< 年龄
@property (assign, nonatomic) float height;  ///< 身高
@property (assign, nonatomic) float width;  ///< 体重
@end

通常归档和解档时的实现方式是这样的:

//解档
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
    if (self = [super init]) {
        _name = [aDecoder decodeObjectForKey:@"name"];
        _age = [aDecoder decodeIntegerForKey:@"age"];
        _height = [aDecoder decodeFloatForKey:@"height"];
        _width = [aDecoder decodeFloatForKey:@"width"];
    }
    return self;
}
//归档
- (void)encodeWithCoder:(NSCoder *)aCoder {
    [aCoder encodeObject:_name forKey:@"name"];
    [aCoder encodeInteger:_age forKey:@"age"];
    [aCoder encodeFloat:_height forKey:@"height"];
    [aCoder encodeFloat:_width forKey:@"width"];
}

一旦模型类的属性多时,这种重复劳动显得毫无意义。既然模型类继承自NSOject类,我们可以给NSObject增加实现自动归档和解档功能的分类,从而一劳永逸。

//NSObject+AutoArchive.h
#import <Foundation/Foundation.h>

//将归解档两个方法定义为宏,方便调用
#define  NSOBJECT_AUTOARCHIVE_METHOD \
- (instancetype)initWithCoder:(NSCoder *)aDecoder {\
    if (self = [super init]) {\
        [self decode:aDecoder];\
    }\
    return self;\
}\
\
- (void)encodeWithCoder:(NSCoder *)aCoder {\
    [self encode:aCoder];\
}

@interface NSObject (AutoArchive)
//不需要归解档的属性数组
- (NSArray *)ignoredPropertyNames;
//Archiver
- (void)encode:(NSCoder *)aCoder;
//Unarchiver
- (void)decode:(NSCoder *)aDecoder;
@end

//NSObject+AutoArchive.m
#import "NSObject+AutoArchive.h"
#import <objc/runtime.h>

@implementation NSObject (AutoArchive)

- (NSArray *)ignoredPropertyNames {
    return nil;
}

- (void)encode:(NSCoder *)aCoder {
    // 一层层父类往上查找,对父类的属性执行归档方法
    Class c = self.class;
    while (c &&c != [NSObject class]) {
        unsigned int count = 0;
        Ivar *ivars = class_copyIvarList([self class], &count);
        for (int i = 0; i < count; ++i) {
            Ivar ivar = ivars[i];
            NSString *ivarName = [NSString stringWithCString:ivar_getName(ivar) encoding:NSUTF8StringEncoding];
            if ([self respondsToSelector:@selector(ignoredPropertyNames)]) {
                if ([[self ignoredPropertyNames] containsObject:ivarName]) {
                    continue;
                }
            }
            //        ivarName = [ivarName substringFromIndex:1];
            id value = [self valueForKey:ivarName];
            [aCoder encodeObject:value forKey:ivarName];
        }
        free(ivars);
        c = [c superclass];
    }
}

- (void)decode:(NSCoder *)aDecoder {
    // 一层层父类往上查找,对父类的属性执行解档方法
    Class c = self.class;
    while (c &&c != [NSObject class]) {
        unsigned int count = 0;
        Ivar *ivars = class_copyIvarList([self class], &count);
        for (int i = 0; i < count; ++i) {
            Ivar ivar = ivars[i];
            NSString *ivarName = [NSString stringWithCString:ivar_getName(ivar) encoding:NSUTF8StringEncoding];
            if ([self respondsToSelector:@selector(ignoredPropertyNames)]) {
                if ([[self ignoredPropertyNames] containsObject:ivarName]) {
                    continue;
                }
            }
            //        ivarName = [ivarName substringFromIndex:1];
            id value = [aDecoder decodeObjectForKey:ivarName];
            [self setValue:value forKey:ivarName];
        }
        free(ivars);
        c = [c superclass];
    }
}
@end

于是乎,Person类的归档和解档就可以这样写:

#import "Person.h"
#import "NSObject+AutoArchive.h"

@interface Person ()<NSCoding>
@end

@implementation Person
NSOBJECT_AUTOARCHIVE_METHOD
@end

是不是变得很简洁了啊,如果有不需要归解档的属性就实现ignoredPropertyNames方法。源码可以参考这里。

5.实现字典和模型的自动转换

字典转模型的应用可以说是每个app必然会使用的场景,实现原理大概是:利用runtime遍历模型中所有属性,根据属性名,去字典中查找key,取出对应的值,给模型的属性赋值(ps:也可以使用KVC的setValuesForKeysWithDictionary:方法来字典转模型,但对于属性是其他模型或者模型数组的情况,我们需要重写setValue:forUndefinedKey:方法来判断key,防止crash,这样是不是还复杂一些、效率更低呢)。

字典转模型我们需要考虑三种特殊情况:

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

对于第一种情况,若模型属性个数小于字典键值数,这时候不需要任何处理,因为runtime是先遍历模型所有属性,再去字典中根据属性名找对应值进行赋值,多余的键值对不会去遍历;若模型属性个数大于字典键值数,在字典中取不到值,利用kvc赋值nil时会抛出异常,所以要判断一下,取值为nil时,直接跳过。代码如下:

- (void)setDict:(NSDictionary *)dict {
    unsigned int count = 0;
    Ivar *ivars = class_copyIvarList([self class], &count);
    for (int i = 0; i < count; ++i) {
        Ivar ivar = ivars[i];
        NSString *ivarName = [NSString stringWithCString:ivar_getName(ivar) encoding:NSUTF8StringEncoding];
        // 成员变量名转为属性名(去掉下划线 _ )
        ivarName = [ivarName substringFromIndex:1];
        // 取出字典的值
        id value = [dict objectForKey:ivarName];
        if (!value) {//取值为nil时,直接跳过
            continue;
        }//kvc赋值
        [self setValue:value forKey:ivarName];
    }
    free(ivars);
}

对于第二种情况,我们要取得嵌套模型的类型,然后递归生成模型对象再赋值给外层模型的对应模型属性,runtime的ivar_getTypeEncoding 方法获取模型对象类型,比如NSString *name–对应–>@"NSString", Person *p–对应–>@"Person", NSInteger age–对应–>i(可参看runtime type encoding)。注意的是,这里要剔除Foundation框架的以NS为前缀的类。代码如下:

- (void)setDict:(NSDictionary *)dict {
    unsigned int count = 0;
    Ivar *ivars = class_copyIvarList([self class], &count);
    for (int i = 0; i < count; ++i) {
        Ivar ivar = ivars[i];
        NSString *ivarName = [NSString stringWithCString:ivar_getName(ivar) encoding:NSUTF8StringEncoding];
        // 成员变量名转为属性名(去掉下划线 _ )
        ivarName = [ivarName substringFromIndex:1];
        // 取出字典的值
        id value = [dict objectForKey:ivarName];
        if (!value) {//取值为nil时,直接跳过
            continue;
        }
        // 获得成员变量的类型
        NSString *ivarType = [NSString stringWithCString:ivar_getTypeEncoding(ivar) encoding:NSUTF8StringEncoding];
        if ([ivarType hasPrefix:@"@"]) { //是对象
            ivarType = [ivarType substringWithRange:NSMakeRange(2, ivarType.length-3)]; //@"Person"----->Person
            if (![ivarType hasPrefix:@"NS"]) {//去除NS类
                Class class = NSClassFromString(ivarType);
                value =  [class objectWithDict:value];
            }
        }
        //kvc赋值
        [self setValue:value forKey:ivarName];
    }
    free(ivars);
}

第三种情况是模型的属性是一个数组,数组中是一个个模型对象,对象类型未知,调用objectClassName方法获取对象类型,然后递归创建对象类型,加入数组中,最后赋值给外层模型的数组属性,代码如下:

- (void)setDict:(NSDictionary *)dict {
    unsigned int count = 0;
    Ivar *ivars = class_copyIvarList([self class], &count);
    for (int i = 0; i < count; ++i) {
        Ivar ivar = ivars[i];
        NSString *ivarName = [NSString stringWithCString:ivar_getName(ivar) encoding:NSUTF8StringEncoding];
        // 成员变量名转为属性名(去掉下划线 _ )
        ivarName = [ivarName substringFromIndex:1];
        // 取出字典的值
        id value = [dict objectForKey:ivarName];
        if (!value) {//取值为nil时,直接跳过
            continue;
        }
        // 获得成员变量的类型
        NSString *ivarType = [NSString stringWithCString:ivar_getTypeEncoding(ivar) encoding:NSUTF8StringEncoding];
        if ([ivarType hasPrefix:@"@"]) { //是对象
            ivarType = [ivarType substringWithRange:NSMakeRange(2, ivarType.length-3)]; //@"Person"----->Person
            if (![ivarType hasPrefix:@"NS"]) {//去除NS类
                Class class = NSClassFromString(ivarType);
                value =  [class objectWithDict:value];
            } else if ([ivarType isEqualToString:@"NSArray"]) {//数组
                if ([self respondsToSelector:@selector(objectClassName)]) {
                    NSString *objectClassName = [self objectClassName];
                    if (objectClassName) {
                        Class class = NSClassFromString(objectClassName);
                        NSMutableArray *arr = [NSMutableArray array];
                        for (id item in value) {
                            [arr addObject:[class objectWithDict:item]];
                        }
                        value = arr;
                    }
                }
            }
        }
        //kvc赋值
        [self setValue:value forKey:ivarName];
    }
    free(ivars);
}

就像归档一样,可以增加Class c = self.class; while (c &&c != [NSObject class]) {...c = [c superclass]};来从子类到父类进行字典转换。

参考

https://github.com/Tuccuay/RuntimeSummary

Mantle

JSONModel

MJExtension

YYModel

https://segmentfault.com/a/1190000003882034

http://www.jianshu.com/p/ab966e8a82e2

http://www.jianshu.com/p/46dd81402f63

http://www.jianshu.com/p/8916ad5662a2