Preparation
源码
文中可能有很多展示runtime中结构体的代码,基本上都没有展示完整代码,而是只放了对理解和解释有必要的部分。有兴趣的可以去下载源码看看哈~
探索工具clang
clang是一个C语言、C++、OC语言的,基于LLVM的轻量级编译器。
# 把目标文件编译成C++文件
clang -rewrite-objc main.m -o main.cpp
# xcrun命令在clang的基础上进行了一些封装
## 模拟器
xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main.m -o main.cpp
## 真机
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp
struct&union的内存对齐
- 数据成员对齐规则:结构体struct/联合union的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员存储的起始位置要从该成员大小/该成员的子成员大小(只要该成员有子成员,如数组,结构体等)的整数倍开始。e.g. int为4字节,则要从4的整数倍地址开始存储。
- 结构体作为成员:如果一个结构中有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储。e.g. struct A中存有struct B,B中有char,int,double等元素,那么B应该从8的整数倍开始存储
- 结构体的总大小(
sizeof),必须是其内部最大成员的整数倍,不足的要补齐。
探索流程
首先我们有这样一个类
@interface YKPerson : NSObject
@property (nonatomic,strong) NSString *name;
- (void)testFunc;
@end
@implementation YKPerson
- (void)testFunc {
NSLog(@"testFunc");
}
@end
通过以下clang指令可以生成的iphoneos26.0架构的cpp文件
clang -x objective-c \
-arch arm64 \
-isysroot $(xcrun --sdk iphoneos26.0 --show-sdk-path) \
-rewrite-objc YKPerson.m \
-o YKPerson.cpp
# 若报错xcrun: error: SDK "iphoneos" cannot be located,可通过xcodebuild -showsdks查看已有sdk
cpp文件如下
// YKPerson
typedef struct objc_object YKPerson;
extern "C" unsigned long OBJC_IVAR_$_YKPerson$_name;
struct YKPerson_IMPL {
struct NSObject_IMPL NSObject_IVARS; // Class isa;
NSString *_name;
};
// NSObject
typedef struct objc_object NSObject;
struct NSObject_IMPL {
Class isa;
};
从上述代码我们可以看出
- 对象在底层的本质就是
objc_object类型的结构体。 - NSObject为ROOT_CLASS (插句话:在著名的isa走位图中ROOT_Class即为NSObject,且它的元类的isa指向的正是NSObject类)
- 由于
YKPerson继承自NSObject,所以除了自身的name属性外,也继承了NSObject的Class isa属性。
在objc4源码中,objc_object长这个样子。
struct objc_object {
Class _Nonnull isa;
};
// Class
typedef struct objc_class *Class;
// 再看objc-runtime-new.h中的objc_class
struct objc_class : objc_object {
// Class isa(继承自objc_object)
Class superclass;
cache_t cache;
class_data_bits_t bits;
···
}
Tips:源码中也可以看到这样一句
typedef struct objc_object *id;这也就解释了为什么我们平时用id修饰对象时不用加*
Class superclass
我们验证一下类的内存结构,运行程序并断点
在此处进行lldb调试,x/6gx YKPerson.class输出结果
根据上面objc_class的内存结构模型,我们知道前两个成员变量都为Class类型,也就是都为8字节,那么可以推断出第二个地址存储的是其父类,那么来执行po 0x00007fff80030660可以看到!

