一、数据结构
数据结构1.1、objc_object
objc_object我们平常用的实例对象都是id类型,对应到runtime 中的objc_object。
-
isa_t
共用体 -
关于isa操作相关
- 实例对象根据isa指针获取类对象。
- 类对象根据isa指针获取元类对象。
-
弱引用相关
- 标记一个对象是否曾经有弱引用指针。
-
关联对象
- 设置关联属性的一些方法。
-
内存管理
- MRC下的retain、release等方法。
- ARC和MRC下用的autorelease方法。
1.2、objc_class
objc_class我们平常用的类,对应到runtime中的objc_class。
-
isa_t
因为继承于objc_object,所以也有isa指针,用来指向类的元类对象。 -
Class superClass
父类对象 -
cache_t cache
方法缓存结构<消息传递会有涉及到> -
class_data_bits_t bits
bit通过&FAST_DATA_MASK获取class_rw_t,class_rw_t包含方法、属性、协议。
问题1:Class这个类是否是一个对象呢?
解释:
是对象,因为Class对应runtime中的objc_class,objc_class继承于objc_object,所以被称为"类对象"。
1.3、isa指针
1.3.1、isa指针的概念
共用体isa_t
问题2:isa指针的含义
解释:
- 包含指针型isa和非指针型isa。
- 指针型isa:isa的值代表Class地址。
- 非指针型isa:isa的值的部分代表Class地址。
-
应用场景:
- 32位架构用指针型的 64位架构用非指针型的。
- 这是一种提升内存利用的一种手段。
1.3.2、isa指向
-
关于对象,其指向类对象。
指向类对象
-
关于类对象,其指向元类对象。
指向元类对象
1.4、cache_t
1.4.1、概念
- 用于快速查找方法执行函数。
- 是可增量扩展的哈希表结构。
- 是局部性原理的最佳应用。
1.4.2、结构
cache_t是一个结构体。
cache_t结构
可以理解为:
- 一个数组中有bucket_t。
- bucket_t是结构体。
- bucket_t包含两个成员变量:key、IMP。
- key就是我们使用的selector。
- 根据key + 哈希算法,可以快速查找IMP。
1.5、class_data_bits_t
- class_data_bits_t主要是对class_rw_t的封装。
- class_rw_t代表了类相关的读写信息、对class_ro_t的封装。
- class_ro_t代表了类相关的只读信息。
1.5.1、class_rw_t
class_rw_t- methods、properties、protocols是runtime动态添加的。
比如:分类中的方法会在运行时添加到methods中。 - methods、properties、protocols是可读可写的。
- methods、properties、protocols是二维数组。
- methods中存放有method_t对象。
1.5.2、class_ro_t
class_ro_t- name指的是类名
- ivars指的是成员变量
- methods、properties、protocols是编译器内部生成的。
- methods、properties、protocols是只读。
- methods、properties、protocols是一维数组。
- methods中存放有method_t对象。
1.5.3、method_t
method_t- SEL name
方法名称 - const char* types
返回值 + 参数 - IMP imp
无类型的函数指针,指向函数体
1.5.3.1、Type Encodings
Type Encodings- 不可变的字符指针
- 结构:返回值 + n个参数
实例分析:
-(void)aMethod;对应的Type Encodings
解析如下:
- aMethod的types === v@:。
- v代表返回值是void,无返回值。
- @代表对象id。
- :代表SEl(方法)
注意:
- 这里的第一个参数和第二个参数是固定的,并且是不可变的。
- 因为对于OC方法的调用或者说消息传递,到达runtime层面,都会转换成objc_msgSend,它的前面两个参数就是固定的objc_msgSend(obj, sel/@selector(aMethod)/);**。
1.6、整体数据结构
整体数据结构二、对象、类对象、元类对象
2.1、概念
- 类对象
存储实例方法列表等信息。 - 元类对象
存储类方法列表等信息。
2.2、isa指针指向图
isa指针指向图isa指针
- instance实例对象 --> Class类对象
- Class类对象 --> meta元类对象
- meta元类对象 --> 根meta元类对象
- 根meta元类对象 --> 根meta元类对象(自己)
superclass指针
- Rootclass类对象 --> nil
- Superclass类对象 --> Rootclass类对象
- Subclass类对象 --> Superclass类对象
- 根meta元类对象 --> Rootclass类对象
问题3:类对象、元类对象分别是什么,有什么区别和联系?
- 概念
类对象是存储实例方法列表等信息。
元类对象是存储类方法列表等信息。 - isa指针指向图
类对象的isa指针指向元类对象。
元类对象的isa指针指向根元类对象。
根元类对象的isa指针指向自己。
问题4:如果说调用的一个类方法,没有对应的实现;但是,有同名的实例方法实现,会不会产生崩溃或者实际调用?
- 由于根meta元类对象的superclass指针指向的是根类对象,如果在根元类对象中没有找到对应的类方法,就会去根类对象中去查找同名的实例方法。
问题5:笔试题
笔试题解释:
- [self class]转换为objc_msgSend(self, @selector(class))
- [super class]转换为objc_msgSendSuper(super,@selector(class))
- [super class]虽然是super调用,但是super是一个结构体,里面包含一个id类型对象receiver,也就是当前对象。
- [self class]传递流程:在缓存中查找,没有➡️在类对象中查找,没有➡️在父类对象中查找,没有➡️在NSObject中查找,有,返回信息。
- [super class]传递流程:直接在父类对象中查找,没有➡️在NSObject中查找,有,返回信息。
三、消息传递
3.1、函数介绍
void objc_msgSend(void /* id self, SEL op, ... */ )
消息传递
备注:
- 这里有2个固定参数,一个消息接受者(id类型),一个是方法选择器名称(SEL类型),其它才是后续方法传递的参数。
- 当前的接受者就是:当前对象。
void objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */ )
Super 消息传递备注:
- 这里有2个固定参数,一个是结构体类型指针(objc_super),一个是方法选择器名称(SEL类型),其它才是后续方法传递的参数。
- 因为objc_super内部有一个receiver(id类型),就是当前对象;这个super就是编译器关键字。
-
objc_msgSendSuper:是从父类对象的方法列表中开始查找;
objc_msgSend:是从当前对象缓存列表中查找。
3.2、消息传递流程
消息传递过程-
首先调用方法,查找缓存;如果有,就通过函数指针调用函数,完成方法调用。
这里的缓存指的是类对象的缓存,是通过哈希查找。 -
如果缓存中没有对应方法,就会根据当前实例的isa指针去查找类对象的方法列表;如果有,就通过函数指针调用函数,完成方法调用。
这里分二分查找和一般遍历查找。 -
如果当前类方法中没有对应方法,就会逐级父类方法列表中去查找,是通过superclass指针;如果有,就通过函数指针调用函数,完成方法调用。
这里同样先去缓存查找,再去方法列表查找。 -
如果直到根类NSObject,都没有查到对应方法,就会进入消息转发流程。
四、消息传递三大步骤详解
4.1、缓存查找
给定值是SEL(方法选择器),目标值是对应的bucket_t中的IMP。
- 从缓存查找对应的实现, 是哈希查找。
- SEL(方法选择器)就是key。
- 利用哈希算法,找到bucket_t,从而找到IMP。
- 哈希查找提高了查找效率。
4.2、当前类中查找
- 对于已排序好的列表,采用二分查找算法查找方法对应执行函数IMP。
- 对于没有排序的列表,采用一般遍历查找方法对应执行函数。
4.3、父类逐级查找
父类逐级查找- 首先根据superclass指针查找有没有父类;没有父类,结束父类查找。
- 有父类,先去缓存查找;缓存查到了,就返回。
- 缓存没有查到,就去方法列表查找;方法列表查到了,就返回。
- 如果上面两步骤都没有查到,就接着查下一个父类。
五、消息转发
消息转发这里主要讲的是实例方法的转发流程。
5.1、(BOOL)resolveInstanceMethod:(SEL)sel
- 该方法是类方法。
- 参数是方法选择器(SEL类型)。
- 返回值是BOOL类型。
- 如果返回值是YES,代表解决了当前实例方法的实现,就结束了消息转发流程;如果返回值是NO,代表没有解决,继续消息转发。
5.2、(id)forwardingTargetForSelector:(SEL)aSelector
- 该方法是实例方法。
- 参数是方法选择器(SEL类型)。
- 返回值是一个对象,相当于指定的转发目标(自己不处理,让别人处理)。
- 如果指定了转发目标,就结束了消息转发流程;如果没有指定转发目标,继续消息转发。
5.3、(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
- 该方法是实例方法。
- 参数是方法选择器(SEL类型)。
- 返回值是一个NSMethodSignature对象,是对一个方法选择器的返回值类型,参数个数以及参数类型的封装。
- 如果返回了方法签名,则继续调用forwardInvocation;否则就被标记为消息无法处理。
也就是常见的错误:unrecognized selector sent to instance 0x600000da1530。
5.4、(void)forwardInvocation:(NSInvocation *)anInvocation
- 该方法是实例方法。
- 参数是NSInvocation。
- 无返回值。
- 如果这里面能够处理这条消息,消息转发流程就结束了。
六、Method-Swizzling
Method-Swizzling主要代码如下:
//获取test方法
Method test = class_getInstanceMethod(self, @selector(test));
//获取otherTest方法
Method otherTest = class_getInstanceMethod(self, @selector(otherTest));
//交换两个方法的实现
method_exchangeImplementations(test, otherTest);
七、动态添加方法
class_addMethod(<#Class _Nullable __unsafe_unretained cls#>, <#SEL _Nonnull name#>, <#IMP _Nonnull imp#>, <#const char * _Nullable types#>)
- 第一个参数:为哪个类添加方法。
- 第二个参数:方法选择器名称(SEL类型)。
- 第三个参数:函数指针
- 第四个参数:Type Encodings。
八、动态方法解析
问题6、你是否使用过@dynamic关键字?
解释:
也是借此考察运行时内容,因为使用@dynamic实际上是告诉编译器,自己来实现setter与getter方法,不自动生成。
这样声明的属性,即使你没有实现setter与getter方法,编译时,是不会报错的;但是当你使用到setter与getter方法时(运行时),就会报错。
问题7:编译时语言与运行时语言的区别?
解释:
- 动态运行时,语言将函数决议推迟到运行时。
- 编译时,语言在编译期进行函数决议。
问题8: [obj foo]和obj_msgSend()函数之间有什么关系?
解释:
在经过编译器转换之后,就变成了obj_msgSend(obj,@selector(foo)),然后就开始了消息传递过程。
问题9:runtime如何通过Selector找到对应的IMP地址的?
解释:
考察的是消息传递机制。
查找当前实例对应类对象的缓存➡️查找类对象的方法列表➡️逐级查找父级方法列表。
问题10:能否向编译后的类增加实例变量?
解释:
考察的是runtime数据结构。
因为编译后的类,它已经完成了实例变量的布局,这个实例变量是存放在class_ro内部,这个是只读的,所以不能向编译后的类增加实例变量。
但是,可以向动态添加的类中添加实例变量。
问题11:函数调用与消息传递有怎样的区别?
解释:
考察消息传递机制
问题12:当我们调用一个方法没有实现的时候,系统是如何为我们实现消息转发过程的?
解释:
考察消息传递机制
网友评论