Objective-C 对象定义
Objective-C 是一种面向对象的语言,NSObject 是所有类的基类。我们可以打开 NSObject.h 文件查看到 NSObject 的类定义如下:
@interface NSObject{
Class isa OBJC_ISA_AVAILABILITY;
}
这里表示一个 NSObject 拥有一个 Class 类型的成员变量,那么这个 Class 有是什么呢?我们可以在 objc4-706 源码的 objc-private.h 中看到如下两个定义:
typedef struct objc_class *Class;typedef struct objc_object *id;
从第一个定义中可以看出,Class 其实就是 C 语言定义的结构体类型(struct objc_class)的指针,这个声明说明 Objective-C 的类实际上就是 struct objc_class。
第二个定义中出现了我们经常遇到的 id 类型,这里可以看出 id 类型是 C 语言定义的结构体类型(struct objc_object)的指针,我们知道我们可以用 id 来声明一个对象,所以这也说明了 Objective-C 的对象实际上就是 struct objc_object。
在 objc4-680 源码中我们跳转到 objc_class 的定义:
// note:这里没有列出结构体中定义的方法struct objc_class : objc_object {// Class ISA;Class superclass;cache_t cache; // formerly cache pointer and vtableclass_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags};
从上面可以看到 objc_class 是继承自 objc_object 的,所以 Objective-C 中的类自身也是一个对象,只是除了 objc_object 中定义的成员变量外,还有另外三个成员变量:superclass、cache 和 bits。
深入理解 isa_t
下面只分析一下其成员变量nonpointer:
nonpointer: 表示是否对 isa 指针开启指针优化
在说明 nonpointer 意义前,先简单介绍一下苹果为 64 位设备提出的节省内存和提高执行效率的一种优化方案:Tagged Pointer。
设想在 32 位和 64 位设备上分别存储一个 NSNumber 对象,其值是一个 NSInteger 整数。
首先,分析一下内存占用情况:
-
读写 NSNumber 对象的指针。在 32 位设备上,一个指针需要 4byte。在 64 位设备上,一个指针需要 8byte。
-
存储 NSNumber 对象值的内存。在 32 位设备上,NSInteger 占用 4byte。在 64 位设备上,NSInteger 占用 8byte。
-
Objective-C 内存管理采用引用计数的方式,我们需要使用额外的空间来存储引用计数,如果引用计数使用 NSInteger,那么 64 位设备会比 32 位设备多用 4byte。
此外,从效率上讲,引用计数、生命周期标识等存储在其他地方,也有不少处理逻辑(例如为引用计数动态分配内存等)。
一般来说,32 位已经足够存储我们通常遇到的整数和指针地址了,那么在 64 位设备上,就有 32 位地址空间浪费掉了,存储一个值为 NSInteger 的 NSNumber 对象就浪费了 8byte 的空间(4byte 指针和 4byte value)。
为了节省内存以及提高程序执行效率,苹果提出了 Tagged Pointer,Tagged Pointer 简单来说就是使用存储指针的内存空间存储实际的数据。
例如,NSNumber 指针在 64 位设备上占用 8byte 内存空间,指针优化可以将 NSNumber 的值通过某种规则放入到存储 NSNumber 指针地址的 8byte 中,这样就减少了 NSInteger 所需的 8byte 内存空间,从而节省了内存。
另外,Tagged Pointer 已经不再是对象指针,它里面存放着实际数据,只是一个普通变量,所以它的内存无需在堆上 calloc/free,从而提高了内存读取效率。但是由于 Tagged Pointer 不是合法的对象指针,所以我们无法通过 Tagged Pointer 获取 isa 信息。关于 Tagged Pointer 更详细的介绍可参考:深入理解 Tagged Pointer-唐巧,这里不做深入介绍。
了解 Tagged Pointer 的概念后,再来看 nonpointer 变量。nonpointer 变量占用 1bit 内存空间,可以有两个值:0 和 1,分别代表不同的 isa_t 的类型:
-
0 表示 isa_t 没有开启指针优化,不使用 isa_t 中定义的结构体。访问 objc_object 的 isa 会直接返回 isa_t 结构中的 cls 变量,cls 变量会指向对象所属的类的结构,在 64 位设备上会占用 8byte。
-
1 表示 isa_t 开启了指针优化,不能直接访问 objc_object 的 isa 成员变量(因为 isa 已经不是一个合法的内存指针了,见 Tagged Pointer 的介绍),从其名字 nonpointer 也可获知这个 isa 已经不是一个指针了。但是 isa 中包含了类信息、对象的引用计数等信息,在 64 位设备上充分利用了内存空间。
对于 nonpointer 为 1 的 isa_t 的结构就是 isa_t 内部定义的结构体,该结构体中包含了对象的所属类信息、引用计数等。从这里也可以看出,指针优化减少了内存使用,并且引用计数等对象关联信息都存放在 isa_t 中,也减少了很多获取对象信息的逻辑,提高了执行效率。
对象、类、元类
第一部分讨论了 NSObject、objc_object、objc_class 的定义,可以看出 Class 其实就是 C 语言定义的 objc_class 结构体,而 objc_class 继承自 objc_object,所以 Objective-C 中 Class 也是一个对象。第二部分深入解析了 objc_object 中唯一的成员变量的类型:isa_t,这部分讨论了 isa_t 中存放着对象所属的类的指针。第三部分我们来讨论在 Objective-C 的对象、类和元类(meta-class,后面会介绍)的关系。
刚刚提到,objc_class 继承自 objc_object,所以 objc_class 也是有 isa_t 类型的 isa 成员变量的,那么 objc_class 的 isa_t 中的 shiftcls 表示了什么意思呢?这里引入一个新的概念:元类。objc_class 的 isa_t 中的 shiftcls 就指向了 objc_object 的元类。什么是元类?
先来看一下 objc_class 除了继承自 objc_object 的成员变量 isa_t 外的三个成员变量:
-
Class superclass:该变量指向父类的 objc_class;
-
cache_t cache:该变量存放着实例方法的缓存,为了提高每次执行;
-
class_data_bits_t bits:存放着实例的所有方法。
关于 Objective-C 的 Runtime 的方法查找这里不进深入讨论。不过,通过上面三个属性的解释,我们可以窥探出一个对象可以调用的方法列表是存储在对象的类结构中的。其实不存储在对象中的原因也很好理解,如果方法列表存放在对象结构中,那每创建一个对象,就要增加一个实例方法列表,资源消耗过大,所以存储在了类结构中。但是,除了实例方法外,一般还会法就存放在了上面提到的元类里。元类也是一个 objc_class 结构,结构中有 isa 和 superclass 指针,下图是 Objective-C Runtime 讲解中最经典的一张图:
对象、类、元类.png注意:上图中 isa 的箭头,其实并不是 isa 指针直接指向了相应结构,而是 isa_t 中的 shiftcls 指向了相应结构。
这里根据上述分析以及上图给出几个总结点:
-
每个类都有其对应的元类;
-
一个类的类结构中存储着该类所有实例方法,对象通过 isa 去类结构中获取实例方法实现,若该类结构中没有所需的实例方法,则通过 superclass 指针去父类结构查找,直到 Root class(class)。
-
一个类的元类中存储着该类所有类方法,类对象通过 isa 去元类结构中获取类方法实现,若元类结构中没有所需的类方法,则通过 superclass 指针去父类元类结构查找,直到 Root class(class)。
-
在 Objective-C 中,Root class(class)其实就是 NSObject,NSObject 的 superclass 指向 nil。
-
在 Objective-C 中,所有的对象(包含 instance、class、meta-class)都可以调用 NSObject 的实例方法。
-
在 Objective-C 中,所有的 class 以及 meta-class 都可以调用 NSObject 的类方法。
如果对于元类(meta-class)还不理解,推荐阅读 what is meta class in objective-c?,此文对 meta-class 的解释通俗易懂,建议阅读。
网友评论