深入理解 Objective-C ☞ Class

0.前言

从事 iOS 开发已有 3 年多时间,大部分时间都是在用 Objective-C 开发 App(最近也在做 OC 与 Swift 的混编实践),虽然对 OC 底层知识有一定的了解,不过都是零散的片段,计划趁着过年的时间将这些片段梳理串联起来,于是便有了这个系列。

本文是第 1 篇,从我们平常用的最多的对象开始,深入探究他们的实现机理。

1.概述

我们平常编写的 OC 代码都会先编译成 C/C++代码,然后再依次翻译成 汇编代码机器码(01代码),最后,机器会自动运行该机器语言程序,并将计算结果输出。为了探究 OC 的本质,通过 C/C++ 是比较合适的方式,因为之后的汇编和01代码看着太费劲(主要是自己只了解点皮毛(⊙﹏⊙)b),而 OC 本身又不是开源的。


2.从一个 🌰 开始

2.1 最简单的例子

我们先来看一个例子:

1
2
3
4
5
6
7
8
// 以下代码位于 main.m
int main(int argc, const char * argv[]) {
@autoreleasepool {
// 创建一个 NSObject 的实例对象
NSObject *obj = [[NSObject alloc] init];
}
return 0;
}

如上所示,在 main() 函数里边创建了一个 NSObject 的实例对象,然后终端执行下边的指令,将代码编译成 C/C++ 代码 (新代码在 main.cpp 文件中):

1
clang -rewrite-objc main.m -o main.cpp //  -o main.cpp 可以忽略

在 main.cpp 中我们发现了下边的结构体,从名字推断,应该是 NSObject 的底层实现:

1
2
3
struct NSObject_IMPL { // NSObject_IMPL <=> NSObject implementation
Class isa;
};

而我们直接查看 NSobject 的声明:

1
2
3
@interface NSObject <NSObject> { // 移除了用于消除警告的代码
Class isa OBJC_ISA_AVAILABILITY;
}

NSObject_IMPL 对比后,进一步印证了 NSObject_IMPL 是 NSObject 的底层结构的推断。这里有一个 Class 类型的 isa,下面是 Class 的定义:

1
2
/// An opaque type that represents an Objective-C class. 表示 OC 中的 class。
typedef struct objc_class *Class;

也就是说,isa 实际是一个指向 struct objc_class 的指针,而且 objc_class 就是 Class 的底层结构。

2.2 稍微复杂点的例子

现在来看一种更加复杂的情况:依次创建 HHStaff 和 HHManager 这 2 个类,其中,后者继承自前者,然后在 main() 函数中创建一个 HHManager 的实例。

HHStaff

1
2
3
4
5
6
7
8
@interface HHStaff : NSObject {
NSString *name;
}

- (void)doInstanceStaffWork; // 对象方法
+ (void)doClassStaffWork; // 类方法

@end

HHManager

1
2
3
4
5
6
@interface HHManager : HHStaff {
NSInteger officeNum;
}

- (void)doInstanceManagerWork; // 对象方法
+ (void)doClassManagerWork; // 类方法

main.m 文件

1
2
3
4
5
6
7
int main(int argc, const char * argv[]) {
@autoreleasepool {
// 创建实例对象
HHManager *mgr = [[HHManager alloc] init];
}
return 0;
}

终端执行 clang -rewrite-objc main.m 将其转成 C/C++ 代码,整理相关代码后,我们可以得出下图的关系:

稍微复杂的情况.png

其中,HHManager_IMPL 是 HHManager 的底层结构,而 HHStaff_IMPL 是其父类 HHStaff 的底层结构,即子类中包含一个父类类型的变量,而父类结构中又包含一个父类的父类(此处是基类)类型变量,而基类中包含一个名为 isa 的指针变量,据此,可以认为子类 HHManger 经编译后的结构是这样的:

1
2
3
4
5
struct HHManager_IMPL {
Class isa;
NSString *name;
NSInteger officeNum;
};

我们发现,这里包含了一个 isa 指针,而 isa 来自 NSObject,因为大部分类都是直接或间接继承自 NSObject 的,所以可以认为每一个对象都包含了一个 isa 指针,至于这个 isa 指针到底是干什么用的,下一小节就会讲到。

3.OC 的 3 种对象间的关系

3.1 OC 中的 3 种对象

为了搞清楚 isa 指针的作用,有必要先了解一下 OC 的对象,总共有以下 3 种:

  • 实例对象(instance),通过 +alloc 方法创建出来的,如下边的 staffAstaffB:
