【iOS】源码学习-类与对象底层原理
OC对象本质
探索对象本质
clang是一个由Apple主导编写,基于LLVM的C/C++/OC的编译器。主要用于底层编译,将一些文件输出成C++文件,其目的是为了更好地观察底层呢的一些结构及实现的逻辑,方便理解底层原理。
常用的几个跑编译器代码的OC命令:
- 基础命令:将 main.m 编译成 main.cpp:
bashclang -rewrite-objc main.m -o main.cpp
- 完整SDK命令:将 ViewController.m 编译成 ViewController.cpp:
bashclang -rewrite-objc -fobjc-arc -fobjc-runtime=ios-13.0.0 -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator13.7.sdk ViewController.m以下两种方式是通过指定架构模式的命令行,使用xcode工具 xcrun:
- 模拟器文件编译:自动匹配当前Xcode模拟器SDK
bashxcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp
- 真机文件编译:编译成真机arm64架构的C++代码
bashxcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main- arm64.cpp
通过一个实例来哦看一下:
objc
#import <Foundation/Foundation.h>
@interface LYDPerson : NSObject
@property (nonatomic, strong) NSString *name;
@end
@implementation LYDPerson
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
}
return 0;
}
clang编译一下,我们可以看到:
cpp
// NSObject的底层编译
struct NSObject_IMPL {
Class isa;
};
// LYDPerson的底层编译
struct LYDPerson_IMPL {
struct NSObject_IMPL NSObject_IVARS;
NSString *_name;
};
我们发现isa的类型是Class,这是因为我们定义alloc的核心方法initInstance的源码中初始化isa指针是通过isa_t类型初始化的。
为了让开发人员更加清晰明确,需要在isa返回的时候做一个类型强制转换。
总结一下:
- OC对象的本质就是结构体。
- LYDPerson中的isa继承自NSObject中的isa。
- isa的作用是让对象在运行时找到自己对应的类,从而继续查找方法、属性、缓存等信息。也就是OC对象的本质是一个以结构体像是存在,并通过isa与类建立联系的运行时对象。
探索objc_setProperty源码
OC对象的本质清楚后,类中还有属性的setter和getter方法。
cpp
// getter方法
static NSString * _I_LYDPerson_name(LYDPerson * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_LYDPerson$_name)); }
// setter方法
static void _I_LYDPerson_setName_(LYDPerson * self, SEL _cmd, NSString *name) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct LYDPerson, _name), (id)name, 1, 1); }
我们可以看到setter方法依赖于一个objc_setProperty方法:
objc
// self:当前对象实例
// SEL _cmd:当前调用的选择器,即setter方法
// ptrdiff_t offset:实例变量在对象内存中的偏移量,是为了直接通过指针运算找到内存地址,而不是通过字符串名字查找、效率更高
// newValue:要设置的新值
// atomic:表示该属性是否声明为atomic
// shouldCopy:标志位,指示是否需要复制值。可能为0(NO),1(YES/COPY),2(MUTABLE_COPY)
void objc_setProperty(id self, SEL _cmd, ptrdiff_t offset, id newValue, BOOL atomic, signed char shouldCopy)
{
bool copy = (shouldCopy && shouldCopy != MUTABLE_COPY);
bool mutableCopy = (shouldCopy == MUTABLE_COPY);
reallySetProperty(self, _cmd, newValue, offset, atomic, copy, mutableCopy);
}
objc
static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{
// 在OC对象内存布局中,偏移量为0的位置对应的是对象的isa指针,指向类的原数据。如果偏移量为0说明不是在设置普通实例变量,而是在动态修改对象的类(修改类不涉及常规的retain/release逻辑,类对象本身就是单例且常驻内存)
if (offset == 0) {
// 修改isa指针
object_setClass(self, newValue);
return;
}
id oldValue;
// 计算对象内部存储该属性的具体内存地址即实例变量的地址
id *slot = (id*) ((char*)self + offset);
// 决定怎么保存
if (copy) {
newValue = [newValue copyWithZone:nil];
} else if (mutableCopy) {
newValue = [newValue mutableCopyWithZone:nil];
} else {
if (*slot == newValue) return;
// 如果发现当前内存槽位的值和新值是同一个对象指针,那么直接返回。这里的判断非常重要,如果没有,后续release的旧值再retain新值会导致对象被提前释放
newValue = objc_retain(newValue); // 新值retain
}
// 决定要不要加锁
if (!atomic) {
oldValue = *slot;
*slot = newValue; // 设置新值
} else {
// 给atomic属性赋值时加锁,防止多个线程同时改同一个属性
// 顺序为加锁->取出旧值->把属性改成新值->解锁
spinlock_t& slotlock = PropertyLocks.get()[slot];
slotlock.lock();
oldValue = *slot;
*slot = newValue; // 设置新值
slotlock.unlock();
}
objc_release(oldValue); // release旧值
}
objc_setProperty方法本质是就是一个接口,适用于关联上层的set方法以及底层的set方法。这么设计的原因是:上层的set方法有很多,如果直接把底层的reallySetProperty()方法结合到每一个set方法中,会带来很大开销。因此iOS采用了适配器设计模式(即把底层接口适配为客户端需要的接口),对外提供一个接口,供上层set方法使用,对内调用底层的set方法,使其相互不受影响,达到上下层接口隔离的目的。

