runtime 概念

Objective-C 是基于 C 的,它为 C 添加了面向对象的特性。它将很多静态语言在编译和链接时期做的事放到了 runtime 运行时来处理,可以说 runtime 是我们 Objective-C 幕后工作者。

runtime(简称运行时),是一套 纯C(C和汇编写的) 的API。而 OC 就是 运行时机制,也就是在运行时候的一些机制,其中最主要的是 消息机制。

对于 C 语言,函数的调用在编译的时候会决定调用哪个函数。

OC的函数调用成为消息发送,属于 动态调用过程。在编译的时候并不能决定真正调用哪个函数,只有在真正运行的时候才会根据函数的名称找到对应的函数来调用。

事实证明:在编译阶段,OC 可以 调用任何函数,即使这个函数并未实现,只要声明过就不会报错,只有当运行的时候才会报错,这是因为OC是运行时动态调用的。而 C 语言 调用未实现的函数 就会报错。

runtime 消息机制

我们写 OC 代码,它在运行的时候也是转换成了 runtime 方式运行的。任何方法调用本质:就是发送一个消息(用 runtime发送消息,OC 底层实现通过 runtime 实现)。

消息机制原理:对象根据方法编号SEL去映射表查找对应的方法实现。

每一个 OC 的方法,底层必然有一个与之对应的 runtime 方法。

简单示例:

验证:方法调用,是否真的是转换为消息机制?

  • 必须要导入头文件 #import

注解1:我们导入系统的头文件,一般用尖括号。

注解2:OC 解决消息机制方法提示步骤【查找build setting -> 搜索msg -> objc_msgSend(YES --> NO)】

注解3:最终生成消息机制,编译器做的事情,最终代码,需要把当前代码重新编译,用xcode编译器,【clang -rewrite-objc main.m 查看最终生成代码】,示例:cd main.m --> 输入前面指令,就会生成 .opp文件(C++代码)

2230763-95d6b7bddc1fe4e8.png

说明: eat(无参) 和 run(有参) 是 Person模型类中的私有方法「可以帮我调用私有方法」;
// Person *p = [Person alloc];
// 底层的实际写法
Person *p = objc_msgSend(objc_getClass("Person"), sel_registerName("alloc"));
 
// p = [p init];
p = objc_msgSend(p, sel_registerName("init"));
 
// 调用对象方法(本质:让对象发送消息)
//[p eat];
 
// 本质:让类对象发送消息
objc_msgSend(p, @selector(eat));
objc_msgSend([Person class], @selector(run:),20);
 
//---------------------------  ------------------------------//
// 也许下面这种好理解一点
 
// id objc = [NSObject alloc];
id objc = objc_msgSend([NSObject class], @selector(alloc));
 
// objc = [objc init];
objc = objc_msgSend(objc, @selector(init));

runtime 方法调用流程「消息机制」

面试:消息机制方法调用流程

  • 怎么去调用eat方法,对象方法:(保存到类对象的方法列表) ,类方法:(保存到元类(Meta Class)中方法列表)。

1.OC 在向一个对象发送消息时,runtime 库会根据对象的 isa指针找到该对象对应的类或其父类中查找方法。。

2.注册方法编号(这里用方法编号的好处,可以快速查找)。

3.根据方法编号去查找对应方法。

4.找到只是最终函数实现地址,根据地址去方法区调用对应函数。

  • 补充:一个objc 对象的 isa 的指针指向什么?有什么作用?

每一个对象内部都有一个isa指针,这个指针是指向它的真实类型,根据这个指针就能知道将来调用哪个类的方法。

- (void)viewDidLoad {
    [super viewDidLoad];
    // 方案一:先搞个分类,定义一个能加载图片并且能打印的方法+ (instancetype)imageWithName:(NSString *)name;
    // 方案二:交换 imageNamed 和 ln_imageNamed 的实现,就能调用 imageNamed,间接调用 ln_imageNamed 的实现。
    UIImage *image = [UIImage imageNamed:@"123"];
}
 
#import
@implementation UIImage (Image)
/**
 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(@"runtime添加额外功能--加载成功");
    } else {
        NSLog(@"runtime添加额外功能--加载失败");
    }
    return image;
}
 
/**
 不能在分类中重写系统方法imageNamed,因为会把系统的功能给覆盖掉,而且分类中不能调用super
 所以第二步,我们要 自己实现一个带有扩展功能的方法.
 + (UIImage *)imageNamed:(NSString *)name {
 
 }
 */
@end
 
 
// 打印输出
2017-02-17 17:52:14.693 runtime[12761:543574] runtime添加额外功能--加载成功

runtime 给分类动态添加属性

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

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

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

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

@interface NSObject (Property)
 
