1.runtime

runtime 常见作用

1.动态交换两个方法的实现

2.动态添加属性

3.实现字典转模型的自动转换

4.发送消息

5.动态添加方法

6.拦截并替换方法

7.实现 NSCoding 的自动归档和解档

该文件的目的是为读者谁可能有兴趣学习的Objective-C的运行时。
因为这不是一个关于C文件,它假定了一些以前的熟人与该语言。然而,它并不必须是一个广泛的熟人。
职能任务
使用类
class_getName

class_getSuperclass

class_isMetaClass

class_getInstanceSize

class_getInstanceVariable

class_getClassVariable

class_addIvar

class_copyIvarList

class_getIvarLayout

class_setIvarLayout

class_getWeakIvarLayout

class_setWeakIvarLayout

class_getProperty

class_copyPropertyList

class_addMethod

class_getInstanceMethod

class_getClassMethod

class_copyMethodList

class_replaceMethod

class_getMethodImplementation

class_getMethodImplementation_stret

class_respondsToSelector

class_addProtocol

class_addProperty

class_replaceProperty

class_conformsToProtocol

class_copyProtocolList

class_getVersion

class_setVersion

objc_getFutureClass

objc_setFutureClass

class_setSuperclass
已过时的OS X V10.5

添加类
objc_allocateClassPair

objc_disposeClassPair

objc_registerClassPair

objc_duplicateClass

类的实例化
class_createInstance

objc_constructInstance

objc_destructInstance

用工作实例
object_copy

object_dispose

object_setInstanceVariable

object_getInstanceVariable

object_getIndexedIvars

object_getIvar

object_setIvar

object_getClassName

object_getClass

object_setClass

获取类定义
objc_getClassList

objc_copyClassList

objc_lookUpClass

objc_getClass

objc_getRequiredClass

objc_getMetaClass

使用实例变量工作
ivar_getName

ivar_getTypeEncoding

ivar_getOffset

联想参考
objc_setAssociatedObject

objc_getAssociatedObject

objc_removeAssociatedObjects

查看objc/runtime.h中objc_class结构体的定义如下:

struct objc_class {
    Class isa  OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
    Class super_class                       OBJC2_UNAVAILABLE;  // 父类
    const char *name                        OBJC2_UNAVAILABLE;  // 类名
    long version                            OBJC2_UNAVAILABLE;  // 类的版本信息,默认为0
    long info                               OBJC2_UNAVAILABLE;  // 类信息,供运行期使用的一些位标识
    long instance_size                      OBJC2_UNAVAILABLE;  // 该类的实例变量大小
    struct objc_ivar_list *ivars            OBJC2_UNAVAILABLE;  // 该类的成员变量链表
    struct objc_method_list **methodLists   OBJC2_UNAVAILABLE;  // 方法定义的链表
    struct objc_cache *cache                OBJC2_UNAVAILABLE;  // 方法缓存
    struct objc_protocol_list *protocols    OBJC2_UNAVAILABLE;  // 协议链表
#endif
} OBJC2_UNAVAILABLE;

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方法。

1.runtime_第1张图片
image.png

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:这里一般不会直接导入


image.png

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([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.找到只是最终函数实现地址,根据地址去方法区调用对应函数。

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

runtime 常见作用

1.动态交换两个方法的实现

2.动态添加属性

3.实现字典转模型的自动转换

4.发送消息

5.动态添加方法

6.拦截并替换方法

7.实现 NSCoding 的自动归档和解档

runtime 常用开发应用场景「工作掌握」

1.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

// 打印输出

2016-02-17 17:52:14.693 runtime[12761:543574] runtime添加额外功能--加载成功

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

2.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);

// 打印输出

2016-02-17 19:37:10.530 runtime[12761:543574] runtime动态添加属性--name == 123

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

3.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 方式实现

说明:下面这个示例,是考虑三种情况包含在内的转换示例,具体可以看图上的注解

1.runtime_第2张图片
image.png

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;

}
1.runtime_第3张图片
image.png

runtime字典转模型-->数组中装着模型 打印输出

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

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

//--------------------------- <#我是分割线#> ------------------------------//

4.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

// 打印输出

2016-02-17 19:05:03.917 runtime[12761:543574] runtime动态添加方法--跑了10米

第二种方法

