iOS runtime之元素认知(一)

本文深入解析Objective-C Runtime机制,涵盖其基本概念、与程序交互方式、核心元素的认知等内容,帮助读者理解Objective-C运行时的工作原理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、什么是runtime

简而言之,Objective-C Runtime是一个将C语言转化为面向对象语言的扩展。
我们将C++Objective进行对比,虽然C++Objective-C都是在C的基础上加入面向对象的特性扩充而成的程序设计语言,但二者实现的机制差异很大。C++是基于静态类型,而Objective-C是基于动态运行时类型。也就是说用C++编写的程序编译时就直接编译成了可令机器读懂的机器语言;用Objective-C编写的程序不能直接编译成可令机器读懂的机器语言,而是在程序运行的时候,通过Runtime把程序转为可令机器读懂的机器语言。RuntimeObjective不可缺少的重要一部分。

runtime是一个c汇编写的动态库,它就像一个小小的系统,将OC和C紧密关联,这个系统主要做两件事 :

  • 1、封装C语言的结构体和函数,让开发者在运行时创建、检查或者修改类、对象和方法等等。
  • 2、传递消息,找出方法的最终执行代码。
  • 1、无参数

OC定义方法

[receiver message]

runtime转化C语言的代码:

objc_msgSend(receiver, selector)
  • 2、有参数

OC定义方法

[receiver message:arg1 ...]

runtime转化C语言的代码:

objc_msgSend(receiver, selector, arg1, arg2, ...)
  • 3、多方法

OC定义方法

NSObject *object = [[NSObject alloc] init];

runtime转化C语言的代码:

objc_msgSend(objc_msgSend([NSObject class],@selector(alloc)),@selector(init));

二、与runtime交互

bjc 从三种不同的层级上与 Runtime 系统进行交互,分别是:

  • 通过 Objective-C 源代码
  • 通过 Foundation 框架的NSObject类定义的方法
  • 通过对 runtime 函数的直接调用。

1、Objective-C源代码

编写OC代码,程序在运行时,runtime会自动将OC转化成C语言代码。

2、NSObject的方法

Cocoa 中大多数类都继承于NSObject类,也就自然继承了它的方法。最特殊的例外是NSProxy,它是个抽象超类,它实现了一些消息转发有关的方法,可以通过继承它来实现一个其他类的替身类或是虚拟出一个不存在的类。

有的NSObject中的方法起到了抽象接口的作用,比如description方法需要你重载它并为你定义的类提供描述内容。NSObject还有些方法能在运行时获得类的信息,并检查一些特性,比如class返回对象的类;isKindOfClass:isMemberOfClass:则检查对象是否在指定的类继承体系中;respondsToSelector:检查对象能否响应指定的消息;conformsToProtocol:检查对象是否实现了指定协议类的方法;methodForSelector:则返回指定方法实现的地址。

3、Runtime的函数

Runtime 系统是一个由一系列函数和数据结构组成,具有公共接口的动态共享库。头文件存放于/usr/include/objc目录下。许多函数允许你用纯C代码来重复实现 Objc 中同样的功能。虽然有一些方法构成了NSObject类的基础,但是你在写 Objc 代码时一般不会直接用到这些函数的,除非是写一些 Objc 与其他语言的桥接或是底层的debug工作。

三、Runtime元素认知

objc_msgSend:方法,它的真身是这样的:

id objc_msgSend ( id self, SEL op, ... );

1、SEL(objc.h)

objc_msgSend函数第二个参数类型为SEL,它是selectorObjc中的表示类型(Swift中是Selector类)。selector是方法选择器,可以理解为区分方法的 ID,而这个 ID 的数据结构是SEL:

typedef struct objc_selector *SEL;

objc_selector的定义如下:

struct objc_selector {
    char *name;   OBJC2_UNAVAILABLE;// 名称
    char *types;  OBJC2_UNAVAILABLE;// 类型
};

nametypes都是char类型。

其实它就是个映射到方法的C字符串,你可以用 Objc 编译器命令@selector()或者 Runtime 系统的sel_registerName函数来获得一个SEL类型的方法选择器。

不同类中相同名字的方法所对应的方法选择器是相同的,即使方法名字相同而变量类型不同也会导致它们具有相同的方法选择器,于是 Objc 中方法命名有时会带上参数类型;

2、id(objc.h)

objc_msgSend第一个参数类型为id,它是一个指向类实例的指针:

typedef struct objc_object *id;

objc_object定义如下:

struct objc_object {
    Class isa  OBJC_ISA_AVAILABILITY;
};
  • isa:objc_object结构体包含一个isa指针,根据isa指针就可以找到对象所属的类。objc_object(实例对象)中isa指针指向的类结构称为class(也就是该对象所属的类)其中存放着普通成员变量与动态方法(“-”开头的方法);