cache_t cache
// 以下仅摘取cache_t成员变量
struct cache_t {
private:
// 1
// typedef unsigned long uintptr_t;8字节
explicit_atomic<uintptr_t> _bucketsAndMaybeMask;
// 2
union {
struct {
// typedef uint32_t mask_t;4字节
explicit_atomic<mask_t> _maybeMask;
#if __LP64__
uint16_t _flags; // 2字节
#endif
uint16_t _occupied; // 2字节
};
// 8字节
explicit_atomic<preopt_cache_t *> _originalPreoptCache;
};
因为static变量存储在全局区,方法存储在方法区,均不占用结构体内存,所以一个对象内部的cache_t的大小就取决于_bucketsAndMaybeMask的大小加上联合体的大小。也就是16字节。
class_data_bits_t bits
首先我们通过上述分析知道了class_data_bits_t的地址,就在YKPerson的类地址偏移8+8+16=32个字节的位置。那么执行po 0x103d2fe20+0x20获得地址4359126592,再执行p/x 4359126592

// 仅摘取部分成员变量和重要方法
struct class_data_bits_t {
friend objc_class;
uintptr_t bits;
public:
// 获取class_rw_t
class_rw_t* data() const {···}
// 获取class_ro_t
const class_ro_t *safe_ro() const {···}
}
可以看到通过bits可以获取到两个关键结构体class_rw_t和class_ro_t,我们来看下这两个结构体中都有些什么
class_rw_t
首先根据名字,可以判断出这个结构体中的内容是read-write的,看下objc4源码中的定义
// 仅摘取class_rw_t的成员变量和部分核心方法
struct class_rw_t {
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;
const method_array_t methods() const {···}
const property_array_t properties() const {···}
const protocol_array_t protocols() const {···}
}
可以看到,类信息的class_data_bits_t bits->class_rw_t结构体中存储了该类的方法、属性、实现协议的信息。
感兴趣的话我们也可看下这三个结构体中都存着什么信息
method_array_t
存储方法结构体method_t的列表。每一条数据都是一个指针。根据下面的结构可以看出,每个method_t占用24个字节。
struct method_t {
···
struct big {
SEL name; // 方法名指针/选择器,具有唯一性,所以可以使用指针相等来比较
const char *types; // 类型编码。表示参数和返回类型的字符串【不是用来发送消息的,但是是运行时introspection和message forwarding所必需的东西】
MethodListIMP imp; // 指向方法实现的指针
};
}
property_array_t
存储属性结构体property_t的列表
struct property_t {
const char *name; // 属性名
const char *attributes; // 属性的属性(类型等)
};
protocol_array_t
存储协议结构体protocol_t的指针protocol_ref_t的列表
typedef uintptr_t protocol_ref_t; // protocol_t *, but unremapped
// 以下仅摘取【部分】protocol_t的成员变量
struct protocol_t : objc_object {
struct protocol_list_t *protocols;
method_list_t *instanceMethods; // 实例方法
method_list_t *classMethods; // 类方法
method_list_t *optionalInstanceMethods; // 可选实例方法
method_list_t *optionalClassMethods; // 可选类方法
property_list_t *instanceProperties; // 实例的属性
uint32_t size; // sizeof(protocol_t) 内存大小
property_list_t *_classProperties; // 类属性
···
}
class_ro_t
// 以下仅摘取【部分】class_ro_t的成员变量
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;
property_list_t *baseProperties;
}
class_ro_t补充说明
- Swift类和Objective-C类,共享这一数据结构。
- 当类第一次从磁盘加载到内存中时,一开始也是这样的,但是一经使用,就会发生变化
clean memory与dirty memory
clean memory:加载后不会发生更改的内存。如
class_ro_t,因为它是只读的。 dirty memory:在进程运行时会发生更改的内存。类结构一经使用就会变成dirty memory,因为运行时会向它写入新的数据(E.g. 创建一个新的方法缓存,并从类中指向它;分类扩展方法等)如class_rw_t
- dirty memory比clean memory要昂贵的多,只要进程在运行,它就必须一直存在。
- clean memory可以进行移除,从而节省更多的内存空间。
- 为什么
class_ro_t和class_rw_t都存有方法列表呢?因为主类中的方法存在class_ro_t中,而当category被加载时,可以向类中添加新的方法,即添加到class_rw_t中
class_rw_ext_t
从class_rw_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;
};
- 待完善
- 待修正
- 欢迎指出错误和没解释清楚的地方!!!