Objective-C 1.0 中类与对象的定义

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
2
3
4
5
6
7
#ifndef __OBJC2__
# if TARGET_OS_OSX && !TARGET_OS_IOSMAC && __i386__
// old ABI
# else
# define __OBJC2__ 1
# endif
#endif
  • objc-private.h

objc-private.h 文件中声明该头文件必须在其它头文件之前导入,避免与其它地方定义的 idClass 产生冲突(因为在 Objective-C 1.0 和 2.0 中,定义类和对象的结构体是不同的,objc4-750.1 源码有多处分别定义了 objc_classobjc_object,他们通过相关宏来区分):

1
2
3
4
5
6
7
/* Isolate ourselves from the definitions of id and Class in the compiler 
* and public headers.
*/

#ifdef _OBJC_OBJC_H_
#error include objc-private.h before other headers
#endif

此外,在该文件中还声明了如下两个宏,作为后续不同版本下定义类和对象的区分:

1
2
3
#define OBJC_TYPES_DEFINED 1
#undef OBJC_OLD_DISPATCH_PROTOTYPES
#define OBJC_OLD_DISPATCH_PROTOTYPES 0

Objective-C 1.0

Class 和 id

objc.h 中,

  • Class 被定义为指向 struct objc_class指针
  • id 被定义为指向 struct objc_object指针
1
2
3
4
5
6
7
8
9
10
11
12
#if !OBJC_TYPES_DEFINED
/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;

/// Represents an instance of a class.
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};

/// A pointer to an instance of a class.
typedef struct objc_object *id;
#endif

上述代码被定义在 !OBJC_TYPES_DEFINED 宏内,如前面所述,在最新的 objc4-750.1(Objective-C 2.0)源码中定了 OBJC_TYPES_DEFINED 宏为 1,所以上述代码只在老版本的 Objective-C 1.0 中生效。

其中 objc_class 结构体在 runtime.h 中定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#if !OBJC_TYPES_DEFINED

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

} OBJC2_UNAVAILABLE;
/* Use `Class` instead of `struct objc_class *` */

#endif
  • OBJC_ISA_AVAILABILITY

OBJC_ISA_AVAILABILITYobjc-api.h 文件中定义:

1
2
3
4
5
6
7
8
9
/* OBJC_ISA_AVAILABILITY: `isa` will be deprecated or unavailable 
* in the future */
#if !defined(OBJC_ISA_AVAILABILITY)
# if __OBJC2__
# define OBJC_ISA_AVAILABILITY __attribute__((deprecated))
# else
# define OBJC_ISA_AVAILABILITY /* still available */
# endif
#endif

可以看出,旧版本中,类型为 Classisa 指针在 Objective-C 2.0 中被废弃了,在 2.0 中,isa 的类型为 union isa_t,在下文会介绍。

  • OBJC2_UNAVAILABLE

OBJC2_UNAVAILABLEobjc-api.h 文件中定义:

1
2
3
4
5
6
7
8
9
10
11
12
/* OBJC2_UNAVAILABLE: unavailable in objc 2.0, deprecated in Leopard */
#if !defined(OBJC2_UNAVAILABLE)
# if __OBJC2__
# define OBJC2_UNAVAILABLE UNAVAILABLE_ATTRIBUTE
# else
/* plain C code also falls here, but this is close enough */
# define OBJC2_UNAVAILABLE \
__OSX_DEPRECATED(10.5, 10.5, "not available in __OBJC2__") \
__IOS_DEPRECATED(2.0, 2.0, "not available in __OBJC2__") \
__TVOS_UNAVAILABLE __WATCHOS_UNAVAILABLE __BRIDGEOS_UNAVAILABLE
# endif
#endif

同样地,通过相关宏的限定,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
2
/// An opaque type that represents a method selector.
typedef struct objc_selector *SEL;

但在 objc4 源码中,我们找不到 objc_selector 结构体的具体定义,不过通过调试,我们可以把 SEL 理解为就是“一个保存方法名的字符串”。

IMP

IMP 定义为一个函数指针,指向方法调用时对应的函数实现:

1
2
3
4
5
6
/// A pointer to the function of a method implementation. 
#if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ... */ );
#else
typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...);
#endif

