前言
前文主要对对象的原理进行了分析,本文将结合源码探究OC类的内存结构。
isa的走位
isa的走位可见经典图,可结合object_getClass, class_getSuperclass
函数自行验证一下。
这里引入了一个概念——元类metaClass
,对象的isa
指向类,类的isa
指向元类,那么元类的作用是什么?
我个人理解是为了存放类方法,OC中调用对象方法总的是消息发送机制,如下代码,调用对象obj
的func
函数其实就是向对象obj
的class
发送消息。而类方法我们知道是直接通过类进行调用,那么自然需要有一个类的类,即metaclass
来存放类方法。一些具体过程可见:metaclass作用——属性,类方法与实例方法
[obj func]; --> (void (*)()objc_msgSend)([obj class], @selector(func));
类的结构体
OC底层探索——对象原理之ISA中曾追踪到,OC类的源码层结构体即objc_class
,结构如下:
struct objc_class : objc_object {
...
// isa_t isa; 继承自objc_object
// 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
...
}
struct objc_object {
private:
isa_t isa;
...
}
这里使用的源码版本是objc4-818.2
,有一些老版本乃至objc2
的类结构还在广为流传,大家要注意留意区别。
lldb调试验证
定义一对继承的类,直接读取子类的对应内存信息,发现确实符合上面源码的前两个成员变量isa
和superclass
。
cache_t
cache_t
在后面消息发送时会有更详细的讲解,本文主要以了解其结构和大小。
源码定义
struct cache_t {
private:
explicit_atomic<uintptr_t> _bucketsAndMaybeMask;
union {
struct {
explicit_atomic<mask_t> _maybeMask;
#if __LP64__
uint16_t _flags;
#endif
uint16_t _occupied;
};
explicit_atomic<preopt_cache_t *> _originalPreoptCache;
};
}
#if __LP64__
typedef uint32_t mask_t; // x86_64 & arm64 asm are less efficient with 16-bits
#else
typedef uint16_t mask_t;
#endif
cache_t的结构体大小
由源码可见,cache_t
主要包含一个uintptr_t
与一个联合体,联合体内是一个大小为8字节的结构体和一个指针,所以联合体大小为8字节,综上cache_t
的大小为16字节。
_bucketsAndMaybeMask
_bucketsAndMaybeMask,顾名思义,既是bucket_t
的指针,也是一个掩码,两者合用8字节的内存大小。其中对于内存的使用规则在不同架构下有不同的规范,对于arm64
&LP64
架构对应的标准CACHE_MASK_STORAGE_HIGH_16
是我们重点关注的。
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
// _bucketsAndMaybeMask is a buckets_t pointer in the low 48 bits
// _maybeMask is unused, the mask is stored in the top 16 bits.
// How much the mask is shifted by.
static constexpr uintptr_t maskShift = 48;
// Additional bits after the mask which must be zero. msgSend
// takes advantage of these additional bits to construct the value
// `mask << 4` from `_maskAndBuckets` in a single instruction.
static constexpr uintptr_t maskZeroBits = 4;
// The largest mask value we can store.
static constexpr uintptr_t maxMask = ((uintptr_t)1 << (64 - maskShift)) - 1;
// The mask applied to `_maskAndBuckets` to retrieve the buckets pointer.
static constexpr uintptr_t bucketsMask = ((uintptr_t)1 << (maskShift - maskZeroBits)) - 1;
// Ensure we have enough bits for the buckets pointer.
static_assert(bucketsMask >= MACH_VM_MAX_ADDRESS,
"Bucket field doesn't have enough bits for arbitrary pointers.");
如上图定义,此时低48位存储bucket_t
的指针,高16位存储mask
。
bucket_t
bucket_t
的源码定义如下:
struct bucket_t {
private:
// IMP-first is better for arm64e ptrauth and no worse for arm64.
// SEL-first is better for armv7* and i386 and x86_64.
#if __arm64__
explicit_atomic<uintptr_t> _imp;
explicit_atomic<SEL> _sel;
#else
explicit_atomic<SEL> _sel;
explicit_atomic<uintptr_t> _imp;
#endif
...
}
bucket_t
中存储了类对象的方法编号_sel
,和方法指针_imp
。
lldb调试验证
-
打印出
Foo
类的地址,偏移16位(前16位是isa
和superclass
)即cache_t
的地址。 -
打印
cache_t
-
打印
bucket_t *
,寻找其中是否含有[Foo test]
函数的缓存。
综上,cache_t
的内存结构通过lldb调试成功验证。
class_data_bits_t
源码分析
首先直接看class_data_bits_t
的定义,其只有一个成员变量bits
,可我们期望找的是方法,属性等定义,我们继续看一下其内部的方法,关注到两个关键词class_rw_t
和class_ro_t
,通过自己看定义或网上搜索这两个类,我们可以发现其就是用来存放类的属性、方法等信息的结构体。
struct class_data_bits_t {
friend objc_class;
// Values are the FAST_ flags above.
uintptr_t bits;
public:
// !!重点--获取class_rw_t!!
class_rw_t* data() const {
return (class_rw_t *)(bits & FAST_DATA_MASK);
}
...
// !!重点--获取class_ro_t!!
const class_ro_t *safe_ro() const {
class_rw_t *maybe_rw = data();
if (maybe_rw->flags & RW_REALIZED) {
// maybe_rw is rw
return maybe_rw->ro();
} else {
// maybe_rw is actually ro
return (class_ro_t *)maybe_rw;
}
}
class_ro_t
ro
代表只读readonly,其中存储了当前类在编译期就已经确定的成员变量、属性、方法以及遵循的协议等信息。这一点从下面源码定义中也可以较清晰的看出。
struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize;
#ifdef __LP64__
uint32_t reserved;
#endif
union {
const uint8_t * ivarLayout;
Class nonMetaclass;
};
explicit_atomic<const char *> name;
void *baseMethodList;
protocol_list_t * baseProtocols;
const ivar_list_t * ivars;
const uint8_t * weakIvarLayout;
property_list_t *baseProperties;
};
lldb验证ivar
示例代码
@interface Father : NSObject
{
int ivarInt;
NSString *ivarString;
}
@property (nonatomic, assign) int age;
- (void)instanceFunc;
+ (void)classFunc;
@end
从lldb中逐步获取class_ro_t
中的成员变量如下。因为我们上节知道cache_t
占16字节,所以我们可以直接计算出class_data_bits_t
的内存地址。
这里补充一个知识点,每个成员变量还专门存取了一个type
,OC中变量类型的符号编码可见:
Type Encodings
class_rw_t
rw
代表可读可写readwrite,其存储了类中的属性、方法还有遵循的协议等信息。其内部包含class_ro_t
的指针,可以理解为类的动态信息结构体。当一个类第一次被使用时,运行时会为其分配额外的内存,即class_rw_t
。
2020WWDC有相关优化可见:developer.apple.com/videos/play…
源码中定义如下,先过滤出了重点关注的内容。
struct class_rw_t {
// Be warned that Symbolication knows the layout of this structure.
uint32_t flags;
uint16_t witness;
#if SUPPORT_INDEXED_ISA
uint16_t index;
#endif
// 只读或可读可写的指针(原子类型)
explicit_atomic<uintptr_t> ro_or_rw_ext;
Class firstSubclass; // 首个子类
Class nextSiblingClass; // 下一个兄弟类
...
public:
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};
}
}
};
从源码定义中,我们看到了三个关键方法methods(), properties(), protocols()
,结合其实现发现class_rw_t
中并没有直接存储(旧版runtime是直接存储)属性、方法、协议等信息,而是用指针ro_or_rw_ext
指向了这些信息的地址。
我们点开其中property_array_t
的定义往下稍微找一下,能找到属性的最下层定义property_t
。
// 结构体组的封装定义
class property_array_t :
public list_array_tt<property_t, property_list_t, RawPtr>
{
typedef list_array_tt<property_t, property_list_t, RawPtr> Super;
public:
property_array_t() : Super() { }
property_array_t(property_list_t *l) : Super(l) { }
};
// 属性的结构体定义
struct property_t {
const char *name;
const char *attributes;
};
lldb验证property
实例代码
@interface Father : NSObject
{
NSString *instanceName;
}
@property (nonatomic, assign) int age;
@property (nonatomic, strong) NSString *name1;
@property (nonatomic, strong) NSString *name2;
- (void)test;
+ (void)test0;
@end
下图展示了lldb中一步一步获取类的属性的过程。
lldb验证method
实例代码
@interface Father : NSObject
- (void)instanceFunc;
+ (void)classFunc;
@end
下图展示了lldb中逐步获取类的方法的过程。
从上图发现Father
类只有两个方法,类方法classFunc
并没有在这里,回顾上文,可以知道是因为我们查找的是Father
的类信息而不是其metaclass
。
由图可见类方法确实存在了类对应的元类中。
ro_or_rw_ext
回过头来看class_rw_t
中最关键的变量无疑是explicit_atomic<uintptr_t> ro_or_rw_ext;
,首先它无疑是个指针类型,其次它故名思义,有可能是两种类型(分别是class_ro_t
和class_rw_ext_t
)的指针。从下面源码也可见其定义。
// 自定义的指针联合体
using ro_or_rw_ext_t = objc::PointerUnion<const class_ro_t, class_rw_ext_t, PTRAUTH_STR("class_ro_t"), PTRAUTH_STR("class_rw_ext_t")>;
const ro_or_rw_ext_t get_ro_or_rwe() const {
return ro_or_rw_ext_t{ro_or_rw_ext};
}
const property_array_t properties() const {
// 将ro_or_rw_ext类型转为联合指针ro_or_rw_ext_t
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};
}
}
class_rw_ext_t
struct class_rw_ext_t {
DECLARE_AUTHED_PTR_TEMPLATE(class_ro_t)
class_ro_t_authed_ptr<const class_ro_t> ro;
method_array_t methods;
property_array_t properties;
protocol_array_t protocols;
char *demangledName; // 解构的名字
uint32_t version;
};
class_rw_ext_t
的结构定义还是比较直观的,但我们有一个疑问,为什么这些变量不直接存储在class_rw_t
中,而要专门定一个更小的类型?
答案是为了节省内存,只有在类的属性、方法等发生改变时,才会初始化class_rw_ext_t
存储变化后的属性、方法等;否则直接访问class_ro_t
来查找属性、方法等,有点懒加载的味道。更多可见:WWDC20中objc增加的结构class_rw_ext_t