Runtime源代码解读7(深入探讨面向对象实现细节)

1,898 阅读14分钟

2019-10-17

在前面6篇文章中,通过分析 runtime 源代码介绍了 Objective-C 的类和对象的实现原理。本文则主要探讨以下问题:面向对象的 Objective-C 语言代码,是如何解析成 C 语言代码的。探讨该问题的过程,可以得到更多 runtime 关于面向对象的实现细节。

本文使用的编译环境是 Mac OS X 10.14 + Apple LLVM 10.0.1 (clang-1001.0.46.4)。新版本 Mac OS X 10.15 使用 LLVM 11.x.x,runtime 本身应该也存在更新,因此实际操作时可能会和文中的描述有少许出入,例如:LLVM 11.x.x rewrite Objective-C 时,原__DATA数据段将根据是否为常量,分别保存于__DATA__DATA_CONST数据段中。这里的__DATA数据段(data segment)和后文的__objc_classlist等数据段(data section)的概念是不同的,data segment 包含 data section。由于没想到合适的译名,所以都笼统地称为数据段,通常名称全大写的数据段是 data segment,全小写则为 data section。

注意:前面6篇介绍 runtime 实现类和对象时,并没有单独介绍协议,是因为协议的使用、实现、保存、加载过程,与类十分相似。若想了解其实现细节可以看源代码中:protocol_tprotocol_list_t数据结构的定义,objc_getProtocol(...)函数实现,class_conformsToProtocol(...)函数实现,以及_read_images(...)函数中协议信息载入的相关代码。

一、思路

Objective-C 工程通常使用xcodebuild命令行工具编译,Clang 作为前端编译器主要负责预处理、编译生成语言无关的中间代码,LLVM 作为后端编译器主要负责汇编、链接生成平台相关的二进制文件。其中,在 Clang 预处理阶段将工程中的 Objective-C 语言代码转化为 C 语言代码。可以通过clang -rewrite-objc命令执行该过程,转化的主要内容如下:

  • 类型:Objective-C 类、分类、协议、成员变量、方法、分类等类型转化为 C 语言结构体类型;
  • 数据:Objective-C 类型定义的 Objective-C 元数据转化为 C 语言结构体数据,并记录为变量存储到数据段中;
  • 函数:Objective-C 类的方法、block的实现逻辑封装成 C 语言函数。

设计 Objective-C 编写的 demo 程序,保存为main.m源文件。代码中基本涵盖了 Objective-C 面向对象的基本元素(除了协议外),包括类、成员变量、方法、属性、分类定义,对象构建,对象的成员变量、属性访问,方法的调用。main.m的代码如下:

#import <Foundation/Foundation.h>

#pragma mark - TestClass类定义
@interface TestClass : NSObject {
    NSString* stringIvar;
}

@property(strong, nonatomic, getter=stringPropertyGetter, setter=stringPropertySetter:) NSString* stringProperty;

-(NSString*)printContent:(NSString*)content;

@end

@implementation TestClass

+(void)load{
    NSLog(@"");
}

-(NSString*) printContent:(NSString*)content {
    NSLog(@"Print content: %@", content);
}

@end

#pragma mark - TestClass类的TestCategory分类定义
@interface TestClass (TestCategory)

@property(strong) NSString* stringCategoryProperty;

-(NSString*)printCategoryContent:(NSString*)content;

@end

@implementation TestClass (TestCategory)

-(NSString*) printCategoryContent:(NSString*)content {
    NSLog(@"Print category content: %@", content);
}

@end

#pragma mark - 主入口
int main(int argc, char * argv[]) {
    @autoreleasepool {
        // 构建TestClass类的实例 
        TestClass* testObj = [[TestClass alloc] init];
        
        // 访问TestClass类的对象的属性值 
        NSString* strProp = testObj.stringProperty;
        
        // 调用TestClass类的方法 
        [testObj printContent:@"Something"];
    }
}

打开 Terminal 命令行工具,cdmain.m所在目录,执行命令clang -rewrite-objc main.m将 Objective-C 语言代码,转化为 C 语言代码,在当前目录生成main.cpp的 C++ 语言源文件。main.cpp的源代码就是回答开篇所提出的问题的突破口。main.cpp总共有十几万行,仅需要关注其中几百行关键代码。

二、数据结构

由于objc_class结构体类型是私有类型,因此需要定义同构的_objc_class结构体以暴露类的数据结构。针对保存类的数据,只需要定义class_ro_t的同构结构体,不需要class_rw_t的同构结构体。因为定义这些同构结构体,是为了描述类的编译时决议数据,而class_rw_t中的数据是运行时决议的,因此编译时不需要保存类的class_rw_t数据。所有同构结构体类型的定义代码如下。

// 属性
struct _prop_t {
	const char *name;
	const char *attributes;
};

// 协议结构的定义
struct _protocol_t;