当我们向一个Objective-C对象发送消息时,运行时库会根据实例对象的isa指针找到这个实例对象所属的类。Runtime库会在类的方法列表及父类的方法列表中去寻找与消息对应的selector指向的方法。找到后即运行这个方法。

当创建一个特定类的实例对象时,分配的内存包含一个objc_object数据结构,然后是类的实例变量的数据。NSObject类的allocallocWithZone:方法使用函数class_createInstance来创建objc_object数据结构。

另外还有我们常见的id,它是一个objc_object结构类型的指针。它的存在可以让我们实现类似于C++中泛型的一些操作。该类型的对象可以转换为任何一种对象,有点类似于C语言中void *指针类型的作用。

PS:isa指针不总是指向实例对象所属的类,不能依靠它来确定类型,而是应该用class方法来确定实例对象的类。因为KVO的实现机理就是将被观察对象的isa指针指向一个中间类而不是真实的类,这是一种叫做 isa-swizzling 的技术;

3、Class(objc.h,runtime.h)

之所以说isa是指针是因为Class其实是一个指向objc_class结构体的指针:

typedef struct objc_class *Class;

objc_classruntime.h定义如下:

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;// 类的版本信息,默认为0,可以通过runtime函数class_setVersion或者class_getVersion进行修改、读取
    long info                                                OBJC2_UNAVAILABLE;;// 类信息,供运行时期使用的一些位标识,如CLS_CLASS (0x1L) 表示该类为普通 class,其中包含实例方法和变量;CLS_META (0x2L) 表示该类为 metaclass,其中包含类方法;
    long instance_size                                       OBJC2_UNAVAILABLE;// 该类的实例变量大小(包括从父类继承下来的实例变量)
    struct objc_ivar_list * _Nullable ivars                  OBJC2_UNAVAILABLE;// 该类的成员变量地址列表
    struct objc_method_list * _Nullable * _Nullable methodLists                    OBJC2_UNAVAILABLE;// 方法地址列表,与 info 的一些标志位有关,如CLS_CLASS (0x1L),则存储实例方法,如CLS_META (0x2L),则存储类方法;
    struct objc_cache * _Nonnull cache                       OBJC2_UNAVAILABLE;// 缓存最近使用的方法地址,用于提升效率;
    struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;// 存储该类声明遵守的协议的列表
#endif

} OBJC2_UNAVAILABLE;


PS:OBJC2_UNAVAILABLE之类的宏定义是苹果在 Objc 中对系统运行版本进行约束的黑魔法,为的是兼容非Objective-C 2.0的遗留逻辑,但我们仍能从中获得一些有价值的信息,有兴趣的可以查看源代码。

  • (1)、isa:此处isa指针指向的类结构称为metaclass,其中存放着static类型的成员变量与static类型的方法(“+”开头的方法)。

  • (2)、 super_class: 指向该类的父类的指针,如果该类是根类(如NSObjectNSProxy),那么super_class就为nil

  • (3)、cache:用于缓存最近使用的方法。一个接收者对象接收到一个消息时,它会根据isa指针去查找能够响应这个消息的对象。在实际使用中,这个对象只有一部分方法是常用的,很多方法其实很少用或者根本用不上。这种情况下,如果每次消息来时,我们都是methodLists中遍历一遍,性能势必很差。这时,cache就派上用场了。在我们每次调用过一个方法后,这个方法就会被缓存到cache列表中,下次调用的时候runtime就会优先去cache中查找,如果cache没有,才去methodLists中查找方法。这样,对于那些经常用到的方法的调用,但提高了调用的效率。
    针对cache,我们用下面例子来说明其执行过程:

      NSArray *array = [[NSArray alloc] init];
    

    其流程是:

    • ①、[NSArray alloc]先被执行。因为NSArray没有+alloc方法,于是去父类NSObject去查找。
    • ②、检测NSObject是否响应+alloc方法,发现响应,于是检测NSArray类,并根据其所需的内存空间大小开始分配内存空间,然后把isa指针指向NSArray类。同时,+alloc也被加进cache列表里面。
    • ③、接着,执行-init方法,如果NSArray响应该方法,则直接将其加入cache;如果不响应,则去父类查找。
    • ④、在后期的操作中,如果再以[[NSArray alloc] init]这种方式来创建数组,则会直接从cache中取出相应的方法,直接调用。
  • (4)、version:我们可以使用这个字段来提供类的版本信息。这对于对象的序列化非常有用,它可是让我们识别出不同类定义版本中实例变量布局的改变。

总结:类与对象的区别就是类比对象多了很多特征成员,类也可以当做一个objc_object来对待,也就是说类和对象都是对象,分别称作类对象(class object)和实例对象(instance object),这样我们就可以区别对象和类了。