- (void)viewDidLoad
{
    [super viewDidLoad];
        
    
    XMView *vc = [[XMView alloc] init];
//
    vc.frame = CGRectMake(100, 100, 200, 200);
//    XMView *vc = [[XMView alloc] initWithFrame:CGRectMake(100, 100, 200, 200)];
    

    [self.view addSubview:vc];
    
//    [vc run];
    
//    [vc performSelector:@selector(run:) withObject:@10];

    class_addMethod([XMView class],
                    @selector(printPerson),
                    class_getMethodImplementation([XMViewController class], @selector(find)),
                    "v@:");
    
    
    [vc performSelector:@selector(printPerson)];
    
    
}

- (void)find {

    // Initialization code
  
    NSLog(@"=搜索==123==");

}
首先我们看看这个方法里面的参数:

Class cos:我们需要一个class,比如我的[Person class]。

SEL name:这个很有意思,这个名字自己可以随意想,就是添加的方法在本类里面叫做的名字,但是方法的格式一定要和你需要添加的方法的格式一样,比如有无参数。(有几个小伙伴问我Demo里面的findInSelf这个方法没有找到,请看这里呀。这个就是里面为啥没有findInSelf方法而可以直接调用的原因)

IMP imp:IMP就是Implementation的缩写,它是指向一个方法实现的指针,每一个方法都有一个对应的IMP。这里需要的是IMP,所以你不能直接写方法,需要用到一个方法:

OBJC_EXPORT IMP class_getMethodImplementation(Class cls, SEL name)

__OSX_AVAILABLE_STARTING(__MAC_10_5, __IPHONE_2_0);

这个方法也是runtime的方法,就是获得对应的方法的指针,也就是IMP。

const char *types:这一个也很有意思,我刚开始也很费解,结果看了好多人的解释我释然了,知道吗,我释然啦,。这个东西其实也很好理解:

比如:”v@:”意思就是这已是一个void类型的方法,没有参数传入。

再比如 “i@:”就是说这是一个int类型的方法,没有参数传入。

再再比如”i@:@”就是说这是一个int类型的方法,又一个参数传入。

好了,参数解释完了,还有一点需要注意,用这个方法添加的方法是无法直接调用的,必须用performSelector:调用。为甚么呢???

因为performSelector是运行时系统负责去找方法的,在编译时候不做任何校验;如果直接调用编译是会自动校验。

知道为甚么了吧,你添加方法是在运行时添加的,你在编译的时候还没有这个本类方法,所以当然不行啦。

//--------------------------- <#我是分割线#> ------------------------------//
5.动态变量控制

现在有一个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 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, var, @"20");
            
            break;
            
        }
        
    }
    
    NSLog(@"XiaoMing's age is %@",self.age);
    
}

//--------------------------- <#我是分割线#> ------------------------------//
6.实现NSCoding的自动归档和解档

如果你实现过自定义模型数据持久化的过程,那么你也肯定明白,如果一个模型有许多个属性,那么我们需要对每个属性都实现一遍encodeObject和decodeObjectForKey方法,如果这样的模型又有很多个,这还真的是一个十分麻烦的事情。下面来看看简单的实现方式。

假设现在有一个Movie类,有3个属性。先看下.h文件

// Movie.h文件

//1. 如果想要当前类可以实现归档与反归档,需要遵守一个协议NSCoding

@interface Movie : NSObject

@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

@implementation Movie