// 方法
struct _objc_method {
	struct objc_selector * _cmd;
	const char *method_type;
	void  *_imp;
};

// 协议结构的实现
struct _protocol_t {
	void * isa;  // NULL
	const char *protocol_name;
	const struct _protocol_list_t * protocol_list; // super protocols
	const struct method_list_t *instance_methods;
	const struct method_list_t *class_methods;
	const struct method_list_t *optionalInstanceMethods;
	const struct method_list_t *optionalClassMethods;
	const struct _prop_list_t * properties;
	const unsigned int size;  // sizeof(struct _protocol_t)
	const unsigned int flags;  // = 0
	const char ** extendedMethodTypes;
};

// 成员变量
struct _ivar_t {
	unsigned long int *offset;  // pointer to ivar offset location
	const char *name;
	const char *type;
	unsigned int alignment;
	unsigned int  size;
};

// 类的class_ro_t数据。
// 注意:不需要定义class_rw_t的同构结构体
struct _class_ro_t {
	unsigned int flags;
	unsigned int instanceStart;
	unsigned int instanceSize;
	unsigned int reserved;
	const unsigned char *ivarLayout;
	const char *name;
	const struct _method_list_t *baseMethods;
	const struct _objc_protocol_list *baseProtocols;
	const struct _ivar_list_t *ivars;
	const unsigned char *weakIvarLayout;
	const struct _prop_list_t *properties;
};

// 类
struct _class_t {
	struct _class_t *isa;
	struct _class_t *superclass;
	void *cache;
	void *vtable;
	struct _class_ro_t *ro;
};

// 分类
struct _category_t {
	const char *name;
	struct _class_t *cls;
	const struct _method_list_t *instance_methods;
	const struct _method_list_t *class_methods;
	const struct _protocol_list_t *protocols;
	const struct _prop_list_t *properties;
};

注意:最新版本的 runtime 包含的 preoptimize 选项貌似支持编译时生成 class_rw_t,这部分代码比较晦涩而且不是主流处理,所以没细看,有兴趣可以去翻看源代码。

三、面向对象元素定义

3.1 类的实现

定义_class_t结构体类型的OBJC_CLASS_$_TestClass变量表示TestClass类,其中bits保存_OBJC_CLASS_RO_$_TestClass变量的地址(在 3.2 中详细介绍),_OBJC_CLASS_RO_$_TestClassTestClass类的class_ro_t数据。

定义_class_t结构体类型的OBJC_METACLASS_$_TestClass变量表示TestClass类的元类,其中bits保存_OBJC_METACLASS_RO_$_TestClass变量的地址(在 3.2 中详细介绍),_OBJC_METACLASS_RO_$_TestClassTestClass的元类的class_ro_t数据。

static void OBJC_CLASS_SETUP_$_TestClass(void )静态函数用于初始化TestClass类及元类的数据。

