转自掘金链接:https://juejin.im/post/593f77085c497d006ba389f0

相信对于从事开发人员来说 runtime 这个名称都不陌生,就像我起初只知道「 runtime 叫运行时 」,后来知道 runtime 同样可以像 KVC 一样访问私有成员变量,还有「 给类动态添加属性:LNTextField.placeholderColor || 交换方法:imageNamed => ln_imageNamed 」,还有深入的 「 消息机制的调用流程 || 字典转模型 || 实现NSCoding归解档 」以及我们常说的“黑魔法” 是什么?

runtime 是编程中比较难的模块,想要深入学习,这个模块你必须掌握,同样还有写的另一篇 runloop 模块,下面是我对 runtime 的整理,从零开始,由浅入深,且带了几个 Runtime 实践场景 --> 大厂来的工友们可选择性路过。

目录: ?、 ?‍?、?

  1. runtime.h释义
  2. 消息机制 1.示例:方法调用,是否真的是转换为消息机制? 2.objc_msgSend 参数概念释义
  3. 消息机制(方法调用流程)
  4. 常见作用
  5. 开发场景「工作掌握」 1.交换方法 2.给系统分类动态添加属性 3.字典转模型(Runtime 考虑三种情况实现)
  6. 其它作用「面试熟悉」 1.动态添加方法 2.动态变量控制 3.实现NSCoding的自动归档和解档 4.runtime 部分函数 5.method swizzling(俗称黑魔法)
  7. 一道面试题的注解
  8. 模块博文推荐(❤️数量较多)
  9. Runtime & Runloop 常面问题整理(附答案)
  10. Demo 重要的部分代码中都有相应的注解和文字打印,运行程序可以很直观的表现
  11. iOS 模块注解—「Runloop面试、工作」看我就 ? 了 ^_^.

释义


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

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

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

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

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

消息机制


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

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

消息机制原理:对象根据方法编号SEL去映射表查找对应的方法实现。注解: 1、必须要导入头文件 #import <objc/message.h>2、我们导入系统的头文件,一般用尖括号。 3、OC 解决消息机制方法提示步骤【查找build setting-> 搜索msg-> objc_msgSend(YES --> NO)】 4、最终生成消息机制,编译器做的事情,最终代码,需要把当前代码用xcode重新编译,【clang -rewrite-objc main.m查看最终生成代码】,示例:cd main.m --> 输入前面指令,就会生成 .opp文件(C++代码)5、这里一般不会直接导入<objc/runtime.h>

示例代码:OC 方法 <--> runtime 方法

