用 isa 承載物件的類資訊

發表於2016-10-07
 Effective Objective-C 2.0 – 52 Specific Ways to Improve Your iOS and OS X Programs 一書中,tip 14 中提到了,執行時檢查物件型別自省 (Introspection) 特性。那麼先來說說 自省反射 的定義是什麼。

自省與反射的簡單認識

第一次聽說這兩個概念,是在 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) 是指計算機程式可以在執行時動態監測並修改它自己的結構和行為,比如值、後設資料、屬性和函式等的能力。通過反射,可以在執行時動態監測、生成、修改自己實際執行的等效程式碼。

兩種方法可以達到同樣的效果。但是,第一種方法是我們所說的常規方法,建立 HelloClass 這個 class 的一個例項,然後呼叫其中的方法。第二種我們用到了反射機制,通過 globals() 這個字典中來查詢 HelloClass 這個類,並加以引數進行例項化於 obj,之後通過 getattr 函式獲得 say_hello 方法,傳參呼叫。

反射的好處在於,class_namemethod 變數的值可以在執行時獲取或者修改,這樣可以動態地改變程式的行為。

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 方法。

通過 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

會發現在 objc_object 這個基類中只有一個成員,即 isa_t 聯合體(union) 型別的 isa 成員。而對於類物件的定義,可以從 objc_class 檢視其結構:

runtime 的開源作者怕學習者不知道 isa 已經從 objc_object 繼承存在,用註釋加以提示。

其實,開發中所使用的類和例項,都會擁有一個記錄自身資訊的 isa 指標,只是因為 runtime 從 objc_object 繼承出的,所以不會顯式看到。

11objc_object_structure

需要知道的是,class_data_bits_t 中存有 Class 的對應方法,具體如何儲存,會在後續的文中記錄。

isa 優化下的資訊記錄

isa 是一個聯合體型別,其結構如下:

該定義是在 __arm64__ 環境下的 isa_t 聯合體結構。因為 iOS 應用為 __arm64__ 架構環境。

可以看到在 isa_t 聯合體中不僅僅表明了指向物件的地址資訊,而且這個 64 位資料還記錄了其 bits 情況以及該例項每一位儲存的物件資訊。來驗證一下(記住要使用真機除錯, real device 和 simulator 的架構環境是有一定區別):

輸出結果為:

首先先來看一下這 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_associndex 兩個位都是 1 。根據程式碼我們可以知道我們手動為其設定了 associated object,所以以上的含義表是正確的。這裡詳細的再說一下 indexed 的含義。

12isa-bits

isa 初始化行為,indexed 以及 magic 段的預設值

isa 指標會通過 initIsa 來初始化。

在以上程式碼中,可以看到在一個 isa_t 結構中,magic 段是一個固定值,在 arm64 架構下其值為 0x1a,而在 x86 下則為 0x1d,筆者猜測這一位也有判斷架構型別之意。而觀察 isa 初始化的呼叫棧,可以發現是 callAlloc 函式進行呼叫。這段程式碼的解讀,將放在以後的文中。

ISA() 獲取非 Tagged Pointer 物件

13isa

從中發現,其有效區域也就是 isa_t 中的 shiftcls 區域。而且這種掩碼方式,也是從 isa_t 中查詢資訊的主要方式,再很多方法中可以看見類似的做法。

isa 的主地址檢索

無論在新舊版本的 Objective-C 中,都會有 isa 指標來記錄類的資訊。而在現在的 runtime 庫中,由於 64 位的優勢,使用聯合體又增加了類資訊記錄的補充。而對於 isa 的主要部分,其記錄的主要資訊是什麼呢?

在之前的一些文章中,筆者通過了 ObjC 的訊息轉發機制稍微提及了一些關於 isa 的知識,可以參考這篇文章 objc_msgSend訊息傳遞學習筆記 – 物件方法訊息傳遞流程 。 在訊息傳遞的主要流程中,最重要的一個環節就是快速查詢 isa 操作 GetIsaFast ,其中要繼續的搜尋所屬 Class 的方法列表(所有成員方法所對應的 Hash Table)。可見 isa 記錄的地址資訊和當前例項的 Class 有直接關係。

下面通過實驗來驗證我們的猜測:

在真機上執行該程式碼片段,可以發現其輸出的結果:

在輸出 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? ):

這段程式碼所做的事情是在 runtime 時期建立 NSError 的一個子類 RuntimeErrorSubclassobjc_allocateClassPair 方法會建立一個新的 Class ,然後取出 Class 的物件,使用 class_addMethod 方法,為該 Class 新增方法,需要開發者傳入新增方法的 Class 、方法名、實現函式、以及定義該函式返回值型別和引數型別的字串。最後呼叫 objc_registerClassPair 對其進行註冊即可。

要點:在呼叫 objc_allocateClassPair 方法增加新的 Class 的時候,可以呼叫 class_addIvar 增加成員屬性和 objc_registerClassPair 增加成員方法。

objc_allocateClassPair 方法可以說是 objc_initializeClassPair_internal 的方法入口,其主要的功能是 根據 superclass 的資訊和 Class 中的一些標記成員來確定 cls 和 meta 指標的指向,並呼叫 addSubclass 方法將其加入到 superclass 中

通過 objc_i nitializeClassPair_internal 方法中,呼叫 meta -> initClassIsa(); 來初始化 isa 指標。下面通過 objc_initializeClassPair_internal 來看看 isa 指標和 meta 的初始化方式。

在語法上需要注意這幾個地方:

  • ivarLayout 和 weakIvarLayout:分別記錄了哪些 ivar 是 strong 或是 weak,都未記錄則為 __unsafe_unretained 的物件型別。
  • strdup(const char *s):可以複製字串。先回撥用 malloc() 配置與引數 s 字串的內容複製到該記憶體地址,然後把該地址返回。返回值是一個字串指標,該指標指向複製後的新字串地址。若返回 NULL 表示記憶體不足。

在上述程式碼中,會發現一個問題。當建立的 Class 沒有父類的時候,其 meta 是指向 cls 自身的,而 meta 原本就是 cls 的子類,所以在這裡,使得一個基類物件的 isa 指標形成自環指向自身。下圖用 NSObject 舉例(其指標下方有原始碼標註):

14isa_obj_x

而當建立 Class 擁有父類的時候,isa 和 superclass 都要指向父類,而對應的 meta 通過兩次的 isa 查詢找到根類 meta ,更新指向。用 NSError 來舉例:

15isa_metaclass

其中要之一 meta 的 isa 操作 meta->initClassIsa(superclass->ISA()->ISA()); ,這不是單純的指向父類 meta 的操作,而是指向根類的 meta 。

Talk is cheap! ,用程式碼來實驗一下:

通過以上分析,我們知道了 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 的結構和作用。後續博文將會探究 retainrelease 方法,敬請期待。

相關文章