注意:代码中__attribute__用于指定编译特性:包括:Function AttributesVariable AttributesType Attributes。在这里明显是作为修饰变量的 variable attributes。unused表示变量未必会被使用,section用于指定变量所保存到的数据段,参数为数据段名。(关于数据段参考:linux目标文件

// 定义空方法缓冲
extern "C" __declspec(dllimport) struct objc_cache _objc_empty_cache;

...

// 导入OBJC_METACLASS_ $_NSObject元类变量
extern "C" __declspec(dllimport) struct _class_t OBJC_METACLASS_$_NSObject;

// 定义OBJC_METACLASS_ $_TestClass变量表示TestClass类的元类
extern "C" __declspec(dllexport) struct _class_t OBJC_METACLASS_$_TestClass __attribute__ ((used, section ("__DATA,__objc_data"))) = {
	0, // &OBJC_METACLASS_$_NSObject,
	0, // &OBJC_METACLASS_$_NSObject,
	0, // (void *)&_objc_empty_cache,
	0, // unused, was (void *)&_objc_empty_vtable,
	&_OBJC_METACLASS_RO_$_TestClass,
};

// 导入OBJC_CLASS_$_NSObject类变量
extern "C" __declspec(dllimport) struct _class_t OBJC_CLASS_$_NSObject;

// 定义OBJC_CLASS_$_TestClass变量表示TestClass类
extern "C" __declspec(dllexport) struct _class_t OBJC_CLASS_$_TestClass __attribute__ ((used, section ("__DATA,__objc_data"))) = {
	0, // &OBJC_METACLASS_$_TestClass,
	0, // &OBJC_CLASS_$_NSObject,
	0, // (void *)&_objc_empty_cache,
	0, // unused, was (void *)&_objc_empty_vtable,
	&_OBJC_CLASS_RO_$_TestClass,
};

// 初始化OBJC_CLASS_$_TestClass变量,即初始化TestClass类的操作
static void OBJC_CLASS_SETUP_$_TestClass(void ) {
        // 初始化TestClass类的元类
	OBJC_METACLASS_$_TestClass.isa = &OBJC_METACLASS_$_NSObject;
	OBJC_METACLASS_$_TestClass.superclass = &OBJC_METACLASS_$_NSObject;
	OBJC_METACLASS_$_TestClass.cache = &_objc_empty_cache;

        // 初始化TestClass类
	OBJC_CLASS_$_TestClass.isa = &OBJC_METACLASS_$_TestClass;
	OBJC_CLASS_$_TestClass.superclass = &OBJC_CLASS_$_NSObject;
	OBJC_CLASS_$_TestClass.cache = &_objc_empty_cache;
}
#pragma section(".objc_inithooks$B", long, read, write)
__declspec(allocate(".objc_inithooks$B")) static void *OBJC_CLASS_SETUP[] = {
	(void *)&OBJC_CLASS_SETUP_$_TestClass,
};

3.1.1 保存类的 class_ro_t

用于保存TestClass类和元类的class_ro_t数据如下。重点是TestClass类的class_ro_t,元类包含的元数据相对较少,主要是类方法:

  • flags:标志设置为0,重点是RO_META位为0,表示非元类;
  • instanceStart:实例偏移量置为第一个成员变量stringIvar的偏移量。其中,__OFFSETOFIVAR__(struct TestClass, stringIvar),用于获取TestClass结构体中stringIvar成员的偏移量;
  • instanceSize:实例大小为TestClass_IMPL所占用的空间;
  • reserved:置0
  • ivarLayout:置0,在 class realizing 阶段再生成;
  • name:类名为"TestClass"
  • baseMethods:保存_OBJC_$_INSTANCE_METHODS_TestClass数组的地址,_OBJC_$_INSTANCE_METHODS_TestClass数组保存类的基本方法列表;
  • baseProtocols:置0TestClass未继承任何协议;
  • ivars:保存_OBJC_$_INSTANCE_VARIABLES_TestClass数组的地址,_OBJC_$_INSTANCE_VARIABLES_TestClass数组保存类的成员变量列表;
  • weakIvarLayout:置0,在 class realizing 阶段再生成;
  • properties:保存_OBJC_$_PROP_LIST_TestClass数组的地址,_OBJC_$_PROP_LIST_TestClass数组保存类的基本属性列表;

TestClass元类的class_ro_t的方法列表保存了TestClass中实现的load类方法。同时也证实了类方法保存在元类的基本方法列表中,实例方法保存在类的基本方法列表中。

static struct _class_ro_t _OBJC_METACLASS_RO_$_TestClass __attribute__ ((used, section ("__DATA,__objc_const"))) = {
	1, sizeof(struct _class_t), sizeof(struct _class_t), 
	(unsigned int)0, 
	0, 
	"TestClass",
	(const struct _method_list_t *)&_OBJC_$_CLASS_METHODS_TestClass, 
	0, 
	0, 
	0, 
	0, 
};

// 用于计算TYPE结构体中MEMBER成员的偏移量
#define __OFFSETOFIVAR__(TYPE, MEMBER) ((long long) &((TYPE *)0)->MEMBER)

static struct _class_ro_t _OBJC_CLASS_RO_$_TestClass __attribute__ ((used, section ("__DATA,__objc_const"))) = {
	0, __OFFSETOFIVAR__(struct TestClass, stringIvar), sizeof(struct TestClass_IMPL), 
	(unsigned int)0, 
	0, 
	"TestClass",
	(const struct _method_list_t *)&_OBJC_$_INSTANCE_METHODS_TestClass,
	0, 
	(const struct _ivar_list_t *)&_OBJC_$_INSTANCE_VARIABLES_TestClass,
	0, 
	(const struct _prop_list_t *)&_OBJC_$_PROP_LIST_TestClass,
};

3.2 对象的实现

TestClass类的对象的数据类型被定义为TestClass_IMPL结构体。也就是说构建TestClass对象分配的内存区块的结构是按TestClass_IMPL的内存结构来布局的。

  • TestClass_IMPL的第一个成员,保存的是父类的 ivar layout 需要保存的数据,由于TestClass继承NSObject所以才是NSObject_IMPL,否则是其他****_IMPL
  • TestClass_IMPL的其他成员,保存的类定义的成员的数据;

...

struct NSObject_IMPL {
	Class isa;
};

...

typedef struct objc_object NSString;

...

struct TestClass_IMPL {
	struct NSObject_IMPL NSObject_IVARS;
	NSString *stringIvar;
	NSString *_stringProperty;
};

3.2.1 构建对象

main.mmain函数中构建对象代码TestClass* testObj = [[TestClass alloc] init];转化为 C 语言代码如下。看起来代码很长,原理其实很简单,就是向TestClass类发送alloc消息(类方法向类发送),然后向构建的对象发送init消息(实例方法向对象发送)。

TestClass* testObj = ((TestClass *(*)(id, SEL))(void *)objc_msgSend)((id)((TestClass *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("TestClass"), sel_registerName("alloc")), sel_registerName("init"));

3.3 成员变量

TestClass成员变量列表保存在_OBJC_$_INSTANCE_VARIABLES_TestClass变量中。此处定义了ivar_list_t的同构结构体用于保存成员变量列表,列表长度固定为2。成员变量列表其中第一个元素保存stringIvar成员变量对应的ivar_t结构体,第二个元素保存_stringProperty成员变量。以stringIvar为例:

  • offset:成员变量偏移量,类型为unsigned long int *,初始值设置为OBJC_IVAR_$_TestClass$stringIvar变量的地址,该地址保存的初始值是TestClass结构体中stringIvar成员的偏移量;
  • name:成员变量名"stringIvar"
  • type:标记成员变量类型"@\"NSString\""
  • alignment:置为3,因为按8字节对齐;
  • size:置为8,因为占用8字节;
extern "C" unsigned long int OBJC_IVAR_$_TestClass$stringIvar __attribute__ ((used, section ("__DATA,__objc_ivar"))) = __OFFSETOFIVAR__(struct TestClass, stringIvar);
extern "C" unsigned long int OBJC_IVAR_$_TestClass$_stringProperty __attribute__ ((used, section ("__DATA,__objc_ivar"))) = __OFFSETOFIVAR__(struct TestClass, _stringProperty);

static struct /*_ivar_list_t*/ {
	unsigned int entsize;  // sizeof(struct _prop_t)
	unsigned int count;
	struct _ivar_t ivar_list[2];
} _OBJC_$_INSTANCE_VARIABLES_TestClass __attribute__ ((used, section ("__DATA,__objc_const"))) = {
	sizeof(_ivar_t),
	2,
	{{(unsigned long int *)&OBJC_IVAR_$_TestClass$stringIvar, "stringIvar", "@\"NSString\"", 3, 8},
	 {(unsigned long int *)&OBJC_IVAR_$_TestClass$_stringProperty, "_stringProperty", "@\"NSString\"", 3, 8}}
};

注意:之所以将成员变量偏移量设置为extern(外部引用全局变量),是为了支持 non-fragile instance variable,运行时类进行到 class realizing 阶段时,需要根据父类的instanceSize动态调整类的 ivar layout,此时可能需要修改成员变量偏移量。

3.3.1 访问成员变量值

Runtime源代码解读5(属性)介绍属性时,曾提出过关于运行时,对象的成员变量值如何获取的问题。此时可以从代码中找到答案。通过对象的地址self,以及记录对象成员变量偏移量的OBJC_IVAR_$_TestClass$stringIvar变量高效获取。代码为(*(NSString **)((char *)self + OBJC_IVAR_$_TestClass$_stringIvar))

3.4 方法列表

TestClass的基本方法列表保存在_OBJC_$_INSTANCE_METHODS_TestClass变量中。此处定义了method_list_t的同构结构体用于保存方法列表,列表长度固定为3,注意不包含分类的方法列表的,因为分类的方法列表不属于class_ro_t。方法列表中:

  • 第一个元素保存printContent:成员变量对应的method_t结构体;
  • 第二个元素保存stringPropertyGetter成员变量;
  • 第二个元素保存stringPropertySetter:成员变量;

printContent:为例:

  • _cmd:方法名为"printContent:"
  • _type:类型编码设置为"@24@0:8@16"
  • _imp:方法的IMP为指向_I_TestClass_printContent_的函数指针;

TestClass的元类的基本方法列表保存在_OBJC_$_CLASS_METHODS_TestClass变量中,仅包含TestClass类中实现的load类方法。

static __NSConstantStringImpl __NSConstantStringImpl__var_folders_qh_b2s8cgys01g3jxc0vq1y5ks80000gn_T_main_adaf3a_mi_0 __attribute__ ((section ("__DATA, __cfstring"))) = {__CFConstantStringClassReference,0x000007c8,"Print content: %@",17};

static __NSConstantStringImpl __NSConstantStringImpl__var_folders_qh_b2s8cgys01g3jxc0vq1y5ks80000gn_T_main_d92eec_mi_0 __attribute__ ((section ("__DATA, __cfstring"))) = {__CFConstantStringClassReference,0x000007c8,"",0};


static void _C_TestClass_load(Class self, SEL _cmd) {
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_qh_b2s8cgys01g3jxc0vq1y5ks80000gn_T_main_d92eec_mi_0);
}

static NSString * _I_TestClass_printContent_(TestClass * self, SEL _cmd, NSString *content) {
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_qh_b2s8cgys01g3jxc0vq1y5ks80000gn_T_main_adaf3a_mi_0, content);
}