说明:
eat(无参) 和 run(有参NSInteger) 是 LNPerson模型类中的私有方法「runtime 作用:可以调用私有方法」
示例分别以 OC写法 和 最底层写法 对照验证.
- (void)msgSend
{// 方法一://id objc = [NSObject alloc];LNPerson *person = objc_msgSend(objc_getClass("LNPerson"), sel_registerName("alloc"));//objc = [objc init];person = objc_msgSend(person, sel_registerName("init"));// 调用//[objc eat];//[objc run:10];objc_msgSend(person,@selector(eat)); // 无参objc_msgSend(person,@selector(run:),10); // 有残
}
/注解:// 用最底层写objc_getClass(const char *name) 获取当前类sel_registerName(const char *str) 注册个方法编号objc_msgSend(id self:谁发送消息, SEL op:发送什么消息, ...)让LNPerson这个类对象发送了一个alloc消息,返回一个分配好的内存对象给你,再发送一个消息初始化.*/// 方法二:
#pragma mark - 也许下面这种好理解一点
- (void)test
{// id objc = [NSObject alloc];id objc = objc_msgSend([NSObject class], @selector(alloc));// objc = [objc init];objc = objc_msgSend(objc, @selector(eat));}
复制代码

objc_msgSend 参数概念

/objc_msgSend(<#id  _Nullable self#>, <#SEL  _Nonnull op, ...#>)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`指针就可以顺藤摸瓜找到对象所属的类。*/
复制代码

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


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

怎么去调用eat方法,对象方法:(保存到类对象的方法列表) ,类方法:(保存到元类(Meta Class)中方法列表)。 1、OC 在向一个对象发送消息时,runtime 库会根据对象的 isa指针找到该对象对应的类或其父类中查找方法。。 2、注册方法编号(这里用方法编号的好处,可以快速查找)。 3、根据方法编号去查找对应方法。 4、找到只是最终函数实现地址,根据地址去方法区调用对应函数。

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

常见作用


/1、动态交换两个方法的实现2、动态添加属性3、实现字典转模型的自动转换4、动态添加方法5、拦截并替换方法6、实现 NSCoding 的自动归档和解档补充常用runtime示例:Demo中有体现1.添加属性和交换方法示例:UITextField占位文字颜色placeholderColor2.交换方法示例:交换dealloc方法实现,添加功能那个控制器被销毁了*/
复制代码

开发场景「工作掌握」


runtime 交换方法

场景:当第三方框架 或者 系统原生方法功能不能满足我们的时候,我们可以在保持系统原有方法功能的基础上,添加额外的功能。

需求:加载一张图片直接用[UIImage imageNamed:@"image"];是无法知道到底有没有加载成功。给系统的imageNamed添加额外功能(是否加载图片成功)。 方案一:继承系统的类,重写方法.(弊端:每次使用都需要导入) 方案二:使用 runtime,交换方法.

实现步骤: 1、给系统的方法添加分类 2、自己实现一个带有扩展功能的方法 3、交换方法,只需要交换一次,

场景代码:方法+调用+打印输出

#import "UIImage+Image.h"
#import <objc/message.h>@implementation UIImage (Image)/看清楚下面是不会有死循环的调用 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 {}*//作用:把类加载进内存的时候调用,只会调用一次调用:方法应先交换,再去调用*/
+ (void)load {// 1.获取 imageNamed方法地址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);
}- - -
//方案一:先搞个分类,定义一个能加载图片并且能打印的方法+ (instancetype)imageWithName:(NSString *)name;
//方案二:交换 imageNamed 和 ln_imageNamed 的实现,就能调用 imageNamed,间接调用 ln_imageNamed 的实现。
- (void)viewDidLoad
{[super viewDidLoad];self.imageView.image = [UIImage imageNamed:@"白水ln"];
}- - -
// 打印输出
2016-03-17 17:52:14.693 runtime[12761:543574] runtime交互方法 -> 图片加载成功
复制代码

总结: 我们所做的就是在方法调用流程第三步的时候,交换两个方法地址指向。而且我们改变指向要在系统的imageNamed:方法调用前,所以将代码写在了分类的load方法里。最后当运行的时候系统的方法就会去找我们的方法的实现。

给系统分类动态添加属性

场景:给系统的类添加额外属性的时候,可以使用runtime动态添加属性方法。原理:给一个类声明属性,其实本质就是给这个类添加关联,并不是直接把这个值的内存空间添加到类存空间。注解:给系统 NSObject添加一个分类,我们知道在分类中是不能够添加成员属性的,虽然我们用了@property,但是仅仅会自动生成getset方法的声明,并没有带下划线的属性和方法实现生成。但是我们可以通过runtime就可以做到给它方法的实现。

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

场景代码:方法+调用+打印

#import <Foundation/Foundation.h>
@interface NSObject (Property)@property NSString *name;
@end- - -
#import "NSObject+Property.h"
#import <objc/message.h>
//#import <objc/runtime.h>@implementation NSObject (Property)- (NSString *)name
{// 利用参数key 将对象object中存储的对应值取出来return objc_getAssociatedObject(self, @"name");
}- (void)setName:(NSString *)name
{/**将某个值跟某个对象关联起来,将某个值存储到某个对象中objc_setAssociatedObject(<#id  _Nonnull object#>:给哪个对象添加属性, <#const void * _Nonnull key#>:属性名称, <#id  _Nullable value#>:属性值, <#objc_AssociationPolicy policy#>:保存策略)*/objc_setAssociatedObject(self, @"name", name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);NSLog(@"name---->%p",name);
}
@end// 调用
NSObject *objc = [[NSObject alloc] init];
objc.name = @"CoderLN";
NSLog(@"runtime动态添加属性name==%@",objc.name);// 打印输出
2016-03-17 19:37:10.530 runtime[12761:543574] runtime动态添加属性name == CoderLN
复制代码

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

字典转模型

字典转模型的方式

  • 给模型中属性,在 .m 依次赋值(初学者)。
  • 字典转模型 KVC 实现
    • KVC 字典转模型弊端:必须保证,模型中的属性和字典中的key一一对应。
    • 如果不一致,就会调用[<Status 0x7fa74b545d60> setValue:forUndefinedKey:]key找不到的错。
    • 分析:模型中的属性和字典的key不一一对应,系统就会调用setValue:forUndefinedKey:报错。
    • 解决:重写对象的setValue:forUndefinedKey:,把系统的方法覆盖,就能继续使用KVC,字典转模型了。
  • 字典转模型 Runtime 实现
    • 思路:利用运行时,遍历模型中所有属性,根据模型的属性名,去字典中查找key,取出对应的值,给模型的属性赋值(从提醒:字典中取值,不一定要全部取出来);提供一个NSObject分类,专门字典转模型,以后所有模型都可以通过这个分类实现字典转模型。

    • 考虑情况: 1、当字典的key和模型的属性匹配不上。 2、模型中嵌套模型(模型属性是另外一个模型对象)。 3、数组中装着模型(模型的属性是一个数组,数组中是一个个模型对象)。

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

  • MJExtension 字典转模型实现
    • 底层也是对 runtime的封装,才可以把一个模型中所有属性遍历出来。(我之所以看不懂,是MJ封装了很多层而已^_^.)。

示例:runtime 字典转模型考虑三种情况

1、runtime 字典转模型-->字典的 key 和模型的属性不匹配「模型属性数量大于字典键值对数」,这种情况处理如下:

#import "NSObject+Model.h"
#import <objc/message.h>@implementation NSObject (Model)// 思路:利用runtime 遍历模型中所有属性,根据模型中属性,去字典中取出对应的value给模型属性赋值
+ (instancetype)modelWithDict:(NSDictionary *)dict
{// 1.创建对应的对象id objc = [[self alloc] init];// 2.利用runtime给对象中的属性赋值/**获取类中的所有成员变量class_copyIvarList(Class _Nullable cls:表示获取哪个类中的成员变量, unsigned int * _Nullable outCount:表示这个类有多少成员变量,传入一个Int变量地址,会自动给这个变量赋值)返回值Ivar * =指的是一个ivar数组,会把所有成员属性放在一个数组中,通过返回的数组就能全部获取到*/// 成员变量个数unsigned int count = 0;// 获取类中的所有成员变量Ivar *ivarList = class_copyIvarList(self, &count);// 遍历所有成员变量for (int i = 0; i < count; i++) {// 根据角标,从数组取出对应的成员变量(Ivar:成员变量,以下划线开头)Ivar ivar = ivarList[i];// 获取成员变量名字NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];// 处理成员变量名,字典中的key(去掉 _ ,从第一个角标开始截取)NSString *key = [ivarName substringFromIndex:1];// 根据成员属性名去字典中查找对应的valueid 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(去掉 _ ,从第一个角标开始截取) 得到属性名。

原因

{int _a; // 成员变量
}
@property (nonatomic, assign) NSInteger attitudes_count; // 属性`Ivar:成员变量,以下划线开头`,
`Property 属性`
`class_copyPropertyList` 获取类里面属性
`class_copyIvarList` 获取类中的所有成员变量
这里有成员变量,就不会漏掉属性;如果有属性,可能会漏掉成员变量;
使用`runtime`字典转模型获取模型属性名的时候,最好获取成员属性名`Ivar`因为可能会有个属性是没有`setter`和`getter`方法的。
复制代码

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

// 思路:利用runtime 遍历模型中所有属性,根据模型中属性,去字典中取出对应的value给模型属性赋值
+ (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 ivar = ivarList[i];// 获取成员变量名字NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];// 获取成员变量类型NSString *ivarType = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];// 替换: @\"User\" -> UserivarType = [ivarType stringByReplacingOccurrencesOfString:@"\"" withString:@""];ivarType = [ivarType stringByReplacingOccurrencesOfString:@"@" withString:@""];// 处理成员变量名->字典中的key(去掉 _ ,从第一个角标开始截取)NSString *key = [ivarName substringFromIndex:1];// 根据成员属性名去字典中查找对应的valueid 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 ivar = ivarList[i];// 获取成员变量名字NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];// 处理成员属性名->字典中的key(去掉 _ ,从第一个角标开始截取)NSString *key = [ivarName substringFromIndex:1];// 根据成员属性名去字典中查找对应的valueid 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];}// 把模型数组赋值给valuevalue = arrM;     }}// 如果模型属性数量大于字典键值对数理,模型属性会被赋值为nil,而报错if (value) {// 给模型中属性赋值[objc setValue:value forKey:key];}}return objc;
}
复制代码

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

这里提到的你如果不是很清楚,建议参考我的Demo,重要的部分代码中都有相应的注解和文字打印,运行程序可以很直观的表现。

其它作用「面试熟悉」


动态添加方法

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

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

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

场景代码:方法+调用+打印输出

#import "Person.h"
#import <objc/message.h>@implementation Person/**调用:只要一个对象调用了一个未实现的方法就会调用这个方法,进行处理作用:动态添加方法,处理未实现注解:任何方法默认都有两个隐式参数,self,_cmd(当前方法的方法编号)*/
+ (BOOL)resolveInstanceMethod:(SEL)sel {if (sel == NSSelectorFromString(@"roll:")) {/**class_addMethod(<#Class  _Nullable __unsafe_unretained cls#>:给哪个类添加方法, <#SEL  _Nonnull name#>:添加哪个方法,即添加方法的方法编号, <#IMP  _Nonnull imp#>:方法实现 => 函数 => 函数入口 => 函数名(添加方法的函数实现(函数地址)), <#const char * _Nullable types#>:方法类型,(返回值+参数类型) v:void @:对象->self :表示SEL->_cmd)*/// 给类添加roll:滚了多远方法class_addMethod(self, sel, (IMP)LNRoll, "v@:@");return YES;}if ([NSStringFromSelector(sel) isEqualToString:@"go:"]) {// 给类添加go:走了多远方法class_addMethod(self, sel, (IMP)LNGO, "v@:@");return YES;}return [super resolveInstanceMethod:sel];
}// 调用
Person *p = [[Person alloc] init];
// 执行某个方法
[p performSelector:@selector(roll:) withObject:@"11"];
[p performSelector:@selector(go:) withObject:@10];// 打印输出
2016-03-17 19:05:03.917 runtime[12761:543574] 我滚了 11 米远的屎蛋
2016-03-17 19:05:04.617 runtime[12761:543574] 我走了 10 公里才到的家
复制代码

实现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的各项操作

1、runtime 部分函数

#warning - 以下为功能模块相关的方法示例, 具体方法作用、使用、注解请移步 -> github.com/CoderLN
以下的这些方法应该算是`runtime`在实际场景中所应用的大部分的情况了,平常的编码中差不多足够用了。0、class_copyPropertyList 获取类中所有的属性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]);}0、class_copyMethodList 获取类的所有方法Method *methodList = class_copyMethodList([self class], &count);for (unsigned int i; i<count; i++) {Method method = methodList[i];NSLog(@"method---->%@", NSStringFromSelector(method_getName(method)));}0、class_copyIvarList 获取类中所有的成员变量(outCount 会返回成员变量的总数)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]);}0、class_copyProtocolList 获取协议列表__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]);}0、object_getClass 获得类方法Class PersonClass = object_getClass([Person class]);SEL oriSEL = @selector(test1);Method oriMethod = _class_getMethod(xiaomingClass, oriSEL);0、class_getInstanceMethod 获得实例方法Class PersonClass = object_getClass([xiaoming class]);SEL oriSEL = @selector(test2);Method cusMethod = class_getInstanceMethod(xiaomingClass, oriSEL);0、class_addMethod 动态添加方法BOOL addSucc = class_addMethod(xiaomingClass, oriSEL, method_getImplementation(cusMethod), method_getTypeEncoding(cusMethod));0、class_replaceMethod 替换原方法实现class_replaceMethod(toolClass, cusSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));0、method_exchangeImplementations 交换两个方法的实现method_exchangeImplementations(method1, method2);0、根据名字得到类变量的Ivar指针,但是这个在OC中好像毫无意义Ivar oneCVIvar = class_getClassVariable([Person class], name);0、根据名字得到实例变量的Ivar指针Ivar oneIVIvar = class_getInstanceVariable([Person class], name);0、找到后可以直接对私有成员变量赋值(强制修改name属性)object_setIvar(_per, oneIVIvar, @"age");0、动态添加方法class_addMethod([person class]:Class cls 类型, @selector(eat):待调用的方法名称, (IMP)myAddingFunction:(IMP)myAddingFunction,IMP是一个函数指针,这里表示指定具体实现方法myAddingFunction, 0:0代表没有参数);0、获得某个类的类方法Method class_getClassMethod(Class cls , SEL name)0、获得成员变量的名字const char *ivar_getName(Ivar v);0、将某个值跟某个对象关联起来,将某个值存储到某个对象中void objc_setAssociatedObject(id object:表示关联者,是一个对象,变量名理所当然也是object , const void *key:获取被关联者的索引key ,id value :被关联者 ,objc_AssociationPolicy policy:关联时采用的协议,有assign,retain,copy等协议,一般使用OBJC_ASSOCIATION_RETAIN_NONATOMIC)0、利用参数key 将对象object中存储的对应值取出来id objc_getAssociatedObject(id object , const void *key)*/
复制代码

