简介
-
Runtime: 即运行时,是一套底层的 C 语言 API,是 iOS 系统的核心之一。开发者在编码过程中,可以给任意一个对象发送消息,在编译阶段只是确定了要向接收者发送这条消息,而接受者将要如何响应和处理这条消息,那就要看运行时来决定了。
-
结构模型
-
runtime 的内存模型
-
isa
-
类型为 isa_t() 的结构体
-
从结构上来说, isa_t() 的本质是 union 类型, 与 struct 相比, union 可以节省更多的空间.
- union 的定义是它使几个不同类型的变量共占一段内存, 占用空间由结构体里占最大字节的成员类型决定
- union 中变量可以相互覆盖, 使几个不同的变量存放到同一段内存单元中
-
作用上来说, 还是类似于指针, 实例的 isa 指向类, 类的 isa 指向元类(metaclass)
-
-
对象
- 是 objc_object 是 C 语言结构体, 只包含 isa_t 类型的结构体 isa.
- 实际开发中被 typedef 为 id 类型
-
类
-
继承于 objc_object, 所以类也是对象的一种
-
定义了可以接受消息的方法名
-
除 isa 外, 还有 3 个成员变量
- superclass 父类的指针
- cache 方法缓存
- bits 实例方法列表
-
-
metaclass
- 存储着一个类的所有类方法
- 每个类都会有 metaclass.
-
对象-类-metaclass
- 对象的 isa 指向类
- 类的 isa 指向 metaclass
- metaclass 的 isa 都指向根类, 即 Root class
- 类的 superclass 指向父类
- metaclass 的 superclass 指向父类的 metaclass
- Root class 的 superclass 指向 NSObject
- 对象的 isa 不断查找, 可以找到根元类(Root metaclass)
- 根元类的 superclass 指向 NSObject
-
-
方法调用(消息发送)
-
一个对象的实例方法被调用时, 会通过 isa 找到相应的类, 然后在该类的 class_data_bits_t bits 中查找方法.
- class_data_bits_t 是指向了类对象的数据区域的机构体
-
objc_object 对象的实例方法调用时, 通过对象的 isa 在类中获取方法的实现
-
objc_class 对象的类方法调用时, 通过类的 isa 在 metaclass 中获取方法的实现
-
如果该类没有一个方法的实现, 则向父类继续查找
-
_objc_msgSend 流程
-
判断消息接受者是否为 nil 或者使用使用了 tagPointer
-
根据消息接受者的 isa 找到
- 消息接受者是类, isa 找到元类 metaclass
- 消息接收者是实例, isa 找到类
-
进入 CacheLookup 流程, 寻找方法缓存
-
缓存有记录则直接调用
- 调用 TailCallCechedImp 验证 IMP 有效性
-
缓存没记录则进入 __objc_msgSend_uncached 流程
-
-
__objc_msgSend_uncached 中调用 __class_lookupMethodAndLoadCache3 方法, 该方法会返回 IMP 对象, 里面再调用了 lookUpImpOrForward 方法
-
会再次从类的方法缓存中查找
-
在类的方法列表中查找, 成功则写入缓存并调用, 否则
-
从父类的缓存中查找, 找不到则从父类的方法列表查找, 如此循环知道基类(NSObject)
-
基类还没有找到方法, 进入动态方法解析流程(resolveInstanceMethod | resolveClassMethod)
-
动态解析失败, 则进入消息转发流程(forwardingTargetForSelector | forewordInvocation | methodSignatureForSelector)
-
消息转发失败, IMP=nil, crash
-
-
-
-
1、介绍下runtime的内存模型(isa、对象、类、metaclass、结构体的存储信息等)
对象:
OC中的对象指向的是一个objc_object指针类型,typedef struct objc_object *id;从它的结构体中可以看出,它包括一个isa指针,指向的是这个对象的类对象,一个对象实例就是通过这个isa找到它自己的Class,而这个Class中存储的就是这个实例的方法列表、属性列表、成员变量列表等相关信息的。
/// Represents an instance of a class.
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
复制代码
类:
在OC中的类是用Class来表示的,实际上它指向的是一个objc_class的指针类型,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
}
复制代码
从结构体中定义的变量可知,OC的Class类型包括如下数据(即:元数据metadata):super_class(父类类对象);name(类对象的名称);version、info(版本和相关信息);instance_size(实例内存大小);ivars(实例变量列表);methodLists(方法列表);cache(缓存);protocols(实现的协议列表);
当然也包括一个isa指针,这说明Class也是一个对象类型,所以我们称之为类对象,这里的isa指向的是元类对象(metaclass),元类中保存了创建类对象(Class)的类方法的全部信息。
以下图中可以清楚的了解到OC对象、类、元类之间的关系
从图中可知,最终的基类(NSObject)的元类对象isa指向的是自己本身,从而形成一个闭环。
元类(Meta Class):是一个类对象的类,即:Class的类,这里保存了类方法等相关信息。
我们再看一下类对象中存储的方法、属性、成员变量等信息的结构体
**objc_ivar_list:**存储了类的成员变量,可以通过object_getIvar或class_copyIvarList获取;另外这两个方法是用来获取类的属性列表的class_getProperty和class_copyPropertyList,属性和成员变量是有区别的。
struct objc_ivar {
char * _Nullable ivar_name OBJC2_UNAVAILABLE;
char * _Nullable ivar_type OBJC2_UNAVAILABLE;
int ivar_offset OBJC2_UNAVAILABLE;
#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
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;
}
复制代码
**objc_method_list:**存储了类的方法列表,可以通过class_copyMethodList获取。
结构体如下:
struct objc_method {
SEL _Nonnull method_name OBJC2_UNAVAILABLE;
char * _Nullable method_types OBJC2_UNAVAILABLE;
IMP _Nonnull method_imp OBJC2_UNAVAILABLE;
} OBJC2_UNAVAILABLE;
struct objc_method_list {
struct objc_method_list * _Nullable 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;
}
复制代码
**objc_protocol_list:**储存了类的协议列表,可以通过class_copyProtocolList获取。
结构体如下:
struct objc_protocol_list {
struct objc_protocol_list * _Nullable next;
long count;
__unsafe_unretained Protocol * _Nullable list[1];
};
复制代码
2、为什么要设计metaclass
metaclass代表的是类对象的对象,它存储了类的类方法,它的目的是将实例和类的相关方法列表以及构建信息区分开来,方便各司其职,符合单一职责设计原则。
其实这里涉及到了关于面向对象设计的一些东西,具体可以参考这篇文章
3、class_copyIvarList
& class_copyPropertyList
区别
class_copyIvarList:获取的是类的成员变量列表,即:@interface{中声明的变量}
class_copyPropertyList:获取的是类的属性列表,即:通过@property声明的属性
4、class_rw_t
和 class_ro_t
的区别
class_rw_t:代表的是可读写的内存区,这块区域中存储的数据是可以更改的。
class_ro_t:代表的是只读的内存区,这块区域中存储的数据是不可以更改的。
OC对象中存储的属性、方法、遵循的协议数据其实被存储在这两块儿内存区域的,而我们通过runtime动态修改类的方法时,是修改在class_rw_t区域中存储的方法列表。
参考这篇文章
5、category
如何被加载的,两个category的load
方法的加载顺序,两个category的同名方法的加载顺序
category的加载是在运行时发生的,加载过程是,把category的实例方法、属性、协议添加到类对象上。把category的类方法、属性、协议添加到metaclass上。
category的load方法执行顺序是根据类的编译顺序决定的,即:xcode中的Build Phases中的Compile Sources中的文件从上到下的顺序加载的。
category并不会替换掉同名的方法的,也就是说如果 category 和原来类都有 methodA,那么 category 附加完成之后,类的方法列表里会有两个 methodA,并且category添加的methodA会排在原有类的methodA的前面,因此如果存在category的同名方法,那么在调用的时候,则会先找到最后一个编译的 category 里的对应方法。
参考这篇文章
6、category
& extension
区别,能给NSObject添加Extension吗,结果如何?
category:分类
- 给类添加新的方法
- 不能给类添加成员变量
- 通过@property定义的变量,只能生成对应的getter和setter的方法声明,但是不能实现getter和setter方法,同时也不能生成带下划线的成员属性
- 是运行期决定的
注意:为什么不能添加属性,原因就是category是运行期决定的,在运行期类的内存布局已经确定,如果添加实例变量会破坏类的内存布局,会产生意想不到的错误。
extension:扩展
- 可以给类添加成员变量,但是是私有的
- 可以給类添加方法,但是是私有的
- 添加的属性和方法是类的一部分,在编译期就决定的。在编译器和头文件的@interface和实现文件里的@implement一起形成了一个完整的类。
- 伴随着类的产生而产生,也随着类的消失而消失
- 必须有类的源码才可以给类添加extension,所以对于系统一些类,如nsstring,就无法添加类扩展
不能给NSObject添加Extension,因为在extension中添加的方法或属性必须在源类的文件的.m文件中实现才可以,即:你必须有一个类的源码才能添加一个类的extension。
7、消息转发机制,消息转发机制和其他语言的消息机制优劣对比
消息转发机制:当接收者收到消息后,无法处理该消息时(即:找不到调用的方法SEL),就会启动消息转发机制,流程如下:
第一阶段:咨询接收者,询问它是否可以动态增加这个方法实现
第二阶段:在第一阶段中,接收者无法动态增加这个方法实现,那么系统将询问是否有其他对象可能执行该方法,如果可以,系统将转发给这个对象处理。
第三阶段:在第二阶段中,如果没有其他对象可以处理,那么系统将该消息相关的细节封装成NSInvocation对象,再给接收者最后一次机会,如果这里仍然无法处理,接收者将收到doesNotRecognizeSelector方法调用,此时程序将crash。
具体方法如下:
// 第一阶段 咨询接收者是否可以动态添加方法
+ (BOOL)resolveInstanceMethod:(SEL)selector
+ (BOOL)resolveClassMethod:(SEL)selector //处理的是类方法
// 第二阶段:询问是否有其他对象可以处理
- (id)forwardingTargetForSelector:(SEL)selector
// 第三阶段
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
- (void)forwardInvocation:(NSInvocation *)invocation
复制代码
参考这篇文章
8、在方法调用的时候,方法查询-> 动态解析-> 消息转发
之前做了什么
OC中的方法调用,编译后的代码最终都会转成objc_msgSend(id , SEL, …)方法进行调用,这个方法第一个参数是一个消息接收者对象,runtime通过这个对象的isa指针找到这个对象的类对象,从类对象中的cache中查找是否存在SEL对应的IMP,若不存在,则会在 method_list中查找,如果还是没找到,则会到supper_class中查找,仍然没找到的话,就会调用_objc_msgForward(id, SEL, …)进行消息转发。
9、IMP
、SEL
、Method
的区别和使用场景
IMP:是方法的实现,即:一段c函数
SEL:是方法名
Method:是objc_method类型指针,它是一个结构体,如下:
struct objc_method {
SEL _Nonnull method_name OBJC2_UNAVAILABLE;
char * _Nullable method_types OBJC2_UNAVAILABLE;
IMP _Nonnull method_imp OBJC2_UNAVAILABLE;
}
复制代码
使用场景:
实现类的swizzle的时候会用到,通过class_getInstanceMethod(class, SEL)来获取类的方法Method,其中用到了SEL作为方法名
调用method_exchangeImplementations(Method1, Method2)进行方法交换
我们还可以给类动态添加方法,此时我们需要调用class_addMethod(Class, SEL, IMP, types),该方法需要我们传递一个方法的实现函数IMP,例如:
static void funcName(id receiver, SEL cmd, 方法参数...) {
// 方法具体的实现
}
复制代码
函数第一个参数:方法接收者,第二个参数:调用的方法名SEL,方法对应的参数,这个顺序是固定的。
10、load
、initialize
方法的区别什么?在继承关系中他们有什么区别
load:当类被装载的时候被调用,只调用一次
- 调用方式并不是采用runtime的objc_msgSend方式调用的,而是直接采用函数的内存地址直接调用的
- 多个类的load调用顺序,是依赖于compile sources中的文件顺序决定的,根据文件从上到下的顺序调用
- 子类和父类同时实现load的方法时,父类的方法先被调用
- 本类与category的调用顺序是,优先调用本类的(注意:category是在最后被装载的)
- 多个category,每个load都会被调用(这也是load的调用方式不是采用objc_msgSend的方式调用的),同样按照compile sources中的顺序调用的
- load是被动调用的,在类装载时调用的,不需要手动触发调用
注意:当存在继承关系的两个文件时,不管父类文件是否排在子类或其他文件的前面,都是优先调用父类的,然后调用子类的。
例如:compile sources中的文件顺序如下:SubB、SubA、A、B,load的调用顺序是:B、SubB、A、SubA。
分析:SubB是排在compile sources中的第一个,所以应当第一个被调用,但是SubB继承自B,所以按照优先调用父类的原则,B先被调用,然后是SubB,A、SubA。
第二种情况:compile sources中的文件顺序如下:B、SubA、SubB、A,load调用顺序是:B、A、SubA、SubB,这里我给大家画个图梳理一下:
initialize:当类或子类第一次收到消息时被调用(即:静态方法或实例方法第一次被调用,也就是这个类第一次被用到的时候),只调用一次
- 调用方式是通过runtime的objc_msgSend的方式调用的,此时所有的类都已经装载完毕
- 子类和父类同时实现initialize,父类的先被调用,然后调用子类的
- 本类与category同时实现initialize,category会覆盖本类的方法,只调用category的initialize一次(这也说明initialize的调用方式采用objc_msgSend的方式调用的)
- initialize是主动调用的,只有当类第一次被用到的时候才会触发
参考这篇文章
内存管理
1、weak
的实现原理?SideTable
的结构是什么样的
weak:其实是一个hash表结构,其中的key是所指对象的地址,value是weak的指针数组,weak表示的是弱引用,不会对对象引用计数+1,当引用的对象被释放的时候,其值被自动设置为nil,一般用于解决循环引用的。
weak的实现原理
1、初始化时:runtime会调用objc_initWeak函数,初始化一个新的weak指针指向对象的地址。
2、添加引用时:objc_initWeak函数会调用 objc_storeWeak() 函数, objc_storeWeak() 的作用是更新指针指向,创建对应的弱引用表。
3、释放时,调用clearDeallocating函数。clearDeallocating函数首先根据对象地址获取所有weak指针地址的数组,然后遍历这个数组把其中的数据设为nil,最后把这个entry从weak表中删除,最后清理对象的记录。
SideTable的结构如下:
struct SideTable {
// 保证原子操作的自旋锁
spinlock_t slock;
// 引用计数的 hash 表
RefcountMap refcnts;
// weak 引用全局 hash 表
weak_table_t weak_table;
}
复制代码
参考这篇文章
2、关联对象的应用?系统如何实现关联对象的?
应用:
- 可以在不改变类的源码的情况下,为类添加实例变量(注意:这里指的实例变量,并不是真正的属于类的实例变量,而是一个关联值变量)
- 结合category使用,为类扩展存储属性。
关联对象实现原理:
关联对象的值实际上是通过AssociationsManager对象负责管理的,这个对象里有个AssociationsHashMap静态表,用来存储对象的关联值的,关于AssociationsHashMap存储的数据结构如下:
AssociationsHashMap:
——添加属性对象的指针地址(key):ObjectAssociationMap(value:所有关联值对象)
ObjectAssociationMap:
——关联值的key:关联值的value
具体runtime的方法实现请参考这篇文章
3、关联对象的如何进行内存管理的?关联对象如何实现weak属性?
内存管理方面是通过在赋值的时候设置一个policy,根据这个policy的类型对设置的对象进行retain/copy等操作。
当policy为OBJC_ASSOCIATION_ASSIGN的时候,设置的关联值将是以weak的方式进行内存管理的。
这个题跟上面的问题差不多,可以参考上面的那篇文章。
4、Autoreleasepool
的原理?所使用的的数据结构是什么?
自动释放池是一个 AutoreleasePoolPage
组成的一个page
是4096字节大小,每个 AutoreleasePoolPage
以双向链表连接起来形成一个自动释放池
pop
时是传入边界对象,然后对page
中的对象发送release
的消息
AutoreleasePool的释放有如下两种情况:
- 一种是Autorelease对象是在当前的runloop迭代结束时释放的,而它能够释放的原因是系统在每个runloop迭代中都加入了自动释放池Push和Pop。
- 手动调用AutoreleasePool的释放方法(drain方法)来销毁AutoreleasePool或者@autoreleasepool{}执行完释放
参考这篇文章