OC -- Runtime了解一下
一、Runtime机制
1 > OC是一门面向对象的动态语言,所以具有面向对象的特性,如封装、继承、多态、动态特性表现在动态类型、动态加载、动态绑定。他会将一些工作放在运行时处理,很多类和成员变量在编译期是不知道的,那么仅仅在编译期是不够的,需要一个运行时的系统来处理编译后的代码。这些都是由底层的API -- runtime机制所支持的,是指在一个程序运行时候的状态,OC代码在程序运行时最终会转成runtime的C语言代码
2 > objc通过三种层面上与Runtime交互
1、通过OC源代码
我们只需要编写OC代码就好,编译器会将OC的代码转成运行时的代码,在运行时确定数据结构和函数
2、通过Foundation框架的NSObject类定义的方法
在cocoa中大部分类都是NSObject的子类,都继承了NSObject的行为
1>NSObject仅定义了完成时间的模板,并没有提供所需要的代码,例如:-(NSString *)description,当打印一个对象的时候就会调用,返回一个字符串而已
2> NSObject从Runtime中获取信息;比如:isKindOfClass、isMemberOfclass
3 > 通过Runtime函数库直接调用,runtime是有公共接口的动态共享库,只要引入:<objc/runtime.h>就可以用纯C来写相关的代码
二、常见Runtime中定义的数据结构术语
SEL、id、Class、Ivar、IMP、Cache、Property、isa指针
1、SEL:在objc/runtime中的定义
SEL是selector表示方法选择器,通过方法的名字来辨别。在objc中同一个类不会有命名相同的两个方法,selector对方法名包装,以便找到对应的方法实现
typedef struct objc_selector *SEL;
他是映射到方法的c字符串,不同类中相同名字的方法所对应的selector是相同的,由于变量的类型不同所以不会混乱
2、id 是一个参数类型,他是指向某个类的实例的指针,在运行时才会决定数据结构
typeef struct objc_object *id
struct objc_object{Class isa}
在objc_object结构体中包含了一个isa指针,根据isa指针就可以找到对象所属的类,但是在代码运行时并不总指向实例对象所属的类型,所以不能依靠它来确定类型,想要确定类型还是需要– class方法的
⭐️ KVO的实现就是isa指针指向了一个元类的派生类NSKVONotifying_xx的类,重写setter方法来实现的。
3、isa指针
是一个定义在oc底层的class类型的指针。在oc中任何类的定义都是对象,类和类的实例没有本质上的区别,每一个对象其实都是一个类的实例,其中定义了成员变量和成员方法,对象通过对象的isa指针指向类。任何对象都有isa指针,当类的方法被调用时先回从本身查找类方法的实现,如果没有会向父类查找该方法,如果还没有找到就返回null。所有的元类最终都继承一个根元类,根元类的isa指针指向本身,形成一个封闭的内循环。
4、Class 在objc/runtime的定义:typedef struct objc_class *Class;
struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class _Nullable super_class OBJC2_UNAVAILABLE;
const char * _Nonnull name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE;
struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE;
struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE;
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
可以看到他其实是指向objc_class结构体的指针,一个运行时类中关联了他的父类指针、类名、成员变量、方法、缓存以及附属协议,
其中 objc_ivar_list
和 objc_method_list
分别是成员变量列表和方法列表
// 成员变量列表
struct objc_ivar_list {
int ivar_count OBJC2_UNAVAILABLE;
#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
/* variable length structure */
struct objc_ivar ivar_list[1] OBJC2_UNAVAILABLE;
} OBJC2_UNAVAILABLE;
// 方法列表
struct objc_method_list {
struct objc_method_list *obsolete OBJC2_UNAVAILABLE;
int method_count OBJC2_UNAVAILABLE;
#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
/* variable length structure */
struct objc_method method_list[1] OBJC2_UNAVAILABLE;
}
所以我们可以动态修改 *methodList
的值来添加成员方法,这也是 Category 实现的原理,同样解释了 Category 不能添加属性的原因,参考美团Category的开发文档:美团Category深入理解
objc_ivar_list结构体用来存储成员变量的列表,而objc_ivar存储了单个成员变量的信息。Objc_mothod_list结构体存储了方法数组列表,objc_method存储单个的方法信息。
objc_class中也有一个isa指针,这说明objc也是一个对象,为了处理类和对象的关系,Runtime库创建了个Meta class(元类)的东东,类对象所属的类就叫做元类,我们所说的类方法就是源于Meta Class,我们可以理解为类方法就是类对象的实例方法,每个类仅有一个类对象,而每个类对象仅有一个与之相关的元类,当你发送一个类方法[类 alloc]的消息,这个消息被发送给了一个类对象(class Object),这个类对象必须是一个元类的实例,所有的元类的isa指针最终都指向根元类,这也就是我们发送消息时运行代码objc_msgSend()会去他的元类中查找能够响应消息的方法实现,如果找到了,就会对这个类对象执行方法调用(如图—runtime-元类)
图中实线是super-class指针,虚线是isa指针。根元类的父类是NSObject,isa指向了自己,而NSObject没有父类,object_class中还有一个object_cache缓存
5、Method
Method表示类中某个方法的类型
struct objc_method {
SEL _Nonnull method_name OBJC2_UNAVAILABLE;
char * _Nullable method_types OBJC2_UNAVAILABLE;
IMP _Nonnull method_imp OBJC2_UNAVAILABLE;
}
objec_method存储了:method_name 方法名、 method_types 方法类型 、 method_imp指向了方法的实现,本质是一个函数指针
6、Ivar 结构体 表示成员变量、包含了变量名称、类型、偏移量
typedef struct objc_ivar *Ivar;
struct objc_ivar {
char *ivar_name OBJC2_UNAVAILABLE;
char *ivar_type OBJC2_UNAVAILABLE;
int ivar_offset OBJC2_UNAVAILABLE;
#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
}
7、IMP: typedefid(*IMP)(id, SEL, ...);
他是一个函数指针,当发起一个OC的消息之后,最终会执行那段代码就是由这个函数指针指定的,IMP的函数指针指向这个方法的实现,IMP指向的方法与objc_msgsend函数类型相同,都包含id和SEL类型,每个方法都回应一个sel类型的方法选择器,每个实例对象的sel对应的方法实现肯定是唯一的,魅族id和SEL就能确定唯一的方法实现地址,为一个确定的方法只有一组id和SEL参数
8、Cache
typedef struct objc_cache *Cache
struct objc_cache {
unsigned int mask /* total = mask + 1 */ OBJC2_UNAVAILABLE;
unsigned int occupied OBJC2_UNAVAILABLE;
Method buckets[1] OBJC2_UNAVAILABLE;
};
Cache为方法调用的性能优化,每当实例对象接受到一个消息时,他不会直接在isa指针指向的类方法列表中查找响应的方法,因为需要遍历,所以效率太低,而是在cache中查找,因为RUntime会把被调用的方法存到Cache中,如果一个方法被调用,那么他有可能还会被调用,会加入到cache中方便查找,就想CPU绕过主存优先访问cache和SDWebImage的实现原理一样
9、Property
typedef struct objc_property *Property;
typedef struct objc_property *objc_property_t;//这个更常用
通过class_copyPropertyList和procotol_copyPropertList方法获取类和协议中的属性
可以通过class_copypropertyList和procotol_copupropertyList方法获取 类 和 协议中的属性
objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount)
objc_property_t *protocol_copyPropertyList(Protocol *proto, unsigned int *outCount)
class_getProperty
和 protocol_getProperty
通过给出属性名在类和协议中获得属性的引用
property_getName
用来查找属性的名称,返回 c 字符串。
property_getAttributes
函数挖掘属性的真实名称和 @encode
类型,返回 c 字符串。
三、发送消息
消息发送时runtime通过selector快速查找IMP函数指针的郭晨,有了函数指针就可以找到相应的方法实现,消息直到运行时才会对方法绑定也就是动态绑定
比如:[self class];会被编译器转化成runtime下的函数方法objc_msgsend(self,selector(class)),如果后边有参数,则objc_msgsend(self,selector(class)string1,string2.....);(runtime底层是由汇编和C写的,而objc_msgsend函数是汇编)
消息接收者如果能找到selector对应的IMP函数指针,则会找到对应的实现方法。如果找不到那么消息就会被转发或者向接收者动态添加一个sel方法实现的内容,要么直接crash,(在X86)系统中定义了一个NilText的宏,会判断发送的对象是不是nil,如果是nil就返回,这也就是为啥可以给nil发送消息了
1、发送消息的步骤:
1、检测这个selector是不是要忽略。比如有了垃圾回收就不用理会retain、release这些方法
2、检测这个selector的发送者是不是nil,如果是nil就返回。但不会crash。
3、如果不是要忽略掉的,发送者也不是nil,那么就开始找这个类的实现IMP指针。先从cache中招,如果找到就去执行相应的实现
4、如果在cache中找不到就去方法列表中找,如果找到就执行
5、如果还没找到就去父类的方法列表中查找
6、如果还找不到就会动态解析了。
在消息传递中,编译器会在objc_msgsend ,objc_msgSend_stret,objc_msgsendSuper,objc_msgSendSuper_stret这四个方法中选择调用,如果消息传递给父类的,那么会调用super的函数。如果消息返回的是数据结构而不是单一的值会选择stret函数
2、self、super隐藏参数解析
1️⃣self关键字是如何获取当前方法的对象呢?
当objc_msgsend找到方法的对应实现时,他将直接调用方法实现,并将消息中所有参数都传递给方法的同时还会传递两个隐藏参数:1> 接收消息的对象(self所指向的内容、当前方法的对象指针)2> 方法选择器(_cmd指向的内容,当前方法的SEL指针),self是在方法实现中访问消息接收者对象的实例变量的途径。
2️⃣、super:实际上super关键字接收消息时,编译器会创建一个objc_super的结构体:
struct objc_super { id receiver; Class class; };
结构体指明了这个消息被传递给特定的父类,receiver任然是自己也就是self,当我们想通过[super class]获取父类时,编译器其实是指向self的id指针和class的SEL传递给objc_msgsendSuper函数,只有在NSObject的类中才能找到class方法。然后class底层被转换成object_getClass()。接着底层编译器会将代码转换为objc_msgsen(objc_super--->receiver,@selector(class)),传入第一个参数是指向self的id指针,与调用[self class]一样,我们永远得到的是self的类型
// 这句话并不能获取父类的类型,只能获取当前类的类型名
NSLog(@"%@", NSStringFromClass([super class]));
3、获取方法地址
在NSObject中有个实例方法 -(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector 可以来获取某个方法选择器对应的IMP,这个方法是由runtime提供的
4、动态解析
如果我们使用关键字@dynamic在类的实现文件中修饰一个属性,表名我们自己手动实现这个属性的setter方法和getter方法,编译器不会再默认提供,那么就需要手动添加一个方法,动态方法会在消息转发机制之前执行,动态解析器会首先给对应的IMP指针机会,如果想转发就直接让resolveInstanceMethod方法返回NO
@dynamic name;
这时我们可以通过分别重写 resolveInstanceMethod方法和 resolveClassMethod 方法添加实例方法和类方法实现
当runtime系统在自己和父类 的 cahe 和 类方法列表中 找不到对应的方法时,Runtime会调用 resolvInstanceMothod 方法 或 resolveClassMethod方法动态添加实现方法。底部会调用class_addMethod函数来向特定的类加特定的方法
四、消息转发
1、重定向
在消息转发执行之前,Runtime系统允许我们替换消息的接收者为其他对象。通过方法 -(id)forwardingTargetForSelector:(SEL)aSelector 改变接收者,他返回nil或者self则进入转发机制,否则将向返回的对象重新发送消息
2、转发
当动态解析方法返回NO时,则会触发消息转发机制,这时会执行方法 -(void)forwardInvocation:(NSInvocation *)anInvocation 参数NSInvocation,该对象封装了原始消息和消息的参数。我们通过forwardInvocation方法来处理不能处理的方法做处理或者将消息转发给其他对象而不报错
在forwardInvocation消息发送之前,Runtime系统会向对象发送 methodSignatureForSelector 消息,并取到返回的方法签名生成NSInvocation对象。所以在重写forwardInvocation的同时也要重写methodSignatureForSelector方法,不然会报错。
而方法forwardInvocation方法就是一个不能识别消息的分发中心,将这些消息转发给不同的接受对象。当一个对象由于没有响应的方法而无法响应某消息时,运行时系统将通过forwardInvocation消息通知该对象,每个对象都从NSObject类中继承了这个方法,而NSObject中的方法实现只是简单的调用了doesNotRecognizeSelector方法,通过我们自己的forwardInvocation方法实现中将消息转发给其他对象。