method swizzling(俗称黑魔法)


  • 简单说就是进行方法交换
  • Objective-C中调用一个方法,其实是向一个对象发送消息,查找消息的唯一依据是selector的名字。利用Objective-C的动态特性,可以实现在运行时偷换selector对应的方法实现,达到给方法挂钩的目的
  • 每个类都有一个方法列表,存放着方法的名字和方法实现的映射关系,selector的本质其实就是方法名,IMP有点类似函数指针,指向具体的Method实现,通过selector就可以找到对应的IMP

  • 交换方法的几种实现方式

    • 利用 method_exchangeImplementations交换两个方法的实现
    • 利用 class_replaceMethod替换方法的实现
    • 利用 method_setImplementation来直接设置某个方法的IMP

这里可以参考简友这篇:Runtime Method Swizzling开发实例汇总

一道面试题的注解


下面的代码输出什么?

@implementation Son : NSObject
- (id)init
{self = [super init];if (self) {NSLog(@"%@", NSStringFromClass([self class]));NSLog(@"%@", NSStringFromClass([super class]));}return self;
}
@end
复制代码

先思考一下,会打印出来什么❓


答案:都输出 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]调用相同了,故上述输出结果仍然返回 Sonobjc Runtime 开源代码对- (Class)class方法的实现
-(Class)class { return object_getClass(self);
}
复制代码

