【iOS】类与对象底层探索
类与对象底层探索
- Clang
- 探索对象本质
- `objc_setProperty`源码探索
- cls与类的关联原理
- isa的类型isa_t
- 原理探索
- 类&类的结构
- 什么是元类
- NSObject到底有几个
- isa走位&继承关系图
- objc_class&objc_object
- 类结构分析
- 计算cache类中的内存大小
- 获取bits
- 属性列表(property_list)&& 方法列表(methods_list)
- rw、ro、rwe之间的关系
- 参考文章
Clang
我们在探索OC对象的本质之前,先来了解一个编译器:Clang
。我们通过终端将main.m
文件输出为main.cpp
,这样的目的是为了更好的观察底层的一些结构和实现逻辑,方便了解底层。
//1、将 main.m 编译成 main.cpp
clang -rewrite-objc main.m -o main.cpp//2、将 ViewController.m 编译成 ViewController.cpp
clang -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
//3、模拟器文件编译
- xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp //4、真机文件编译
- xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main- arm64.cpp
探索对象本质
打开编译好的main.cpp
,找到LGPerson
的定义,可以发现其在底层被编译成了struct
结构体。
-
LGPerson_IMPL
中的第一个属性 其实就是isa
,是继承自NSObject
,属于伪继承
,伪继承的方式
是直接将NSObject
结构体定义为LGPerson
中的第一个属性
,意味着LGPerson
拥有NSObject
中的所有成员变量
-
LGPerson
中的第一个属性
NSObject_IVARS等效于
NSObject中的
isa
struct LGPerson_IMPL {struct NSObject_IMPL NSObject_IVARS;//相当于Class isaNSString *_name;
};
这里提出一个问题:为什么
isa
的类型是class
?这是为了适配 Objective - C 的对象模型、消息传递机制以及类层次结构的管理,让对象能够正确地找到所属的类并调用相应的方法。
总结:
- OC对象的本质其实就是结构体
LGPerson
中的isa
是继承自NSObject
中的isa
objc_setProperty
源码探索
除了LGPerson
的底层定义,我们可以发现属性name对应的set
和get
方法,其中set方法的实现依赖的就是runtime中的objc_setProperty
。
static NSString * _I_LGPerson_name(LGPerson * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_LGPerson$_name)); }//gei方法
extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);
//set方法
static void _I_LGPerson_setName_(LGPerson * self, SEL _cmd, NSString *name) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct LGPerson, _name), (id)name, 0, 1); }
下面我们来一步步了解其底层实现:
进入reallySetProperty
源码实现,这里的原理为:新值retain
旧值release
:
static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{if (offset == 0) {object_setClass(self, newValue);//设置isa指向return;}//通过检查偏移量,说明要设置的是对象的类信息,设置完成后直接返回,无需执行后续代码id oldValue;//用于存储属性的旧值id *slot = (id*) ((char*)self + offset);//计算属性在对象内存中的地址,使用指针slot指向属性的存储位置、if (copy) {newValue = [newValue copyWithZone:nil];//如果copy为true,执行不可变赋值操作} else if (mutableCopy) {newValue = [newValue mutableCopyWithZone:nil];//如果mutableCopy为true,执行可变赋值操作} else {if (*slot == newValue) return;newValue = objc_retain(newValue);//新增retain}//当不复制时调用,检查指针是否等于newValue,相等直接返回;否则,调用objc_retain增加newValue的引用计数,保证设置新值的时候不会被提前释放// 如果不是原子操作,直接更新内存位置的值if (!atomic) {oldValue = *slot;*slot = newValue;//设置新值} else {// 如果是原子操作,使用锁来保证线程安全spinlock_t& slotlock = PropertyLocks[slot];//获取自旋锁slotlockslotlock.lock();//锁定自旋锁,更新属性值时不会有其他线程同时访问该属性oldValue = *slot;//将*slot复制给oldValue*slot = newValue;//设置新值 slotlock.unlock();//解锁自旋锁}objc_release(oldValue);//release旧值
}
自旋锁:一种用于多线程编程的同步机制,用于保护共享资源,防止多个线程同时访问和修改这些资源而导致的数据不一致或其他并发问题。
总结:
在这段底层探索中,这里需要进行几点说明:
objc_setProperty
方法的目的适用于关联 上层的set方法 以及 底层的set方法,其本质就是一个接口- 这么设计的原因是,上层的set方法有很多,如果直接调用底层set方法中,会产生很多的临时变量,当你想查找一个sel时,会非常麻烦
- 基于上述原因,苹果采用了适配器设计模式(即将底层接口适配为客户端需要的接口),对外提供一个接口,供上层的set方法使用,对内调用底层的set方法,使其相互不受影响,即无论上层怎么变,下层都是不变的,或者下层的变化也无法影响上层,主要是达到上下层接口隔离的目的
cls与类的关联原理
这里我们来了解一下initInstanceIsa
如何将cls和isa关联在一起的。
isa的类型isa_t
这里先给出isa指针的类型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
通常来说,isa指针占用的内存大小为8字节,即64位
由于这里使用的是联合体,由于联合体的定义我们可以得知,这里的cls和bits这两个成员是互斥的,所以当初始化isa指针的时候,有两种初始化方法:
- 通过cls初始化,bits无默认值
- 通过bits初始化,cls有默认值
这里还提供了一个结构体定义的位域,用于存储类信息以及其他信息,结构体成员ISA_BITFIELD
这是一个宏定义,有两个版本分别对应ios移动端以及maxOS,如下所示:
# if __arm64__//对应ios移动端
// 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__//对应macOS
# 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
下面讲解一下其中的内容表示:
- nonpointer`表示自定义类,占1位:
- 0:纯isa指针。
- 1:不只是类对象地址,isa包含了类信息,对象的引用计数。
has_assoc
表示关联对象标志,占1位:- 0:没有关联对象。
- 1:存在关联对象
has_cxx_dtor
表示该对象是否有C++/OC的析构器,占1位:- 如果有析构函数,就需要做析构逻辑
- 如果没有,就可以更快的释放对象
shiftcls
表示存储类的指针类型,即类对象- arm64中占33位,开启指针优化的情况下,在arm64架构中有33位用来存储类指针
- x86_64中占44位
magic
用于调试器判断当前对象是真的对象还是没有初始化的空间,占6位weakly_refrenced
是指对象是否被指向或者曾经指向一个ARC的弱变量(没有弱引用的对象可以更快释放)deallocating
标志对象是是否正在释放内存has_sidetable_rc
表示当对象引用计数大于10时,则需要借用该变量存储进位extra_rc
表示该对象的引用计数值,实际上是引用计数值减1。- 如果对象的引用计数为10,那么extra_rc为9(这个仅为举例说明),实际上iPhone 真机上的 extra_rc 是使用 19位来存储引用计数的
下面展示两种不同的平台中,isa存储情况:
原理探索
通过alloc --> _objc_rootAlloc --> callAlloc --> _objc_rootAllocWithZone --> _class_createInstanceFromZone
方法路径,查找到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_ISAASSERT(cls->classArrayIndex() > 0);newisa.bits = ISA_INDEX_MAGIC_VALUE;// isa.magic is part of ISA_MAGIC_VALUE// isa.nonpointer is part of ISA_MAGIC_VALUEnewisa.has_cxx_dtor = hasCxxDtor;newisa.indexcls = (uintptr_t)cls->classArrayIndex();
#elsenewisa.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_BITnewisa.has_cxx_dtor = hasCxxDtor;
# endifnewisa.setClass(cls, this);
#endifnewisa.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 instantiationisa = newisa;
}
这里的逻辑主要为:
- 通过isa初始化isa
- 通过bits初始化isa
验证isa与类的关联:
- 通过
initIsa
方法中的通过initIsa
方法中的newisa.shiftcls = (uintptr_t)cls >> 3;
验证 - 通过isa指针地址与
ISA_MSAK
的值 & 来验证 - 通过runtime的方法
object_getClass
验证 - 通过位运算验证
类&类的结构
什么是元类
这里先展示一张调试的图片:
这里我们需要注意一个问题:为何图中的p/x 0x00000001000081e8 & 0x00007ffffffffff8ULL
以及p/x 0x00000001000081c0 & 0x00007ffffffffff8ULL
中的类信息打印的结果都是CJLPerson?
- 0x00000001000081e8是perosn对象的isa指针地址,他&后得到的结果是创建person的类CJLPerson。
- 而0x00000001000081c0是isa中获取的类信息所指的类的isa的指针地址,就是CJLPerson类的类的isa指针地址,在Apple中我们将其简称为元类。
- 故而,我们可以了解到两个打印都是CJLPerson的根本原因是元类导致的。
在 Objective - C 的运行时环境里,0x00007ffffffffff8ULL 常作为一个掩码用于提取对象的 isa 指针中的类信息。
元类的说明:
- 对象的isa是指向类,但是其实类也是一个对象叫做类对象,他的isa的位域只想Apple定义的元类
- 元类是系统给的,他的定义和创建都是由编译器完成,在该过程中,类的归属来自元类。
- 元类是类对象的类,每个类都有独一无二的元类来存储同类名的相关信息
- 元类本身自己是没有名称的,但是由于与类相关联,故而使用同类名一样的名称
这里我们继续通过lldb命令来探索元类的走向,即isa的走位,这里我们可以得出一个关系链:对象 – 类 – 元类 – NSObject – NSObject
总结:
- 对象的isa指向类
- 类的isa指向元类
- 元类的isa指向根元类,即NSObject
- 根元类的isa指向其自己
NSObject到底有几个
我们从上面的图片中可以看出,NSObject
类的元类也是NSObject
,与上面的CJLPerson
中的根元类的元类是同一个,这里我们得出结论为:内存中只存在一份根元类NSObject,根元类的元类是指向它自己的,在内存中永远只存在一份。
isa走位&继承关系图
对象、类、元类、根元类的关系示意图:
isa走位:
- 实例对象的isa指向类
- 类对象的isa指向元类
- 元类的isa指向根元类
- 根元类的isa指向他自己本身,形成闭环,根元类即为NSObject。
superclass走位
- 类继承自父类
- 父类继承自根类,此时根类指NSObject
- 根类继承与nil,即根类(NSObject)可以理解为无中生有。
元类继承关系基本如上,在最后一条中,根元类继承与根类,这时的根类指NSObject。
实例对象之间没与继承关系,类之间有继承关系
objc_class&objc_object
这里我们先来理解一下对象和类为什么都有isa属性呢?这里我们需要注意的两个结构体类型为:objc_class
以及objc_object
。
NSObject
的底层编译是NSObject_IMPL
结构体:其中Class是isa指针的类型,是由objc_class
定义的类型;而objc_class
是一个结构体,在iOS中,所有的Class都是以objc_class为模版创建的。
struct NSObject_IMPL {Class isa;
};typedef struct objc_class* Class;
下面展示一下objc_class
以及objc_object
在源码中的代码:
struct objc_object {Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
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 vtableclass_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属性。 - 在main.cpp底层编译文件中,
NSObject
中的isa属性在底层是由Class定义的,其中class的底层编码来自objc_class
类型,所以NSObect
拥有isa属性。 NSObject
是一个类,用它初始化一个实例对象objc
,objc
满足objc_object
的特性(即有isa属性),主要是因为isa
是由NSObject
从objc_class
继承过来的,而objc_class
继承自objc_object
,objc_object
有isa
属性。所以对象都有一个isa
,isa
表示指向,来自于当前的objc_object
。objc_object
是当前的类对象,所有的对象都有这样一个特性,故拥有isa属性。
总结:
- 所有的对象、类、元类都有isa属性
- 所有的对象都是由
objc_object
继承来的 - 所以我们可以概括为所有以
objc_object
为模版创建的对象都有isa属性,所有以objc_class
为模版创建的对象都有isa属性。万物皆对象 - 在结构体层面来说,我们可以理解为是上层OC和底层的对接:
- 下层是通过结构体定义的模版,例如
objc_class
、objc_object
。 - 上层是通过底层的模版创建的一些类型,例如
CJLPerson
。
- 下层是通过结构体定义的模版,例如
类结构分析
在这个部分中,我们来探索一下类信息中都有哪些内容,来帮助我们更好的理解该部分的内容。
这里我们先来认识一下objc_class
这个结构体:
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 vtableclass_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
//后面这里不做展示
isa
: 主要指向类对象或是元类对象
superclass
:指向当前类的父类
cache
:方法缓存,提高调用方法的性能
bits
:封装了类的其他信息,例如成员变量、方法列表、协议、属性。
元类对象结构亦是如此,只不过元类对象中存放的是类方法。
计算cache类中的内存大小
struct cache_t {
private:explicit_atomic<uintptr_t> _bucketsAndMaybeMask;union {struct {explicit_atomic<mask_t> _maybeMask;
#if __LP64__uint16_t _flags;
#endifuint16_t _occupied;};explicit_atomic<preopt_cache_t *> _originalPreoptCache;};
上面我们先展示cache_t
的定义,这段代码的核心功能是管理方法缓存,通过联合体实现了两种缓存模式的切换,节省内存并提高了灵活性。
从上面代码中我们可以看出,在LP64位的一个情况下是只有16个字节,这里我们展示一下cache_t
的结构:
获取bits
我们可以现在objc4的源码中获取到CJLperson类对象的首地址,由于bits举例objc_class
的首地址还需要偏移32字节,将这个bits强转成class_data_bits_t
:
这里我们faxing去处了bits中的值,我们可以通过看class_data_bits_t
的声明,发现里面确实有bits成员:
struct class_data_bits_t {friend objc_class;// Values are the FAST_ flags above.uintptr_t bits;
但是这里的bits并不是我们想要的内容,我们继续向后看class_data_bits_t
的源码,可以看到两个方法:data()
和safe_ro()
方法,这两个方法一个返回class_rw_t
,另一个返回class_ro_t
;在64位架构CPU下,bits 的第3到第46字节存储 class_rw_t
。class_rw_t
中存储flags
、witness
、firstSubclass
、nextSiblingClass
以及class_rw_ext_t
。
下面我们直接探索属性列表和方法列表
属性列表(property_list)&& 方法列表(methods_list)
我们查看class_rw_t
定义的源码可以发现,结构体中有提供相应的方法去获取属性列表、方法列表:
const method_array_t methods() const {auto v = get_ro_or_rwe();if (v.is<class_rw_ext_t *>()) {return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->methods;} else {return method_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseMethods};}}const property_array_t properties() const {auto v = get_ro_or_rwe();if (v.is<class_rw_ext_t *>()) {return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->properties;} else {return property_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseProperties};}}const protocol_array_t protocols() const {auto v = get_ro_or_rwe();if (v.is<class_rw_ext_t *>()) {return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->protocols;} else {return protocol_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseProtocols};}}
这里注意一个内容,这里的存储在class_rw_t这个类是存储一个属性的,并不存储一个成员变量成员变量是存储在另一个类中:class_ro_t的这里我们对比看一下这两个类的区别:
struct class_ro_t {uint32_t flags;uint32_t instanceStart;uint32_t instanceSize;
#ifdef __LP64__uint32_t reserved;
#endifunion {const uint8_t * ivarLayout;Class nonMetaclass;};explicit_atomic<const char *> name;WrappedPtr<method_list_t, method_list_t::Ptrauth> baseMethods;protocol_list_t * baseProtocols;const ivar_list_t * ivars;const uint8_t * weakIvarLayout;property_list_t *baseProperties;// This field exists only when RO_HAS_SWIFT_INITIALIZER is set._objc_swiftMetadataInitializer __ptrauth_objc_method_list_imp _swiftMetadataInitializer_NEVER_USE[0];_objc_swiftMetadataInitializer swiftMetadataInitializer() const {if (flags & RO_HAS_SWIFT_INITIALIZER) {return _swiftMetadataInitializer_NEVER_USE[0];} else {return nil;}}const char *getName() const {return name.load(std::memory_order_acquire);}class_ro_t *duplicate() const {
这里有一个成员变量列表也就是我们这里的ivars,这个列表包含的内容不仅仅包含一个成员变量列表,除了包括在{}中定义的一个成员变量,还包括通过属性定义的成员变量.bits --> data() -->ro() --> ivars
通过这个流程来获取成员变量表.
通过@property定义的属性,也会存储在bits属性中,通过bits --> data() --> properties() --> list
获取属性列表,其中只包含属性
rw、ro、rwe之间的关系
rw里面有methods、properties、protocols和ro;
ro里面有ivars、baseMethods、baseProtocols、baseProperties
ro
:在类第一次从磁盘被加载到内存中产生,它是一块纯净的内存clear memory(只读的),它保存了类最纯净的成员变量、实例方法、协议、属性等等。当进程内存不够时候,ro可以被移除,在需要的时候再去磁盘中加载,从而节省更多内存。
rw
:程序运行就必须一直存在,在进程运行时类一经使用后,runtime就会把ro写入新的数据结构dirty memory(读写的),这个数据结构存储了只有在运行时才会生成的新信息。(例如创建一个新的方法缓存并从类中指向它)。故而所有类都会链接成一个树状结构,这是通过First Subclass和Next Sibling Class指针实现的,这就决定了runtime能够遍历当前使用的所有类。
类的方法和属性保存在ro中了,为什么还要在rw中有呢?
因为它们可以在运行时被修改,当category被加载时,它可以向类中添加新的方法,也可以通过Runtime API去动态添加和修改,因为ro是只读的,所以需要再rw去跟踪这些东西。
rwe
:是在category被加载或者通过 Runtime API 对类的方法属性协议等等进行修改后产生的,它保存了原本rw中类可能会被修改的东西(Methods / Properties / Protocols / Demangled Name),它的作用是给rw瘦身。(rwe是类被修改才会产生,没有被修改的类不会产生。注意:category被加载时候产生的rwe的条件是:分类和本类是必须是非懒加载类(重写+load))
参考文章
Objective-C 类的底层探索
iOS-底层原理 08:类 & 类结构分析
iOS-底层原理 07:isa与类关联的原理
OC底层探索(五) 类的结构分析
类结构中的class_rw_t与class_ro_t
OC底层探索(四) ISA的结构与类的关联、ISA走位分析