内容简介:iOS 模式详解—「runtime 面试、工作」看我就 ???? 了 ^_^.
引导
对于从事 iOS 开发人员来说,所有的人都会答出「 runtime 是运行时
」,什么情况下用 runtime
?,大部分人能说出「 给分类动态添加属性 || 交换方法
」,再问一句「 runtime 消息机制的调用流程 || 能体现runtime 强大之处的应用场景
」,到这,能知道答案的寥寥无几,很少有人会说到 “黑魔法”
这三个字,
runtime是 iOS 编程中比较难的模块,想要深入学习 OC,那 runtime 是你必须要熟练掌握的东西,下面是我对 runtime
的整理,从零开始,由浅入深,并且带了几个 runtime
实际开发的应用场景。
不管谁的博客上面写的文章(也包括自己),阅读的你要敢于去验证,停止无意义的⏹copy :two_men_holding_hands: paste。
在「时间和知识 」有限内,总结的文章难免有「未全、不足 」的地方,还望各位好友指出,以提高文章质量。
目录:
- 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模块简友文章推荐(:heart:数量较多)
-
实战应用场景(持续) - 期待 & 后续
本篇文章较长一些,强烈建议先 :heart: 收藏,在进行阅读 !
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 <objc/message.h>
-
注解1:我们导入系统的头文件,一般用尖括号。
-
注解2:OC 解决消息机制方法提示步骤【查找
build setting
-> 搜索msg
->objc_msgSend
(YES --> NO)】 -
注解3:最终生成消息机制,编译器做的事情,最终代码,需要把当前代码重新编译,用xcode编译器,【
clang -rewrite-objc main.m
查看最终生成代码】,示例:cd main.m --> 输入前面指令,就会生成 .opp文件(C++代码)
-
注解4:这里一般不会直接导入
<objc/runtime.h>
-
-
示例代码:OC 方法-->runtime 方法
说明: 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.找到只是最终函数实现地址,根据地址去方法区调用对应函数。
- 1.OC 在向一个对象发送消息时,
- 补充:一个
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 <objc/message.h> @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
一一对应。 - 如果不一致,就会调用
[<Status 0x7fa74b545d60> setValue:forUndefinedKey:]
报key
找不到的错。 - 分析:模型中的属性和字典的
key
不一一对应,系统就会调用setValue:forUndefinedKey:
报错。 - 解决:重写对象的
setValue:forUndefinedKey:
,把系统的方法覆盖,就能继续使用KVC,字典转模型了。
- KVC 字典转模型弊端:必须保证,模型中的属性和字典中的
-
字典转模型 Runtime 实现
-
思路:利用运行时,遍历模型中所有属性,根据模型的属性名,去字典中查找
key
,取出对应的值,给模型的属性赋值(从提醒:字典中取值,不一定要全部取出来)。 -
考虑情况:
- 1.当字典的
key
和模型的属性匹配不上。 - 2.模型中嵌套模型(模型属性是另外一个模型对象)。
- 3.数组中装着模型(模型的属性是一个数组,数组中是一个个模型对象)。
- 1.当字典的
-
注解:根据上面的三种特殊情况,先是字典的
key
和模型的属性不对应的情况。不对应有两种,一种是字典的键值大于模型属性数量,这时候我们不需要任何处理,因为runtime
是先遍历模型所有属性,再去字典中根据属性名找对应值进行赋值,多余的键值对也当然不会去看了;另外一种是模型属性数量大于字典的键值对,这时候由于属性没有对应值会被赋值为nil
,就会导致crash
,我们只需加一个判断即可。 考虑三种情况下面一一注解 ; -
步骤:提供一个
NSObject
分类,专门字典转模型,以后所有模型都可以通过这个分类实现字典转模型。
-
-
MJExtension字典转模型实现
- 底层也是对
runtime
的封装,才可以把一个模型中所有属性遍历出来。(你之所以看不懂,是 MJ 封装了很多层而已^_^.)。
- 底层也是对
这里针对字典转模型 KVC 实现,就不做详解了,如果你 对 KVC 详解使用或是实现原理 不是很清楚的,可以参考 实用「KVC编码 & KVO监听
字典转模型 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 = 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方法的。
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; }
总结:我们既然能获取到属性类型,那就可以拦截到模型的那个数组属性,进而对数组中每个模型遍历并字典转模型,但是我们不知道数组中的模型都是什么类型,我们可以声明一个方法,该方法目的不是让其调用,而是让其实现并返回模型的类型。
本文为「简书-白开水ln」作者原创;我的写作,希望能简化到初学者尽快入门和老司机繁琐回顾 ^_^.
这里提到的你如果不是很清楚,建议参考我的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; i<count; i++) { Ivar var = ivar[i]; const char *varName = ivar_getName(var); NSString *name = [NSString stringWithUTF8String:varName]; if ([name isEqualToString:@"_age"]) { object_setIvar(self.xiaoMing, var, @"20"); break; } } NSLog(@"XiaoMing's age is %@",self.xiaoMing.age); }
实现NSCoding的自动归档和解档
如果你实现过自定义模型数据持久化的过程,那么你也肯定明白,如果一个模型有许多个属性,那么我们需要对每个属性都实现一遍 encodeObject
和 decodeObjectForKey
方法,如果这样的模型又有很多个,这还真的是一个十分麻烦的事情。下面来看看简单的实现方式。
假设现在有一个 Movie
类,有3个属性。先看下 .h 文件
// Movie.h文件 //1. 如果想要当前类可以实现归档与反归档,需要遵守一个协议NSCoding @interface Movie : NSObject<NSCoding> @property (nonatomic, copy) NSString *movieId; @property (nonatomic, copy) NSString *movieName; @property (nonatomic, copy) NSString *pic_url; @end
如果是正常写法, .m 文件应该是这样的:
// Movie.m文件 @implementation Movie - (void)encodeWithCoder:(NSCoder *)aCoder { [aCoder encodeObject:_movieId forKey:@"id"]; [aCoder encodeObject:_movieName forKey:@"name"]; [aCoder encodeObject:_pic_url forKey:@"url"]; } - (id)initWithCoder:(NSCoder *)aDecoder { if (self = [super init]) { self.movieId = [aDecoder decodeObjectForKey:@"id"]; self.movieName = [aDecoder decodeObjectForKey:@"name"]; self.pic_url = [aDecoder decodeObjectForKey:@"url"]; } return self; } @end
如果这里有100个属性,那么我们也只能把100个属性都给写一遍吗。
不过你会使用 runtime
后,这里就有更简便的方法,如下。
#import "Movie.h" #import <objc/runtime.h> @implementation Movie - (void)encodeWithCoder:(NSCoder *)encoder { unsigned int count = 0; Ivar *ivars = class_copyIvarList([Movie class], &count); for (int i = 0; i<count; i++) { // 取出i位置对应的成员变量 Ivar ivar = ivars[i]; // 查看成员变量 const char *name = ivar_getName(ivar); // 归档 NSString *key = [NSString stringWithUTF8String:name]; id value = [self valueForKey:key]; [encoder encodeObject:value forKey:key]; } free(ivars); } - (id)initWithCoder:(NSCoder *)decoder { if (self = [super init]) { unsigned int count = 0; Ivar *ivars = class_copyIvarList([Movie class], &count); for (int i = 0; i<count; i++) { // 取出i位置对应的成员变量 Ivar ivar = ivars[i]; // 查看成员变量 const char *name = ivar_getName(ivar); // 归档 NSString *key = [NSString stringWithUTF8String:name]; id value = [decoder decodeObjectForKey:key]; // 设置到成员变量身上 [self setValue:value forKey:key]; } free(ivars); } return self; } @end
这样的方式实现,不管有多少个属性,写这几行代码就搞定了。怎么,代码有点多,
好说下面看看更加简便的方法:两句代码搞定。
#import "Movie.h" #import <objc/runtime.h> #define encodeRuntime(A) \ \ unsigned int count = 0;\ Ivar *ivars = class_copyIvarList([A class], &count);\ for (int i = 0; i<count; i++) {\ Ivar ivar = ivars[i];\ const char *name = ivar_getName(ivar);\ NSString *key = [NSString stringWithUTF8String:name];\ id value = [self valueForKey:key];\ [encoder encodeObject:value forKey:key];\ }\ free(ivars);\ \ #define initCoderRuntime(A) \ \ if (self = [super init]) {\ unsigned int count = 0;\ Ivar *ivars = class_copyIvarList([A class], &count);\ for (int i = 0; i<count; i++) {\ Ivar ivar = ivars[i];\ const char *name = ivar_getName(ivar);\ NSString *key = [NSString stringWithUTF8String:name];\ id value = [decoder decodeObjectForKey:key];\ [self setValue:value forKey:key];\ }\ free(ivars);\ }\ return self;\ \ @implementation Movie - (void)encodeWithCoder:(NSCoder *)encoder { encodeRuntime(Movie) } - (id)initWithCoder:(NSCoder *)decoder { initCoderRuntime(Movie) } @end
优化:上面是 encodeWithCoder
和 initWithCoder
这两个方法抽成宏。我们可以把这两个宏单独放到一个文件里面,这里以后需要进行数据持久化的模型都可以直接使用这两个宏。
runtime 下Class的各项操作
下面是 runtime 下Class的常见方法 及 带有使用示例代码。各项操作, 【转载原著】http://www.jianshu.com/p/46dd81402f63
unsigned int count;
-
获取属性列表
objc_property_t *propertyList = class_copyPropertyList([self class], &count); for (unsigned int i=0; i<count; i++) { const char *propertyName = property_getName(propertyList[i]); NSLog(@"property---->%@", [NSString stringWithUTF8String:propertyName]); }
-
获取方法列表
Method *methodList = class_copyMethodList([self class], &count); for (unsigned int i; i<count; i++) { Method method = methodList[i]; NSLog(@"method---->%@", NSStringFromSelector(method_getName(method))); }
-
获取成员变量列表
Ivar *ivarList = class_copyIvarList([self class], &count); for (unsigned int i; i<count; i++) { Ivar myIvar = ivarList[i]; const char *ivarName = ivar_getName(myIvar); NSLog(@"Ivar---->%@", [NSString stringWithUTF8String:ivarName]); }
-
获取协议列表
__unsafe_unretained Protocol **protocolList = class_copyProtocolList([self class], &count); for (unsigned int i; i<count; i++) { Protocol *myProtocal = protocolList[i]; const char *protocolName = protocol_getName(myProtocal); NSLog(@"protocol---->%@", [NSString stringWithUTF8String:protocolName]); }
现在有一个Person类,和person创建的xiaoming对象,有test1和test2两个方法
-
获得类方法
Class PersonClass = object_getClass([Person class]); SEL oriSEL = @selector(test1); Method oriMethod = _class_getMethod(xiaomingClass, oriSEL);
-
获得实例方法
Class PersonClass = object_getClass([xiaoming class]); SEL oriSEL = @selector(test2); Method cusMethod = class_getInstanceMethod(xiaomingClass, oriSEL);
-
添加方法
BOOL addSucc = class_addMethod(xiaomingClass, oriSEL, method_getImplementation(cusMethod), method_getTypeEncoding(cusMethod));
-
替换原方法实现
class_replaceMethod(toolClass, cusSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
-
交换两个方法的实现
method_exchangeImplementations(oriMethod, cusMethod);
常用方法
// 得到类的所有方法 Method *allMethods = class_copyMethodList([Person class], &count); // 得到所有成员变量 Ivar *allVariables = class_copyIvarList([Person class], &count); // 得到所有属性 objc_property_t *properties = class_copyPropertyList([Person class], &count); // 根据名字得到类变量的Ivar指针,但是这个在OC中好像毫无意义 Ivar oneCVIvar = class_getClassVariable([Person class], name); // 根据名字得到实例变量的Ivar指针 Ivar oneIVIvar = class_getInstanceVariable([Person class], name); // 找到后可以直接对私有变量赋值 object_setIvar(_per, oneIVIvar, @"Mike");//强制修改name属性 /* 动态添加方法: 第一个参数表示Class cls 类型; 第二个参数表示待调用的方法名称; 第三个参数(IMP)myAddingFunction,IMP是一个函数指针,这里表示指定具体实现方法myAddingFunction; 第四个参数表方法的参数,0代表没有参数; */ class_addMethod([_per class], @selector(sayHi), (IMP)myAddingFunction, 0); // 交换两个方法 method_exchangeImplementations(method1, method2); // 关联两个对象 objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy) /* id object :表示关联者,是一个对象,变量名理所当然也是object const void *key :获取被关联者的索引key id value :被关联者,这里是一个block objc_AssociationPolicy policy : 关联时采用的协议,有assign,retain,copy等协议,一般使用OBJC_ASSOCIATION_RETAIN_NONATOMIC */
runtime 几个参数概念
以上的几种方法应该算是 runtime
在实际场景中所应用的大部分的情况了,平常的编码中差不多足够用了。
这里在对 runtime
几个参数概念,做一简单说明
1、objc_msgSend
这是个最基本的用于发送消息的函数。
其实编译器会根据情况在 objc_msgSend
, objc_msgSend_stret
,, objc_msgSendSuper
, 或 objc_msgSendSuper_stret
四个方法中选择一个来调用。如果消息是传递给超类,那么会调用名字带有 Super
的函数;如果消息返回值是数据结构而不是简单值时,那么会调用名字带有 stret
的函数。
2、SEL
objc_msgSend
函数第二个参数类型为 SEL
,它是 selector
在Objc中的表示类型(Swift中是Selector类)。 selector
是方法选择器,可以理解为区分方法的 ID
,而这个 ID
的数据结构是 SEL
:
typedef struct objc_selector *SEL;
其实它就是个映射到方法的C字符串,你可以用 Objc 编译器命令 @selector()``或者 Runtime
系统的 sel_registerName
函数来获得一个 SEL
类型的方法选择器。
3、id
objc_msgSend
第一个参数类型为 id
,大家对它都不陌生,它是一个指向类实例的指针:
typedef struct objc_object *id;
那 objc_object
又是啥呢:
struct objc_object { Class isa; };
objc_object
结构体包含一个 isa
指针,根据 isa
指针就可以顺藤摸瓜找到对象所属的类。
4、runtime.h里Class的定义
struct objc_class { Class isa OBJC_ISA_AVAILABILITY;//每个Class都有一个isa指针 #if !__OBJC2__ Class super_class OBJC2_UNAVAILABLE;//父类 const char *name OBJC2_UNAVAILABLE;//类名 long version OBJC2_UNAVAILABLE;//类版本 long info OBJC2_UNAVAILABLE;//!*!供运行期使用的一些位标识。如:CLS_CLASS (0x1L)表示该类为普通class; CLS_META(0x2L)表示该类为metaclass等(runtime.h中有详细列出) long instance_size OBJC2_UNAVAILABLE;//实例大小 struct objc_ivar_list *ivars OBJC2_UNAVAILABLE;//存储每个实例变量的内存地址 struct objc_method_list **methodLists OBJC2_UNAVAILABLE;//!*!根据info的信息确定是类还是实例,运行什么函数方法等 struct objc_cache *cache OBJC2_UNAVAILABLE;//缓存 struct objc_protocol_list *protocols OBJC2_UNAVAILABLE;//协议 #endif } OBJC2_UNAVAILABLE;
可以看到运行时一个类还关联了它的超类指针,类名,成员变量,方法,缓存,还有附属的协议。
在 objc_class
结构体中: ivars是
objc_ivar_list 指针;
methodLists 是指向
objc_method_list 指针的指针。也就是说可以动态修改
*methodLists 的值来添加成员方法,这也是
Category`实现的原理。
上面讲到的所有东西都在Demo里,如果你感觉这样难以理解,那强烈建议你下载Demo ,运行代码加上文字注解,效果会更好,如果你觉得不错,还请为我的Demo star一个。
什么是 method swizzling(俗称黑魔法)
-
简单说就是进行方法交换
-
在
Objective-C
中调用一个方法,其实是向一个对象发送消息,查找消息的唯一依据是selector
的名字。利用Objective-C
的动态特性,可以实现在运行时偷换selector
对应的方法实现,达到给方法挂钩的目的 -
每个类都有一个方法列表,存放着方法的名字和方法实现的映射关系,
selector
的本质其实就是方法名,IMP
有点类似函数指针,指向具体的Method
实现,通过selector
就可以找到对应的IMP
。
- 交换方法的几种实现方式
- 利用
method_exchangeImplementations
交换两个方法的实现 - 利用
class_replaceMethod
替换方法的实现 - 利用
method_setImplementation
来直接设置某个方法的IMP
。
- 利用
这里可以参考简友这篇: 【Runtime Method Swizzling开发实例汇总】http://www.jianshu.com/p/f6dad8e1b848
这里可以参考权威这篇: OC运行时黑魔法 Method Swizzling
最后一道面试题的注解
下面的代码输出什么?
@implementation Son : NSObject - (id)init { self = [super init]; if (self) { NSLog(@"%@", NSStringFromClass([self class])); NSLog(@"%@", NSStringFromClass([super class])); } return self; } @end
先思考一下,会打印出来什么:question:
关注我的更多干货分享 ^_^.
答案:都输出 Son
-
class
获取当前方法的调用者的类,superClass
获取当前方法的调用者的父类,super
仅仅是一个编译指示器,就是给编译器看的,不是一个指针。 - 本质:只要编译器看到
super
这个标志,就会让当前对象去调用父类方法,本质还是当前对象在调用
这个题目主要是考察关于 objc
中对 self
和 super
的理解:
-
self
是类的隐藏参数,指向当前调用方法的这个类的实例。而super
本质是一个编译器标示符,和self
是指向的同一个消息接受者 -
当使用
self
调用方法时,会从当前类的方法列表中开始找,如果没有,就从父类中再找; -
而当使用
super
时,则从父类的方法列表中开始找。然后调用父类的这个方法 -
调用
[self class]
时,会转化成objc_msgSend
函数
id objc_msgSend(id self, SEL op, ...) - 调用 `[super class]`时,会转化成 `objc_msgSendSuper` 函数. id objc_msgSendSuper(struct objc_super *super, SEL op, ...) 第一个参数是 objc_super 这样一个结构体,其定义如下 struct objc_super { __unsafe_unretained id receiver; __unsafe_unretained Class super_class; }; 第一个成员是 receiver, 类似于上面的 objc_msgSend函数第一个参数self 第二个成员是记录当前类的父类是什么,告诉程序从父类中开始找方法,找到方法后,最后内部是使用 objc_msgSend(objc_super->receiver, @selector(class))去调用, 此时已经和[self class]调用相同了,故上述输出结果仍然返回 Son objc Runtime 开源代码对- (Class)class方法的实现 -(Class)class { return object_getClass(self); }
runtime模块简友文章推荐(:heart:数量多)
简友 | runtime模块推荐阅读文章 |
---|---|
西木 | 完整总结 http://www.jianshu.com/p/6b905584f536 |
天口三水羊 | objc_msgSend http://www.jianshu.com/p/9e1bc8d890f9 |
夜千寻墨 | 详解 http://www.jianshu.com/p/46dd81402f63 |
袁峥Seemygo | 快速上手 http://www.jianshu.com/p/e071206103a4 |
郑钦洪_ | 实现自动化归档 http://www.jianshu.com/p/bd24c3f3cd0a |
HenryCheng | 消息机制 http://www.jianshu.com/p/f6300eb3ec3d |
runtime效果图
期待
-
如果在阅读过程中遇到 error,希望你能 Issues 我,谢谢。
-
如果你想为【本文相关】分享点什么,也希望你能 Issues 我,我非常想为这篇文章增加更多实用的内容,谢谢。
-
「博客原文」,对本文我会【不定时、持续更新、一些 学习心得与文章、实用才是硬道理】^_^.
About me
【我也是对所花费时间的一个总结】
我只是个【有思想的伐码猿:monkey:】加上【自己的学习总:coffee:️】写出来的文章。
以上所述就是小编给大家介绍的《iOS 模式详解—「runtime 面试、工作」看我就 ???? 了 ^_^.》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- 详解RunLoop之面试题
- 前端面试题—vue部分详解
- 【Android面试】HashMap详解(一)
- 非面试向跨域实践详解
- 2019 JavaScript面试题详解(基础+进阶)
- 数据结构和算法面试题系列—二分查找算法详解
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
The Definitive Guide to MongoDB
Peter Membrey、Wouter Thielen / Apress / 2010-08-26 / USD 44.99
MongoDB, a cross-platform NoSQL database, is the fastest-growing new database in the world. MongoDB provides a rich document orientated structure with dynamic queries that you’ll recognize from RDMBS ......一起来看看 《The Definitive Guide to MongoDB》 这本书的介绍吧!