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_t、protocol_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 命令行工具,cd到main.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_$_TestClass是TestClass类的class_ro_t数据。
定义_class_t结构体类型的OBJC_METACLASS_$_TestClass变量表示TestClass类的元类,其中bits保存_OBJC_METACLASS_RO_$_TestClass变量的地址(在 3.2 中详细介绍),_OBJC_METACLASS_RO_$_TestClass是TestClass的元类的class_ro_t数据。
static void OBJC_CLASS_SETUP_$_TestClass(void )静态函数用于初始化TestClass类及元类的数据。
注意:代码中
__attribute__用于指定编译特性:包括:Function Attributes、Variable Attributes、Type 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:置0,TestClass未继承任何协议;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.m的main函数中构建对象代码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.m的main函数中调用方法的代码[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_INFO的flag值,与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数据在运行时动态生成; -
下一篇介绍应用加载过程。