1
2
3
4
HHStaff *staffA = [[HHStaff alloc] init];
HHStaff *staffB = [[HHStaff alloc] init];

NSLog(@"实例对象:%p - %p", staffA, staffB);

实例对象在内存中存储的信息包括:isa 指针 和 其他成员变量。

  • 类对象(class),如下边的 staffClassAstaffClassB:
1
2
3
4
5
Class staffClassA = [staffA class]; // <==> Class staffClassA = [[staffA class] class];
Class staffClassB = object_getClass(staffB);
Class staffClassC = [HHStaff class]; // <==> Class staffClassC = [[HHStaff class] class];

NSLog(@"类对象: %p - %p - %p", staffClassA, staffClassB, staffClassC);

类对象中包含的信息如下图所示,其中,成员变量信息指的是成员变量的描述信息,而非成员变量的值(在实例对象里边)。

  • 元类对象(meta-class),如下边的 staffMetaClassAstaffMetaClassB:
1
2
3
4
Class staffMetaClassA = object_getClass(staffClassA);
Class staffMetaClassB = object_getClass(staffClassB);

NSLog(@"元类对象:%p - %p", staffMetaClassA, staffMetaClassB);

元类对象的存储结构与类对象相似,只不过只有 isa、superclass 和 类方法有值,其它均为空。

运行上边的程序后,控制台的输出如下:

1
2
3
4
2019-01-28 17:36:33.990939+0800 TTTTT[10186:1017842] 实例对象:0x100605920 - 0x100606060
2019-01-28 17:36:33.991128+0800 TTTTT[10186:1017842] 类对象: 0x100001260 - 0x100001260 - 0x100001260
2019-01-28 17:36:33.991180+0800 TTTTT[10186:1017842] 元类对象:0x100001238 - 0x100001238
Program ended with exit code: 0

从上述打印结果可以看出,一个类的实例对象可以有多个,但是类对象和元类对象各自只有一个。

3.2 isa 和 superclass

通过上一小节,我们知道类里边的信息并不是存在一个地方,而是分开存放在实例对象、类对象和元类对象里边。而将这些对象联系起来的纽带就是本小节要重点讨论的 isa 和 superclass 指针。

isa

isa.png

isa 是用来联系同一个类的实例对象、类对象和元类对象的(isa 类型是 isa_t,后文会讲到),如上图所示,通过实例对象里边的 isa 指针可以找到类对象,根据类对象里边的 isa 指针可以找到元类对象。

注意,这里并没有说 isa 指向哪里,而是说通过 isa 可以找到哪里,这是因为从 64bit 架构开始,isa 里边存储的不再是类对象或者元类对象的地址,而是需要进行一次位运算 isa & ISA_MASK(依据见后文 isa_t 的介绍)才能得到相应的地址,其中 ISA_MASK 的定义如下:

1
2
3
4
5
6
7
# if __arm64__	    // 64位 真机
# define ISA_MASK 0x0000000ffffffff8ULL
# elif __x86_64__ // 64位 模拟器
# define ISA_MASK 0x00007ffffffffff8ULL
# else
# error unknown architecture for packed isa
# endif

注意到 ISA_MASK 中有些位是 0,而和 0 与的话,结果会被置为 0,所以可以推测,64bit 架构下,isa 里边可能还存储了其它信息。

superclass

superclass 是用来在继承体系中搜寻父类的,如下图所示:

superclass.png

  • 对于类对象:子类(HHManager)的类对象的 superclass 指向父类(HHStaff)的类对象,父类的类对象的 superclass 指向它的父类的类对象;
  • 对于元类对象:子类(HHManager)的元类对象的 superclass 指向父类(HHStaff)的元类对象,父类的元类对象的 superclass 指向它的父类的元类对象;

3.3 应用

下面我们来看看在消息发送过程中,这 3 种对象之间是如何亲密协作的。

先贴一张经典的关系图,实际就是将上一节中的 isa 和 superclass 指针放到了一起:

现在以 2.2 节中的例子为基础,执行下边的操作,即子类执行父类的对象方法。

1
2
3
4
5
6
7
8
9
10
int main(int argc, const char * argv[]) {
@autoreleasepool {
// 创建实例对象
HHManager *mgr = [[HHManager alloc] init];
// 执行父类的方法
[mgr doInstanceStaffWork];
// => objc_msgSend(mgr, @selector(doInstanceStaffWork));
}
return 0;
}