为了处理类和对象的关系,runtime 库创建了一种叫做元类 (Meta Class) 的东西,类对象所属类型就叫做元类,它用来表述类对象本身所具备的元数据。类方法就定义于此处,因为这些方法可以理解成类对象的实例方法。每个类仅有一个类对象,而每个类对象仅有一个与之相关的元类。当你发出一个类似[NSObject alloc]的消息时,你事实上是把这个消息发给了一个类对象 (Class Object) ,这个类对象必须是一个元类的实例,而这个元类同时也是一个根元类 (root meta class) 的实例。所有的元类最终都指向根元类为其超类。所有的元类的方法列表都有能够响应消息的类方法。所以当 [NSObject alloc] 这条消息发给类对象的时候。

PS:在objc_class结构体中:ivarsobjc_ivar_list指针;methodLists是指向objc_method_list指针的指针。也就是说可以动态修改*methodLists的值来添加成员方法,这也是Category实现的原理,同样解释了Category不能添加属性的原因。

4、IMP(objc.h)

IMP在objc.h中的定义是:

#if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ... */ ); 
#else
typedef id (*IMP)(id, SEL, ...); 
#endif

IMP是“implementation”的缩写,它是由编译器生成的一个函数指针。当你发起一个ObjC消息后,这个函数指针决定了最终执行哪段代码。

iOSRuntime中,Method通过selectorIMP两个属性,实现了快速查找方法及实现,相对提高了性能,又保持了灵活性。

5、Method(runtime.h)

Method代表类中的某个方法的类型。

/// An opaque type that represents a method in a class definition.
typedef struct objc_method *Method;

objc_method的定义如下:

struct objc_method {
    SEL method_name        OBJC2_UNAVAILABLE;// 方法名
    char *method_types     OBJC2_UNAVAILABLE;// 方法类型
    IMP method_imp         OBJC2_UNAVAILABLE;// 方法实现
}OBJC2_UNAVAILABLE;

  • (1)、method_name(方法名):类型为SEL,前面提到过相同名字的方法即使在不同类中定义,它们的方法选择器也相同;
  • (2)、method_types(方法类型):是个char指针,其实存储着方法的参数类型和返回值类型;
  • (3)、method_imp(方法实现):指向了方法的实现,本质上是一个函数指针。

6、Ivar(runtime.h)

Ivar代表类中实例变量的类型

/// An opaque type that represents an instance variable.
typedef struct objc_ivar *Ivar;

objc_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、Category(runtime.h)

Category的定义如下:

/// An opaque type that represents a category.
typedef struct objc_category *Category;

objc_category的定义如下:

struct objc_category {
    char *category_name                                      OBJC2_UNAVAILABLE;// 类别名称
    char *class_name                                         OBJC2_UNAVAILABLE;// 类名
    struct objc_method_list *instance_methods                OBJC2_UNAVAILABLE;// 实例方法列表
    struct objc_method_list *class_methods                   OBJC2_UNAVAILABLE;// 类方法列表
    struct objc_protocol_list *protocols                     OBJC2_UNAVAILABLE;// 协议列表
}  

8、objc_property_t(runtime.h)

objc_property是类中的属性,它的定义如下

/// An opaque type that represents an Objective-C declared property.
typedef struct objc_property *objc_property_t;

objc_property是内置的类型,与之关联的还有一个objc_property_attribute_t,它是属性的attribute,也就是其实是对属性的详细描述,包括属性名称、属性编码类型、原子类型/非原子类型等。它的定义如下:

/// Defines a property attribute
typedef struct {
    const char *name;     // 名称
    const char *value;    // 值(通常是空的)
} objc_property_attribute_t;

9、Cache(runtime.h)

  • 用于快速查找方法执行函数
  • 是可增量扩展的哈希表结构
  • 是局部性原理的最佳应用

Catch的定义如下:

typedef struct objc_cache *Cache      OBJC2_UNAVAILABLE;

objc_cache的定义如下

struct objc_cache {
    unsigned int mask /* total = mask + 1 */                 OBJC2_UNAVAILABLE;
    unsigned int occupied                                    OBJC2_UNAVAILABLE;
    Method buckets[1]                                        OBJC2_UNAVAILABLE;
};
  • (1)、mask:指定分配cache buckets的总数。在方法查找中,Runtime使用这个字段确定数组的索引位置。
  • (2)、occupied:实际占用cache buckets的总数。
  • (3)、buckets:指定Method数据结构指针的数组。这个数组可能包含不超过mask+1个元素。需要注意的是,指针可能是NULL,表示这个缓存bucket没有被占用,另外被占用的bucket可能是不连续的。这个数组可能会随着时间而增长。

objc_msgSend每调用一次方法后,就会把该方法缓存到cache列表中,下次的时候,就直接优先从cache列表中寻找,如果cache没有,才从methodLists中查找方法。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

xiaoxiaobukuang

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值