简化如下:

1
typedef id (*IMP)(id, SEL, ...);

Method

runtime.h 中,Method 被定义为一个指向 struct objc_method 指针,

1
2
3
4
5
6
#if !OBJC_TYPES_DEFINED

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

#endif

objc_method 结构体的定义如下:

1
2
3
4
5
struct objc_method {
SEL _Nonnull method_name OBJC2_UNAVAILABLE;
char * _Nullable method_types OBJC2_UNAVAILABLE;
IMP _Nonnull method_imp OBJC2_UNAVAILABLE;
} OBJC2_UNAVAILABLE;

它包含了一个 SEL(方法的名称)和 IMP(方法的具体函数实现),以及方法的类型 method_types,它是个 char 指针,存储着方法的参数类型和返回值类型。

实际上,Method 的作用,相当于是在 SELIMP 之间做了一个映射,让我们可以通过 SEL 方法名找到其对应的函数实现 IMP

struct objc_class 中的方法列表结构体 objc_method_list 的定义如下:

1
2
3
4
5
6
7
8
9
10
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;
} OBJC2_UNAVAILABLE;

其中,__LP64__ 宏由预处理器定义,用于表示当前操作系统为 64 位,该宏在头文件中无法找到的,我们可以在 Mac 的终端中执行 cpp -dM /dev/null 进行查看。‘’

objc_class 中,methodLists 是一个 struct objc_method_list 类型的二级指针,其中每个元素都是一个数组,数组中的每个元素则是一个方法。

Ivar

同样地,Ivar 被定义为一个指向 struct objc_ivar 的指针:

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

objc_ivar 结构体的定义如下:

1
2
3
4
5
6
7
8
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_class 中的成员变量列表结构体 objc_ivar_list 的定义如下:

1
2
3
4
5
6
7
8
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_class 中,所有的成员变量、属性的信息是放在链表 ivars 中的。ivars 中有一个数组,数组中每个元素是指向 Ivar(变量信息)的指针。

Property

objc_property_t 是表示 Objective-C 声明的属性的类型,其实际是指向 objc_property 结构体的指针,其定义如下:

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

objc_property_attribute_t 定义了属性的特性(attribute),它是一个结构体,定义如下:

1
2
3
4
5
/// Defines a property attribute
typedef struct {
const char * _Nonnull name; /**< The name of the attribute */
const char * _Nonnull value; /**< The value of the attribute (usually empty) */
} objc_property_attribute_t;

Cache

Cache 被定义为一个指向 struct objc_cache 的指针,objc_cache 结构体定义如下:

1
2
3
4
5
6
7
typedef struct objc_cache *Cache                             OBJC2_UNAVAILABLE;

struct objc_cache {
unsigned int mask /* total = mask + 1 */ OBJC2_UNAVAILABLE;
unsigned int occupied OBJC2_UNAVAILABLE;
Method _Nullable buckets[1] OBJC2_UNAVAILABLE;
};
  • mask:一个整数,指定分配的缓存 bucket 的总数;
  • occupied:一个整数,指定实际占用的缓存 bucket 的总数。
  • buckets:指向 Method 数据结构指针的数组。这个数组可能包含不超过 mask+1 个元素。需要注意的是,指针可能是 NULL,表示这个缓存 bucket 没有被占用,另外被占用的 bucket 可能是不连续的。这个数组可能会随着时间而增长。

Category

Category 是表示一个指向分类的结构体 objc_category 的指针,其定义如下:

1
2
/// An opaque type that represents a category.
typedef struct objc_category *Category;
1
2
3
4
5
6
7
struct objc_category {
char * _Nonnull category_name OBJC2_UNAVAILABLE;
char * _Nonnull class_name OBJC2_UNAVAILABLE;
struct objc_method_list * _Nullable instance_methods OBJC2_UNAVAILABLE;
struct objc_method_list * _Nullable class_methods OBJC2_UNAVAILABLE;
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
}

这个结构体主要包含了分类定义的实例方法与类方法,其中 instance_methods 列表是 objc_class 中方法列表的一个子集,而 class_methods 列表是元类方法列表的一个子集。

