先上结论:OC
对象的本质是结构体,结构体内部是一个指针;
![](https://i-blog.csdnimg.cn/blog_migrate/073f2903de9b3605f03ceaaaa666766e.png)
1. 如何探究
通常我们编写的OC
代码,其底层都是 C/C++
代码,所以 OC
对象都会转换成 C/C++
的某种数据结构。
那么我们可以通过clang,将相关的OC
文件编译成C/C++
文件:
clang -rewrite-objc main.m -o main.cpp
2. 源码分析
main.m文件源码:
@interface Student : NSObject
@property (nonatomic, copy) NSString *name;
@end
@implementation Student
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
Student *s = [[Student alloc] init];
s.name = @"Tom";
NSLog(@"%@", s.name);
}
return 0;
}
打开clang
编译好的文件,搜索我们自定的类Student
,不难发现我们的Student
类最终被编译成了一个叫objc_object
结构体,而Student_IMPL
则保存着一些相关的类信息,比如isa
指针,成员变量name
等。
typedef struct objc_object Student;
struct Student_IMPL {
struct NSObject_IMPL NSObject_IVARS;
NSString *_name;
}
往下翻翻,我们还能发现成员变量name
的set
方法和get
方法,还有一些隐藏参数self
、SEL
、_cmd
:
static NSString * _I_Student_name(Student * self, SEL _cmd) {
return (*(NSString **)((char *)self + OBJC_IVAR_$_Student$_name));
}
static void _I_Student_setName_(Student * self, SEL _cmd, NSString *name) {
objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct Student, _name), (id)name, 0, 1);
}
以及还能发现struct _protocol_t
, struct _category_t
等等一些信息。
那么成员变量NSObject_IVARS
代表什么呢? 其实对应的就是我们的isa指针。经阅读相关的文章得知:
在早期的32位系统中isa就是一个单一的指针,用于存储当前对象的类或者类的元类。 但是在64位操作系统上,用一个8字节指针的长度只存储一个对象地址显然是浪费的(操作系统只有一部分地址是可用于存储对象地址的空间),所以apple对这个isa指针进行了优化。
typedef struct objc_object NSObject;
struct NSObject_IMPL {
Class isa;
}
而Class
就是结构体objc_class
的指针。
typedef struct objc_class *Class;
同时还发现
typedef struct objc_object *id;
这样就解释的清:用id
定义变量时,为何不加*
。所以id
可以指向任何一个合法对象。
3. isa_t分析
通过我的前几篇文章,类cls
和isa
进行绑定是通过obj->initInstanceIsa(cls, hasCxxDtor)
来实现的,那我们来看下initIsa函数的底层实现:
inline void
objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
ASSERT(!cls->instancesRequireRawIsa());
ASSERT(hasCxxDtor == cls->hasCxxDtor());
initIsa(cls, true, hasCxxDtor);
}
objc_object::initIsa(Class cls, bool nonpointer, UNUSED_WITHOUT_INDEXED_ISA_AND_DTOR_BIT bool hasCxxDtor)
{
...
isa_t newisa(0);
if (!nonpointer) {
...
} else {
...
#if SUPPORT_INDEXED_ISA
ASSERT(cls->classArrayIndex() > 0);
newisa.bits = ISA_INDEX_MAGIC_VALUE;
newisa.has_cxx_dtor = hasCxxDtor;
newisa.indexcls = (uintptr_t)cls->classArrayIndex();
#else
newisa.bits = ISA_MAGIC_VALUE;
# if ISA_HAS_CXX_DTOR_BIT
newisa.has_cxx_dtor = hasCxxDtor;
# endif
newisa.setClass(cls, this);
#endif
newisa.extra_rc = 1;
}
...
}
那么isa_t
是个啥玩意呢?点进去查看我们发现是一个联合体
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; // defined in isa.h
};
...
#endif
...
};
上文中提到过,apple优化了64位系统下isa指针所占的空间,那么是如何优化的呢?增加了uintptr_t(unsigned long)
类型的变量bits
,由于使用的是联合体+位域,所以该结构只占用一个指针的空间。当使用bits
变量进行存储时,利用位域结构将变量的各个位进行拆分赋予不同的含义,充分利用了内存空间。关于结构体、联合体与位域的解释可以查看这篇文章,我们再这里不过多讨论。
这样利用位域使得变量内不仅仅保存了指针值,同时还保存了很多有用的信息。
# 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_MASK 0x007ffffffffffff8ULL
# define ISA_MAGIC_MASK 0x0000000000000001ULL
# define ISA_MAGIC_VALUE 0x0000000000000001ULL
# define ISA_HAS_CXX_DTOR_BIT 0
# 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
# define RC_ONE (1ULL<<56)
# define RC_HALF (1ULL<<7)
# else
# define ISA_MASK 0x0000000ffffffff8ULL
# define ISA_MAGIC_MASK 0x000003f000000001ULL
# define ISA_MAGIC_VALUE 0x000001a000000001ULL
# define ISA_HAS_CXX_DTOR_BIT 1
# 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
# define RC_ONE (1ULL<<45)
# define RC_HALF (1ULL<<18)
# endif
# elif __x86_64__
# define ISA_MASK 0x00007ffffffffff8ULL
# define ISA_MAGIC_MASK 0x001f800000000001ULL
# define ISA_MAGIC_VALUE 0x001d800000000001ULL
# define ISA_HAS_CXX_DTOR_BIT 1
# 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
# define RC_ONE (1ULL<<56)
# define RC_HALF (1ULL<<7)
# else
# error unknown architecture for packed isa
# endif
以主流的arm64为例,
nonpointer
占用1bit
,标识是否开启isa
优化.如果是一个指针值该位为0,则表示当前结构的值只是一个指针没有保存其他信息;如果为1,则表示当前结构不是指针,而是一个包含了其他信息的位域结构has_assoc
当前对象是否使用objc_setAssociatedObject
动态绑定了额外的属性has_cxx_dtor
是否含有C++
或者OC
的析构函数,不包含析构函数时对象释放速度会更快shiftcls
这个值相当于早期实现中的isa
指针,是真实的指针值,在arm64
处理器上只占据33位,可见其实在内存中可以用来存储对象指针的空间是很有限的magic
用于判断对象是否已经完成了初始化,在arm64
中0x16
是调试器判断当前对象是真的对象还是没有初始化的空间(在x86_64
中该值为0x3b
)weakly_referenced
是否是弱引用对象deallocating
对象是否正在执行析构函数(是否在释放内存)has_sidetable_rc
判断是否需要用sidetable
去处理引用计数extra_rc
存储该对象的引用计数值减一后的结果. 当对象的引用计数使用extra_rc
足以存储时unused
对象是否被释放has_sidetable_rc=0
当对象的引用计数使用extra_rc
不能存储时has_sidetable_rc=1
.可见对象的引用计数主要存储在两个地方:如果isa
中extra_rc
足以存储则存储在isa
的位域中;如果isa
位域不足以存储,就会使用sidetable
去存储。
根据ISA_MASK
还原类信息还未研究透彻,请等待后续更新