iOS runtime 详解
一、runtime 简介
OC是一门动态语言,所以它总想办法把一些决定工作从编译推迟到运行时。也就是说只有编译器是不够的,它还需要一个运行时系统来执行编译后的代码。这就是Runtime系统存在的意义,它是整个OC的一个基石。
Runtime基本是用C和汇编语言写的,可见苹果为动态系统的高效做出的努力。
Runtime库主要做下面几件事:
找出方法的最终执行代码:当程序执行[object doSomething]时,会向消息接收者(object)发送一条消息(doSomething),runtime会根据消息接收者是否能响应该消息而做出不同的反应。这将在后面详细介绍。
消息机制方法调用流程
怎么去调用类方法和实例方法,实例方法:(保存到类对象的方法列表) ,类方法:(保存到元类(Meta Class)中方法列表)。
1.OC在向一个对象发送消息时,runtime库会根据对象的isa指针找到该对象对应的类或其父类中查找方法。
2.注册方法编号(这里用方法编号的好处,可以快速查找)。
3.根据方法编号去查找对应方法。
4.找到只是最终函数实现地址,根据地址去方法区调用对应函数。
原理:给一个类声明属性,其实本质就是给这个类添加关联,并不是直接把这个值的内存空间添加到类存空间。
应用场景:给系统的类添加属性的时候,可以使用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,交换方法.
实现步骤:
<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添加额外功能--加载成功
应用场景:如果一个类的方法非常多,加载类到内存的时候比较耗资源,需要给每个方法生成映射表,可以使用动态给某个类添加方法解决。
注解: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米
字典转模型的方式:
-
一个一个给模型属性赋值
-
字典转模型KVC实现
1、KVC字典转模型弊端:必须保证,模型中的属性和字典中的key一一对应
2、如果不一致,就会调用[<Status 0x7fa74b545d60> setValue:forUndefinedKey:]
报key
找不到的错。
3、分析:模型中的属性和字典中的key
不一一对应,系统就会调用setValue:forUndefinedKey:
报错。
4、解决:重写对象的setValue:forUndefinedKey:
,把系统的方法覆盖,就能继续使用KVC字典转模型。 -
字典转模型Runtime实现
思路:利用运行时,遍历模型中的所有属性,根据模型中的属性名,去字典中查找key
,取出对应的值,给模型的属性赋值(注:字典中的取值,不一定会全部取出来)。
考虑情况:
1、当字典中的key
和模型的属性匹配不上。
2、模型中嵌套模型(模型属性是另一个模型对象)。
3、模型的属性是一个数组,数组中是一个个模型对象。
注解:字典中的key
和模型的属性不对应的情况有两种,一种是字典的键值对大于模型的属性数量,这时候我们不需要任何处理,因为runtime
是先遍历模型所有属性,再去字典中根据属性名找对应的值进行赋值,多余的键值对不需要去看;另外一种情况是模型属性数量大于字典中的键值对,这时候由于属性没有对应值会被赋值为nil
,就会导致crash
,只需加一个判断即可。
实现步骤:提供一个NSObject分类,专门字典转模型,以后所有模型都可以通过这个分类实现字典转模型。
MJExtension字典转模型实现也是通过底层对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 = 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
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术