Protocol

1
2
3
4
5
#ifdef __OBJC__
@class Protocol;
#else
typedef struct objc_object Protocol;
#endif
1
2
3
4
5
struct objc_protocol_list {
struct objc_protocol_list * _Nullable next;
long count;
__unsafe_unretained Protocol * _Nullable list[1];
};

Objective-C 2.0

在 Objective-C 2.0 中,类与对象的定义被重写了,且看下文分析

问题思考

下面我们根据 Objective-C 1.0 上述定义,思考并回答如下几个问题。

类与对象的本质

  • 实例对象(Object)

我们知道,在面向对象(OOP)的编程语言中,每一个对象都是某个类的实例。

如前面所述,在 Objective-C 中,所有对象的本质都是一个 objc_object 结构体,定义如下:

1
2
3
struct objc_object {
Class isa;
};

每一个实例对象都有一个 isa 指针,指向该对象对应的类,每一个类描述了一系列它的实例对象的信息,包括对象占用的内存大小,成员变量列表、成员方法列表 … 等。

对于一个具体的实例对象被初始化后,它的内存结构大致如下:

其中,isa 指针为该结构体的第一个成员变量,而其类声明的成员变量依次排列在其后,排列顺序为:根类在前,父类在后,最后才为当前类。

所以网上也有这样的说法:“凡是首地址是 *isa 的 struct 指针,都可以被认为是 objc 中的对象。”

  • 类对象(Class)

在 Objective-C 中,类也被设计为一个对象,我们称之为类对象。每一个类也有一个名为 isa 的指针,也可以接受消息(类方法调用):

1
2
3
4
5
struct objc_class {
    Class isa;
    Class super_class;
    // ...
}

Cocoa 中绝大多数的类都继承了 NSObject 基类(除了 NSProxy),其定义如下,与上述 objc_class 结构体基本一致。

1
2
3
@interface NSObject <NSObject> {
Class isa OBJC_ISA_AVAILABILITY;
}
  • 元类对象(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 类成员变量深度剖析

类的成员变量布局以及其实例对象的大小在编译时已经确定,设想一下如果 Objective-C 允许给一个类动态增加成员变量,会带来一个问题是:为基类动态增加成员变量会导致所有已创建出的子类实例都无法使用。

我们所说的“类实例”概念(对象),指的是一块内存区域,包含了 isa 指针和所有的成员变量。所以假如允许动态修改类成员变量布局,已经创建出的类实例就不符合类定义了,变成了无效对象。

而方法的定义是在 objc_class 中管理的,不管如何增删类方法,都不影响类实例的内存布局,已经创建出的类实例仍然可正常使用。

通过 Category 给类添加属性的原理

Objective-C 中类的属性可以理解为:成员变量 + getter 方法 + setter 方法,虽然我们无法动态地给一个类添加成员变量,但我们可以通过 Category 给类添加成员方法。所以在类的 Category 中添加一个属性时,我们可以在其 getter 和 setter 方法中通过关联对象的方式达到添加“成员变量”的效果。

示例代码如下:

  • MyClass+Category.h
1
2
3
4
5
6
7
#import "MyClass.h"

@interface MyClass (Category)

@property (nonatomic, copy) NSString *name;

@end
  • MyClass+Category.m:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#import "MyClass+Category.h"
#import <objc/runtime.h>

@implementation MyClass (Category)

- (NSString *)name {
return objc_getAssociatedObject(self, _cmd);
}

- (void)setName:(NSString *)name {
objc_setAssociatedObject(self, @selector(name), name, OBJC_ASSOCIATION_COPY);
}

@end

但进一步思考一下,关联对象又是存在什么地方呢? 如何存储?对象销毁时候如何处理关联对象呢?通过查阅 objc4 源码,可以看到所有的关联对象都由 AssociationsManager 管理,AssociationsManager 里面是由一个静态 AssociationsHashMap 来存储所有的关联对象的。最后,在 Runtime 中负责销毁对象的函数 objc_destructInstance 里面会判断被销毁的对象有没有关联对象,如果有,会调用 _object_remove_assocations 做关联对象的清理工作。

Category 理解参考文章:

参考链接