static NSString * _I_TestClass_stringPropertyGetter(TestClass * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_TestClass$_stringProperty)); }
static void _I_TestClass_stringPropertySetter_(TestClass * self, SEL _cmd, NSString *stringProperty) { (*(NSString **)((char *)self + OBJC_IVAR_$_TestClass$_stringProperty)) = stringProperty; }


static struct /*_method_list_t*/ {
	unsigned int entsize;  // sizeof(struct _objc_method)
	unsigned int method_count;
	struct _objc_method method_list[3];
} _OBJC_$_INSTANCE_METHODS_TestClass __attribute__ ((used, section ("__DATA,__objc_const"))) = {
	sizeof(_objc_method),
	3,
	{{(struct objc_selector *)"printContent:", "@24@0:8@16", (void *)_I_TestClass_printContent_},
	{(struct objc_selector *)"stringPropertyGetter", "@16@0:8", (void *)_I_TestClass_stringPropertyGetter},
	{(struct objc_selector *)"stringPropertySetter:", "v24@0:8@16", (void *)_I_TestClass_stringPropertySetter_}}
};

static struct /*_method_list_t*/ {
	unsigned int entsize;  // sizeof(struct _objc_method)
	unsigned int method_count;
	struct _objc_method method_list[1];
} _OBJC_$_CLASS_METHODS_TestClass __attribute__ ((used, section ("__DATA,__objc_const"))) = {
	sizeof(_objc_method),
	1,
	{{(struct objc_selector *)"load", "v16@0:8", (void *)_C_TestClass_load}}
};

