前面我们探索过NSObject
的alloc
方法及内存对齐
的规则,我们本文主要探索OC
对象的本质。
查找对象的定义 提到对象的本质,我们最先想到的就是看源码,那怎么定位源码中对象的定义的内容呢。感觉无从下手,那我们就先定义一个对象初始化一下,利用clang
命令,将OC
代码转换成c++
代码找一下线索。我们先定义一个类JSPerson
,然后初始化对象:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @interface JSPerson : NSObject @property (nonatomic, strong) NSString *firstName; @property (nonatomic, strong) NSString *lastName; @end @implementation JSPerson @end #import "JSPerson.h" int main(int argc, const char * argv[]) { @autoreleasepool { JSPerson *person = [[JSPerson alloc] init]; person.firstName = @"Jason"; person.lastName = @"Test"; NSLog(@"%@",person.firstName); NSLog(@"%@",person.firstName); } return 0; }
cd
到main.m
文件的目录,我们使用clang
命令将main.m
文件转换成c++
代码: clang -rewrite-objc main.m -o main.cpp
在目录里生成了main.cpp
文件。我们在main.cpp
文件中搜索JSPerson
,搜查查找到了下面这段代码:
1 2 3 4 5 6 7 typedef struct objc_object JSPerson ;struct JSPerson_IMPL { struct NSObject_IMPL NSObject_IVARS ; NSString *_firstName; NSString *_lastName; };
这个结构体JSPerson_IMPL
里包含了我们定义的两个属性_firstName
、_lastName
,说明对象的本质是objc-object
类型的结构体
,我们发现`
1 2 3 4 5 6 7 typedef struct objc_object JSPerson; ///省略代码 struct JSPerson_IMPL { struct NSObject_IMPL NSObject_IVARS; NSString *_firstName; NSString *_lastName; };
这个结构体JSPerson_IMPL
里包含了我们定义的两个属性_firstName
、_lastName
,说明对象的本质是objc-object
类型的结构体
,我们发现`
1 2 3 4 5 6 7 typedef struct objc_object JSPerson; ///省略代码 struct JSPerson_IMPL { struct NSObject_IMPL NSObject_IVARS; NSString *_firstName; NSString *_lastName; };
这个结构体JSPerson_IMPL
里包含了我们定义的两个属性_firstName
、_lastName
,说明对象的本质是objc-object
类型的结构体
,我们发现JSPerson_IMPL
第一个属性是NSObject_IVARS
是什么呢,它并不是我们定义的属性,我们搜索它的类型NSObject_IMPL
发现:
1 2 3 struct NSObject_IMPL { Class isa; };
显而易见NSObject_IVARS
就是isa
指针。我们接下来开始探索isa
,在探索isa
指针之前,我们先看一个概念位域
位域 我们先定义两个结构体JSCar1
和JSCar2
,我们在main
方法里打印一下两个结构体的大小
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 struct JSCar1 { BOOL front; BOOL back; BOOL left; BOOL right; }; struct JSCar2 { BOOL front: 1; BOOL back : 1; BOOL left : 1; BOOL right: 1; }; int main(int argc, const char * argv[]) { @autoreleasepool { struct JSCar1 car1; struct JSCar2 car2; NSLog(@"%ld-%ld",sizeof(car1),sizeof(car2)); }
发现打印的结果是
为什么结构体的属性个数和类型都相同,而结构体占用的内存的大小不一样呢,这里就是位域
的作用,在结构体属性后面加上:1
表示这个属性只占用一位(注意这里不是字节,一个字节是8位),所以car2
的四个属性只占用了4位,但是内存分配最少是一个字节(8位),所以car2的内存大小是1个字节。示例图如下:
可以看出使用位域
我们可以节省内存(4B->1B)。
联合体 除了在结构体里使用位域
,我们使用联合体
也能达到节省作用:
1 2 3 4 5 6 7 8 9 10 union JSCar3 { BOOL front; BOOL back; BOOL left; BOOL right; }; struct JSCar1 car1; struct JSCar2 car2; union JSCar3 car3; NSLog(@"%ld-%ld-%ld",sizeof(car1),sizeof(car2),sizeof(car3));
我们定义一个联合体JSCar3
,然后打印其大小:
可以看到car3
联合体的内存大小也是1
,我们很容易猜想car2
和car3
的内存大小相同,那内存存储结构是相同的吗?带着这个疑问我们继续探索,在图中箭头位置使用lldb
打印相应结构体:
下面是打印的结果:
1 2 3 4 5 6 7 8 9 10 11 12 (lldb) p car2 (JSCar2) $0 = (front = NO, back = NO, left = NO, right = NO) (lldb) p car2 (JSCar2) $1 = (front = 255, back = NO, left = NO, right = NO) (lldb) p car2 (JSCar2) $2 = (front = 255, back = 255, left = NO, right = NO) (lldb) p car3 (JSCar3) $3 = (front = NO, back = NO, left = NO, right = NO) (lldb) p car3 (JSCar3) $4 = (front = YES, back = YES, left = YES, right = YES) (lldb) p car3 (JSCar3) $5 = (front = YES, back = YES, left = YES, right = YES)
从打印结果我们发现,car2
一个属性的赋值并不会影响其他属性,而car3
一个属性的值变化了,其他属性的值也会变化,说明car2
和car3
的内存存储结构是不一样的,结构体
各成员变量是共存的,联合体
各成员变量是互斥的,一般联合体
和位域
配合使用。
联合体
的内存规则如下
联合体中可以定义多个成员,联合体的大小由最大的成员大小决定。
联合体的成员公用一个内存,一次只能使用一个成员。
对某一个成员赋值,会覆盖其他成员的值。
isa
指针有了上面知识的铺垫,我们继续研究isa
指针。我们打开objc
的源码,找到isa
指针的定义:
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 27 union isa_t { isa_t () { } isa_t (uintptr_t value) : bits(value) { } uintptr_t bits; private : Class cls; public :#if defined(ISA_BITFIELD) struct { ISA_BITFIELD; }; bool isDeallocating () { return extra_rc == 0 && has_sidetable_rc == 0 ; } void setDeallocating () { extra_rc = 0 ; has_sidetable_rc = 0 ; } #endif void setClass (Class cls, objc_object *obj) ; Class getClass (bool authenticated) ; Class getDecodedClass (bool authenticated) ; };
通过源码可以看到isa
是一个联合体
,它有一个位域
成员ISA_BITFIELD
,我们继续看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 27 28 29 30 31 32 33 34 # if __arm64__ # if __has_feature(ptrauth_calls) || TARGET_OS_SIMULATOR # define ISA_BITFIELD \ uintptr_t nonpointer : 1 ; \ uintptr_t has_assoc : 1 ; \ uintptr_t weakly_referenced : 1 ; \ uintptr_t shiftcls_and_sig : 52 ; \ uintptr_t has_sidetable_rc : 1 ; \ uintptr_t extra_rc : 8 # else # define ISA_BITFIELD \ uintptr_t nonpointer : 1 ; \ uintptr_t has_assoc : 1 ; \ uintptr_t has_cxx_dtor : 1 ; \ uintptr_t shiftcls : 33 ; \ uintptr_t magic : 6 ; \ uintptr_t weakly_referenced : 1 ; \ uintptr_t unused : 1 ; \ uintptr_t has_sidetable_rc : 1 ; \ uintptr_t extra_rc : 19 # elif __x86_64__ # define ISA_BITFIELD \ uintptr_t nonpointer : 1 ; \ uintptr_t has_assoc : 1 ; \ uintptr_t has_cxx_dtor : 1 ; \ uintptr_t shiftcls : 44 ; \ uintptr_t magic : 6 ; \ uintptr_t weakly_referenced : 1 ; \ uintptr_t unused : 1 ; \ uintptr_t has_sidetable_rc : 1 ; \ uintptr_t extra_rc : 8
这些位代表的含义分别是:
nonpointer :表示是否对 isa 指针开启指针优化 0 :纯isa 指针,1 :不止是类对象地址,isa 中包含了类信息、对象的引用计数等
has_assoc :关联对象标志位,0 没有,1 存在
has_cxx_dtor :该对象是否有 C++ 或者 Objc 的析构器, 如果有析构函数, 则需要做析构逻辑, 如果没有, 则可以更快的释放对象
shiftcls: 存储类指针的值。开启指针优化的情况下,在 arm64 架构中有 33 位用来存储类指针。
magic :用于调试器判断当前对象是真的对象还是没有初始化的空间
weakly_referenced :志对象是否被指向或者曾经指向一个 ARC 的弱变量,没有弱引用的对象可以更快释放。
deallocating :标志对象是否正在释放内存
has_sidetable_rc :当对象引用技术大于 10 时,则需要借用该变量存储进位
extra_rc :当表示该对象的引用计数值,实际上是引用计数值减 1 , 例如,如果对象的引用计数为 10 ,那么 extra_rc 为 9 。如果引用计数大于 10 , 则需要使用到下面的 has_sidetable_rc 。
可以看到x86
平台下,isa
指针的第4-48存储的是类的地址,下面我们通过位运算验证一下我们的理解是否正确,我们在main
方法初始化JSPerson
的实例:
1 2 3 4 5 6 7 int main (int argc, const char * argv[]) { @autoreleasepool { JSPerson *p = [JSPerson alloc]; NSLog(@"%@" ,p); } return 0 ; }
我们在注释位置打断点,使用lldb
命令,打印p
的地址:
1 2 3 4 5 6 7 8 9 10 11 (lldb) x/4 gx p 0x1005186b0 : 0x001d800100008309 0x0000000000000000 0x1005186c0 : 0x63756f54534e5b2d 0x746e6f4372614268 (lldb) p/x 0x001d800100008309 >> 3 (long ) $2 = 0x0003b00020001061 (lldb) p/x 0x0003b00020001061 << 20 (long ) $3 = 0x0002000106100000 (lldb) p/x 0x0002000106100000 >> 17 (long ) $4 = 0x0000000100008308 (lldb) p/x JSPerson.class (Class) $5 = 0x0000000100008308 JSPerson
通过上面的位运算,我们可以确定shiftcls
存储的就是JSPerson
类的地址。
在我们实际开发中我们定义的对象的isa
基本都是nonpointer
,好处不言而喻可以增加内存的利用率,减少内存浪费。对象的本质就先探索到这里,后续文章我们继续探索。