cls与类的关联原理
笔者总是混淆几个概念,再次明确一下:
- obj:对象地址
- isa:对象内部的指针指向cls类对象
- cls:类对象地址
探索isa_t类型
isa指针的类型是isa_t,它是通过一个联合体定义的。
- 结构体:把不同的数据组合成一个整体,其变量是共存的,变量不管是否使用,都会分配内存。存储容量大,包容性强,成员之间不会相互影响,但比较浪费内存。
- 联合体(共用体):由不同数据类型组成吧,变量互相排斥,所有成员共占一段内存。其采用内存覆盖技术,同一时刻只能保存一个成员的值,如果对新的成员赋值,会将原来成员的值覆盖掉,因此修改一个成员会影响其余所有成员。相比结构体,包容性弱,但节省内存空间。
结构体内存>=所有成员内存总和,联合体内存等于最大成员占用内存。
objc
#include "isa.h"
union isa_t {
// 默认的一个构造函数
isa_t() { }
// 用value初始化isa的原始bits
isa_t(uintptr_t value) : bits(value) { }
// isa联合体的第一个成员,是isa的原始二进制数据
uintptr_t bits;
private:
// Accessing the class requires custom ptrauth operations, so // 访问class指针需要特殊的ptrauth(pointer authentication,指针认证)操作
// force clients to go through setClass/getClass by making this // 强制外部调用者通过setClass/getClass来访问class
// private.
// isa联合体的第二个成员,是一个指向类对象的指针
Class cls;
public:
// 条件编译:定义了ISA_BITFIELD宏,下面代码才能参与编译
#if defined(ISA_BITFIELD)
// isa联合体的第三个成员,把isa拆成多个位域字段
struct {
// isa的位域定义,不同平台、结构的isa位域可能不同,通过宏来适配
ISA_BITFIELD; // defined in isa.h
};
#if ISA_HAS_INLINE_RC
// 判断对象是否处于正在释放状态
bool isDeallocating() const {
// extra_rc == 0:表示isa内联引用计数为0
// has_sidetable_rc == 0:表示引用计数没有溢出到SideTable,或者SideTable里没有额外引用计数标记
// SideTable:runtime给对象准备的一张外挂信息表,存储isa空间里放不下的信息
// 主要存:1. 溢出的引用计数 2. weak 弱引用表 3. 一些对象额外管理信息
return extra_rc == 0 && has_sidetable_rc == 0;
}
// 标记对象进入销毁状态
void setDeallocating() {
extra_rc = 0;
has_sidetable_rc = 0;
}
#endif // ISA_HAS_INLINE_RC
#endif
// 用来设置当前isa里面保存的类对象信息,可能会对cls进行编码、签名或写入bits位域。这里只是函数声明,没有函数实现。
// cls:要写入isa中的类对象指针
// obj:当前isa所属的对象
void setClass(Class cls, objc_object *obj);
// authenticated:表示取出Class时是否需要进行ptrauth指针认证
// 用来从当前isa中取出对象所属的类对象Class
Class getClass(bool authenticated) const;
// 用来从isa的原始bits中解码出(Decoded表示"解码后的")真正的类对象Class,返回从isa bits中经过位移、掩码、解码、认证后得到的类对象指针
Class getDecodedClass(bool authenticated) const;
};
从isa_t的定义中可以看出:
- 提供了两个成员,cls和bits共用一块内存,彼此互斥,意味着初始化isa指针有两种初始化方式:
- 通过cls初始化
- 通过bits初始化
- 提供了一个结构体定义的位域,用于存储类信息及其他信息。结构体成员ISA_BITFIELD有两个版本:
__arm64__(对应ios 移动端) 和__x86_64__(对应macOS)。
__x86_64__(对应macOS):
objc
# define ISA_BITFIELD
uintptr_t nonpointer : 1; // 是否对isa指针开启指针优化,0表示纯isa指针,1表示不只是类对象
uintptr_t has_assoc : 1; // 是否有关联对象,0表示没有关联对象,1表示存在关联对象
uintptr_t has_cxx_dtor : 1;
// 在isa位域里,用一个bit记录这个对象所属的类是否需要执行C++析构逻辑/OC析构相关清理(类似于dealloc)
// 0表示没有复杂析构逻辑,可以更快释放,1表示这个对象销毁时不能直接释放内存,还要先走析构清理逻辑
// 析构清理逻辑:对象真正释放内存之前,要先把内部持有的资源清理干净
uintptr_t shiftcls : 44; /*MACH_VM_MAX_ADDRESS 0x7fffffe00000*/ // 存储类的指针的值
uintptr_t magic : 6; // 调试器判断对象是真对象还是未初始化空间
uintptr_t weakly_referenced : 1; // 对象是否被指向或者曾指向一个ARC的弱变量,没有弱引用可以更快释放
uintptr_t unused : 1; // 未被使用
uintptr_t has_sidetable_rc : 1; // 表示当对象引用计数大于10时,则需要借用该变量存储进位
uintptr_t extra_rc : 8 // 额外的引用计数,表示该对象的引用计数值,值为实际引用计数值减1
__arm64__(对应ios 移动端):
objc
# 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