Runtime 模块博文推荐 (❤️数量较多)


作者 Runtime 模块推荐阅读博文
西木 完整总结 www.jianshu.com/p/6b905584f…
天口三水羊 objc_msgSend www.jianshu.com/p/9e1bc8d89…
夜千寻墨 详解 www.jianshu.com/p/46dd81402…
袁峥Seemygo 快速上手 www.jianshu.com/p/e07120610…
郑钦洪_ 实现自动化归档 www.jianshu.com/p/bd24c3f3c…
HenryCheng 消息机制 www.jianshu.com/p/f6300eb3e…
卖报的小画家Sure Method Swizzling开发实例汇总 www.jianshu.com/p/f6dad8e1b…
滕大鸟 OC最实用的runtime总结 www.jianshu.com/p/ab966e8a8…
黑花白花 Runtime在实际开发中的应用 www.jianshu.com/p/851b21870…

Runtime & Runloop 常面问题整理(附答案)

说明: 同一个面试问题并非只有一个答案,而同一个答案并不是在任何面试场合都有效,关键在于应聘者掌握了规律后,对面试的具体情况进行把握,有意识地揣摩面试官提出问题的心理 (真实问答),要 get 的到问的点,然后答其所问,算是“ 投其所好 ”吧。

此面试题针对性的摘录整理,只为方便在面试路上准备的你。后续遇到针对 runtime&runloop 功能和常面相关,会及时在作者github 替换、补充 ~