// @property分类:只会生成get,set方法声明,不会生成实现,也不会生成下划线成员属性
@property NSString *name;
@property NSString *height;
@end
 
@implementation NSObject (Property)
 
- (void)setName:(NSString *)name {
 
    // objc_setAssociatedObject(将某个值跟某个对象关联起来,将某个值存储到某个对象中)
    // object:给哪个对象添加属性
    // key:属性名称
    // value:属性值
    // policy:保存策略
    objc_setAssociatedObject(self, @"name", name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
 
- (NSString *)name {
    return objc_getAssociatedObject(self, @"name");
}
 
// 调用
NSObject *objc = [[NSObject alloc] init];
objc.name = @"123";
NSLog(@"runtime动态添加属性name==%@",objc.name);
 
// 打印输出
2017-02-17 19:37:10.530 runtime[12761:543574] runtime动态添加属性--name == 123

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

runtime 字典转模型

字典转模型的方式:

  • 一个一个的给模型属性赋值(初学者)。

  • 字典转模型KVC实现

        KVC 字典转模型弊端:必须保证,模型中的属性和字典中的key 一一对应。

        如果不一致,就会调用[setValue:forUndefinedKey:] 报key找不到的错。

        分析:模型中的属性和字典的key不一一对应,系统就会调用setValue:forUndefinedKey:报错。

        解决:重写对象的setValue:forUndefinedKey:,把系统的方法覆盖,就能继续使用KVC,字典转模型了。

  • 字典转模型 Runtime 实现

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

  • 考虑情况

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

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

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

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

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

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

  • 这里有成员变量,就不会漏掉属性;如果有属性,可能会漏掉成员变量;

    使用runtime字典转模型获取模型属性名的时候,最好获取成员属性名Ivar因为可能会有个属性是没有setter和``getter方法的。

    1

    2

    3

    4

    5

    {

        int _a; // 成员变量

    }

     

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

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

+ (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;
}

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;
}

2230763-d163d8ac0c736b7e.png

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

runtime 其它作用「面试熟悉」

动态添加方法

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

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

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

- (void)viewDidLoad {
    [super viewDidLoad];   
    Person *p = [[Person alloc] init];
    // 默认person,没有实现run:方法,可以通过performSelector调用,但是会报错。
    // 动态添加方法就不会报错
    [p performSelector:@selector(run:) withObject:@10];
}
 
@implementation Person
// 没有返回值,1个参数
// void,(id,SEL)
void aaa(id self, SEL _cmd, NSNumber *meter) {
    NSLog(@"跑了%@米", meter);
}
 
// 任何方法默认都有两个隐式参数,self,_cmd(当前方法的方法编号)
// 什么时候调用:只要一个对象调用了一个未实现的方法就会调用这个方法,进行处理
// 作用:动态添加方法,处理未实现
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    // [NSStringFromSelector(sel) isEqualToString:@"run"];
    if (sel == NSSelectorFromString(@"run:")) {
        // 动态添加run方法
        // class: 给哪个类添加方法
        // SEL: 添加哪个方法,即添加方法的方法编号
        // IMP: 方法实现 => 函数 => 函数入口 => 函数名(添加方法的函数实现(函数地址))
        // type: 方法类型,(返回值+参数类型) v:void @:对象->self :表示SEL->_cmd
        class_addMethod(self, sel, (IMP)aaa, "v@:@");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}
@end
 
// 打印输出
2017-02-17 19:05:03.917 runtime[12761:543574] runtime动态添加方法--跑了10米

动态使用runtime实现两个不同的类但类的属性都一样,其中一个类有值,把有值的这个类的属性值赋给另一个类的属性

@interface SYTest : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) int count;
@end
@interface SYTestInfo : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) int count;
@end

@implementation SYTest

@end
@implementation SYTestInfo

@end

// 实例
- (void)viewDidLoad {
    [super viewDidLoad];
    
    SYTest *test = [[SYTest alloc] init];
    test.name = @"123";
    test.count = 10000;
    
    SYTestInfo *info = [[SYTestInfo alloc] init];
    unsigned int count = 0;
    Ivar *ivarList = class_copyIvarList(NSClassFromString(@"SYTestInfo"), &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
        [info setValue:[test valueForKey:key] forKey:key];
        NSLog(@"****");
    }
}

动态变量控制

现在有一个Person类,创建 xiaoming对象

  • 动态获取 XiaoMing 类中的所有属性 [当然包括私有]

1

Ivar *ivar = class_copyIvarList([self.xiaoming class], &count);

  • 遍历属性找到对应name字段

1

const char *varName = ivar_getName(var);

  • 修改对应的字段值成20

1

object_setIvar(self.xiaoMing, var, @"20");

 

 

转载于:https://my.oschina.net/zsyzone/blog/1083206

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值