3.4.1 方法调用

main.mmain函数中调用方法的代码[testObj printContent:@"Something"];转化为 C 语言代码如下。又是很长的一串,但是原理也很简单,就是向testObj对象发送printContent:消息,传递参数"Something",该字符串常量保存在__cfstring数据段中。

static __NSConstantStringImpl __NSConstantStringImpl__var_folders_qh_b2s8cgys01g3jxc0vq1y5ks80000gn_T_main_adaf3a_mi_2 __attribute__ ((section ("__DATA, __cfstring"))) = {__CFConstantStringClassReference,0x000007c8,"Something",9};

((NSString *(*)(id, SEL, NSString *))(void *)objc_msgSend)((id)testObj, sel_registerName("printContent:"), (NSString *)&__NSConstantStringImpl__var_folders_qh_b2s8cgys01g3jxc0vq1y5ks80000gn_T_main_adaf3a_mi_2);

3.5 属性

TestClass的基本属性列表保存在_OBJC_$_PROP_LIST_TestClass变量中。由于类声明属性默认指定为@synthesize,表示自动生成与属性关联的成员变量,以及自动生成属性关联的 getter 和 setter 方法。所以,TestClass定义了stringProperty属性,在 3.4 的成员变量列表中生成了关联属性的_stringProperty成员变量及其对应数据,以及在 3.5 的方法列表中生成属性的 getter 方法stringPropertyGetter及其IMP所指向的函数_I_TestClass_stringPropertyGetter、setter 方法stringPropertySetter:及其IMP所指向的函数_I_TestClass_stringPropertySetter_

属性列表长度固定为1,仅包含表示stringProperty属性的_prop_t机构体:

  • name:属性名设置为"stringProperty"
  • attributes:特性置为"T@\"NSString\",&,N,GstringPropertyGetter,SstringPropertySetter:,V_stringProperty"
static struct /*_prop_list_t*/ {
	unsigned int entsize;  // sizeof(struct _prop_t)
	unsigned int count_of_properties;
	struct _prop_t prop_list[1];
} _OBJC_$_PROP_LIST_TestClass __attribute__ ((used, section ("__DATA,__objc_const"))) = {
	sizeof(_prop_t),
	1,
	{{"stringProperty","T@\"NSString\",&,N,GstringPropertyGetter,SstringPropertySetter:,V_stringProperty"}}
};

3.5.1 访问属性

访问属性的代码在_I_TestClass_stringPropertyGetter_I_TestClass_stringPropertySetter_的实现代码是 runtime 自动生成的,其原理是通过记录的关联成员变量的内存偏移量,直接操作关联成员变量的内存,因此有很高的效率。main()函数中访问属性的代码NSString* strProp = testObj.stringProperty;被转化以下 C 语言代码。原理是通过属性的attribute获取 getter 方法名,直接调用 getter 方法。

NSString* strProp = ((NSString *(*)(id, SEL))(void *)objc_msgSend)((id)testObj, sel_registerName("stringPropertyGetter"));

3.6 分类

分类保存在_OBJC_$_CATEGORY_TestClass_$_TestCategory

  • name:分类名"TestClass"(Fixme: 不知道为什么是设置成类名);
  • cls:置0
  • instance_methods:实例方法列表保存_OBJC_$_CATEGORY_INSTANCE_METHODS_TestClass_$_TestCategory数组的地址,_OBJC_$_CATEGORY_INSTANCE_METHODS_TestClass_$_TestCategory数组保存TestCategory分类定义的实例方法列表;
  • class_methods:置0,因为TestCategory分类不包含类方法;
  • protocols:置0,因为TestCategory未遵循任何协议;
  • properties:置0,属性列表保存_OBJC_$_PROP_LIST_TestClass_$_TestCategory数组的地址,_OBJC_$_PROP_LIST_TestClass_$_TestCategory数组保存TestCategory分类定义的属性列表;