1、整理原文:2017年5月iOS招人心得(附面试题)

Runtime

1、objc在向一个对象发送消息时,发生了什么? 2、什么时候会报unrecognized selector错误?iOS有哪些机制来避免走到这一步? 3、能否向编译后得到的类中增加实例变量?能否向运行时创建的类中添加实例变量?为什么? 4、runtime如何实现weak变量的自动置nil? 5、给类添加一个属性后,在类结构体里哪些元素会发生变化?

RunLoop

1、runloop是来做什么的?runloop和线程有什么关系?主线程默认开启了runloop么?子线程呢? 2、runloop的mode是用来做什么的?有几种mode? 3、为什么把NSTimer对象以NSDefaultRunLoopMode(kCFRunLoopDefaultMode)添加到主运行循环以后,滑动scrollview的时候NSTimer却不动了? 5、苹果是如何实现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: 从字面意思看:运行循环、跑圈,其实它内部就是do-while循环,在这个循环内部不断地处理各种任务(比如Source、Timer、Observer)事件。runloop和线程的关系:一个线程对应一个RunLoop,主线程的RunLoop默认创建并启动,子线程的RunLoop需手动创建且手动启动(调用run方法)。RunLoop只能选择一个Mode启动,如果当前Mode中没有任何Source(Sources0、Sources1)、Timer,那么就直接退出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.

