iOS 面试题(十):runtime 使用——(动态添加方法/动态交换方法/动态添加属性)

1.动态添加方法

应用场景:如果一个类方法非常多,加载类到内存的时候也比较耗费资源,需要给每个方法生成映射表,可以使用动态给某个类,添加方法解决。

注解:OC 中我们很习惯的会用懒加载,当用到的时候才去加载它,但是实际上只要一个类实现了某个方法,就会被加载进内存。当我们不想加载这么多方法的时候,就会使用到 runtime 动态的添加方法。

需求:runtime 动态添加方法处理调用一个未实现的方法 和 去除报错。

案例代码:方法+调用+打印输出

//调用

Person *pers=[[Person alloc]init];

[pers performSelector:@selector(eat)];



// void(*)()

// 默认方法都有两个隐式参数,

void eat(id self,SEL sel)

{

    NSLog(@"%@ %@",self,NSStringFromSelector(sel));

}

// 当一个对象调用未实现的方法,会调用这个方法处理,并且会把对应的方法列表传过来.

// 刚好可以用来判断,未实现的方法是不是我们想要动态添加的方法

+ (BOOL)resolveInstanceMethod:(SEL)sel

{

    if (sel == @selector(eat)) {

        // 动态添加eat方法

        

        // 第一个参数:给哪个类添加方法

        // 第二个参数:添加方法的方法编号

        // 第三个参数:添加方法的函数实现(函数地址)

        // 第四个参数:函数的类型,(返回值+参数类型) v:void @:对象->self :表示SEL->_cmd

        class_addMethod(self, @selector(eat), eat, "v@:");

    }

    return [super resolveInstanceMethod:sel];

}

//打印输出

2018-03-09 21:55:44.788062+0800 2[1489:107592] <Person: 0x60400001ae60> eat

 

2.runtime 交换方法

应用场景:当第三方框架 或者 系统原生方法功能不能满足我们的时候,我们可以在保持系统原有方法功能的基础上,添加额外的功能。

需求:加载一张图片直接用[UIImage imageNamed:@"image"];是无法知道到底有没有加载成功。给系统的imageNamed添加额外功能(是否加载图片成功)。

方案一:继承系统的类,重写方法.(弊端:每次使用都需要导入)

方案二:使用 runtime,交换方法.

实现步骤:

1.给系统的方法添加分类

2.自己实现一个带有扩展功能的方法

3.交换方法,只需要交换一次,

案例代码:方法+调用+打印输出

//调用

UIImage *image=[UIImage imageNamed:@"1"];



/**

 load方法: 把类加载进内存的时候调用,只会调用一次

 方法应先交换,再去调用

 */

+ (void)load {

    

    // 1.获取 imageNamed方法地址

    // class_getClassMethod(获取某个类的方法)

    Method imageNamedMethod = class_getClassMethod(self, @selector(imageNamed:));

    // 2.获取 ln_imageNamed方法地址

    Method ln_imageNamedMethod = class_getClassMethod(self, @selector(ln_imageNamed:));

    

    // 3.交换方法地址,相当于交换实现方式;「method_exchangeImplementations 交换两个方法的实现」

    method_exchangeImplementations(imageNamedMethod, ln_imageNamedMethod);

}



/**

 看清楚下面是不会有死循环的

 调用 imageNamed => ln_imageNamed

 调用 ln_imageNamed => imageNamed

 */

// 加载图片 且 带判断是否加载成功

+ (UIImage *)ln_imageNamed:(NSString *)name {

    

    UIImage *image = [UIImage ln_imageNamed:name];

    if (image) {

        NSLog(@"加载成功");

    } else {

        NSLog(@"加载失败");

    }

    return image;

}

//打印输出

2018-03-09 22:01:43.907363+0800 2[1553:112712] 加载成功

 

总结:我们所做的就是在方法调用流程第三步的时候,交换两个方法地址指向。而且我们改变指向要在系统的imageNamed:方法调用前,所以将代码写在了分类的load方法里。最后当运行的时候系统的方法就会去找我们的方法的实现。

3.runtime 给分类动态添加属性

原理:给一个类声明属性,其实本质就是给这个类添加关联,并不是直接把这个值的内存空间添加到类存空间。

应用场景:给系统的类添加属性的时候,可以使用runtime动态添加属性方法。

注解:系统 NSObject 添加一个分类,我们知道在分类中是不能够添加成员属性的,虽然我们用了@property,但是仅仅会自动生成getset方法的声明,并没有带下划线的属性和方法实现生成。但是我们可以通过runtime就可以做到给它方法的实现。

