自省與反射的簡單認識
第一次聽說這兩個概念,是在 Thinking in Java (4th Edition) 中的,而深入學習他們則是在 Python 語言的學習中,以下我用 Python 來舉例說明。
wikipedia: In computer science, reflection is the ability of a computer program to examine and modify its own structure and behavior (specifically the values, meta-data, properties and functions) at runtime.
反射(Reflection) 是指計算機程式可以在執行時動態監測並修改它自己的結構和行為,比如值、後設資料、屬性和函式等的能力。通過反射,可以在執行時動態監測、生成、修改自己實際執行的等效程式碼。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
class HelloClass(object): def __init__(self, method): self.method = method print('You are calling me from ' + self.method) def say_hello(self): print("Hello -- From: " + self.method) print() # Normal obj = HelloClass('Normal') obj.say_hello() # Reflection class_name = "HelloClass" method = "say_hello" obj = globals()[class_name]('Reflection') getattr(obj, method)() |
1 2 3 4 5 6 |
You are calling me from Normal Hello -- From: Normal () You are calling me from Reflection Hello -- From: Reflection () |
兩種方法可以達到同樣的效果。但是,第一種方法是我們所說的常規方法,建立 HelloClass 這個 class 的一個例項,然後呼叫其中的方法。第二種我們用到了反射機制,通過 globals()
這個字典中來查詢 HelloClass
這個類,並加以引數進行例項化於 obj
,之後通過 getattr
函式獲得 say_hello
方法,傳參呼叫。
反射的好處在於,class_name
和 method
變數的值可以在執行時獲取或者修改,這樣可以動態地改變程式的行為。
wikipedia: In computing, type introspection is the ability of a program to examine the type or properties of an object at runtime.
自省(Introspection) 是程式在執行時檢測自己的某個物件的型別或者屬性、方法的能力。例如在 Python 中的 dir
方法。
1 2 3 4 5 6 7 8 9 10 11 |
class HelloClass(object): def __init__(self, method): self.method = method print('You are calling me from ' + self.method) def say_hello(self): print("Hello -- From: " + self.method) print() obj = HelloClass('Normal') obj_msg = dir(obj) for x in obj_msg: print (x) |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
__class__ __delattr__ __dict__ __doc__ __format__ __getattribute__ __hash__ __init__ __module__ __new__ __reduce__ __reduce_ex__ __repr__ __setattr__ __sizeof__ __str__ __subclasshook__ __weakref__ method say_hello |
通過 dir()
函式從而做到自省,它可以返回某個物件的所有屬性、方法等列表。
通過上述簡單描述,我們大概知道了反射其實是包含著自省能力的,不僅可以獲取到物件的各種屬性資訊,而且還可以動態修改自身的結構和行為。
objc_class 結構
在 ObjC 中,也支援在執行時檢查物件型別這一操作,並且這個特性是內建於 Foundation 框架的 NSObject 協議中的。凡是公共基類(Common Root Class),即 NSObject 或 NSProxy ,繼承而來的物件都要遵循此協議。
雖然 ObjC 支援自省這一特性,就一定會對 Class 資訊做儲存。這裡我們便要引出 isa 指標。倘若對 ObjC 有一定的學習基礎,都會知道 Objective-C 物件都可以通過 clang 進行 c 的語法格式轉換,從而以 struct 來描述。所有的物件中都有一個 isa
指標,其含義是: it is a object! 而在最新的 runtime 庫中,其 isa 指標的結構已經發生了變化。
以下程式碼均參考 runtime 版本為 objc4-680.tar.gz。
1 2 3 4 |
struct objc_object { private: isa_t isa; } |
會發現在 objc_object 這個基類中只有一個成員,即 isa_t 聯合體(union) 型別的 isa 成員。而對於類物件的定義,可以從 objc_class 檢視其結構:
1 2 3 4 5 6 |
struct objc_class : objc_object { // Class ISA; Class superclass; // 父類引用 cache_t cache; // 用來快取指標和虛擬函式表 class_data_bits_t bits; // class_rw_t 指標加上 rr/alloc 標誌 } |
runtime 的開源作者怕學習者不知道 isa 已經從 objc_object 繼承存在,用註釋加以提示。
其實,開發中所使用的類和例項,都會擁有一個記錄自身資訊的 isa 指標,只是因為 runtime 從 objc_object 繼承出的,所以不會顯式看到。
需要知道的是,class_data_bits_t 中存有 Class 的對應方法,具體如何儲存,會在後續的文中記錄。
isa 優化下的資訊記錄
isa 是一個聯合體型別,其結構如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
union isa_t { isa_t() { } isa_t(uintptr_t value) : bits(value) { } Class cls; uintptr_t bits; struct { uintptr_t indexed : 1; uintptr_t has_assoc : 1; uintptr_t has_cxx_dtor : 1; uintptr_t shiftcls : 33; uintptr_t magic : 6; uintptr_t weakly_referenced : 1; uintptr_t deallocating : 1; uintptr_t has_sidetable_rc : 1; uintptr_t extra_rc : 19; }; }; |
該定義是在
__arm64__
環境下的 isa_t 聯合體結構。因為 iOS 應用為__arm64__
架構環境。
可以看到在 isa_t 聯合體中不僅僅表明了指向物件的地址資訊,而且這個 64 位資料還記錄了其 bits 情況以及該例項每一位儲存的物件資訊。來驗證一下(記住要使用真機除錯, real device 和 simulator 的架構環境是有一定區別):
1 2 3 4 5 6 7 8 |
- (void)viewDidLoad { NSObject *object = [NSObject new]; // 在 ARC 模式下,通過 __bridge 轉換 id 型別為 (void *) 型別 NSLog(@"isa: %p ", *(void **)(__bridge void *)object); static void *someKey = &someKey; objc_setAssociatedObject(object, someKey, @"Desgard_Duan", OBJC_ASSOCIATION_RETAIN); NSLog(@"isa: %p ", *(void **)(__bridge void *)object); } |
輸出結果為:
1 2 |
2016-09-25 23:01:44.257 isa: 0x1a1ae5a3ea1 2016-09-25 23:01:44.257 isa: 0x1a1ae5a3ea3 |
首先先來看一下這 64 個二進位制位每一位的含義:
區域名 | 代表資訊 |
---|---|
indexed | 0 表示普通的 isa 指標,1 表示使用優化,儲存引用計數 |
has_assoc | 表示該物件是否包含 associated object ,如果沒有,則析構時會更快 |
has_cxx_dtor | 表示該物件是否有 C++ 或 ARC 的解構函式,如果沒有,則析構時更快 |
shiftcls | 類的指標 |
magic | 固定值,用於在除錯時分辨物件是否未完成初始化 |
weakly_referenced | 表示該物件是否有過 weak 物件,如果沒有,則析構時更快 |
deallocating | 表示該物件是否正在析構 |
has_sidetable_rc | 表示該物件的引用計數值是否過大無法儲存在 isa 指標 |
extra_rc | 儲存引用計數值減一後的結果 |
將 16 進位制的 0x1a1ae5a3ea3
轉換成二進位制。發現在 has_assoc
和 index
兩個位都是 1 。根據程式碼我們可以知道我們手動為其設定了 associated object
,所以以上的含義表是正確的。這裡詳細的再說一下 indexed
的含義。
isa 初始化行為,indexed 以及 magic 段的預設值
isa
指標會通過 initIsa
來初始化。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
#define ISA_MASK 0x0000000ffffffff8ULL #define ISA_MAGIC_MASK 0x000003f000000001ULL #define ISA_MAGIC_VALUE 0x000001a000000001ULL inline void objc_object::initInstanceIsa(Class cls, bool hasCxxDtor) { // initIsa 入口函式 // 傳入 Class 物件,是否為 isa 優化量, initIsa(cls, true, hasCxxDtor); } inline void objc_object::initIsa(Class cls, bool indexed, bool hasCxxDtor) { if (!indexed) { // 如果沒有使用 isa 優化,其內部只記錄地址資訊 isa.cls = cls; } else { // ISA_MAGIC_VALUE 為 bits(isa 資訊)賦初值 // 注意在 arm64 下 mask 部分固定為 0x1a isa.bits = ISA_MAGIC_VALUE; // 是否擁有 C++ 中的解構函式 isa.has_cxx_dtor = hasCxxDtor; // 由於使用了 isa 優化,所以第三位擁有其他資訊 // 需要將 cls 初始資料左移,儲存在 shiftcls 對應位置 isa.shiftcls = (uintptr_t)cls >> 3; } } |
在以上程式碼中,可以看到在一個 isa_t
結構中,magic 段是一個固定值,在 arm64 架構下其值為 0x1a
,而在 x86 下則為 0x1d
,筆者猜測這一位也有判斷架構型別之意。而觀察 isa 初始化的呼叫棧,可以發現是 callAlloc
函式進行呼叫。這段程式碼的解讀,將放在以後的文中。
ISA() 獲取非 Tagged Pointer 物件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
#define ISA_MASK 0x0000000ffffffff8ULL // 簡單 isa 初始化方式 inline void objc_object::initIsa(Class cls) { initIsa(cls, false, false); } inline void objc_object::initClassIsa(Class cls) { // non-pointer isa 情況 if (DisableIndexedIsa) { initIsa(cls, false, false); } else { initIsa(cls, true, false); } } inline void objc_object::initProtocolIsa(Class cls) { return initClassIsa(cls); } inline Class objc_object::ISA() { assert(!isTaggedPointer()); // 與有效位全 1 碼進行與運算來過濾非有效位 return (Class)(isa.bits & ISA_MASK); } |
從中發現,其有效區域也就是 isa_t 中的 shiftcls
區域。而且這種掩碼方式,也是從 isa_t 中查詢資訊的主要方式,再很多方法中可以看見類似的做法。
isa 的主地址檢索
無論在新舊版本的 Objective-C 中,都會有 isa 指標來記錄類的資訊。而在現在的 runtime 庫中,由於 64 位的優勢,使用聯合體又增加了類資訊記錄的補充。而對於 isa 的主要部分,其記錄的主要資訊是什麼呢?
在之前的一些文章中,筆者通過了 ObjC 的訊息轉發機制稍微提及了一些關於 isa 的知識,可以參考這篇文章 objc_msgSend訊息傳遞學習筆記 – 物件方法訊息傳遞流程 。 在訊息傳遞的主要流程中,最重要的一個環節就是快速查詢 isa 操作 GetIsaFast ,其中要繼續的搜尋所屬 Class 的方法列表(所有成員方法所對應的 Hash Table)。可見 isa 記錄的地址資訊和當前例項的 Class 有直接關係。
下面通過實驗來驗證我們的猜測:
1 2 3 4 5 6 |
- (void)viewDidLoad { NSObject *object = [NSObject new]; NSLog(@"isa: %p ", *(void **)(__bridge void *)object); NSObject *object_2 = [NSObject new]; NSLog(@"isa: %p ", *(void **)(__bridge void *)object_2); } |
在真機上執行該程式碼片段,可以發現其輸出的結果:
1 2 |
2016-09-30 10:34:15.577813 isa: 0x1a1a96cbea1 2016-09-30 10:34:15.577897 isa: 0x1a1a96cbea1 |
在輸出 isa 的指標後,可以發現其記錄的值完全相等。並且再通過對其 isa 指向地址的 Class Name 輸出,可知其 isa 指標是指向所屬 Class 物件地址。這只是對於物件例項的 isa 指標而言。
至此我們可能會產生另外一個疑問:
既然 Objective-C 將所有的事物物件化,那麼其所屬 Class 也會擁有 isa 指標,那麼所屬 Class 的 isa 是如何規定指向問題的?
下面引出 元類 meta-class 的概念。
Class 的 isa 指向:meta-class
在 Objective-C 中,每一個 Class 都會擁有一個與之相關聯的 meta-class 。但是在業務開發中,可能永遠不會接觸,因為這個 Class 是用來記錄一些類資訊,而不會直接將其成員的屬性介面暴露出來。下面來逐一探究一番(以下例子參考文章 What is a meta-class in Objective-C? ):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
- (void)viewDidLoad { [super viewDidLoad]; Class newClass = objc_allocateClassPair([NSError class], "RuntimeErrorSubclass", 0); class_addMethod(newClass, @selector(report), (IMP)ReportFunction, "v@:"); objc_registerClassPair(newClass); } void ReportFunction(id self, SEL _cmd) { NSLog(@"This object is %p.",self); NSLog(@"Class is %<a href="http://www.jobbole.com/members/Famous_god">@,</a> and super is %@.",[self class],[self superclass]); Class currentClass = [self class]; for( int i = 1; i 5; ++i ) { NSLog(@"Following the isa pointer %d times gives %p", i, currentClass); } NSLog(@"NSObject's class is %p", [NSObject class]); NSLog(@"NSObject's meta class is %p",object_getClass([NSObject class])); } |
這段程式碼所做的事情是在 runtime 時期建立 NSError
的一個子類 RuntimeErrorSubclass
。objc_allocateClassPair
方法會建立一個新的 Class ,然後取出 Class 的物件,使用 class_addMethod
方法,為該 Class 新增方法,需要開發者傳入新增方法的 Class 、方法名、實現函式、以及定義該函式返回值型別和引數型別的字串。最後呼叫 objc_registerClassPair
對其進行註冊即可。
要點:在呼叫
objc_allocateClassPair
方法增加新的 Class 的時候,可以呼叫class_addIvar
增加成員屬性和objc_registerClassPair
增加成員方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
Class objc_allocateClassPair(Class superclass, const char *name, size_t extraBytes) { Class cls, meta; rwlock_writer_t lock(runtimeLock); // 如果 Class 名重複則建立失敗 // 如果父類沒有通過認證則建立失敗 if (getClass(name) || !verifySuperclass(superclass, true/*rootOK*/)) { return nil; } // 為 cls 和 meta 分配空間 cls = alloc_class_for_subclass(superclass, extraBytes); meta = alloc_class_for_subclass(superclass, extraBytes); // 對 cls 和 meta 做指向判定 objc_initializeClassPair_internal(superclass, name, cls, meta); return cls; } |
在 objc_allocateClassPair
方法可以說是 objc_initializeClassPair_internal
的方法入口,其主要的功能是 根據 superclass 的資訊和 Class 中的一些標記成員來確定 cls 和 meta 指標的指向,並呼叫 addSubclass
方法將其加入到 superclass 中。
通過 objc_i nitializeClassPair_internal
方法中,呼叫 meta -> initClassIsa();
來初始化 isa 指標。下面通過 objc_initializeClassPair_internal
來看看 isa 指標和 meta 的初始化方式。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 |
// objc_initializeClassPair_internal 方法 // superclass: 父類指標 // name: 類名 // cls: 主類索引 // meta: metaclass 索引 // 解鎖操作,寫操作要求 runtimeLock.assertWriting(); // 只讀結構 read only // 分別宣告 cls 和 meta 兩個 class_ro_t *cls_ro_w, *meta_ro_w; // 快取初始化操作 cls->cache.initializeToEmpty(); meta->cache.initializeToEmpty(); // 資料設定操作 // data() -> ro 成員,與方法列表,屬性,協議相關 cls->setData((class_rw_t *)calloc(sizeof(class_rw_t), 1)); meta->setData((class_rw_t *)calloc(sizeof(class_rw_t), 1)); cls_ro_w = (class_ro_t *)calloc(sizeof(class_ro_t), 1); meta_ro_w = (class_ro_t *)calloc(sizeof(class_ro_t), 1); cls->data()->ro = cls_ro_w; meta->data()->ro = meta_ro_w; // 進行 allocate 分配,但沒有註冊 #define RW_CONSTRUCTING (1 // ro 成員已經 copy 到 heap 空間上儲存 #define RW_COPIED_RO (1 // data 成員為可讀寫許可權 #define RW_REALIZED (1 // 表示該類已經記錄,但尚未實現 #define RW_REALIZING (1 // 進步資訊資料操作 cls->data()->flags = RW_CONSTRUCTING | RW_COPIED_RO | RW_REALIZED | RW_REALIZING; meta->data()->flags = RW_CONSTRUCTING | RW_COPIED_RO | RW_REALIZED | RW_REALIZING; cls->data()->version = 0; meta->data()->version = 7; // 表示為 metaclass 型別 #define RO_META (1 // cls 的 flags 屬性不進行標記 cls_ro_w->flags = 0; // meta_ro_w 的 flags 屬性進行 metaclass 型別標記 meta_ro_w->flags = RO_META; if (!superclass) { // 如果沒有父類的話,則當前類也為 metaclass cls_ro_w->flags |= RO_ROOT; meta_ro_w->flags |= RO_ROOT; } if (superclass) { // 有無父類情況,傳遞 instanceStart cls_ro_w->instanceStart = superclass->unalignedInstanceSize(); meta_ro_w->instanceStart = superclass->ISA()->unalignedInstanceSize(); cls->setInstanceSize(cls_ro_w->instanceStart); meta->setInstanceSize(meta_ro_w->instanceStart); } else { cls_ro_w->instanceStart = 0; meta_ro_w->instanceStart = (uint32_t)sizeof(objc_class); cls->setInstanceSize((uint32_t)sizeof(id)); meta->setInstanceSize(meta_ro_w->instanceStart); } // 記錄 Class 名 cls_ro_w->name = strdup(name); meta_ro_w->name = strdup(name); // 屬性修飾符佈局 // ivarLayout strong引用表 cls_ro_w->ivarLayout = &UnsetLayout; // weakIvarLayout weak引用表 cls_ro_w->weakIvarLayout = &UnsetLayout; // 通過獲取到的 cls 指標,呼叫 isa 初始化命令 cls->initClassIsa(meta); if (superclass) { // 如果擁有父類,更新 meta 的 isa 指向 meta->initClassIsa(superclass->ISA()->ISA()); // 更新 cls 父類資訊 cls->superclass = superclass; // meta 的父類指向父類的 isa meta->superclass = superclass->ISA(); // 向父類中增加該類資訊 addSubclass(superclass, cls); // 向父類的 isa 中記錄該資訊 addSubclass(superclass->ISA(), meta); } else { // 為 meta 初始化 isa 資訊 meta->initClassIsa(meta); // 由於該類為 rootclass,無父類資訊 // 讓其父類指向 Nil cls->superclass = Nil; // 令 meta 的父類指向 cls meta->superclass = cls; // 向 cls 中增加 meta 指標資訊 addSubclass(cls, meta); } |
在語法上需要注意這幾個地方:
- ivarLayout 和 weakIvarLayout:分別記錄了哪些 ivar 是 strong 或是 weak,都未記錄則為 __unsafe_unretained 的物件型別。
strdup(const char *s)
:可以複製字串。先回撥用 malloc() 配置與引數 s 字串的內容複製到該記憶體地址,然後把該地址返回。返回值是一個字串指標,該指標指向複製後的新字串地址。若返回 NULL 表示記憶體不足。
在上述程式碼中,會發現一個問題。當建立的 Class 沒有父類的時候,其 meta 是指向 cls 自身的,而 meta 原本就是 cls 的子類,所以在這裡,使得一個基類物件的 isa 指標形成自環指向自身。下圖用 NSObject
舉例(其指標下方有原始碼標註):
而當建立 Class 擁有父類的時候,isa 和 superclass 都要指向父類,而對應的 meta 通過兩次的 isa 查詢找到根類 meta ,更新指向。用 NSError
來舉例:
其中要之一 meta 的 isa 操作 meta->initClassIsa(superclass->ISA()->ISA());
,這不是單純的指向父類 meta 的操作,而是指向根類的 meta 。
Talk is cheap! ,用程式碼來實驗一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
- (void)viewDidLoad { [super viewDidLoad]; DGObject *desgard = [[DGObject alloc] init]; Class cls = object_getClass(desgard); NSLog(@"%s\n", class_getName(cls)); // DGObject NSLog(@"%d\n", class_isMetaClass(cls)); // 0 Class meta = object_getClass(cls); NSLog(@"%s\n", class_getName(meta)); // DGObject NSLog(@"%d\n", class_isMetaClass(cls)); // 0 Class meta_meta = object_getClass(meta); NSLog(@"%s\n", class_getName(meta_meta)); // NSObject NSLog(@"%d\n", class_isMetaClass(meta_meta)); // 1 } |
通過以上分析,我們知道了 metaclass 是一個 Class ,而這個 Class 是作為基礎 Class 的所屬類,用於構建繼承網圖,使得 runtime 訪問相關聯的 Class 更加的快捷方便。在 What is a meta-class in Objective-C? 一文中,作者將其稱作 NSObject繼承體系(NSObject hierarchy) ,其根類所有的 Class 和相關 metaclass 都是聯通的,並且在根類 NSObject 中的成員方法,對其體系中的所有 Class 和對應 metaclass 也是操作有效的。
metaclass 的存在,將物件化的例項、類組織成了一個連通圖,進一步靈活了 ObjC 的動態特性。
至此,我們通過原始碼,系統瞭解了 isa 指標對於物件的資訊記錄,以及 metaclass 的結構和作用。後續博文將會探究 retain
和 release
方法,敬請期待。