由于对象方法存放在类对象里边,所以首先根据 mgr 的 isa 指针找到它的类对象,然后在类对象的方法列表里边查找这个方法,发现找不到,接着再根据类对象的 superclass 指针找到父类的类对象,然后在父类的类对象里边查找该方法,如果还找不到,就根据父类的 superclass 指针沿着继承体系继续往上找,直到根类,如果还是找不到,就会执行消息转发的流程(详见 Objective-C 的消息转发机制)。不过,本例中父类的类对象里有这个方法,就不用再往上找了O(∩_∩)O。

如果是类方法,则通过类对象的 isa 指针找到元类对象,然后就依照类似查找对象方法的方式查找类方法,只不过这次是在元类对象的继承体系里边查找。

其实,上边的逻辑省略了一个非常重要的缓存问题,即在每一级查找时,都会先查找缓存,然后才去查找方法列表。找到之后,也会在缓存里边存一份(即使是在父类的类对象或元类对象里边找到的,也要始终缓存在当前类对象或元类对象里),以便提高查找效率。

特例

注意观察上边那张关系图的右上角,就会发现,基类的元类对象的 superclass 指针指向了自己的类对象,真实情况是这样的吗?我们来做一个实验:给 NSObject 添加一个对象方法,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
@interface NSObject (Extern)

- (void)doInstanceWork;

@end

@implementation NSObject (Extern)

- (void)doInstanceWork {
NSLog(@"这是 NSObject 的对象方法");
}

@end

然后,在 main.m 中这样调用:

1
2
3
4
5
6
int main(int argc, const char * argv[]) {
@autoreleasepool {
[HHStaff doInstanceWork];
}
return 0;
}

即调用 HHStaff 的类方法 +doInstanceWork,不过 HHStaff 里边并没有这个类方法,但是运行时并没有报错,控制台输出如下:

1
2019-02-03 16:09:38.454099+0800 HHH[2667:925051] 这是 NSObject 的对象方法

也就是说,确实如关系图所示,执行了基类的类对象里边存储的对象方法。可以这么来理解,OC 的方法调用,实际都是在发送消息,即 objc_msgSend(object, @selector(methodName)) ,这里并不关心是对象方法还是类方法,如果 object 是实例对象,就会去类对象里查找方法,如果 object 是类对象,就会去元类对象里边查找。

4.Class 的结构

前边我们说过,类中的方法、属性、协议等重要信息都存储在类对象元类对象里边,这两者的结构相同,都是 Class 类型的,而 Class 的结构实际就是 struct objc_class,因此我们的目的就是要弄清楚 struct objc_class 的结构。

在 objc 源码的 objc-runtime-new.h 中找到了 objc_class 的最新定义:

1
2
3
4
5
6
7
8
9
10
11
12
struct objc_class : objc_object {
// Class ISA; // isa 不再放这里
Class superclass;
cache_t cache; // 1.缓存

class_data_bits_t bits;
class_rw_t *data() { // 2.class_rw_t
return bits.data();
}

// *** 此处略去好多行 O(∩_∩)O~
}

既然 C++ 的结构体是可以继承的,那么我们来看看它继承的结构体 objc_object 里边都有什么:

1
2
3
4
5
6
7
8
9
10
struct objc_object {
private:
isa_t isa; // 3.isa,注意是私有
public:
// 此方法返回的不是 tagged pointer 对象
Class ISA();
// 此方法返回的可能是一个 tagged pointer 对象
Class getIsa();
// *** 此处又略去好多行 O(∩_∩)O~
}

以上就是 objc_class 的表层结构,下面针对其中的 3 各主要部分做一个相对深入点的讨论。

4.1 cache_t

cache_t 就是前文提到的方法缓存,其结构如下所示(做了适当精简):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct cache_t {
struct bucket_t *_buckets; // 散列表
mask_t _mask; // 散列表的长度 - 1
mask_t _occupied; // 已经缓存的方法数量

public:
struct bucket_t *buckets();
mask_t mask();
mask_t occupied();

// *** 此处又略去好多行 O(∩_∩)O~

// 扩展空间
void expand();
void reallocate(mask_t oldCapacity, mask_t newCapacity);
// 查询缓存
struct bucket_t * find(cache_key_t key, id receiver);

// *** 此处又略去好多行 O(∩_∩)O~
};

