1.OC对象的本质探究
我们要想明白OC对象的本质是什么,它的底层是如何实现类这种结构的,首先,我们就需要创建一个类并使用Clang命令将这个文件编译称为.cpp文件,查看并分析底层C++代码。
1.1 编译OC文件
创建一个LGPerson类,类中添加一个NSString类型的MyName属性,在main.m文件中如下所示:
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
@interface LGPerson : NSObject
@property (nonatomic, copy) NSString *MyName;
@end
@implementation LGPerson
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSLog(@"Hello, World!");
}
return 0;
}
使用clang命令将main.m文件编译为.cpp文件,如下图所示:
1.2 OC类结构底层源码分析
我们查看编译好的C++源码文件,发现其中的代码一共有十几万行的代码,那么我们该从那里分析呢?我们只需要在这个文件中搜索创建的类名就可以了,我们就可以发现一个如下的结构体:
struct LGPerson_IMPL {
struct NSObject_IMPL NSObject_IVARS;
NSString *_MyName;
};
我们发现这个结构体中正好有一个与我们创建的同名的NSString指针类型MyName成员变量,那么通过分析我们就可以知道,其实OC对象的本质其实就是结构体,那么另一个成员变量NSObject_IVARS是什么呢?通过搜索NSObject_IMPL关键字我们发现它也是一个结构体,如下所示:
struct NSObject_IMPL {
Class isa;
};
因此我们发现NSObject_IMPL其实是包含了一个成员变量isa的结构体,而LGPerson_IMPL中这样的写法,表示的意思是LGPerson_IMPL中继承了NSObject_IMPL中的成员变量,那么Class又是个什么类型呢?我们在源码中可以找到如下代码:
typedef struct objc_class *Class;
Class实际上是一个objc_class *类型的结构体指针,而objc_class实际上又是一个包含了一个objc_class *类型的成员变量的结构体,源码中代码如下所示:
struct objc_class {
Class _Nonnull isa __attribute__((deprecated));
} __attribute__((unavailable));
另外,在源码文件中又发现了如下的代码:
typedef struct objc_object LGPerson;
struct objc_object {
Class _Nonnull isa __attribute__((deprecated));
};
typedef struct objc_object *id;
分析以上的代码,我们知道LGPerson继承自NSObject,而下沉到C++的底层代码中,其实objc_object就是NSObject的底层实现,而id实际上是一个objc_object结构体类型的指针,这就是为什么我们在开发中使用id指针不需要加*号的原因。
1.3 OC类的属性方法源码分析
探究完LGPerson这个类的结构之后,我们再来看看关于MyName属性的set以及get方法底层是如何实现的,其底层源码实现如下:
static NSString * _I_LGPerson_MyName(LGPerson * self, SEL _cmd) {
return (*(NSString **)((char *)self + OBJC_IVAR_$_LGPerson$_MyName));
}
extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);
static void _I_LGPerson_setMyName_(LGPerson * self, SEL _cmd, NSString *MyName) {
objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct LGPerson, _MyName), (id)MyName, 0, 1);
}
上面的_I_LGPerson_MyName静态函数就是MyName属性的get方法的底层实现,_I_LGPerson_setMyName_静态函数就是MyName属性的set方法的底层实现,可以看到,这两个函数中都有两个隐藏参数(self,cmd),这就是为什么我们可以在OC文代码中类的实例方法中使用self这个对象的原因,在MyName的get方法中,是使用self(LGPerson实例在内存中的地址)加上OBJC_IVAR_$_LGPerson$_MyName(MyName成员变量在结构体中地址的偏移量)获取得到的MyName属性在内存中的地址,进而访问这个属性的值。实际上,在_I_LGPerson_setMyName_方法中也是通过这样的方式为MyName属性重新赋值的。
2. isa关联类
在第一小节详细探讨了OC对象的本质,我们发现OC对象的本质其实是一个结构体,并且其中有一个(objc_class结构体指针类型的)isa指针,那么我们现在就来探究isa指针的数据存储结构,看看isa是如何关联到类的,但是在探究之前,我们首先需要补充一下位域以及共用体的基本知识。
2.1 位域与共用体
2.1.1 位域
想要了解在什么情况下使用位域这种数据结构,我们首先来看如下代码
typedef struct {
bool front;
bool back;
bool left;
bool right;
} Direction;
在上面的结构体Direction中,一共定义了4个bool类型的成员变量,通过sizeof()运算符的计算,我们可以得到Direction这个结构体的大小为4个字节,但是bool类型的数据使用1 bit就可以表示了,如果用4个字节(也就是32 bit)来存储就过于浪费了。
因此以上4个成员变量的信息在存储时,并不需要占用一个完整的字节,而只需占用一个二进制位。为了节省存储空间,并使处理简便,C语言又提供了一种数据结构,称为"位域"或"位段"。而所谓"位域"是把一个字节中的二进位划分为几个不同的区域,并说明每个区域的位数。每个域有一个域名,允许在程序中按域名进行操作。因此我们可以使用位域将每个成员变量占用的空间为设置为1 bit来节省空间,如下所示:
typedef struct {
bool front : 1;
bool back : 1;
bool left : 1;
bool right : 1;
} Direction;
这样通过sizeof()运算符的计算,Direction这个结构体的大小就变为了1个字节大小。
2.1.2 共用体(联合体)
共用体是一种特殊的数据类型,允许您在相同的内存位置存储不同的数据类型。您可以定义一个带有多成员的共用体,但是任何时候只能有一个成员带有值。共用体提供了一种使用相同的内存位置的有效方式。
定义共用体方式与定义结构类似,假设定义一个有三个类型不同属性的Person共用体,代码如下:
typedef union {
int age;
double height;
char name[20];
} Person;
Person类型的变量可以存储一个整数、或者一个浮点数、或者一个字符串。这意味着一个变量(相同的内存位置)可以存储多个多种类型的数据。您可以根据需要在一个共用体内使用任何内置的或者用户自定义的数据类型。
共用体占用的内存应足够存储共用体中占用内存最大的成员。例如,在上面的实例中,Person 将占用20个字节的内存空间,因为在各个成员中,成员变量name所占用的空间是最大的。
2.1.2 共用体(联合体)与结构体的比较
优点:
- 结构体:结构体(
struct)中所有变量是“共存”的——优点是“有容乃⼤”。 - 共用体:联合体(
union)中是各变量是“互斥”的——优点是内存使⽤更为精细灵活,也节省了内存空间。
缺点:
- 结构体:结构体(
struct)内存空间的分配是粗放的,缺点是不管⽤不⽤,全分配。 - 共用体:联合体(
union)中是各变量是互斥的,缺点是“不够包容”
2.2 isa指针结构探究
前几篇文章我们已经对OC对象在初始化的过程中两个函数(计算内存大小)instanceSize以及(分配内存空间)calloc函数进行了详细的探究,接下来,就该对isa指针的初始化函数进行探究了,首先来看看isa初始化函数的调用流程,首先,打上断点,运行程序,发现调用了如下函数:
initInstanceIsa函数的代码如下:
inline void
objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
ASSERT(!cls->instancesRequireRawIsa());
ASSERT(hasCxxDtor == cls->hasCxxDtor());
initIsa(cls, true, hasCxxDtor);
}
这个函数中实际上调用了initIsa函数,这个函数的代码如下所示:
inline void
objc_object::initIsa(Class cls, bool nonpointer, UNUSED_WITHOUT_INDEXED_ISA_AND_DTOR_BIT bool hasCxxDtor)
{
ASSERT(!isTaggedPointer());

isa_t newisa(0);
if (!nonpointer) {
newisa.setClass(cls, this);
} else {
ASSERT(!DisableNonpointerIsa);
ASSERT(!cls->instancesRequireRawIsa());
#if SUPPORT_INDEXED_ISA
ASSERT(cls->classArrayIndex() > 0);
newisa.bits = ISA_INDEX_MAGIC_VALUE;
// isa.magic is part of ISA_MAGIC_VALUE
// isa.nonpointer is part of ISA_MAGIC_VALUE
newisa.has_cxx_dtor = hasCxxDtor;
newisa.indexcls = (uintptr_t)cls->classArrayIndex();
#else
newisa.bits = ISA_MAGIC_VALUE;
// isa.magic is part of ISA_MAGIC_VALUE
// isa.nonpointer is part of ISA_MAGIC_VALUE
# if ISA_HAS_CXX_DTOR_BIT
newisa.has_cxx_dtor = hasCxxDtor;
# endif
newisa.setClass(cls, this);
#endif
newisa.extra_rc = 1;
}
// This write must be performed in a single store in some cases
// (for example when realizing a class because other threads
// may simultaneously try to use the class).
// fixme use atomics here to guarantee single-store and to
// guarantee memory order w.r.t. the class index table
// ...but not too atomic because we don't want to hurt instantiation
isa = newisa;
}
首先,我们发现这个函数代码最后是将newisa赋值给了isa,而这个isa实际上是一个联合体isa_t类型,因此我们再来看看这个联合体类型的isa_t是如何定义的,其代码如下:
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_t这个联合体中一共有两个成员变量分别是uintptr_t(其实就是unsigned long)类型的bits以及一个Class(其实就是 objc_class *)类型的指针cls,这两个成员变量在64位操作系统中占用空间字节长度都是8字节,而如果系统支持包装ISA指针,在arm64架构中代码中struct中成员变量的定义如下:
# 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
内存结构图示如下:
在x86_64架构中代码中struct中成员变量的定义如下:
# 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,不开启指针优化,仅存储类对象地址;值为1,不仅包含了类对象地址,也包含了类信息、对象引用计数、是否析构等。存储在第0个进制位。
has_assoc:表示关联对象标志位。值为0,没关联;值为1,关联。储存在第1个进制位。
has_cxx_dtor:表示该对象是否有析构函数,值为0,表示没有析构函数,可以更快是否对象;值为1,表示有析构函数。储存在第2个进制位。
shiftcls:表示类指针的值。在x86_64架构中存储在第3到第46位,在arm64架构中占用存储在第3到第35位。
magic:用于调式器判断当前对象是真的对象还是没有初始化的空间。在x86_64架构中存储在第47到第52位;在arm64架构中存储在第36位到第41位。
weakly_referenced:标志对象是否被指向或者曾经指向一个ARC的弱变量,没有弱引用的对象可以更快释放。在x86_64架构中存储在第53位,在arm64架构中存储在第42位。
unused:标志对象是否正在释放内存。在x86_64架构中存储在第54位,在arm64架构中存储在第43位。
has_sidetable_rc:表示当对象引用计数大于10时,则需要借用该变量存储进位。在x86_64架构中存储在第55位,在arm64架构中存储在第44位。
extra_rc:当表示该对象的引用计数值减1之后的值。例如:如果对象的引用计数为8,那么extra_rc值为7,如果引用计数大于10,则需要使用到has_sidetable_rc。在x86_64架构中存储在第56到第63位,在arm64架构中存储在第45到第63位。