先上结论:OC对象的本质是结构体,结构体内部是一个指针;
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还原类信息还未研究透彻,请等待后续更新