需求:给系统 NSObject 类动态添加属性 name 字符串。

案例代码:方法+调用+打印

//调用

- (void)loadPropert{

    image=[[UIImage alloc]init];

    image.names=@"huang.png";

    image.heights=@"123";

}

// @property分类:只会生成get,set方法声明,不会生成实现,也不会生成下划线成员属性

@property NSString *names;

@property NSString *heights;



@implementation UIImage (Property)

- (void)setNames:(NSString *)names {

    // objc_setAssociatedObject(将某个值跟某个对象关联起来,将某个值存储到某个对象中)

    // object:给哪个对象添加属性

    // key:属性名称

    // value:属性值

    // policy:保存策略

    objc_setAssociatedObject(self, @"names", names, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

}



- (NSString *)names {

    return objc_getAssociatedObject(self, @"names");

}

- (void)setHeights:(NSString *)heights{

    objc_setAssociatedObject(self, @"heights", heights, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

}

- (NSString *)heights{

    return objc_getAssociatedObject(self, @"heights");

}

//打印输出

2018-03-09 22:05:08.392957+0800 2[1620:116061] huang.png===123

 

总结:其实,给属性赋值的本质,就是让属性与一个对象产生关联,所以要给NSObject的分类的name属性赋值就是让nameNSObject产生关联,而runtime可以做到这一点。

 

-------------------------------------------新加内容

4.获取类的所有成员变量

一个对象在归档和解档的 encodeWithCoder 和 initWithCoder: 方法中需要该对象所有的属性进行 decodeObjectForKey: 和 encodeObject: ,一般情况下需要对每个属性都写归解档, 添加或删除属性对应也要修改, 十分的不方便, 但是通过 Runtime 我们声明中无论写多少个属性,都不需要再修改实现中的代码了。

(1)比如一个 Person 类,需要对它的成员变量进行归解档, 步骤如下:

通过runtime 获取当前所有成员变量名, 然后获取到各个变量值, 以变量名为 key进行归档:

//归档
- (void)encodeWithCoder:(NSCoder *)coder
{
    [super encodeWithCoder:coder];
    
    //获取所有成员变量
    unsigned int outCount = 0;
    /*
     参数:
     1.哪个类
     2.接收值的地址, 用于存放属性的个数
     3.返回值: 存放所有获取到的属性, 可调出名字和类型
     */
    Ivar *ivarArray = class_copyIvarList([self class], &outCount);
    
    for (int i = 0; i < outCount; i++) {
        Ivar ivar = ivarArray[i];
        //将每个成员变量名转换为NSString对象类型
        NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];
        
        //忽略不需要归档的属性
        if ([[self ignoredNames] containsObject:key]) {
            continue; //跳过本次循环
        }
        
        //通过成员变量名, 取出成员变量的值
        id value = [self valueForKey:key];
        //再把值归档
        [coder encodeObject:value forKey:key];
        //这两部就相当于 [coder encodeObject: @(self.name) forKey:@"_name"];
    }
    free(ivarArray);
}

通过 runtime获取到所有成员变量名, 以变量名为 key 解档取出值:

//解档
- (instancetype)initWithCoder:(NSCoder *)coder
{
    self = [super initWithCoder:coder];
    if (self) {
        //获取所有成员变量
        unsigned int outCount = 0;
        
        Ivar *ivarArray = class_copyIvarList([self class], &outCount);
        
        for (int i = 0; i < outCount; i++) {
            Ivar ivar = ivarArray[i];
            //获取每个成员变量名并转换为NSString对象类型
            NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];
            
            //忽略不需要解档的属性
            if ([[self ignoredNames] containsObject:key]) {
                continue;
            }
            
            //根据变量名解档取值, 无论是什么类型
            id value = [coder decodeObjectForKey:key];
            //取出的值再设置给属性
            [self setValue:value forKey:key];
            //这两步相当于以前的 self.name = [coder decodeObjectForKey:@"_name"];
        }
        free(ivarArray); //释放内存
    }
    return self;
}

以上就实现了利用 runtime 进行归解档, 比之前一个个变量进行方便了很多, 但是在实际的运用中, 如果遇到一个类需要归解档就这样写, 多个需要重复写, 这时候可以 在 NSObject 的分类中时间归解档, 这样各个类使用时候只需要简单的几句就可以实现, 步骤如下:

(1).为 NSObject 创建分类, 并在 .h 中声明归解档的方法, 便于子类的使用;

@interface NSObject (Extension)

- (NSArray *)ignoredNames;
- (void)encode:(NSCoder *)aCoder; //重写方法, 避免覆盖系统方法
- (void)decode:(NSCoder *)aDecoder;

