runtime(「runtime&runloop 面试、工作」)
runtime(简称运行时),是一套 纯C(C和汇编写的) 的API。而 OC 就是 运行时机制,也就是在运行时候的一些机制,其中最主要的是 消息机制。
OC的函数调用成为消息发送,属于 动态调用过程。在编译的时候并不能决定真正调用哪个函数,只有在真正运行的时候才会根据函数的名称找到对应的函数来调用。
runtime 常见作用
- 发送消息
- 动态添加属性 objc_setAssociatedObject/objc_getAssociatedObject
- 动态添加方法 resolveInstanceMethod
- 拦截并替换方法 method_exchangeImplementations
- 字典转模型KVC实现(必须保证,模型中的属性和字典中的key 一一对应)
利用运行时,遍历模型中所有属性,根据模型的属性名,去字典中查找key,取出对应的值,给模型的属性赋值 - 实现 NSCoding 的自动归档和解档
- (void)encodeWithCoder:(NSCoder *)encoder
{
unsigned int count = 0;
Ivar *ivars = class_copyIvarList([Movie class], &count);
for (int i = 0; i
isa、属性列表、方法列表、协议列表
isa
一个objc 对象的 isa 的指针指向什么?有什么作用?
1、每一个对象本质上都是一个类的实例。其中类定义了成员变量和成员方法的列表。对象通过对象的isa指针指向所属类。
2、每一个类本质上都是一个对象,类其实是元类(meteClass)的实例。元类定义了类方法的列表。类通过类的isa指针指向元类。
3、元类保存了类方法的列表。当类方法被调用时,先会从本身查找类方法的实现,如果没有,元类会向他父类查找该方法。同时注意的是:元类(meteClass)也是类,它也是对象。元类通过isa指针最终指向的是一个根元类(root meteClass)。
4、根元类的isa指针指向本身,这样形成了一个封闭的内循环。
属性/方法/协议列表
// 获取协议列表
__unsafe_unretained Protocol **protocolList = class_copyProtocolList([self class], &count);
// 得到类的所有方法
Method allMethods = class_copyMethodList([Person class], &count);
// 获得某个类的实例对象方法
Method class_getInstanceMethod(Class cls , SEL name)
// 获得某个类的类方法
Method class_getClassMethod(Class cls , SEL name)
// 动态添加方法:
// 第一个参数表示Class cls 类型;
// 第二个参数表示待调用的方法名称;
// 第三个参数(IMP)myAddingFunction,IMP是一个函数指针,这里表示指定具体实现方法myAddingFunction;
// 第四个参数表方法的参数,0代表没有参数;
class_addMethod([_per class], @selector(sayHi), (IMP)myAddingFunction, 0);
// 替换原方法实现
class_replaceMethod(toolClass, cusSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
// 交换两个方法
method_exchangeImplementations(method1, method2);
// 得到所有成员变量
Ivar allVariables = class_copyIvarList([Person class], &count);
// 得到所有属性
objc_property_t properties = class_copyPropertyList([Person class], &count);
// 根据名字得到实例变量的Ivar指针 Ivar oneIVIvar = class_getInstanceVariable([Person class], name);
// 找到后可以直接对私有变量赋值
object_setIvar(_per, oneIVIvar, @"Mike");// 强制修改name属性
// 关联两个对象
// id object:表示关联者,是一个对象,变量名理所当然也是object
// const void key :获取被关联者的索引key
// id value :被关联者,这里是一个block
// objc_AssociationPolicy policy : 关联时采用的协议,有assign,retain,copy等协议,一般使用OBJC_ASSOCIATION_RETAIN_NONATOMIC
objc_setAssociatedObject(id object, const void key, id value, objc_AssociationPolicy policy)
// 利用参数key 将对象object中存储的对应值取出来id
objc_getAssociatedObject(id object , const void *key)
// 获得成员变量的名字
const char *ivar_getName(Ivar v)
// 获得成员变量的类型
const char *ivar_getTypeEndcoding(Ivar v)
消息传递机制如何查找方法
根据对象的 isa 指针找到类对象 id,在查询类对象里面的 methodLists 方法函数列表,如果没有在好到,在沿着 superClass ,寻找父类,再在父类 methodLists 方法列表里面查询,最终找到 SEL ,根据 id 和 SEL 确认 IMP(指针函数),在发送消息;
当发送消息的时候,我们会根据类里面的 methodLists 列表去查询我们要动用的SEL,当查询不到的时候,我们会一直沿着父类查询,当最终查询不到的时候我们会报 unrecognized selector 错误,当系统查询不到方法的时候,会调用 +(BOOL)resolveInstanceMethod:(SEL)sel 动态解释的方法来给我一次机会来添加,调用不到的方法。或者我们可以再次使用 -(id)forwardingTargetForSelector:(SEL)aSelector 重定向的方法来告诉系统,该调用什么方法,一来保证不会崩溃。
Category实现原理
在objc_class结构体中:ivars是objc_ivar_list指针;methodLists是指向objc_method_list指针的指针。也就是说可以动态修改*methodLists的值来添加成员方法,这也是Category实现的原理
method swizzling原理
method swizzling,简单说就是进行方法交换。
在Objective-C中调用一个方法,其实是向一个对象发送消息,查找消息的唯一依据是selector的名字。利用Objective-C的动态特性,可以实现在运行时偷换selector对应的方法实现,达到给方法挂钩的目的
selector的本质其实就是方法名,IMP有点类似函数指针,指向具体的Method实现,通过selector就可以找到对应的IMP。
交换方法的几种实现方式
利用 method_exchangeImplementations 交换两个方法的实现
利用 class_replaceMethod 替换方法的实现
利用 method_setImplementation 来直接设置某个方法的IMP。
SEL/IMP(Method = SEL + IMP)
SEL
SEL 本质上是一个整型,是 oc 编译时对函数的编号。相同的函数名具有一样的函数编号,而同名函数在不同类中有不同实现,所以 SEL 和 IMP是一对多的关系。
结构如下:
typedef struct objc_selector *SEL;
Objective-C 在编译的时候,会依据方法的名字、参数序列、生成一个整型标识的地址( int 类型的地址):这个标识就是 SEL
只要方法相同, SEL 就是一样的。
获取 SEL 值:
a、sel_registerName函数
b、Objective-C编译器提供的@selector()
c、NSSelectorFromString()方法
IMP
IMP 本质上是一个函数指针,指向函数的实现。
结构如下:
/*
第一个参数:是指向self的指针(如果是实例方法,则是类实例的内存地址;如果是类方法,则是指向元类的指针)
第二个参数:是方法选择器(selector)
接下来的参数:方法的参数列表。
*/
id (*IMP)(id, SEL,...)
Method 用于表示类定义中的方法
结构如下
typedef struct objc_method *Method
struct objc_method{
SEL method_name OBJC2_UNAVAILABLE; // 方法名
char *method_types OBJC2_UNAVAILABLE;
IMP method_imp OBJC2_UNAVAILABLE; // 方法实现
}
我们可以看到该结构体中包含一个SEL和IMP,实际上相当于在SEL和IMP之间作了一个映射。有了SEL,我们便可以找到对应的IMP,从而调用方法的实现代码。
IMP和SEL关系
在 objective-c的方法调用
[receiver message];
运行时转为:
objc_msgSend(receiver, selector);
系统根据 接收者receiver和函数编号selctor查在类的dispatch table查对应的函数实现imp,然后执行函数并返回执行结果。
dispatch table 查找 imp 的流程及原理
在类的dispatch table 中存放着 sel 和 imp的对一一应关系。类似于字典通过 key 可以知道 value。若 imp 在当前类的dispatch table 中未被找到,则会通过 isa指针在其父类中的 dispatch table 中查找,若在当前类的父类中未被找到,则会在父类的父类中查找,最终回溯到NSObject类中。
若每次函数调用都要通过 sel 在 dispatch table中一层一层的查找对应的 imp, 这将是一个很大的时间开销,因此objective-c的设计者们使用了缓存技术,cache dispatch table 用于缓存调用过方法, 使查找 imp 的过程更加高效。
SEL selector = @selector(foo);
IMP imp = [receiver methodForSelector: selector];
imp();
为什么不直接获得函数指针,而要从SEL这个编号走一圈再回到函数指针呢?
有了SEL这个中间过程,我们可以对一个编号和什么方法映射做些操作,也就是说我们可以一个SEL指向不同的函数指针,这样就可以完成一个方法名在不同时候执行不同的函数体。另外可以将SEL作为参数传递给不同的类执行。也就是说我们某些业务我们只知道方法名但需要根据不同的情况让不同类执行的时候,SEL可以帮助我们。
获取IMP
runtime提供了两种方法
IMP class_getMethodImplementation(Class cls, SEL name);
IMP method_getImplementation(Method m)
第一种方法:class_getMethodImplementation
- (void)getIMP_class_getMethodImplementationFromSelector:(SEL)aSelector{
const char *className = object_getClassName([self class]);
// 获取实例的IMP
IMP instanceIMP = class_getMethodImplementation(objc_getClass(className), aSelector);
// 获取类的IMP
IMP classIMP = class_getMethodImplementation(objc_getMetaClass(className), aSelector);
NSLog(@"instanceIMP:%p classIMP:%p",instanceIMP,classIMP);
}
对于第一种方法而言,类方法和实例方法实际上都是通过调用class_getMethodImplementation()来寻找IMP地址的
第二种方法:method_getImplementation
- (void)getIMP_method_getImplementationFromSelector:(SEL)aSelector{
const char *className = object_getClassName([self class]);
// 获取类中的某个实例方法
Method instanceMethod = class_getInstanceMethod(objc_getClass(className), aSelector);
// 获取类中的某个类方法
Method classMethod = class_getClassMethod(objc_getClass(className), aSelector);
// 获取实例的IMP
IMP instanceIMP = method_getImplementation(instanceMethod);
// 获取类的IMP
IMP classIMP = method_getImplementation(classMethod);
NSLog(@"instanceIMP:%p classIMP:%p",instanceIMP,classIMP);
}
而method_getImplementation而言,传入的参数只有method,区分类方法和实例方法在于封装method的函数
类方法
Method class_getClassMethod(Class cls, SEL name)
实例方法
Method class_getInstanceMethod(Class cls, SEL name)
最后调用IMP method_getImplementation(Method m) 获取IMP地址
其他问答
下面的代码输出什么?
@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);
}
问题: 能否向编译后得到的类中增加实例变量?能否向运行时创建的类中添加实例变量?为什么?
解答: 1、不能向编译后得到的类增加实例变量 2、能向运行时创建的类中添加实例变量。【解释】:1. 编译后的类已经注册在 runtime 中,类结构体中的 objc_ivar_list 实例变量的链表和 instance_size 实例变量的内存大小已经确定,runtime会调用 class_setvarlayout 或 class_setWeaklvarLayout 来处理strong weak 引用.所以不能向存在的类中添加实例变量。2. 运行时创建的类是可以添加实例变量,调用class_addIvar函数. 但是的在调用 objc_allocateClassPair 之后,objc_registerClassPair 之前,原因同上.
问题: runtime如何实现weak变量的自动置nil?
解答: 对于 weak 对象会放入一个 hash 表中。 用 weak 指向的对象内存地址作为 key,当此对象的引用计数为0的时候会 dealloc,假如 weak 指向的对象内存地址是a,那么就会以a为键, 在这个 weak 表中搜索,找到所有以a为键的 weak 对象,从而设置为 nil。
问题: 给类添加一个属性后,在类结构体里哪些元素会发生变化?
解答: instance_size :实例的内存大小;objc_ivar_list *ivars:属性列表