初始化时,设置:

  • cls:指向OBJC_CLASS_$_TestClass
static struct _category_t _OBJC_$_CATEGORY_TestClass_$_TestCategory __attribute__ ((used, section ("__DATA,__objc_const"))) = 
{
	"TestClass",
	0, // &OBJC_CLASS_$_TestClass,
	(const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_TestClass_$_TestCategory,
	0,
	0,
	(const struct _prop_list_t *)&_OBJC_$_PROP_LIST_TestClass_$_TestCategory,
};
static void OBJC_CATEGORY_SETUP_$_TestClass_$_TestCategory(void ) {
	_OBJC_$_CATEGORY_TestClass_$_TestCategory.cls = &OBJC_CLASS_$_TestClass;
}
#pragma section(".objc_inithooks$B", long, read, write)
__declspec(allocate(".objc_inithooks$B")) static void *OBJC_CATEGORY_SETUP[] = {
	(void *)&OBJC_CATEGORY_SETUP_$_TestClass_$_TestCategory,
};

分类定义的方法保存在

static NSString * _I_TestClass_TestCategory_printCategoryContent_(TestClass * self, SEL _cmd, NSString *content) {
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_qh_b2s8cgys01g3jxc0vq1y5ks80000gn_T_main_adaf3a_mi_1, content);
}

static struct /*_method_list_t*/ {
	unsigned int entsize;  // sizeof(struct _objc_method)
	unsigned int method_count;
	struct _objc_method method_list[1];
} _OBJC_$_CATEGORY_INSTANCE_METHODS_TestClass_$_TestCategory __attribute__ ((used, section ("__DATA,__objc_const"))) = {
	sizeof(_objc_method),
	1,
	{{(struct objc_selector *)"printCategoryContent:", "@24@0:8@16", (void *)_I_TestClass_TestCategory_printCategoryContent_}}
};

分类定义的属性保存在_OBJC_$_PROP_LIST_TestClass_$_TestCategory变量。

static struct /*_prop_list_t*/ {
	unsigned int entsize;  // sizeof(struct _prop_t)
	unsigned int count_of_properties;
	struct _prop_t prop_list[1];
} _OBJC_$_PROP_LIST_TestClass_$_TestCategory __attribute__ ((used, section ("__DATA,__objc_const"))) = {
	sizeof(_prop_t),
	1,
	{{"stringCategoryProperty","T@\"NSString\",&"}}
};

四、Objective-C 元素的加载

将源文件定义的类添加到L_OBJC_LABEL_CLASS_$列表,列表长度为1,因为仅定义了一个类TestClass,元素保存OBJC_CLASS_$_TestClass的地址。

将定义的分类添加到L_OBJC_LABEL_CATEGORY_$列表,列表长度为1,因为仅定义了一个分类TestCategory,元素保存_OBJC_$_CATEGORY_TestClass_$_TestCategory的地址。

static struct _class_t *L_OBJC_LABEL_CLASS_$ [1] __attribute__((used, section ("__DATA, __objc_classlist,regular,no_dead_strip")))= {
	&OBJC_CLASS_$_TestClass,
};
static struct _class_t *_OBJC_LABEL_NONLAZY_CLASS_$[] = {
	&OBJC_CLASS_$_TestClass,
};
static struct _category_t *L_OBJC_LABEL_CATEGORY_$ [1] __attribute__((used, section ("__DATA, __objc_catlist,regular,no_dead_strip")))= {
	&_OBJC_$_CATEGORY_TestClass_$_TestCategory,
};
static struct IMAGE_INFO { unsigned version; unsigned flag; } _OBJC_IMAGE_INFO = { 0, 2 };

4.1 类的懒加载

上述代码除定义了保存类元素、分类元素的变量外,还包含_OBJC_LABEL_NONLAZY_CLASS_$,其中保存了TestClass类的地址。该列表是用于保存 nonlazy load class(Objective-C: What is a lazy class?)。所谓 non-lazy load class, 是指实现了load方法的类。这些类需要在镜像(image)加载时立即完成 class realizing 并执行load方法。Runtime 中定义了_getObjc2NonlazyClassList函数用于获取镜像中的__objc_nlclslist数据段中保存的 non-lazy load class。