探索cls与isa关联原理
通过alloc调用方法实现路径,查找到初始化isa指针源码:
objc
inline void
objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
// 断言这个类的实例对象不要求使用raw isa,即是优化后的isa,isa里面不只存了类地址,还有引用计数、弱引用标记、关联对象标记等信息
ASSERT(!cls->instancesRequireRawIsa());
// 检查这个类是否有C++析构逻辑
ASSERT(hasCxxDtor == cls->hasCxxDtor());
initIsa(cls, true, hasCxxDtor);
}
objc
inline void
objc_object::initIsa(Class cls, bool nonpointer, UNUSED_WITHOUT_INDEXED_ISA_AND_DTOR_BIT bool hasCxxDtor)
{
// 断言当前对象不是Tagged Pointer
// Tagged Pointer:是一种特殊对象,比如某些小数字、小字符串,系统不会真的分配对象内存,而是直接把值塞进指针里。这种对象没有正常的内存结构,不能像普通对象一样写isa。
ASSERT(!isTaggedPointer());
// 创建新的isa_t变量并初始化为0
isa_t newisa(0);
if (!nonpointer) {
// 把类对象cls设置进newisa
newisa.setClass(cls, this);
} else {
// 确保支持优化isa指针
ASSERT(!DisableNonpointerIsa);
ASSERT(!cls->instancesRequireRawIsa());
// indexed isa:不直接存完整类地址,而是存一个类索引
#if SUPPORT_INDEXED_ISA
// 断言这个类在数组里的索引必须大于0
ASSERT(cls->classArrayIndex() > 0);
// 先把indexed isa的基本模版写进去
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;
// 把类的索引存到isa里
newisa.indexcls = (uintptr_t)cls->classArrayIndex();
#else
// 不支持indexed isa的设置普通基础值
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
#if ISA_HAS_INLINE_RC
newisa.extra_rc = 1;
#endif
}
// 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;
}
从以上方法我们可以看出:
- initIsa初始化isa主要分为两个部分:
- 通过cls初始化isa
- 通过bits初始化isa
- 初始化isa分两条路线:
- raw isa:isa里只存类对象地址
- nonpointer isa:优化后isa,这种isa里不仅存类信息,还存其他信息(eg:has_cxx_dtor、extra_rc等)
isa有几下几种:
- raw isa:类对象Class object、协议对象Protocol object、要求raw isa的特殊实例对象、禁用nonpointer isa的环境。
- nonpointer isa:普通实例对象
- 没有普通 isa 结构:Tagged Pointer对象
- indexed nonpointer isa:indexed isa环境下的普通实例对象
- cls与isa关联原理就是isa指针中shiftcls位域中存储了类信息,initInstanceIsa的作用是给calloc出来的对象内存初始化isa,把这块裸内存和当前类cls建立关联,真正完成isa写入的是initIsa,其中具体写入类信息的操作在setClass方法中完成。
验证原理
我们进一步验证一下,主要有以下几种验证方式:
- 通过shiftcls赋值
(uintptr_t)newCls >> 3验证

