博客> runtime详解
runtime详解
2017-09-22 19:17 评论:0 阅读:41 青春追逐

iOS模式详解—runtime 对于从事 iOS 开发人员来说,当提到 runtime时,我想都可以说出来 「runtime 运行时」和基本使用的方法。相信很多开发者跟我当初一样,也许当你使用这个重要的模块完成一些工作任务之后(复制粘贴的^_^.),还是不清楚 runtime 知识体系和内在原理。
 runtime 是 iOS 编程中比较难的模块,想要深入学习 OC,那 runtime 是你必须要熟练掌握的东西,下面是我对 runtime 的整理,从零开始,由浅入深,并且带了几个 runtime 实际开发的应用场景。 本篇文章主要从【runtime 模块实用详解】学习总结。在「时间和知识 」有限内,总结的文章难免有「未全、不足 」的地方,还望各位好友指出,以提高文章质量。 目录:
 runtime 概念 runtime 消息机制 runtime 方法调用流程「消息机制」 runtime 运行时常见作用 runtime 常用开发应用场景「工作掌握」 1.runtime 交换方法 2.runtime 给分类动态添加属性 3.runtime 字典转模型(Runtime 考虑三种情况实现) runtime 运行时其它作用「面试熟悉」 1.动态添加方法 2.动态变量控制 3.实现NSCoding的自动归档和解档 4.runtime 下Class的各项操作 5.runtime 几个参数概念 什么是 method swizzling(俗称黑魔法) 最后一道面试题的注解 期待 & 后续 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 方法。

runtime

runtime 方法调用流程「消息机制」 面试:消息机制方法调用流程 怎么去调用eat方法,对象方法:(保存到类对象的方法列表) ,类方法:(保存到元类(Meta Class)中方法列表)。 1.OC 在向一个对象发送消息时,runtime 库会根据对象的 isa指针找到该对象对应的类或其父类中查找方法。。 2.注册方法编号(这里用方法编号的好处,可以快速查找)。 3.根据方法编号去查找对应方法。 4.找到只是最终函数实现地址,根据地址去方法区调用对应函数。 补充:一个objc 对象的 isa 的指针指向什么?有什么作用? 每一个对象内部都有一个isa指针,这个指针是指向它的真实类型,根据这个指针就能知道将来调用哪个类的方法。

runtime 常见作用 动态交换两个方法的实现 动态添加属性 实现字典转模型的自动转换 发送消息 动态添加方法 拦截并替换方法 实现 NSCoding 的自动归档和解档

runtime 常用开发应用场景「工作掌握」 runtime 交换方法 应用场景:当第三方框架 或者 系统原生方法功能不能满足我们的时候,我们可以在保持系统原有方法功能的基础上,添加额外的功能。 需求:加载一张图片直接用[UIImage imageNamed:@"image"];是无法知道到底有没有加载成功。给系统的imageNamed添加额外功能(是否加载图片成功)。 方案一:继承系统的类,重写方法.(弊端:每次使用都需要导入) 方案二:使用 runtime,交换方法. 实现步骤: 1.给系统的方法添加分类 2.自己实现一个带有扩展功能的方法 3.交换方法,只需要交换一次, 案例代码:方法+调用+打印输出

  • (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添加额外功能--加载成功 总结:我们所做的就是在方法调用流程第三步的时候,交换两个方法地址指向。而且我们改变指向要在系统的imageNamed:方法调用前,所以将代码写在了分类的load方法里。最后当运行的时候系统的方法就会去找我们的方法的实现。
 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分类,专门字典转模型,以后所有模型都可以通过这个分类实现字典转模型。 MJExtension 字典转模型实现 底层也是对 runtime 的封装,才可以把一个模型中所有属性遍历出来。(你之所以看不懂,是 MJ 封装了很多层而已^_^.)。 这里针对字典转模型 KVC 实现,就不做详解了,如果你 对 KVC 详解使用或是实现原理 不是很清楚的,可以参考 实用「KVC编码 & KVO监听 字典转模型 Runtime 方式实现: 说明:下面这个示例,是考虑三种情况包含在内的转换示例,具体可以看图上的注解 


runtime Runtime 字典转模型
 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 = classcopyIvarList(self, &count); // 遍历所有成员变量 for (int i = 0; i < count xss=removed xss=removed>字典中的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;
 }
 注:
 这里在获取模型类中的所有属性名,是采取 classcopyIvarList 先获取成员变量(以下划线开头) ,然后再处理成员变量名->字典中的key(去掉 ,从第一个角标开始截取) 得到属性名。 原因: Ivar:成员变量,以下划线开头,Property 属性 获取类里面属性 class_copyPropertyList 获取类中的所有成员变量 class_copyIvarList {
 int _a; // 成员变量 } @property (nonatomic, assign) NSInteger attitudes_count; // 属性 这里有成员变量,就不会漏掉属性;如果有属性,可能会漏掉成员变量; 使用runtime字典转模型获取模型属性名的时候,最好获取成员属性名Ivar因为可能会有个属性是没有setter和``getter方法的。
 2、runtime 字典转模型-->模型中嵌套模型「模型属性是另外一个模型对象」,这种情况处理如下:
  • (instancetype)modelWithDict2:(NSDictionary )dict
 { // 1.创建对应的对象 id objc = [[self alloc] init]; // 2.利用runtime给对象中的属性赋值 unsigned int count = 0; // 获取类中的所有成员变量 Ivar ivarList = classcopyIvarList(self, &count); // 遍历所有成员变量 for (int i = 0; i < count xss=removed xss=removed xss=removed> 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 = classcopyIvarList(self, &count); // 遍历所有成员变量 for (int i = 0; i < count xss=removed xss=removed>字典中的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字典转模型-->数组中装着模型 打印输出
 总结:我们既然能获取到属性类型,那就可以拦截到模型的那个数组属性,进而对数组中每个模型遍历并字典转模型,但是我们不知道数组中的模型都是什么类型,我们可以声明一个方法,该方法目的不是让其调用,而是让其实现并返回模型的类型。 这里提到的你如果不是很清楚,建议参考我的Demo,重要的部分代码中都有相应的注解和文字打印,运行程序可以很直观的表现。 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米 动态变量控制
 现在有一个Person类,创建 xiaoming对象 动态获取 XiaoMing 类中的所有属性 [当然包括私有] Ivar ivar = class_copyIvarList([self.xiaoming class], &count); 遍历属性找到对应name字段 const char varName = ivar_getName(var); 修改对应的字段值成20 object_setIvar(self.xiaoMing, var, @"20"); 代码参考
 -(void)answer{ unsigned int count = 0; Ivar *ivar = class_copyIvarList([self.xiaoMing class], &count); for (int i = 0; ireceiver, @selector(class))去调用, 此时已经和[self class]调用相同了,故上述输出结果仍然返回 Son objc Runtime 开源代码对- (Class)class方法的实现 -(Class)class { return object_getClass(self); }

每一个方法对应一个runtime,每一个线程对应一个runloop

收藏
0
sina weixin mail 回到顶部