2006 年,苹果发布了全新的 Objective-C 2.0,我们可以在苹果官网下载最新的 Objective-C Runtime 源码:objc4-750.1.tar.gz 进行阅读和分析。
疑问:Objective-C 2.0 源码为什么被命名为 objc4 ?
本文我们先来介绍一下 Objective-C 1.0 中类与对象的定义,虽然它早已被废弃,而且在 Objective-C 2.0 中已完全重写了,但由于 1.0 的代码阅读起来相对简单清晰,易于理解,仍具一定参考意义。
相关宏定义和头文件
__OBJC__
根据 Common Predefined Macros,__OBJC__
宏在 Objective-C 编译器中被预定义为 1
。我们可以使用该宏来判断头文件是通过 C 编译器还是 Objective-C 编译器进行编译。
__OBJC2__
__OBJC2__
宏在 objc-config.h 头文件中被定义:
1 |
|
- objc-private.h
在 objc-private.h 文件中声明该头文件必须在其它头文件之前导入,避免与其它地方定义的 id
和 Class
产生冲突(因为在 Objective-C 1.0 和 2.0 中,定义类和对象的结构体是不同的,objc4-750.1 源码有多处分别定义了 objc_class
和 objc_object
,他们通过相关宏来区分):
1 | /* Isolate ourselves from the definitions of id and Class in the compiler |
此外,在该文件中还声明了如下两个宏,作为后续不同版本下定义类和对象的区分:
1 |
Objective-C 1.0
Class 和 id
在 objc.h 中,
Class
被定义为指向struct objc_class
的指针;id
被定义为指向struct objc_object
的指针;
1 |
|
上述代码被定义在 !OBJC_TYPES_DEFINED
宏内,如前面所述,在最新的 objc4-750.1(Objective-C 2.0)源码中定了 OBJC_TYPES_DEFINED
宏为 1
,所以上述代码只在老版本的 Objective-C 1.0 中生效。
其中 objc_class
结构体在 runtime.h 中定义如下:
1 |
|
- OBJC_ISA_AVAILABILITY
宏 OBJC_ISA_AVAILABILITY
在 objc-api.h 文件中定义:
1 | /* OBJC_ISA_AVAILABILITY: `isa` will be deprecated or unavailable |
可以看出,旧版本中,类型为 Class
的 isa
指针在 Objective-C 2.0 中被废弃了,在 2.0 中,isa
的类型为 union isa_t
,在下文会介绍。
- OBJC2_UNAVAILABLE
宏 OBJC2_UNAVAILABLE
在 objc-api.h 文件中定义:
1 | /* OBJC2_UNAVAILABLE: unavailable in objc 2.0, deprecated in Leopard */ |
同样地,通过相关宏的限定,objc_class
结构体及其内部成员只在 Objective-C 1.0 中生效。虽然在 Objective-C 2.0 中通过 C++ 的结构体语法重写了 struct objc_class
的定义,我们这里仍然可以简单分析上述这个纯 C 语法写的 struct objc_class
,窥探一下 Objective-C 1.0 中类的内部实现,仅作为参考:
- isa: 指向该类的元类(Meta Class)的指针
- super_class: 指向父类的指针
- name: 类名
- version: 类的版本信息
- info: 类信息,供运行时使用的一些标记位
- instance_size: 该类的实例对象的大小
- objc_ivar_list: 指向该类成员变量列表的指针
- objc_method_list: 指向方法(函数指针)列表指针的指针
- objc_cache: 指向方法调用缓存的指针
- objc_protocol_list: 指向该类实现的协议列表指针
下面我们逐项分析。
SEL
首先,在 objc.h 中,定义了 SEL
为指向 struct objc_selector
的指针:
1 | /// An opaque type that represents a method selector. |
但在 objc4 源码中,我们找不到 objc_selector
结构体的具体定义,不过通过调试,我们可以把 SEL
理解为就是“一个保存方法名的字符串”。
IMP
而 IMP
定义为一个函数指针,指向方法调用时对应的函数实现:
1 | /// A pointer to the function of a method implementation. |
简化如下:
1 | typedef id (*IMP)(id, SEL, ...); |
Method
在 runtime.h 中,Method
被定义为一个指向 struct objc_method
指针,
1 |
|
objc_method
结构体的定义如下:
1 | struct objc_method { |
它包含了一个 SEL
(方法的名称)和 IMP
(方法的具体函数实现),以及方法的类型 method_types
,它是个 char
指针,存储着方法的参数类型和返回值类型。
实际上,Method
的作用,相当于是在 SEL
和 IMP
之间做了一个映射,让我们可以通过 SEL
方法名找到其对应的函数实现 IMP
。
struct objc_class
中的方法列表结构体 objc_method_list
的定义如下:
1 | struct objc_method_list { |
其中,__LP64__
宏由预处理器定义,用于表示当前操作系统为 64 位,该宏在头文件中无法找到的,我们可以在 Mac 的终端中执行 cpp -dM /dev/null
进行查看。‘’
在 objc_class
中,methodLists
是一个 struct objc_method_list
类型的二级指针,其中每个元素都是一个数组,数组中的每个元素则是一个方法。
Ivar
同样地,Ivar
被定义为一个指向 struct objc_ivar
的指针:
1 | /// An opaque type that represents an instance variable. |
objc_ivar
结构体的定义如下:
1 | struct objc_ivar { |
struct objc_class
中的成员变量列表结构体 objc_ivar_list
的定义如下:
1 | struct objc_ivar_list { |
在 objc_class
中,所有的成员变量、属性的信息是放在链表 ivars
中的。ivars
中有一个数组,数组中每个元素是指向 Ivar
(变量信息)的指针。
Property
objc_property_t
是表示 Objective-C 声明的属性的类型,其实际是指向 objc_property
结构体的指针,其定义如下:
1 | /// An opaque type that represents an Objective-C declared property. |
objc_property_attribute_t
定义了属性的特性(attribute),它是一个结构体,定义如下:
1 | /// Defines a property attribute |
Cache
Cache
被定义为一个指向 struct objc_cache
的指针,objc_cache
结构体定义如下:
1 | typedef struct objc_cache *Cache OBJC2_UNAVAILABLE; |
mask
:一个整数,指定分配的缓存bucket
的总数;occupied
:一个整数,指定实际占用的缓存bucket
的总数。buckets
:指向Method
数据结构指针的数组。这个数组可能包含不超过mask+1
个元素。需要注意的是,指针可能是NULL
,表示这个缓存bucket
没有被占用,另外被占用的bucket
可能是不连续的。这个数组可能会随着时间而增长。
Category
Category
是表示一个指向分类的结构体 objc_category
的指针,其定义如下:
1 | /// An opaque type that represents a category. |
1 | struct objc_category { |
这个结构体主要包含了分类定义的实例方法与类方法,其中 instance_methods
列表是 objc_class
中方法列表的一个子集,而 class_methods
列表是元类方法列表的一个子集。
Protocol
1 |
|
1 | struct objc_protocol_list { |
Objective-C 2.0
在 Objective-C 2.0 中,类与对象的定义被重写了,且看下文分析。
问题思考
下面我们根据 Objective-C 1.0 上述定义,思考并回答如下几个问题。
类与对象的本质
- 实例对象(Object)
我们知道,在面向对象(OOP)的编程语言中,每一个对象都是某个类的实例。
如前面所述,在 Objective-C 中,所有对象的本质都是一个 objc_object
结构体,定义如下:
1 | struct objc_object { |
每一个实例对象都有一个 isa
指针,指向该对象对应的类,每一个类描述了一系列它的实例对象的信息,包括对象占用的内存大小,成员变量列表、成员方法列表 … 等。
对于一个具体的实例对象被初始化后,它的内存结构大致如下:
其中,isa
指针为该结构体的第一个成员变量,而其类声明的成员变量依次排列在其后,排列顺序为:根类在前,父类在后,最后才为当前类。
所以网上也有这样的说法:“凡是首地址是 *isa
的 struct 指针,都可以被认为是 objc 中的对象。”
- 类对象(Class)
在 Objective-C 中,类也被设计为一个对象,我们称之为类对象。每一个类也有一个名为 isa
的指针,也可以接受消息(类方法调用):
1 | struct objc_class { |
Cocoa 中绝大多数的类都继承了 NSObject
基类(除了 NSProxy
),其定义如下,与上述 objc_class
结构体基本一致。
1 | @interface NSObject <NSObject> { |
- 元类对象(Meta Class)
因为类是对象,则它也必须是另一个类的实例,那么类的类型是什么呢?这里就引出了另外一个概念 —— Meta Class(元类)。
在 Objective-C 中,每一个类都有其一一对应的元类。在元类的 methodLists 中保存着类方法列表。
元类(Meta Class)也是一个对象,那么元类对象的 isa
指针又指向哪里呢?为了设计上的完整,所有的元类的 isa
指针都会指向同一个根元类(Root Meta Class)。根元类本身的 isa
指针指向自己,这样就行成了一个闭环,如下图所示:
从图中我们也可以看出,对于 NSObject(基类或者叫根类:Root Class),它的父类指向 nil,它的 isa
指针指向根元类;对于根元类(Root Meta Class),它的父类为根类 NSObject,它的 isa
指针指向自己。
对象的实例方法的调用过程
- 调用一个实例对象
receiver
的方法message
,即:[receiver message]
,在编译时会被转换成objc_msgSend(id, SEL, ...)
,objc_msgSend 的执行逻辑大致如下: - 根据实例对象的
isa
指针,找到该对象对应的Class
; - 在
Class
中根据SEL
方法名寻找所调用方法对应的函数实现IMP
; - 先在当前类的
cache
中查找(因为调用方法的过程是个查找methodLists
的过程,如果每次调用都去查找,效率会非常低。所以对于调用过的方法,会以 map 的方式保存在 cache 中,下次再调用就会快很多)。如果在cache
没找到,就去当前类的methodLists
列表中查找,最后根据super_class
指针找到父类(超类),在父类的methodLists
中查找,直到找到NSObject
类为止。如果都没找到,就会走消息转发流程,最后崩溃掉; - 找到
SEL
对应的Method
后,就可以得到其函数实现IMP
,然后执行。
类对象的类方法的调用过程
当一个类方法被调用时,会先根据类对象的 isa
指针找到其元类,元类会首先查找它本身是否有该类方法的实现,如果没有,则该元类会向它的父类查找该方法,直到一直找到继承链的头。
- 对象 -> 实例方法的调用:在类中寻找方法实现的函数指针地址;
- 类(对象)-> 类方法的调用:在元类中寻找方法实现的函数指针地址;
- 元类(对象):为了定义的完整性引申出的概念,只在编译器中会用到,在实际开发基本不会接触到。
为什么不能给类动态添加成员变量却可以添加成员方法?
类的成员变量布局以及其实例对象的大小在编译时已经确定,设想一下如果 Objective-C 允许给一个类动态增加成员变量,会带来一个问题是:为基类动态增加成员变量会导致所有已创建出的子类实例都无法使用。
我们所说的“类实例”概念(对象),指的是一块内存区域,包含了 isa
指针和所有的成员变量。所以假如允许动态修改类成员变量布局,已经创建出的类实例就不符合类定义了,变成了无效对象。
而方法的定义是在 objc_class
中管理的,不管如何增删类方法,都不影响类实例的内存布局,已经创建出的类实例仍然可正常使用。
通过 Category 给类添加属性的原理
Objective-C 中类的属性可以理解为:成员变量 + getter 方法 + setter 方法,虽然我们无法动态地给一个类添加成员变量,但我们可以通过 Category 给类添加成员方法。所以在类的 Category 中添加一个属性时,我们可以在其 getter 和 setter 方法中通过关联对象的方式达到添加“成员变量”的效果。
示例代码如下:
MyClass+Category.h
:
1 |
|
MyClass+Category.m
:
1 |
|
但进一步思考一下,关联对象又是存在什么地方呢? 如何存储?对象销毁时候如何处理关联对象呢?通过查阅 objc4 源码,可以看到所有的关联对象都由 AssociationsManager
管理,AssociationsManager
里面是由一个静态 AssociationsHashMap
来存储所有的关联对象的。最后,在 Runtime 中负责销毁对象的函数 objc_destructInstance
里面会判断被销毁的对象有没有关联对象,如果有,会调用 _object_remove_assocations
做关联对象的清理工作。
Category 理解参考文章:
- Objective-C Associated Objects 的实现原理
- 深入理解 Objective-C: Category
- 结合 Category 工作原理分析 OC 2.0 中的 Runtime
参考链接
- Classes and metaclasses - Greg Parker
- 新手也看得懂的 iOS Runtime 教程 - 雷曼同学
- Objective-C 对象模型及应用 - 唐巧的博客
- Objective-C 中的类和对象 - ibireme
- 神经病院 Objective-C Runtime 入院第一天 —— isa 和 Class - halfrost
- Objective-C Runtime - 玉令天下的博客
- Objective-C 对象模型 - 雷纯锋的技术博客
- Objective-C 对象解析 - MA806P