我们发现shiftcls存储了当前类的信息。
shiftcls赋值的逻辑是将类进行编码后右移3位,由于指针是8位对齐的,所以后三位一定是000,因此腾出位置存储信息。
通过initIsa中创建isa这里打断点也能看出cls指向了我们的一个类。

这里shiftcls出现强制转换的原因是:内存的存储不能存储字符串,机器码只能识别0、1两种数字,因此需要将其转换为uintptr_t数据类型,这样shiftcls中存储的类信息才能被机器码理解。在64位机器上,intptr_t和uintptr_t分别是long int、unsigned long int的别名;在32位的机器上,intptr_t和uintptr_t分别是int、unsigned int的别名。
- 通过isa&ISA_MASK验证
将isa指针的地址&ISA_MASK,得出当前类信息。

- 通过object_getClass验证
objc
OBJC_EXPORT Class _Nullable
object_getClass(id _Nullable obj)
OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
objc
Class object_getClass(id obj)
{
if (obj) return obj->getIsa();
else return Nil;
}
objc
inline Class
objc_object::getIsa() const
{
if (fastpath(!isTaggedPointer())) return ISA(/*authenticated*/true);
extern objc_class OBJC_CLASS_$___NSUnrecognizedTaggedPointer;
uintptr_t slot, ptr = (uintptr_t)this;
Class cls;
slot = (ptr >> _OBJC_TAG_SLOT_SHIFT) & _OBJC_TAG_SLOT_MASK;
cls = objc_tag_classes[slot];
if (slowpath(cls == (Class)&OBJC_CLASS_$___NSUnrecognizedTaggedPointer)) {
slot = (ptr >> _OBJC_TAG_EXT_SLOT_SHIFT) & _OBJC_TAG_EXT_SLOT_MASK;
cls = objc_tag_ext_classes[slot];
}
return cls;
}
通过getIsa()源码,拿到isa的bits这个位域,再&ISA_MASK,获得当前类信息检验。
- 通过位运算验证
shiftcls只有44位的大小,这44位存储了一个类信息,需要经过位运算,手动通过位运算找到shiftcls对应地址输出cls地址对比。

类与类结构的底层分析
类的分析
我们主要探索一下isa的走向和继承关系。
我们通过一段示例调试实例对象的isa、类对象的isa、元类对象的isa以及superclass继承链来分析:
objc
@interface LYDPerson : NSObject {
NSString *hobby;
}
@property (nonatomic, copy) NSString *name;
- (void)sayHello;
- (void)sayBye;
@end
@implementation LYDPerson
- (void)sayHello {
}
- (void)sayBye {
}
@end
objc
@interface LYDTeather : LYDPerson
@end
@implementation LYDTeather
@end

这里0x0000000100008228是person对象的isa指针地址,其&后得到的是创建person的类LYDPerson;0x0000000100008200是isa中获取的类信息所指的类的isa的指针地址,即LYDPerson类的类的isa指针地址,在苹果中我们把一个类的类叫做元类。因此打印信息相同,都为LYDPerson。
元类
什么是元类:
- 类其实也是一个对象,可以称为类对象,对象的isa指向类,类isa的位域指向苹果定义的元类。
- 元类是系统给的,其定义和创建都是由编译器完成的,在这个过程中,类的归属来自于元类。
- 元类是类对象的类,每个类都有一个独一无二的元类来存储类方法的相关信息。
- 元类本身是没有名称的,由于与类相关联,所以使用了同类名一样的名称。
我们通过lldb调试来探索一下元类的走向:
- person的isa指向LYDPerson(类):

