前面我们探索过NSObject
的alloc
方法及内存对齐
的规则,我们本文主要探索OC
对象的本质。
查找对象的定义
提到对象的本质,我们最先想到的就是看源码,那怎么定位源码中对象的定义的内容呢。感觉无从下手,那我们就先定义一个对象初始化一下,利用clang
命令,将OC
代码转换成c++
代码找一下线索。我们先定义一个类JSPerson
,然后初始化对象:
@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
,搜查查找到了下面这段代码:
typedef struct objc_object JSPerson;
///省略代码
struct JSPerson_IMPL {
struct NSObject_IMPL NSObject_IVARS;
NSString *_firstName;
NSString *_lastName;
};
这个结构体JSPerson_IMPL
里包含了我们定义的两个属性_firstName
、_lastName
,说明对象的本质是objc-object
类型的结构体
,我们发现`
typedef struct objc_object JSPerson;
///省略代码
struct JSPerson_IMPL {
struct NSObject_IMPL NSObject_IVARS;
NSString *_firstName;
NSString *_lastName;
};
这个结构体JSPerson_IMPL
里包含了我们定义的两个属性_firstName
、_lastName
,说明对象的本质是objc-object
类型的结构体
,我们发现`
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
发现:
struct NSObject_IMPL {
Class isa;
};
显而易见NSObject_IVARS
就是isa
指针。我们接下来开始探索isa
,在探索isa
指针之前,我们先看一个概念位域
位域
我们先定义两个结构体JSCar1
和JSCar2
,我们在main
方法里打印一下两个结构体的大小
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));
}
发现打印的结果是
4-1
为什么结构体的属性个数和类型都相同,而结构体占用的内存的大小不一样呢,这里就是位域
的作用,在结构体属性后面加上:1
表示这个属性只占用一位(注意这里不是字节,一个字节是8位),所以car2
的四个属性只占用了4位,但是内存分配最少是一个字节(8位),所以car2的内存大小是1个字节。示例图如下:
可以看出使用位域
我们可以节省内存(4B->1B)。
联合体
除了在结构体里使用位域
,我们使用联合体
也能达到节省作用:
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
,然后打印其大小:
4-1-1
可以看到car3
联合体的内存大小也是1
,我们很容易猜想car2
和car3
的内存大小相同,那内存存储结构是相同的吗?带着这个疑问我们继续探索,在图中箭头位置使用lldb
打印相应结构体:
下面是打印的结果:
(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
指针的定义:
union isa_t {
//构造方法
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
uintptr_t bits;
private:
// Accessing the class requires custom ptrauth operations, so
// force clients to go through setClass/getClass by making this
// private.
Class cls;
public:
#if defined(ISA_BITFIELD)
struct {
ISA_BITFIELD; // defined in isa.h
};
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
的定义:
# if __arm64__
// ARM64 simulators have a larger address space, so use the ARM64e
// scheme even when simulators build for ARM64-not-e.
# 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; /*MACH_VM_MAX_ADDRESS 0x1000000000*/ \
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; /*MACH_VM_MAX_ADDRESS 0x7fffffe00000*/ \
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
的实例:
int main(int argc, const char * argv[]) {
@autoreleasepool {
JSPerson *p = [JSPerson alloc];//打断点
NSLog(@"%@",p);
}
return 0;
}
我们在注释位置打断点,使用lldb
命令,打印p
的地址:
(lldb) x/4gx p
0x1005186b0: 0x001d800100008309 0x0000000000000000
0x1005186c0: 0x63756f54534e5b2d 0x746e6f4372614268
(lldb) p/x 0x001d800100008309 >> 3 //isa指针地址 右移3位
(long) $2 = 0x0003b00020001061
(lldb) p/x 0x0003b00020001061 << 20//左移 20位(因为要补上一步右移的三位)
(long) $3 = 0x0002000106100000
(lldb) p/x 0x0002000106100000 >> 17//右移 17位
(long) $4 = 0x0000000100008308 // 通过位运算 shiftcls的值
(lldb) p/x JSPerson.class
(Class) $5 = 0x0000000100008308 JSPerson // 直接打印 JSPerson类的地址
通过上面的位运算,我们可以确定shiftcls
存储的就是JSPerson
类的地址。
在我们实际开发中我们定义的对象的isa
基本都是nonpointer
,好处不言而喻可以增加内存的利用率,减少内存浪费。对象的本质就先探索到这里,后续文章我们继续探索。