iOS系列之 Runtime二

简介

  • Runtime: 即运行时,是一套底层的 C 语言 API,是 iOS 系统的核心之一。开发者在编码过程中,可以给任意一个对象发送消息,在编译阶段只是确定了要向接收者发送这条消息,而接受者将要如何响应和处理这条消息,那就要看运行时来决定了。

  • 结构模型

    1. 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
    2. 方法调用(消息发送)

      • 一个对象的实例方法被调用时, 会通过 isa 找到相应的类, 然后在该类的 class_data_bits_t bits 中查找方法.

        • class_data_bits_t 是指向了类对象的数据区域的机构体
      • objc_object 对象的实例方法调用时, 通过对象的 isa 在类中获取方法的实现

      • objc_class 对象的类方法调用时, 通过类的 isa 在 metaclass 中获取方法的实现

      • 如果该类没有一个方法的实现, 则向父类继续查找

      • _objc_msgSend 流程

        1. 判断消息接受者是否为 nil 或者使用使用了 tagPointer

        2. 根据消息接受者的 isa 找到

          1. 消息接受者是类, isa 找到元类 metaclass
          2. 消息接收者是实例, isa 找到类
        3. 进入 CacheLookup 流程, 寻找方法缓存

          1. 缓存有记录则直接调用

            1. 调用 TailCallCechedImp 验证 IMP 有效性
          2. 缓存没记录则进入 __objc_msgSend_uncached 流程

        4. __objc_msgSend_uncached 中调用 __class_lookupMethodAndLoadCache3 方法, 该方法会返回 IMP 对象, 里面再调用了 lookUpImpOrForward 方法

          1. 会再次从类的方法缓存中查找

          2. 在类的方法列表中查找, 成功则写入缓存并调用, 否则

          3. 从父类的缓存中查找, 找不到则从父类的方法列表查找, 如此循环知道基类(NSObject)

          4. 基类还没有找到方法, 进入动态方法解析流程(resolveInstanceMethod | resolveClassMethod)

          5. 动态解析失败, 则进入消息转发流程(forwardingTargetForSelector | forewordInvocation | methodSignatureForSelector)

          6. 消息转发失败, 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_tclass_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、IMPSELMethod的区别和使用场景

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、loadinitialize方法的区别什么?在继承关系中他们有什么区别

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{}执行完释放

参考这篇文章

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享