- (void)encodeWithCoder:(NSCoder *)encoder{
    
    unsigned int count = 0;
    
    Ivar *ivars = class_copyIvarList([self 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([self 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

这样的方式实现,不管有多少个属性,写这几行代码就搞定了。怎么,代码有点多,

好说下面看看更加简便的方法:两句代码搞定。

#define encodeRuntime \
unsigned int count = 0;\
Ivar *ivars = class_copyIvarList([self 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 \
if (self = [super init]) {\
unsigned int count = 0;\
Ivar *ivars = class_copyIvarList([self 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;

优化:上面是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

const char *propertyName = property_getName(propertyList[i]);

NSLog(@"property---->%@", [NSString stringWithUTF8String:propertyName]);

}

获取方法列表

Method *methodList = class_copyMethodList([self class], &count);

for (unsigned int i; i

Method method = methodList[i];

NSLog(@"method---->%@", NSStringFromSelector(method_getName(method)));

}

获取成员变量列表

Ivar *ivarList = class_copyIvarList([self class], &count);

for (unsigned int i; 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

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

*/

// 获得某个类的类方法

Method class_getClassMethod(Class cls , SEL name)

// 获得某个类的实例对象方法

Method class_getInstanceMethod(Class cls , SEL name)

// 交换两个方法的实现

void method_exchangeImplementations(Method m1 , Method m2)

// 将某个值跟某个对象关联起来,将某个值存储到某个对象中

void objc_setAssociatedObject(id object , const void *key ,id value ,objc_AssociationPolicy policy)

// 利用参数key 将对象object中存储的对应值取出来

id objc_getAssociatedObject(id object , const void *key)

// 获得某个类的所有成员变量(outCount 会返回成员变量的总数)

Ivar *class_copyIvarList(Class cls , unsigned int *outCount)

// 获得成员变量的名字

const char *ivar_getName(Ivar v)

// 获得成员变量的类型

const char *ivar_getTypeEndcoding(Ivar v)

// 获取类里面所有方法

class_copyMethodList(__unsafe_unretained Class cls, unsigned int *outCount)// 本质:创建谁的对象

// 获取类里面属性

class_copyPropertyList(__unsafe_unretained Class cls, unsigned int *outCount)

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实现的原理。

什么是 method swizzling(俗称黑魔法)

简单说就是进行方法交换

在Objective-C中调用一个方法,其实是向一个对象发送消息,查找消息的唯一依据是selector的名字。利用Objective-C的动态特性,可以实现在运行时偷换selector对应的方法实现,达到给方法挂钩的目的

每个类都有一个方法列表,存放着方法的名字和方法实现的映射关系,selector的本质其实就是方法名,IMP有点类似函数指针,指向具体的Method实现,通过selector就可以找到对应的IMP。

1.runtime_第4张图片
image.png

selector --> 对应的IMP

交换方法的几种实现方式

利用method_exchangeImplementations交换两个方法的实现

利用class_replaceMethod替换方法的实现

利用method_setImplementation来直接设置某个方法的IMP。

1.runtime_第5张图片
image.png

交换方法

这里可以参考简友这篇:【Runtime Method Swizzling开发实例汇总】http://www.jianshu.com/p/f6dad8e1b848

最后一道面试题的注解

下面的代码输出什么?

@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]调用相同了,故上述输出结果仍然返回 Son

objc Runtime 开源代码对- (Class)class方法的实现

-(Class)class { return object_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

Runtime & Runloop 面试最常问到的题整理【建议看】
整理原文:2017年5月iOS招人心得(附面试题)

objc在向一个对象发送消息时,发生了什么?

什么时候会报unrecognized selector错误?iOS有哪些机制来避免走到这一步?

能否向编译后得到的类中增加实例变量?能否向运行时创建的类中添加实例变量?为什么?

runtime如何实现weak变量的自动置nil?

给类添加一个属性后,在类结构体里哪些元素会发生变化?

RunLoop

runloop是来做什么的?runloop和线程有什么关系?主线程默认开启了runloop么?子线程呢?

runloop的mode是用来做什么的?有几种mode?

为什么把NSTimer对象以NSDefaultRunLoopMode(kCFRunLoopDefaultMode)添加到主运行循环以后,滑动scrollview的时候NSTimer却不动了?

苹果是如何实现Autorelease Pool的?

//-------------------- 【我是分割线】 ---------------------//

整理原文:2017年iOS面试题总结,附上答案

Runtime

01

问题:objc在向一个对象发送消息时,发生了什么?

解答:根据对象的 isa 指针找到类对象 id,在查询类对象里面的 methodLists 方法函数列表,如果没有在好到,在沿着 superClass ,寻找父类,再在父类 methodLists 方法列表里面查询,最终找到 SEL ,根据 id 和 SEL 确认 IMP(指针函数),在发送消息;
消息转发过程的关键方法:

动态方法解析
向当前类发送resolveInstanceMethod:消息,检查是否动态向类添加了方 法,如果返回YES,则系统认为方法已经被添加,则会重新发送消息。

快速消息转发
检查当前类是否实现forwardingTargetForSelector:方法,若实现则调 用,如果方法返回值为非nil或非self的对象,则向返回的对象重新发送消息。

标准消息转发
Runtime发送methodSignatureForSelector:消息获取selector对应方法的签名,如果有方法签名返回,则根据方法签名创建描述消息的NSInvocation,向当前对象发送forwardInvocation:消息,如果没有方法签名返回,即返回值为nil,则向当前对象发送doesNotRecognizeSelector:消息,应用崩溃退出
消息转发

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.

其他runtime 完整总结 https://www.jianshu.com/p/6b905584f536

你可能感兴趣的:(1.runtime)