对于从事 iOS 开发人员来说,所有的人都会答出【runtime 是运行时】什么情况下用runtime?大部分人能说出【给分类动态添加属性 || 交换方法】,再问一句【runtime 消息机制的调用流程 || 能体现runtime 强大之处的应用场景】,到这,能知道答案的寥寥无几,很少有人会说到【黑魔法】这三个字
** runtime** 是 iOS 编程中比较难的模块,想要深入学习 OC,那 runtime 是你必须要熟练掌握的东西,下面是我对runtime的整理,从零开始,由浅入深,并且带了几个runtime实际开发的应用场景。
目录:
runtime 概念
runtime 消息机制
runtime 方法调用流程「消息机制」
runtime 运行时常见作用
runtime 常用开发应用场景「工作掌握」
1.runtime 交换方法
2.UITextField占位文字颜色(工具类)
3.runtime 给分类动态添加属性
4.runtime 字典转模型(Runtime 考虑三种情况实现)
runtime 运行时其它作用「面试熟悉」
1.动态添加方法
2.动态变量控制
3.实现NSCoding的自动归档和解档
4.runtime 下Class的各项操作
5.runtime 几个参数概念
什么是 method swizzling(俗称黑魔法)
最后一道面试题的注解
runtime模块简友文章推荐(❤️数量较多)
实战应用场景(持续)OC & Swift runtime & runloop 面试最常问到的题整理【建议看】
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方法。
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++代码)
注解4:这里一般不会直接导入
message.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([Personclass],@selector(run:),20);//--------------------------- <#我是分割线#> ------------------------------//// 也许下面这种好理解一点// id objc = [NSObject alloc];id objc = objc_msgSend([NSObjectclass],@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指针,这个指针是指向它的真实类型,根据这个指针就能知道将来调用哪个类的方法。
runtime 常见作用
动态交换两个方法的实现
动态添加属性
实现字典转模型的自动转换
发送消息
动态添加方法
拦截并替换方法
实现 NSCoding 的自动归档和解档
runtime 常用开发应用场景「工作掌握」
runtime 交换方法
应用场景:当第三方框架 或者 系统原生方法功能不能满足我们的时候,我们可以在保持系统原有方法功能的基础上,添加额外的功能。
需求:加载一张图片直接用[UIImage imageNamed:@"image"];是无法知道到底有没有加载成功。给系统的imageNamed添加额外功能(是否加载图片成功)。
方案一:继承系统的类,重写方法.(弊端:每次使用都需要导入)
方案二:使用 runtime,交换方法.
实现步骤:
1.给系统的方法添加分类
2.自己实现一个带有扩展功能的方法
3.交换方法,只需要交换一次,
案例代码:方法+调用+打印输出
- (void)viewDidLoad { [superviewDidLoad];// 方案一:先搞个分类,定义一个能加载图片并且能打印的方法+ (instancetype)imageWithName:(NSString *)name;// 方案二:交换 imageNamed 和 ln_imageNamed 的实现,就能调用 imageNamed,间接调用 ln_imageNamed 的实现。UIImage*image = [UIImageimageNamed:@"123"];}#import@implementationUIImage(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 = [UIImageln_imageNamed:name];if(image) {NSLog(@"runtime添加额外功能--加载成功"); }else{NSLog(@"runtime添加额外功能--加载失败"); }returnimage;}/**
不能在分类中重写系统方法imageNamed,因为会把系统的功能给覆盖掉,而且分类中不能调用super
所以第二步,我们要 自己实现一个带有扩展功能的方法.
+ (UIImage *)imageNamed:(NSString *)name {
}
*/@end// 打印输出2016-02-1717:52:14.693runtime[12761:543574] runtime添加额外功能--加载成功
总结:我们所做的就是在方法调用流程第三步的时候,交换两个方法地址指向。而且我们改变指向要在系统的imageNamed:方法调用前,所以将代码写在了分类的load方法里。最后当运行的时候系统的方法就会去找我们的方法的实现。
runtime 给分类动态添加属性
原理:给一个类声明属性,其实本质就是给这个类添加关联,并不是直接把这个值的内存空间添加到类存空间。
应用场景:给系统的类添加属性的时候,可以使用runtime动态添加属性方法。
注解:系统NSObject添加一个分类,我们知道在分类中是不能够添加成员属性的,虽然我们用了@property,但是仅仅会自动生成get和set方法的声明,并没有带下划线的属性和方法实现生成。但是我们可以通过runtime就可以做到给它方法的实现。
需求:给系统 NSObject 类动态添加属性name字符串。
案例代码:方法+调用+打印
@interfaceNSObject(Property)// @property分类:只会生成get,set方法声明,不会生成实现,也不会生成下划线成员属性@propertyNSString*name;@propertyNSString*height;@end@implementationNSObject(Property)- (void)setName:(NSString*)name {// objc_setAssociatedObject(将某个值跟某个对象关联起来,将某个值存储到某个对象中)// object:给哪个对象添加属性// key:属性名称// value:属性值// policy:保存策略objc_setAssociatedObject(self,@"name", name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);}- (NSString*)name {returnobjc_getAssociatedObject(self,@"name");}// 调用NSObject*objc = [[NSObjectalloc] init];objc.name =@"123";NSLog(@"runtime动态添加属性name==%@",objc.name);// 打印输出2016-02-1719:37:10.530runtime[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 字典转模型
1、runtime 字典转模型-->字典的key和模型的属性不匹配「模型属性数量大于字典键值对数」,这种情况处理如下:
// Runtime:根据模型中属性,去字典中取出对应的value给模型属性赋值// 思路:遍历模型中所有属性->使用运行时+ (instancetype)modelWithDict:(NSDictionary*)dict{// 1.创建对应的对象idobjc = [[selfalloc] init];// 2.利用runtime给对象中的属性赋值/**
class_copyIvarList: 获取类中的所有成员变量
Ivar:成员变量
第一个参数:表示获取哪个类中的成员变量
第二个参数:表示这个类有多少成员变量,传入一个Int变量地址,会自动给这个变量赋值
返回值Ivar *:指的是一个ivar数组,会把所有成员属性放在一个数组中,通过返回的数组就能全部获取到。
count: 成员变量个数
*/unsignedintcount =0;// 获取类中的所有成员变量Ivar *ivarList = class_copyIvarList(self, &count);// 遍历所有成员变量for(inti =0; i < count; i++) {// 根据角标,从数组取出对应的成员变量Ivar ivar = ivarList[i];// 获取成员变量名字NSString*ivarName = [NSStringstringWithUTF8String:ivar_getName(ivar)];// 处理成员变量名->字典中的key(去掉 _ ,从第一个角标开始截取)NSString*key = [ivarName substringFromIndex:1];// 根据成员属性名去字典中查找对应的valueidvalue = dict[key];// 【如果模型属性数量大于字典键值对数理,模型属性会被赋值为nil】// 而报错 (could not set nil as the value for the key age.)if(value) {// 给模型中属性赋值[objc setValue:value forKey:key]; } }returnobjc;}
注解:这里在获取模型类中的所有属性名,是采取class_copyIvarList先获取成员变量(以下划线开头) ,然后再处理成员变量名->字典中的key(去掉 _ ,从第一个角标开始截取) 得到属性名。
原因:Ivar:成员变量,以下划线开头,Property 属性
获取类里面属性class_copyPropertyList
获取类中的所有成员变量class_copyIvarList
{int_a;// 成员变量}@property(nonatomic,assign)NSIntegerattitudes_count;// 属性这里有成员变量,就不会漏掉属性;如果有属性,可能会漏掉成员变量;
使用runtime字典转模型获取模型属性名的时候,最好获取成员属性名Ivar因为可能会有个属性是没有setter和getter方法的。
2、runtime 字典转模型-->模型中嵌套模型「模型属性是另外一个模型对象」,这种情况处理如下:
+ (instancetype)modelWithDict2:(NSDictionary*)dict{// 1.创建对应的对象idobjc = [[selfalloc] init];// 2.利用runtime给对象中的属性赋值unsignedintcount =0;// 获取类中的所有成员变量Ivar *ivarList = class_copyIvarList(self, &count);// 遍历所有成员变量for(inti =0; i < count; i++) {// 根据角标,从数组取出对应的成员变量Ivar ivar = ivarList[i];// 获取成员变量名字NSString*ivarName = [NSStringstringWithUTF8String:ivar_getName(ivar)];// 获取成员变量类型NSString*ivarType = [NSStringstringWithUTF8String:ivar_getTypeEncoding(ivar)];// 替换: @\"User\" -> UserivarType = [ivarType stringByReplacingOccurrencesOfString:@"\""withString:@""]; ivarType = [ivarType stringByReplacingOccurrencesOfString:@"@"withString:@""];// 处理成员属性名->字典中的key(去掉 _ ,从第一个角标开始截取)NSString*key = [ivarName substringFromIndex:1];// 根据成员属性名去字典中查找对应的valueidvalue = dict[key];//--------------------------- <#我是分割线#> ------------------------------////// 二级转换:如果字典中还有字典,也需要把对应的字典转换成模型// 判断下value是否是字典,并且是自定义对象才需要转换if([value isKindOfClass:[NSDictionaryclass]] && ![ivarType hasPrefix:@"NS"]) {// 字典转换成模型 userDict => User模型, 转换成哪个模型// 根据字符串类名生成类对象Class modelClass =NSClassFromString(ivarType);if(modelClass) {// 有对应的模型才需要转// 把字典转模型value = [modelClass modelWithDict2:value]; } }// 给模型中属性赋值if(value) { [objc setValue:value forKey:key]; } }returnobjc;}
3、runtime 字典转模型-->数组中装着模型「模型的属性是一个数组,数组中是字典模型对象」,这种情况处理如下:
// Runtime:根据模型中属性,去字典中取出对应的value给模型属性赋值// 思路:遍历模型中所有属性->使用运行时+ (instancetype)modelWithDict3:(NSDictionary*)dict{// 1.创建对应的对象idobjc = [[selfalloc] init];// 2.利用runtime给对象中的属性赋值unsignedintcount =0;// 获取类中的所有成员变量Ivar *ivarList = class_copyIvarList(self, &count);// 遍历所有成员变量for(inti =0; i < count; i++) {// 根据角标,从数组取出对应的成员变量Ivar ivar = ivarList[i];// 获取成员变量名字NSString*ivarName = [NSStringstringWithUTF8String:ivar_getName(ivar)];// 处理成员属性名->字典中的key(去掉 _ ,从第一个角标开始截取)NSString*key = [ivarName substringFromIndex:1];// 根据成员属性名去字典中查找对应的valueidvalue = dict[key];//--------------------------- <#我是分割线#> ------------------------------////// 三级转换:NSArray中也是字典,把数组中的字典转换成模型.// 判断值是否是数组if([value isKindOfClass:[NSArrayclass]]) {// 判断对应类有没有实现字典数组转模型数组的协议// arrayContainModelClass 提供一个协议,只要遵守这个协议的类,都能把数组中的字典转模型if([selfrespondsToSelector:@selector(arrayContainModelClass)]) {// 转换成id类型,就能调用任何对象的方法ididSelf =self;// 获取数组中字典对应的模型NSString*type = [idSelf arrayContainModelClass][key];// 生成模型Class classModel =NSClassFromString(type);NSMutableArray*arrM = [NSMutableArrayarray];// 遍历字典数组,生成模型数组for(NSDictionary*dictinvalue) {// 字典转模型idmodel = [classModel modelWithDict3:dict]; [arrM addObject:model]; }// 把模型数组赋值给valuevalue = arrM; } }// 如果模型属性数量大于字典键值对数理,模型属性会被赋值为nil,而报错if(value) {// 给模型中属性赋值[objc setValue:value forKey:key]; } }returnobjc;}
runtime字典转模型-->数组中装着模型 打印输出
总结:我们既然能获取到属性类型,那就可以拦截到模型的那个数组属性,进而对数组中每个模型遍历并字典转模型,但是我们不知道数组中的模型都是什么类型,我们可以声明一个方法,该方法目的不是让其调用,而是让其实现并返回模型的类型。
这里提到的你如果不是很清楚,建议参考我的Demo,重要的部分代码中都有相应的注解和文字打印,运行程序可以很直观的表现。
runtime 其它作用「面试熟悉」
动态添加方法
应用场景:如果一个类方法非常多,加载类到内存的时候也比较耗费资源,需要给每个方法生成映射表,可以使用动态给某个类,添加方法解决。
注解:OC 中我们很习惯的会用懒加载,当用到的时候才去加载它,但是实际上只要一个类实现了某个方法,就会被加载进内存。当我们不想加载这么多方法的时候,就会使用到runtime动态的添加方法。
需求:runtime 动态添加方法处理调用一个未实现的方法 和 去除报错。
案例代码:方法+调用+打印输出
- (void)viewDidLoad { [superviewDidLoad]; Person *p = [[Person alloc] init];// 默认person,没有实现run:方法,可以通过performSelector调用,但是会报错。// 动态添加方法就不会报错[p performSelector:@selector(run:) withObject:@10];}@implementationPerson// 没有返回值,1个参数// void,(id,SEL)voidaaa(idself, 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->_cmdclass_addMethod(self, sel, (IMP)aaa,"v@:@");returnYES; }return[superresolveInstanceMethod:sel];}@end// 打印输出2016-02-1719:05:03.917runtime[12761:543574] runtime动态添加方法--跑了10米
动态变量控制
现在有一个Person类,创建 xiaoming对象
动态获取 XiaoMing 类中的所有属性 [当然包括私有]
Ivar *ivar = class_copyIvarList([self.xiaomingclass], &count);
遍历属性找到对应name字段
constchar*varName = ivar_getName(var);
修改对应的字段值成20
object_setIvar(self.xiaoMing,var, @"20");
代码参考
-(void)answer{unsignedintcount =0; Ivar *ivar = class_copyIvarList([self.xiaoMingclass], &count);for(inti =0; i
实现NSCoding的自动归档和解档
如果你实现过自定义模型数据持久化的过程,那么你也肯定明白,如果一个模型有许多个属性,那么我们需要对每个属性都实现一遍encodeObject和decodeObjectForKey方法,如果这样的模型又有很多个,这还真的是一个十分麻烦的事情。下面来看看简单的实现方式。
假设现在有一个Movie类,有3个属性。先看下.h文件
// Movie.h文件//1. 如果想要当前类可以实现归档与反归档,需要遵守一个协议NSCoding@interfaceMovie: NSObject@property(nonatomic, copy) NSString *movieId;@property(nonatomic, copy) NSString *movieName;@property(nonatomic, copy) NSString *pic_url;@end
如果是正常写法,.m文件应该是这样的:
// Movie.m文件@implementationMovie- (void)encodeWithCoder:(NSCoder *)aCoder{ [aCoderencodeObject:_movieIdforKey:@"id"]; [aCoderencodeObject:_movieNameforKey:@"name"]; [aCoderencodeObject:_pic_urlforKey:@"url"];}- (id)initWithCoder:(NSCoder *)aDecoder{if(self = [superinit]) { self.movieId = [aDecoderdecodeObjectForKey:@"id"]; self.movieName = [aDecoderdecodeObjectForKey:@"name"]; self.pic_url = [aDecoderdecodeObjectForKey:@"url"]; }returnself;}@end
如果这里有100个属性,那么我们也只能把100个属性都给写一遍吗。
不过你会使用runtime后,这里就有更简便的方法,如下。
#import"Movie.h"#import@implementationMovie- (void)encodeWithCoder:(NSCoder*)encoder{unsignedintcount =0; Ivar *ivars = class_copyIvarList([Movieclass], &count);for(inti =0; i
这样的方式实现,不管有多少个属性,写这几行代码就搞定了。怎么,代码有点多,
好说下面看看更加简便的方法:两句代码搞定。
#import"Movie.h"#import#define encodeRuntime(A) \\unsignedintcount =0;\Ivar *ivars = class_copyIvarList([Aclass], &count);\for(inti =0; i
优化:上面是encodeWithCoder和initWithCoder这两个方法抽成宏。我们可以把这两个宏单独放到一个文件里面,这里以后需要进行数据持久化的模型都可以直接使用这两个宏。
runtime 下Class的各项操作
下面是 runtime 下Class的常见方法 及 带有使用示例代码。各项操作,原著 http://www.jianshu.com/p/46dd81402f63
unsigned int count;
获取属性列表
objc_property_t *propertyList = class_copyPropertyList([selfclass], &count);for(unsignedinti=0; i%@", [NSStringstringWithUTF8String:propertyName]); }
获取方法列表
Method*methodList=class_copyMethodList([selfclass], &count);for(unsigned int i; i
Method method = methodList[i];
NSLog(@"method---->%@", NSStringFromSelector(method_getName(method)));
}
获取成员变量列表
Ivar *ivarList = class_copyIvarList([selfclass], &count);for(unsignedinti; i%@", [NSStringstringWithUTF8String:ivarName]); }
获取协议列表
__unsafe_unretainedProtocol **protocolList = class_copyProtocolList([selfclass], &count);for(unsignedinti; i%@", [NSStringstringWithUTF8String:protocolName]); }
现在有一个Person类,和person创建的xiaoming对象,有test1和test2两个方法
获得类方法
ClassPersonClass = object_getClass([Personclass]);SEL oriSEL = @selector(test1);MethodoriMethod= _class_getMethod(xiaomingClass, oriSEL);
获得实例方法
ClassPersonClass = object_getClass([xiaomingclass]);SEL oriSEL = @selector(test2);MethodcusMethod=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([Personclass],&count);// 得到所有成员变量Ivar *allVariables = class_copyIvarList([Personclass],&count);// 得到所有属性objc_property_t *properties = class_copyPropertyList([Personclass],&count);// 根据名字得到类变量的Ivar指针,但是这个在OC中好像毫无意义Ivar oneCVIvar = class_getClassVariable([Personclass],name);// 根据名字得到实例变量的Ivar指针Ivar oneIVIvar = class_getInstanceVariable([Personclass],name);// 找到后可以直接对私有变量赋值object_setIvar(_per, oneIVIvar, @"Mike");//强制修改name属性/* 动态添加方法:
第一个参数表示Class cls 类型;
第二个参数表示待调用的方法名称;
第三个参数(IMP)myAddingFunction,IMP是一个函数指针,这里表示指定具体实现方法myAddingFunction;
第四个参数表方法的参数,0代表没有参数;
*/class_addMethod([_perclass],@selector(sayHi), (IMP)myAddingFunction,0);// 交换两个方法method_exchangeImplementations(method1, method2);// 关联两个对象objc_setAssociatedObject(idobject, 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的定义
structobjc_class{Class isa OBJC_ISA_AVAILABILITY;//每个Class都有一个isa指针#if!__OBJC2__Class super_class OBJC2_UNAVAILABLE;//父类constchar*name OBJC2_UNAVAILABLE;//类名longversion OBJC2_UNAVAILABLE;//类版本longinfo OBJC2_UNAVAILABLE;//!*!供运行期使用的一些位标识。如:CLS_CLASS (0x1L)表示该类为普通class; CLS_META(0x2L)表示该类为metaclass等(runtime.h中有详细列出)longinstance_size OBJC2_UNAVAILABLE;//实例大小structobjc_ivar_list*ivarsOBJC2_UNAVAILABLE;//存储每个实例变量的内存地址structobjc_method_list**methodListsOBJC2_UNAVAILABLE;//!*!根据info的信息确定是类还是实例,运行什么函数方法等structobjc_cache*cacheOBJC2_UNAVAILABLE;//缓存structobjc_protocol_list*protocolsOBJC2_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。
selector --> 对应的IMP
交换方法的几种实现方式
利用method_exchangeImplementations交换两个方法的实现
利用class_replaceMethod替换方法的实现
利用method_setImplementation来直接设置某个方法的IMP。
交换方法
这里可以参考简友这篇:【Runtime Method Swizzling开发实例汇总】http://www.jianshu.com/p/f6dad8e1b848
这里可以参考权威这篇:OC运行时黑魔法 Method Swizzling
-- 小黑不要走,我一定咬吃了你!
最后一道面试题的注解
下面的代码输出什么?
@implementationSon:NSObject- (id)init{self= [superinit];if(self) {NSLog(@"%@",NSStringFromClass([selfclass]));NSLog(@"%@",NSStringFromClass([superclass])); }returnself;}@end
先思考一下,会打印出来什么❓
关注我的更多干货分享 ^_^.
答案:都输出 Son
class获取当前方法的调用者的类,superClass获取当前方法的调用者的父类,super仅仅是一个编译指示器,就是给编译器看的,不是一个指针。
本质:只要编译器看到super这个标志,就会让当前对象去调用父类方法,本质还是当前对象在调用
这个题目主要是考察关于objc中对self和super的理解:
self是类的隐藏参数,指向当前调用方法的这个类的实例。而super本质是一个编译器标示符,和self是指向的同一个消息接受者
当使用self调用方法时,会从当前类的方法列表中开始找,如果没有,就从父类中再找;
而当使用super时,则从父类的方法列表中开始找。然后调用父类的这个方法
调用[self class]时,会转化成objc_msgSend函数
idobjc_msgSend(idself, SEL op, ...)- 调用 `[superclass]`时,会转化成 `objc_msgSendSuper` 函数.idobjc_msgSendSuper(structobjc_super *super, SEL op, ...)第一个参数是 objc_super 这样一个结构体,其定义如下structobjc_super { __unsafe_unretainedidreceiver; __unsafe_unretainedClass super_class; };第一个成员是 receiver, 类似于上面的 objc_msgSend函数第一个参数self第二个成员是记录当前类的父类是什么,告诉程序从父类中开始找方法,找到方法后,最后内部是使用 objc_msgSend(objc_super->receiver,@selector(class))去调用, 此时已经和[selfclass]调用相同了,故上述输出结果仍然返回 Sonobjc Runtime 开源代码对- (Class)class方法的实现-(Class)class{returnobject_getClass(self); }
runtime模块简友文章推荐(❤️数量较多)
简友runtime模块推荐阅读文章
西木完整总结http://www.jianshu.com/p/6b905584f536
天口三水羊objc_msgSendhttp://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
卖报的小画家SureMethod Swizzling开发实例汇总http://www.jianshu.com/p/f6dad8e1b848
滕大鸟OC最实用的runtime总结http://www.jianshu.com/p/ab966e8a82e2
黑花白花Runtime在实际开发中的应用http://www.jianshu.com/p/851b21870d91
OC & Swift Runtime 面试最常问到的题整理【建议看】
说明:此面试题针对性的摘录整理,只为方便 在面试路上准备的你 ,会注有原文。
1、整理原文:2017年5月iOS招人心得(附面试题)
Runtime
objc在向一个对象发送消息时,发生了什么?
什么时候会报unrecognized selector错误?iOS有哪些机制来避免走到这一步?
能否向编译后得到的类中增加实例变量?能否向运行时创建的类中添加实例变量?为什么?
runtime如何实现weak变量的自动置nil?
给类添加一个属性后,在类结构体里哪些元素会发生变化?
RunLoop
runloop是来做什么的?runloop和线程有什么关系?主线程默认开启了runloop么?子线程呢?
runloop的mode是用来做什么的?有几种mode?
为什么把NSTimer对象以NSDefaultRunLoopMode(kCFRunLoopDefaultMode)添加到4. 主运行循环以后,滑动scrollview的时候NSTimer却不动了?
苹果是如何实现Autorelease Pool的?
//-------------------- 【我是分割线】 ---------------------//
整理原文:2017年iOS面试题总结,附上答案
Runtime
01
问题:objc在向一个对象发送消息时,发生了什么?
解答:根据对象的 isa 指针找到类对象 id,在查询类对象里面的 methodLists 方法函数列表,如果没有在好到,在沿着 superClass ,寻找父类,再在父类 methodLists 方法列表里面查询,最终找到 SEL ,根据 id 和 SEL 确认 IMP(指针函数),在发送消息;
03
问题:什么时候会报unrecognized selector错误?iOS有哪些机制来避免走到这一步?
解答:当发送消息的时候,我们会根据类里面的 methodLists 列表去查询我们要动用的SEL,当查询不到的时候,我们会一直沿着父类查询,当最终查询不到的时候我们会报unrecognized selector错误,当系统查询不到方法的时候,会调用+(BOOL)resolveInstanceMethod:(SEL)sel动态解释的方法来给我一次机会来添加,调用不到的方法。或者我们可以再次使用-(id)forwardingTargetForSelector:(SEL)aSelector重定向的方法来告诉系统,该调用什么方法,一来保证不会崩溃。
04
问题:能否向编译后得到的类中增加实例变量?能否向运行时创建的类中添加实例变量?为什么?
解答:1、不能向编译后得到的类增加实例变量 2、能向运行时创建的类中添加实例变量。【解释】:1. 编译后的类已经注册在 runtime 中,类结构体中的 objc_ivar_list 实例变量的链表和 instance_size 实例变量的内存大小已经确定,runtime会调用 class_setvarlayout 或 class_setWeaklvarLayout 来处理strong weak 引用.所以不能向存在的类中添加实例变量。2. 运行时创建的类是可以添加实例变量,调用class_addIvar函数. 但是的在调用 objc_allocateClassPair 之后,objc_registerClassPair 之前,原因同上.
05
问题:runtime如何实现weak变量的自动置nil?
解答:runtime 对注册的类, 会进行布局,对于 weak 对象会放入一个 hash 表中。 用 weak 指向的对象内存地址作为 key,当此对象的引用计数为0的时候会 dealloc,假如 weak 指向的对象内存地址是a,那么就会以a为键, 在这个 weak 表中搜索,找到所有以a为键的 weak 对象,从而设置为 nil。
06
问题:给类添加一个属性后,在类结构体里哪些元素会发生变化?
解答:instance_size :实例的内存大小;objc_ivar_list *ivars:属性列表
RunLoop
01
问题:runloop是来做什么的?runloop和线程有什么关系?主线程默认开启了runloop么?子线程呢?
解答:runloop: 字面意思就是跑圈,其实也就是一个循环跑圈,用来处理线程里面的事件和消息。runloop和线程的关系:每个线程如果想继续运行,不被释放,就必须有一个runloop来不停的跑圈,以来处理线程里面的各个事件和消息。主线程默认是开启一个runloop。也就是这个runloop才能保证我们程序正常的运行。子线程是默认没有开始runloop的
02
问题:runloop的mode是用来做什么的?有几种mode?
解答:model:是runloop里面的模式,不同的模式下的runloop处理的事件和消息有一定的差别。系统默认注册了5个Mode:(1)kCFRunLoopDefaultMode: App的默认 Mode,通常主线程是在这个 Mode 下运行的。(2)UITrackingRunLoopMode: 界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响。(3)UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用。(4)GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到。(5)kCFRunLoopCommonModes: 这是一个占位的 Mode,没有实际作用。注意iOS 对以上5中model进行了封装 NSDefaultRunLoopMode、NSRunLoopCommonModes
03
问题:为什么把NSTimer对象以NSDefaultRunLoopMode(kCFRunLoopDefaultMode)添加到主运行循环以后,滑动scrollview的时候NSTimer却不动了?
解答:nstime对象是在 NSDefaultRunLoopMode下面调用消息的,但是当我们滑动scrollview的时候,NSDefaultRunLoopMode模式就自动切换到UITrackingRunLoopMode模式下面,却不可以继续响应nstime发送的消息。所以如果想在滑动scrollview的情况下面还调用nstime的消息,我们可以把nsrunloop的模式更改为NSRunLoopCommonModes.
04
问题:苹果是如何实现Autorelease Pool的?
解答:Autorelease Pool作用:缓存池,可以避免我们经常写relase的一种方式。其实就是延迟release,将创建的对象,添加到最近的autoreleasePool中,等到autoreleasePool作用域结束的时候,会将里面所有的对象的引用计数器 - autorelease.
后续遇到针对runtime常面相关,会及时在这里补充;
效果图
来杯白开水,写的小样在下面 ~
附上写的小样 Demo,重要的部分代码中都有相应的注解和文字打印,运行程序可以很直观的表现
【@码员真爱 : 真的不点进去阅读一下 ❓】