反之,lazy load class 仅仅表示未实现load方法的类,然而并不是真正意义上的懒加载。严格意义上,类的懒加载是指,将类的解析(包括从镜像中载入类的静态元数据class_ro_t、class realizing生成类的动态元数据class_rw_t、class loading执行load方法初始化类)推迟到正式载入二进制文件阶段。在此之前,runtime 只知道这是一个类,且将其标记为 future class。通常,开发者开发编译的 iOS App、framework、static library等二进制文件,都是静态加载的,其定义的类均在应用的加载阶段静态载入,仅.tbd、.dylib 或者封装了 .tbd、.dylib 的 framework(通常只有 Apple 自带的 framework 才允许封装动态库)才支持动态载入内存

注意:用clang -rewrite-objc main.m得到的main.cpp中,non-lazy load class 列表_OBJC_LABEL_NONLAZY_CLASS_$并没有被添加到__objc_nlclslist。但从 4.3 中打印的main.o的数据段内容看,实际上是有添加的。关于镜像的概念在下一篇文章中详细介绍,在这里仅需要知道镜像保存了源文件所定义的 Objective-C 元数据即可。

4.2 获取数据段的代码

main.cpp中定义面向对象元素的主要代码,是定义这些元素的 C 语言实现,并将这些元数据定义为静态或全局变量,添加到特定的数据段中。

有添加必有读取。Runtime 通过GETSECT宏,定义了一系列读取特定数据段中的数据的函数。例如:GETSECT(_getObjc2ClassList, classref_t, "__objc_classlist")定义:用于获取__objc_classlist数据段的函数_getObjc2ClassList,数据段的存储的数据类型为classref_t。因此,若main.cpp编译成镜像,则 runtime 调用_getObjc2ClassList获取镜像中定义的所有类时,将会获得L_OBJC_LABEL_CLASS_$变量,当然也包括其中保存的TestClass类的定义OBJC_CLASS_$_TestClass变量。

// 获取数据段的外部函数,具体实现逻辑隐藏未公开
extern uint8_t *getsectiondata(
    const struct mach_header_64 *mhp,
    const char *segname,
    const char *sectname,
    unsigned long *size);
    
template <typename T>
T* getDataSection(const headerType *mhdr, const char *sectname, 
                  size_t *outBytes, size_t *outCount)
{
    unsigned long byteCount = 0;
    T* data = (T*)getsectiondata(mhdr, "__DATA", sectname, &byteCount);
    if (!data) {
        data = (T*)getsectiondata(mhdr, "__DATA_CONST", sectname, &byteCount);
    }
    if (!data) {
        data = (T*)getsectiondata(mhdr, "__DATA_DIRTY", sectname, &byteCount);
    }
    if (outBytes) *outBytes = byteCount;
    if (outCount) *outCount = byteCount / sizeof(T);
    return data;
}

#define GETSECT(name, type, sectname)                                   \
    type *name(const headerType *mhdr, size_t *outCount) {              \
        return getDataSection<type>(mhdr, sectname, nil, outCount);     \
    }                                                                   \
    type *name(const header_info *hi, size_t *outCount) {               \
        return getDataSection<type>(hi->mhdr(), sectname, nil, outCount); \
    }

//      function name                 content type     section name
GETSECT(_getObjc2SelectorRefs,        SEL,             "__objc_selrefs"); 
GETSECT(_getObjc2MessageRefs,         message_ref_t,   "__objc_msgrefs"); 
GETSECT(_getObjc2ClassRefs,           Class,           "__objc_classrefs");
GETSECT(_getObjc2SuperRefs,           Class,           "__objc_superrefs");
GETSECT(_getObjc2ClassList,           classref_t,      "__objc_classlist");
GETSECT(_getObjc2NonlazyClassList,    classref_t,      "__objc_nlclslist");
GETSECT(_getObjc2CategoryList,        category_t *,    "__objc_catlist");
GETSECT(_getObjc2NonlazyCategoryList, category_t *,    "__objc_nlcatlist");
GETSECT(_getObjc2ProtocolList,        protocol_t *,    "__objc_protolist");
GETSECT(_getObjc2ProtocolRefs,        protocol_t *,    "__objc_protorefs");
GETSECT(getLibobjcInitializers,       UnsignedInitializer, "__objc_init_func");

4.3 验证

首先,用clang -framework Foundation -o main.o main.m命令编译main.m源文件,在当前目录输出main.o目标文件。然后,用otool -o -v main.o-o print the Objective-C segment)命令查看目标文件中的 Objective-C 相关数据段内容如下。发现_OBJC_IMAGE_INFOflag值,与main.cpp中的定义不一致,应该以 Clang 编译得到的main.o目标文件的数据为准。