附上写的小样 Demo,重要的部分代码中都有相应的注解和文字打印,运行程序可以很直观的表现

Reading


  • 各位厂友,由于「时间 & 知识」有限,总结的文章难免有「未全、不足」,该模块将系统化学习,后替换、补充文章内容 ~
  • 熬夜写者不易 ? ,自愿点开在 Demo | 文章 | Rss 更新时收到提醒通知,便捷阅读

ios 模式讲解runtime runloop相关推荐

  1. iOS模式详解runtime面试工作

    简书:http://www.jianshu.com/p/19f280afcb24 对于从事 iOS 开发人员来说,所有的人都会答出「runtime 是运行时」,什么情况下用runtime?,大部分人能 ...

  2. CALayer与iOS动画 讲解及使用

    iOS CALayer与iOS动画 讲解及使用 关于CoreAnimation 初识CALayer CALayer CAAnimation CAMediaTiming UIView与CALayer动画 ...

  3. IOS NSUserDefaults 讲解 用法

    IOS NSUserDefaults 讲解 用法    NSUserDefaults适合存储轻量级的本地数据,比如要保存一个登陆界面的数据,用户名.密码之类的,个人觉得使用NSUserDefaults ...

  4. rsync本地模式讲解04

    rsync本地模式讲解04说明:本文来自来自北京老男孩linux运维实战培训中心-运维就业课程视频内容,本文内容为系列内容,更多分享信息见:http://oldboy.blog.51cto.com/2 ...

  5. 解决Carla同步模式下Runtime Error的问题(tick hangs)

    解决Carla同步模式下Runtime Error的问题(RuntimeError: time-out of 10000ms while waiting for the simulator, make ...

  6. VMware设置静态ip地址及不同网络模式讲解【Linux网络问题】

    VMware设置静态ip地址及不同网络模式讲解 此处的静态IP配置选用的是使用NAT方式连接网络[如果之前配置有错误,可以尝试暴力方法:将虚拟机网络配置重新恢复为默认,然后从头开始配置] 1 将Lin ...

  7. magento1中的eav模式讲解(入表)

    magento1 magento1中的eav模式讲解 EAV的含义 eav三个字母对应的三个单词为 Entity:实体 Attribute:属性 Value:值 EAV的理解:Entity-Attri ...

  8. art 模式 android runtime

    art 模式  android runtime         空间换时间的概念.(存储空间换运行时间) art:程序在安装时需要预编译读取,将代码转换为机器码,好处:程序运行时,无需时时转换,运行速 ...

  9. iOS 查漏补缺 - RunLoop

    RunLoop 在 0202 年的今天其实已经不是个新鲜的话题了,关于这方面的文章网上有很多大神总结得非常精辟. 作为 iOS 查漏补缺系列,这篇文章是笔者探索 RunLoop 底层的一些知识点总结, ...

最新文章

  1. 《OpenCV3编程入门》学习笔记10 角点检测(三)亚像素级角点检测
  2. java opencv calcCovarMatrix 计算协方差矩阵
  3. java 图片合成 工具类_Java实现的图片上传工具类完整实例
  4. GAN不只会造假:捕获数据中额外显著特征,提高表征学习可解释性,效果超越InfoGAN | IJCAI 2020...
  5. java系统教程_Java 教程(开发环境配置+基础语法)
  6. Codeforces 1246D/1225F Tree Factory (构造)
  7. 成人高考大学计算机基础答案,江苏省2019年成人高考大学计算机基础统考样卷...
  8. 使用pm2启动Node和Vue项目教程
  9. c不是面向对象编程语言 所以不具有面对,go 学习笔记之go是不是面向对象语言是否支持面对对象编程?...
  10. vsftpd 在linux 中的环境配置【部分原创】
  11. 如何迅速分析出系统CPU的瓶颈在哪里?
  12. linux 内存泄露检测工具——valgrind
  13. queue初始化java,如何在java中实例化一个Queue对象?
  14. NSUserDefault
  15. typedef NS_ENUM 等枚举介绍
  16. linux搭建vsftp服务器_Linux(CentOS 7)搭建VSFTP服务器
  17. 5.19 学习日记 活干得差不多了
  18. 使用git小乌龟拉取,更新,上传资料文档
  19. 通达OA2017版工作流触发器应用实例
  20. 音频开发_Microphone Array Beamforming_Delay Sum Beamforming

热门文章

  1. Baxter实战——Ubuntu 14.4+ros indigo 安装Baxter Simulator与 Moveit
  2. 【计算机网络】计算机等级三级网络——选择题
  3. Nat Micro:房刚组揭示细菌表观遗传调节艰难梭菌孢子形成
  4. 电视看板实现原理_拉萨公交看板广告投放公司价格_【狼界】全国点位覆盖
  5. 线性代数|学习笔记|18.065MIT公开课 lecture05
  6. 容器的作用span div10
  7. 2022 hgame pwn wp
  8. DSP看门狗实验源程序
  9. Lake Shore—625 型超导磁体电源
  10. python整段代码注释-Python中注释(多行注释和单行注释)的用法实例