- LYDPerson(类)的isa指向元类:

- 元类的isa指向NSObject(根元类):

- NSObject(根元类)的isa指向自己:

从图中的走向我们可以得到一个关系链:对象->类->元类->NSObject,NSObject指向自身。
NSObject永远只有一个,我们发现打印的NSObject的元类地址和LYDPerson元类地址其实是同一个。因此内存中只存在一份根元类NSObject ,且根元类指向它自己。而由于类的信息在内存中只有一份,因此类对象也只有一份。
isa继承关系

从图中我们可以得到isa和superclass的走位:
- isa走位:
- 实例对象(Instance of Subclass)的isa指向类(class)
- 类对象的isa指向元类(Meta class)
- 元类的isa指向根元类(Root metal class)
- 根元类(NSObject)的isa指向它自己形成闭环
- superclass(继承关系)走位:
- 类之间的继承关系:
- 类(Subclass)继承自父类(Superclass)
- 父类继承自根类(Rootclass),此时的根类是指NSObject
- 根类继承自nil,即根类为无中生有的万物起源
- 元类之间的继承关系:
- 子类的元类(metal Subclass)继承自父类的元类(metal Superclass)
- 父类的元类继承自根元类(root Metalclass)
- 根元类继承自根类(Rootclass),此时的根类是指NSObject
- 类之间的继承关系:
实例对象之间没有继承关系,类之间有继承关系。
我们把刚刚的示例替换到isa继承关系图中更直观地看一下:

objc_class & objc_object
isa走位清楚后,我们需要知道对象和类都有isa属性的原因。
之前我们看过NSObject clang编译后是一个结构体。其中含有一个Class类型的isa指针,而Class是由结构体objc_class定义的类型。
cpp
struct NSObject_IMPL {
Class isa;
};
typedef struct objc_class *Class;
我们具体看一下这两个结构体:
objc
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
objc
struct objc_class : objc_object {
objc_class(const objc_class&) = delete;
objc_class(objc_class&&) = delete;
void operator=(const objc_class&) = delete;
void operator=(objc_class&&) = delete;
// Class ISA;
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
//...
从源码中我们可以看出:
- objc_class与objc_object的关系:
- 结构体objc_class继承自结构体objc_object,其中objc_object有一个isa属性,所以objc_class也拥有了isa属性。
- NSObject中的isa在底层是由Class定义的,其中Class的底层编码来自objc_class类型,所以NSObject也拥有了isa属性。
- NSObject是一个类,用它初始化一个实例对象objc,objc满足objc_object的特性,即有isa属性。这是因为isa是由NSObject从objc_class继承过来的,而objc_class继承自objc_object,objc_object又有isa属性。因此每个对象都有一个isa,isa表示来自于当前的objc_object。
- objc_object与对象的关系:
所有的对象都是从objc_object继承过来的。因为所有的对象都是来源于NSObject,NSObject到底层就是objc_object的结构体
总结一下:
所有OC实例对象、类对象、元类对象都有 isa。实例对象底层可以抽象为objc_object,类对象和元类对象底层都由 objc_class 描述,而objc_class又继承 objc_object,所以类对象和元类对象也拥有 isa。
而在结构层面可以理解为下层通过结构体定义模版,上层通过底层模版创建类型,从而实现上层OC与底层的对接。

类结构分析
objc
struct objc_class : objc_object {
objc_class(const objc_class&) = delete;
objc_class(objc_class&&) = delete;
void operator=(const objc_class&) = delete;
void operator=(objc_class&&) = delete;
// Class ISA;
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
//...
objc_class的结构体里有以下几个属性:
- isa:继承自objc_object的isa,占8字节。
- superclass:Class类型,Class是由objc_object定义的,是一个指针,占8字节。
- cache:方法缓存,提高方法性能,加速objc_msgSend查找方法。class_data_bits_t是一个结构体,内存大小根据内部成员确定。
- bits:只有首地址经过上面三个属性的内存大小总和的平移才能获得bits。
总结
这里简单了解下类结构,iOS类的结构分析笔者将单独写一篇博客总结。