main.o:
Contents of (__DATA,__objc_classlist) section
00000001000010b8 0x1000012b8 _OBJC_CLASS_$_TestClass
           isa 0x100001290 _OBJC_METACLASS_$_TestClass
    superclass 0x0 _OBJC_CLASS_$_NSObject
         cache 0x0 __objc_empty_cache
        vtable 0x0
          data 0x100001210 (struct class_ro_t *)
                    flags 0x0
            instanceStart 8
             instanceSize 24
                 reserved 0x0
               ivarLayout 0x0
                     name 0x100000ef0 TestClass
              baseMethods 0x1000010d0 (struct method_list_t *)
		   entsize 24
		     count 4
		      name 0x100000f60 printCategoryContent:
		     types 0x100000f89 @24@0:8@16
		       imp 0x100000d10 -[TestClass(TestCategory) printCategoryContent:]
		      name 0x100000f0c printContent:
		     types 0x100000f89 @24@0:8@16
		       imp 0x100000c70 -[TestClass printContent:]
		      name 0x100000f1a stringPropertyGetter
		     types 0x100000f94 @16@0:8
		       imp 0x100000cb0 -[TestClass stringPropertyGetter]
		      name 0x100000f2f stringPropertySetter:
		     types 0x100000f9c v24@0:8@16
		       imp 0x100000cd0 -[TestClass stringPropertySetter:]
            baseProtocols 0x0
                    ivars 0x1000011c8
                    entsize 32
                      count 2
			   offset 0x100001288 8
			     name 0x100000f45 stringIvar
			     type 0x100000fa7 @"NSString"
			alignment 3
			     size 8
			   offset 0x100001280 16
			     name 0x100000f50 _stringProperty
			     type 0x100000fa7 @"NSString"
			alignment 3
			     size 8
           weakIvarLayout 0x0
           baseProperties 0x100001138
                    entsize 16
                      count 2
			     name 0x100000ec0 stringCategoryProperty
			attributes 0x100000ed7 T@"NSString",&
			     name 0x100000e47 stringProperty
			attributes 0x100000e56 T@"NSString",&,N,GstringPropertyGetter,SstringPropertySetter:,V_stringProperty
Meta Class
           isa 0x0 _OBJC_METACLASS_$_NSObject
    superclass 0x0 _OBJC_METACLASS_$_NSObject
         cache 0x0 __objc_empty_cache
        vtable 0x0
          data 0x100001180 (struct class_ro_t *)
                    flags 0x1 RO_META
            instanceStart 40
             instanceSize 40
                 reserved 0x0
               ivarLayout 0x0
                     name 0x100000ef0 TestClass
              baseMethods 0x100001160 (struct method_list_t *)
		   entsize 24
		     count 1
		      name 0x100000f07 load
		     types 0x100000f81 v16@0:8
		       imp 0x100000c40 +[TestClass load]
            baseProtocols 0x0
                    ivars 0x0
           weakIvarLayout 0x0
           baseProperties 0x0
Contents of (__DATA,__objc_classrefs) section
0000000100001278 0x1000012b8 _OBJC_CLASS_$_TestClass
Contents of (__DATA,__objc_catlist) section
Contents of (__DATA,__objc_imageinfo) section
  version 0
    flags 0x40 OBJC_IMAGE_HAS_CATEGORY_CLASS_PROPERTIES

注意到上面数据中不包含用于保存分类的__objc_catlist数据段,这是因为main.m代码中没有调用到TestCategory分类中的方法,因此不会加载该分类。有两种方法修正:

  • main函数中加入[testObj printCategoryContent:@"Something"];
  • 使用clang -framework Foundation -all_load -o main.o main.m命令编译,添加-all_load编译选项强制编译所有 Objective-C 元素;

注意:用于保存 non-lazy load class 列表的__objc_nlclslist数据段,以及保存 non-lazy load category 列表的__objc_nlcatlist数据段,不会显示在otool -o命令打印的 Objective-C 元数据中。但只要TestClass类有实现load方法,是可以用otool -v -s __DATA __objc_nlclslist main.o命令查看到__objc_nlclslist数据段是确实有保存TestClass类的地址的。clang -rewrite-objc生成的main.cpp中的 Objective-C 元数据,与编译生成的目标文件main.o实际包含的 Objective-C 元数据之所以有少许出入,很可能是因为 Clang 编译器在 rewrite Objective-C 阶段和编译生成目标文件阶段之间还有其他的元数据处理逻辑。

五、总结

  • 将 Objective-C 元素转化为 C 语言元素时,需要定义这些元素在 runtime 中的结构体的同构结构体类型,以暴露它们的数据结构。通常将它们定义为这些同构结构体类型的静态变量,并保存到特定数据段中;

  • Runtime 定义了一些函数用于从特定的数据段读取数据,以加载镜像(image)中保存的 Objective-C 元数据,例如,_getObjc2ClassList用于获取镜像中__objc_classlist数据段保存的所有类的元数据,_getObjc2CategoryList获取镜像中__objc_catlist数据段中保存的所有分类的元数据;

  • 类在内存中的结构为_class_t结构体,该结构体与 runtime 中的objc_class结构体是同构的,包含类名、超类、class_ro_t中的基本方法列表、基本属性列表、协议列表等元数据。class_rw_t数据在运行时动态生成;

  • 下一篇介绍应用加载过程。