@end

(2)归档:

- (void)encode:(NSCoder *)aCoder{
    
    //一层层父类往上查找, 对父类的属性执行归解档方法
    Class c = self.class;
    while (c && c != [NSObject class]) {
        
        unsigned int outCount = 0;
        Ivar *ivarArray = class_copyIvarList([self class], &outCount);
        
        for (int i = 0; i < outCount; i++) {
            Ivar ivar = ivarArray[i];
            NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];
            
            //如果有实现该方法再去调用
            if ([self respondsToSelector:@selector(ignoredNames)]) {
                if ([[self ignoredNames] containsObject:key]) {
                    continue;
                }
            }
            
            id value = [self valueForKey:key];
            [aCoder encodeObject:value forKey:key]; //归档
        }
        free(ivarArray);
        c = [c superclass]; //向上查找父类
    }
    
}

(3).解档:

- (void)decode:(NSCoder *)aDecoder{
    
    Class c = self.class;
    while (c && c != [NSObject class]) {
        
        unsigned int outCount = 0;
        Ivar *ivarAaary = class_copyIvarList([self class], &outCount);
        
        for (int i = 0; i < outCount; i++) {
            Ivar ivar = ivarAaary[i];
            NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];
            
            if ([self respondsToSelector:@selector(ignoredNames)]) {
                if ([[self ignoredNames] containsObject:key]) {
                    continue;
                }
            }
            
            id value = [aDecoder decodeObjectForKey:key];
            [self setValue:value forKey:key]; //解档并赋值
        }
        free(ivarAaary);
        c = [c superclass];
    }
    
}

上面的代码声明的方法, 我换了一个方法名(不然会覆盖系统原来的方法!),同时加了一个忽略属性方法是否被实现的判断,便于在使用时候对不需要进行归解档的属性进行判断, 同时还加上了对父类属性的归解档循环。

这样再使用之后只需要简单的几行代码就可以实现归解档, 例如对 Cat 类进行归解档:

@implementation Car

//设置需要忽略的属性
- (NSArray *)ignoredNames{
    return @[@"head"];
}

//在系统方法中调用自定义方法
- (instancetype)initWithCoder:(NSCoder *)coder
{
    self = [super init];
    if (self) {
        [self decode:coder];
    }
    return self;
}

- (void)encodeWithCoder:(NSCoder *)coder
{
    [self encode:coder];
}

@end

5.字典转模型

一般我们都是使用 KVC 进行字典转模型,但是它还是有一定的局限性,例如:模型属性和键值对对应不上会crash(虽然可以重写setValue:forUndefinedKey: 方法防止报错),模型属性是一个对象或者数组时不好处理等问题,所以无论是效率还是功能上,利用 runtime 进行字典转模型都是比较好的选择.

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

1.字典的key和模型的属性匹配不上;

2.模型中嵌套模型(模型属性是另外一个模型对象);

3.数组中装着模型(模型的属性是一个数组,数组中是一个个模型对象).

针对上面的三种特殊情况,我们一个个详解下处理过程.

(1).先是字典的 key 和模型的属性不对应的情况。

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

- (void)setDict:(NSDictionary *)dict {
    
    Class c = self.class;
    while (c &&c != [NSObject class]) {
        
        unsigned int outCount = 0;
        Ivar *ivars = class_copyIvarList(c, &outCount);
        for (int i = 0; i < outCount; i++) {
            Ivar ivar = ivars[i];
            NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];
            
            // 成员变量名转为属性名(去掉下划线 _ )
            key = [key substringFromIndex:1];
            // 取出字典的值
            id value = dict[key];
            
            // 如果模型属性数量大于字典键值对数理,模型属性会被赋值为nil而报错,这时候判断值是nil的话, 忽略这个模型的属性即可.
            if (value == nil) continue;
            
            // 将字典中的值设置到模型上
            [self setValue:value forKeyPath:key];
        }
        free(ivars);
        c = [c superclass];
    }
}

(2).模型属性是另外一个模型对象的情况, 这时候我们就需要利用 runtime 的ivar_getTypeEncoding 方法获取模型对象类型,对该模型对象类型再进行字典转模型,也就是进行递归,需要注意的是我们要排除系统的对象类型,例如NSString,下面的方法中我添加了一个类方法方便递归。

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

@implementation NSObject (JSONExtension)

