Runtime源代碼解讀7(深刻探討面向對象實現細節)

2019-10-17html

在前面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。linux

注意:前面6篇介紹 runtime 實現類和對象時,並無單獨介紹協議,是由於協議的使用、實現、保存、加載過程,與類十分類似。若想了解其實現細節能夠看源代碼中:protocol_tprotocol_list_t數據結構的定義,objc_getProtocol(...)函數實現,class_conformsToProtocol(...)函數實現,以及_read_images(...)函數中協議信息載入的相關代碼。objective-c

1、思路

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總共有十幾萬行,僅須要關注其中幾百行關鍵代碼。xcode

2、數據結構

因爲objc_class結構體類型是私有類型,所以須要定義同構的_objc_class結構體以暴露類的數據結構。針對保存類的數據,只須要定義class_ro_t的同構結構體,不須要class_rw_t的同構結構體。由於定義這些同構結構體,是爲了描述類的編譯時決議數據,而class_rw_t中的數據是運行時決議的,所以編譯時不須要保存類的class_rw_t數據。全部同構結構體類型的定義代碼以下。bash

// 屬性
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、面向對象元素定義

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\",&"}}
};
複製代碼

4、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 階段和編譯生成目標文件階段之間還有其餘的元數據處理邏輯。

5、總結

  • 將 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數據在運行時動態生成;

  • 下一篇介紹應用加載過程。

相關文章
相關標籤/搜索