cache_t 里边有一个散列表(哈希表)_buckets,里边是一个个的 struct bucket_t,用于缓存方法。bucket_t 的结构如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
struct bucket_t {
private:
cache_key_t _key; // 用 SEL 做 key
IMP _imp; // 函数的内存地址 做 value

public:
inline cache_key_t key() const { return _key; }
inline IMP imp() const { return (IMP)_imp; }
inline void setKey(cache_key_t newKey) { _key = newKey; }
inline void setImp(IMP newImp) { _imp = newImp; }

void set(cache_key_t newKey, IMP newImp);
};

现在,我们看一下如何查询缓存,即 find() 函数的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
bucket_t * cache_t::find(cache_key_t k, id receiver)
{
assert(k != 0);

bucket_t *b = buckets();
mask_t m = mask();
mask_t begin = cache_hash(k, m); // 根据 k 与 m 算出一个下标:begin = k & m
mask_t i = begin;
do { // 根据下标取值,并验证做了一个异常处理,即不同 key 得到相同下标的问题
if (b[i].key() == 0 || b[i].key() == k) {
return &b[i];
}
} while ((i = cache_next(i, m)) != begin);

// hack
Class cls = (Class)((uintptr_t)this - offsetof(objc_class, cache));
cache_t::bad_cache(receiver, (SEL)k, cls);
}

查询的基本逻辑是:

  • 先根据传入的 k(即key) 和 m(即mask) 算出一个下标 begin = k & m

  • 然后用这个下标 begin 去散列表里取值,用取到的值 (bucket) 里边的 key 与 传入的 k 作比较,

    • 如果相等,就将取到的值 (bucket) 返回;

    • 如果不等,利用 cache_next() 函数 (如下) 算出一个新的下标,再去取值比较;

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      #if __arm__  ||  __x86_64__  ||  __i386__  // 各种模拟器
      static inline mask_t cache_next(mask_t i, mask_t mask) {
      return (i+1) & mask;
      }

      #elif __arm64__ // 64bits 真机
      static inline mask_t cache_next(mask_t i, mask_t mask) {
      // 如果 i 不为 0,则返回 i-1;否则返回 mask
      return i ? i-1 : mask;
      }

      #else
      #error unknown architecture
      #endif
    • 如此循环,最后如果新算出来的下标等于 begin,则退出循环,说明缓存里没有对应的方法。

4.2 class_rw_t、class_ro_t

class_rw_t 是通过 bit 的 data() 函数获取的,从名称可以看出来,它是可读可写的(rw),其基本结构及说明如下:

1
2
3
4
5
6
7
8
9
10
11
12
struct class_rw_t {

// *** 此处又略去好多行 O(∩_∩)O~

const class_ro_t *ro;

method_array_t methods; // 方法列表
property_array_t properties; // 属性列表
protocol_array_t protocols; // 协议列表

// *** 此处又略去好多行 O(∩_∩)O~
}

里边有一个只读(ro)的 class_ro_t *roclass_ro_t 的结构及各元素的说明如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct class_ro_t {

// *** 此处又略去好多行 O(∩_∩)O~

const char * name; // 类名
method_list_t * baseMethodList; // 方法列表
protocol_list_t * baseProtocols; // 协议列表
const ivar_list_t * ivars; // 成员变量列表

const uint8_t * weakIvarLayout;
property_list_t *baseProperties; // 属性列表

method_list_t *baseMethods() const {
return baseMethodList;
}
};

class_ro_t 里边存放的是编译完成时类结构里边的方法、属性、协议及成员变量等信息。class_rw_t 里边是在运行时扩展了累的方法、属性等信息以后的结构,比如在分类中添加的方法就是加到了这个结构里。

class_ro_t 里边有成员变量,而且是只读的,但 class_rw_t 里没有,这也解释了为什么不能通过分类添加成员变量。当然分类里是可以添加属性的,只不过这样添加的属性只能生成 setter 和 getter 的声明,还需要自己设法完成他们的实现,至于如何实现,下一篇 会讲到。

4.3 isa_t

4.3.1 isa_t

objc_object 这个结构体里边 isa 的类型是个共用体 union isa_t ,其结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
union isa_t {
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
uintptr_t bits;

#if defined(ISA_BITFIELD) // 位域
struct {
ISA_BITFIELD; // defined in isa.h
};
#endif
};