- (void)setDict:(NSDictionary *)dict {
    
    Class c = self.class;
    while (c &&c != [NSObject class]) {
        
        unsigned int outCount = 0;
        Ivar *ivars = class_copyIvarList(c, &outCount);
        for (int i = 0; i < outCount; i++) {
            Ivar ivar = ivars[i];
            NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];
            
            // 成员变量名转为属性名(去掉下划线 _ )
            key = [key substringFromIndex:1];
            // 取出字典的值
            id value = dict[key];
            
            // 如果模型属性数量大于字典键值对数理,模型属性会被赋值为nil而报错
            if (value == nil) continue;
            
            // 获得成员变量的类型
            NSString *type = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];
            
            // 如果属性是对象类型
            NSRange range = [type rangeOfString:@"@"];
            if (range.location != NSNotFound) {
                // 那么截取对象的名字(比如@"Dog",截取为Dog)
                type = [type substringWithRange:NSMakeRange(2, type.length - 3)];
                // 排除系统的对象类型
                if (![type hasPrefix:@"NS"]) {
                    // 将对象名转换为对象的类型,将新的对象字典转模型(递归)
                    Class class = NSClassFromString(type);
                    value = [class objectWithDict:value];
                }
            }
            
            // 将字典中的值设置到模型上
            [self setValue:value forKeyPath:key];
        }
        free(ivars);
        c = [c superclass];
    }
}

+ (instancetype )objectWithDict:(NSDictionary *)dict {
    NSObject *obj = [[self alloc]init];
    [obj setDict:dict];
    return obj;
}

(3).第三种情况是模型的属性是一个数组,数组中是一个个模型对象,我们既然能获取到属性类型,那就可以拦截到模型的那个数组属性,进而对数组中每个数据遍历并字典转模型,但是我们不知道数组中的模型都是什么类型,我们可以声明一个方法,该方法目的不是让其调用,而是让其实现并返回数组中模型的类型, 这样就可以对数组中的数据进行字典转模型.

在分类中声明了 arrayObjectClass 方法, 子类调用返回数组中模型的类型即可.

@interface NSObject (JSONExtension)

- (void)setDict: (NSDictionary *)dict;
+ (instancetype)objectWithDict: (NSDictionary *)dict;

//告诉数组中都是什么类型的模型对象
- (NSString *)arrayObjectClass;

@end

然后进行字典转模型:

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

@implementation NSObject (JSONExtension)

- (void)setDict:(NSDictionary *)dict{
    
    Class c = self.class;
    while (c && c != [NSObject class]) {
        unsigned int outCount = 0;
        Ivar *ivarArray = class_copyIvarList([self class], &outCount);
        
        for (int i = 0; i < outCount; i++) {
            Ivar ivar = ivarArray[i];
            NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];
            
            //成员变量名转为属性名(去掉下划线_)
            key = [key substringFromIndex:1];
            //取出字典的值
            id value = dict[key];
            
            //如果模型属性数量大于字典键值对数量,则key对应dict中没有值, 模型属性会被赋值为nil而报错
            if (value == nil) {
                continue;
            }
            
            //获得成员变量的类型
            NSString *type = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];
            
            //如果属性是对象类型
            NSRange range = [type rangeOfString:@""];
            if (range.location != NSNotFound) {
                //那么截取对象的名字(比如@"Dog", 截取为Dog)
                type = [type substringWithRange:NSMakeRange(2, type.length - 3)];
                //排除系统的对象类型
                if (![type hasPrefix:@"NS"]) {
                    //将对象名转换为对象的类型, 将新的对象字典转模型(递归)
                    Class class = NSClassFromString(type);
                    value = [class objectWithDict:value];
                }else if ([type isEqualToString:@"NSArray"]){
                    //如果是数组类型, 将数组中的每个模型进行字典转模型
                    NSArray *array = (NSArray *)value;
                    NSMutableArray *mArray = [NSMutableArray array];//先创建一个临时数组存放模型
                    
                    //获取到每个模型的类型
                    id class;
                    if ([self respondsToSelector:@selector(arrayObjectClass)]) {
                        NSString *classStr = [self arrayObjectClass];
                        class = NSClassFromString(classStr);
                    }else{
                        NSLog(@"数组内模型是未知类型");
                        return;
                    }
                    
                    //将数组中的所有模型进行字典转模型
                    for (int i = 0; i < array.count; i++) {
                        [mArray addObject:[class objectWithDict:value[i]]];
                    }
                    
                    value = mArray;
                }
            }
            
            //将字典中的值设置到模型上
            [self setValue:value forKey:key];
        }
    }
    
}

+ (instancetype)objectWithDict:(NSDictionary *)dict{
    NSObject *obj = [[self alloc] init];
    [obj setDict:dict];
    return obj;
}

@end


 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值