从 64 位架构开始引入了位域,可以在isa 中存储更多信息,上边结构体中的 ISA_BITFIELD 定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// isa.h
# if __arm64__ // 64位真机
# define ISA_BITFIELD \
uintptr_t nonpointer : 1; \
uintptr_t has_assoc : 1; \
uintptr_t has_cxx_dtor : 1; \
uintptr_t shiftcls : 33; /*MACH_VM_MAX_ADDRESS 0x1000000000*/ \
uintptr_t magic : 6; \
uintptr_t weakly_referenced : 1; \
uintptr_t deallocating : 1; \
uintptr_t has_sidetable_rc : 1; \
uintptr_t extra_rc : 19
# elif __x86_64__ // 64位模拟器·
# define ISA_BITFIELD \
uintptr_t nonpointer : 1; \
uintptr_t has_assoc : 1; \
uintptr_t has_cxx_dtor : 1; \
uintptr_t shiftcls : 44; /*MACH_VM_MAX_ADDRESS 0x7fffffe00000*/ \
uintptr_t magic : 6; \
uintptr_t weakly_referenced : 1; \
uintptr_t deallocating : 1; \
uintptr_t has_sidetable_rc : 1; \
uintptr_t extra_rc : 8
# else
# error unknown architecture for packed isa
# endif

下面这张图以 64 位真机为例,详细说明了各位的作用:

isa位域.png

前边 3.2 说过,从 64 位架构开始,需要通过 isa & ISA_MASK 才能得到对应类对象或元类对象的地址,其实就是为了取出 shiftcls 部分。

在前文 struct objc_object 的结构中,我们发现 isa 是私有的,外部只能通过 ISA()getIsa() 这两个方法访问,下面分别看一下 ISA() 的源码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
inline Class objc_object::ISA()
{
// 如果是 TaggedPointer 就会中断言
assert(!isTaggedPointer());

#if SUPPORT_INDEXED_ISA
if (isa.nonpointer) {
uintptr_t slot = isa.indexcls;
return classForIndex((unsigned)slot);
}
return (Class)isa.bits;
#else
return (Class)(isa.bits & ISA_MASK);
#endif
}

上边是 ISA() 的源码,其中条件编译的条件是 SUPPORT_INDEXED_ISA,定义如下:

1
2
3
4
5
6
// Define SUPPORT_INDEXED_ISA=1 on platforms that store the class in the isa field as an index into a class table.
#if __ARM_ARCH_7K__ >= 2 || (__arm64__ && !__LP64__)
# define SUPPORT_INDEXED_ISA 1
#else
# define SUPPORT_INDEXED_ISA 0
#endif

其中,_ _ _ARM_ARCH_7K_ _ _ 是 Apple Watch 会用到的,LP64 指的是 Long Pointer 64位,现在绝大多数 Unix 平台均使用 LP64 数据模型,所以一般情况下 SUPPORT_INDEXED_ISA 的值为 0,也就是说 ISA() 会执行 else 中的代码,即 isa.bits & ISA_MASK。

4.3.2 Tagged Pointer

我们注意到 getIsa() 的如下说明,也就是说,getIsa() 允许当前对象(this)是一个 Tagged Pointer 对象。那么,下面我们就来了解一下这个东东。

getIsa() allows this to be a tagged pointer object.

Tagged Pointer 是苹果在发布 iPhone 5s(搭载 64 位架构的 A7处理器)时提出的,它的优势在于,对于小对象(如NSNumber、NSDate等)能够大大地节省内存和提高执行效率。

我们以下面这行代码为例,比较一下引入 Tagged Pointer 前后占用内存的变化。

1
NSNumber *num = @(2);

如下图所示,那么当从 32 位机器迁移到 64 位机器后,如果没有引入 Tagged Pointer,虽然逻辑未改变,但是所占用的内存会翻倍;如果引入了 Tagged Pointer,NSNumber 的变量 @(2) 本身的值需要占用的内存大小常常用不了 8 个字节,于是可以将一个对象的指针(8 个字节)拆成两部分,一部分直接保存数据,另一部分作为特殊标记,标记是否是 “特殊指针”。

当然,这也是有一定限制的,当 8 字节可以承载要表示的数值时,系统就会以 Tagged Pointer 的方式生成指针;如果 8 字节承载不了时,则又用以前的方式来生成普通的指针。

5.小结

关于 Class 的讨论就先讨论到这里,可能有些地方理解的还不是很到位,后边会及时更新的 O(∩_∩)O~

# 参考

0%