Runtime源代碼解讀(實現面向對象初探)

2019-09-26html

The Objective-C runtime is a runtime library that provides support for the dynamic properties of the Objective-C language, and as such is linked to by all Objective-C apps. Objective-C runtime library support functions are implemented in the shared library found at /usr/lib/libobjc.A.dylib.(Objective-C Runtimegit

文章的開頭是Apple Documentation對runtime的定義,很官方也很抽象。我的對runtime的理解是:在狹義上,runtime用面向過程的C語言實現了面向對象特性,也就是實現了類和對象;在廣義上,runtime實現了Objective-C語言的動態特性(深刻Objective-C的動態特性)。動態特性主要包括動態類型(Dynamic typing)、動態綁定(Dynamic binding)和動態加載(Dynamic loading)。與之相對應,runtime具體實現了ClassNSObject抽象類型、Class的繼承鏈、方法的響應鏈、方法動態解析以及消息轉發流程、運行時動態加載類型/方法/屬性等動態元素。github

動態加載類庫屬於dyld範疇(源代碼),固然runtime爲了實現動態特性須要依賴dyld的API。objective-c

1、類和對象

若是要找出Objective-C中最動態的兩個類型,那必定是Class(類)和id(對象的引用),二者也恰是runtime實現面向對象的基礎。經過#import <objc/runtime.h>#import <objc/objc.h>語句跳轉到runtime.h、objc.h頭文件,能夠從中找到類和對象定義的代碼:算法

  • objc_class結構體表示類。其中super_class指針指向父類用於實現類的繼承特性,instanceSize記錄類的實例佔用內存大小,ivars描述類的成員變量列表,methodLists保存類的方法列表,protocols保存類所遵循的協議列表,cache是方法緩存用於記錄最近使用的方法,其餘成員能夠暫不關注;
  • Class是類的引用;
  • objc_object結構體表示對象,僅包含isa指針,指向對象的類(新版本runtime中isa指針不必定簡單指向對象的類);
  • id表示指向objc_object結構體的指針,也就是對象引用,本質是對象的地址;

重要提醒:從#import <objc/runtime.h>#import <objc/objc.h>語句跳轉到的runtime.hobjc/objc.h頭文件中,都是舊版本runtime的代碼。凡新舊代碼處理邏輯存在不一樣之處的,文中有特別聲明。數組

/* 對象的引用的定義 */
typedef struct objc_object *id;

/** 對象的定義 */
struct objc_object {
    Class _Nonnull isa;  // 對象的類
};

/** 類的定義 */
typedef struct objc_class *Class;
struct objc_class {
    Class _Nonnull isa;  // 元類

    Class _Nullable super_class;  // 父類
    const char * _Nonnull name;   // 類名
    long version;
    long info;
    long instance_size;  // 實例的大小
    struct objc_ivar_list * _Nullable ivars;  // 成員列表
    struct objc_method_list * _Nullable * _Nullable methodLists;  // 方法列表
    struct objc_cache * _Nonnull cache;  // 方法緩存
    struct objc_protocol_list * _Nullable protocols;  // 所遵循的協議列表
};
複製代碼

objc_object結構體只有一個指針類型的isa成員,也就是說一個objc_object僅佔用了8個字節內存,但並不說明對象僅佔8個字節內存空間。當調用類的allocallocWithZone方法構建對象時,runtime會分配類的instanceSize大小的連續內存空間用於保存對象。在該內存塊的前8個字節寫入類的地址,其他空間用於保存其餘成員變量。最後構建方法返回的id其實是指向該內存塊的首地址的指針。緩存

注意:以上是舊版本runtime構建對象的處理,在新版本runtime中略有不一樣。緣由是新版本runtime從新定義了isa指針數據結構,並且在引入Non-fragile instance variable機制後instanceSize再也不是固定的值,這些會在後續的文章中介紹。bash

1.1 繼承鏈

根據objc_classsuper_class成員能夠創建起類的繼承結構,因爲類的super_class指向單一的類,這是Objective-C之因此是單繼承語言的緣由。類的繼承鏈的頂端是是根類(root class),一般是NSObject,根類的super_class指向NULL。數據結構

objc_objectobject_class結構體均包含isa指針,在runtime中類也是對象,是對象就會有類型,類的類型是元類(meta class)object_classisa指針指向元類。元類也是用objc_class結構體保存,也就是說元類也是類。元類也具備繼承特性,繼承鏈的頂端是根元類(root meta class)。根元類的是根類的元類,根元類的super_class指向根類。判斷objc_class是否爲元類的方式有兩種:架構

  • 根據version的值,全部元類的version值爲6(新版本runtime中是7),非元類爲0
  • 元類的isa指針指向根元類,包括根元類本身。

至此,總結出runtime的繼承結構以下圖所示。

Runtime中類的繼承結構.jpg

2、成員變量

objc_ivar結構體表示類的成員變量。

  • objc_ivarivar_name爲成員變量名;
  • ivar_type爲成員變量的類型編碼(官方文檔),用字符串表示成員變量的數據類型,例如:'@'表示成員變量保存對象的引用,可使用@encode()以類型爲參數查詢類型編碼;
  • offset爲成員變量的在實例內存塊中的偏移。
struct objc_ivar {
    char * _Nullable ivar_name;
    char * _Nullable ivar_type;
    int ivar_offset;
#ifdef __LP64__
    int space;
#endif
} 
複製代碼

objc_ivar_list結構體表示類的成員變量列表。objc_ivar_listobjc_ivar結構體的數組ivar_list用於保存類的全部成員變量,ivar_count爲成員變量列表的長度。

struct objc_ivar_list {
    int ivar_count;
#ifdef __LP64__
    int space;
#endif
    /* variable length structure */
    struct objc_ivar ivar_list[1];
} 
複製代碼

2.1 成員變量佈局

類構建成員變量列表的過程,包含肯定成員變量佈局(ivar layout) 的過程。成員變量佈局就是定義對象佔用內存空間中哪塊區域保存哪一個成員變量,具體爲肯定類的instanceSize、內存對齊字節數、成員變量的offset。類的繼承鏈上全部類的成員變量佈局,共同構成了對象內存佈局(object layout)。成員變量佈局和對象內存佈局的關係能夠用一個公式表示:類的對象內存佈局 = 父類的對象內存佈局 + 類的成員變量佈局。成員變量佈局的計算法則以下:

  • 成員變量的偏移量offset必須大於等於父類的instanceSize
  • 成員變量的佈局和結構體的對齊遵循一樣的準則,類的對齊字節數必須大於等於父類的對齊字節數。例如,佔用4字節的int類型成員變量的起始內存地址必須是4的倍數,佔用8字節的id類型成員變量的起始內存地址必須是8的倍數;
  • instanceSize的計算公式是類的instanceSize = 父類的instanceSize + 類的成員變量在實例中佔用的內存空間 + 對齊填補字節instanceSize必須是類的對齊字節數的整數倍;

NSObject類的定義中,包含一個Class類型的isa成員,所以實際上isa指針的8個字節內存空間也屬於對象內存佈局的範疇。

舉個具體的例子:用如下代碼定義一個繼承NSObjectTestObjectLayout類:

@interface TestObjectLayout : NSObject{
    bool bo;
    int num;
    char ch;
    id obj;
}
@end

@implementation TestObjectLayout

@end
複製代碼

其成員變量佈局的計算過程以下:

  • instanceSize初始化爲父類的instanceSize的值,並按父類的對齊字節數對齊。父類NSObject僅包含isa一個成員變量,isa佔用8個字節offset爲0,所以父類instanceSize爲8,按8字節對齊;
  • instanceSize初始化,按照對齊法則依次添加成員變量,並更新instanceSizebo按字節對齊(注意bool類型佔用1字節空間並非1位),偏移量爲8,instanceSize更新爲16;
  • num按4字節對齊,偏移量爲12,instanceSize仍爲16;
  • ch按字節對齊,偏移量爲16,instanceSize更新爲24;
  • obj按8字節對齊,偏移量爲24,instanceSize更新爲32。最終肯定instanceSize爲32字節,按8字節對齊。

由上文對象內存佈局計算公式能夠看出,計算類的對象內存佈局其實是從根類開始遞推計算繼承鏈上全部類成員變量佈局的過程(也可視爲從類遞歸到根類)。假設TestObjectLayout對象的起始內存地址爲0x100BB134000,則按照上述步驟可計算該對象內存佈局以下圖所示:

實例內存圖.jpg

類的構建過程之因此要計算類的成員變量佈局,是由於構建一個對象時須要肯定須要爲對象分配的內存空間大小,且構建對象僅返回對象的內存首地址,而經過成員變量的offset結合ivar_type,則能夠輕而易取地經過對象地址定位到保存成員變量的內存空間。

注意:新版本runtime的成員變量列表保存位置有所變化。

3、方法

objc_method結構體表示方法,其中:

  • SEL類型的method_name表示方法名;
  • 字符串類型的method_types表示方法的類型編碼,類型編碼描述了方法的返回值類型以及參數類型;
  • IMP類型的method_imp表示方法的實現。
/* 對象的方法的定義 */
struct objc_method {
    SEL method_name;      // 方法選擇器,即方法名
    char *method_types;   // 方法的類型編碼
    IMP method_imp;       // 方法的實現,即方法的函數指針、方法的IMP
}  

/* 方法選擇器定義 */
typedef struct objc_selector *SEL;

/* 方法實現定義 */
typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...); 

複製代碼

objc_method_list結構體表示方法列表。objc_method_list結構與objc_ivar_list相似

struct objc_method_list {
    struct objc_method_list *obsolete;

    int method_count;
#ifdef __LP64__
    int space;
#endif
    /* variable length structure */
    struct objc_method method_list[1];
} 
複製代碼

注意到類的定義中,方法列表methodLists的類型爲struct objc_method_list * _Nullable * _Nullable,所以類的方法列表的保存形式是二維數組,也就是數組的數組。

注意:新版本runtime的方法列表數據結構有較大變化。

3.1 方法的響應鏈

類包含實例方法(instance method)和類方法(class method),兩種方法是保存在徹底不一樣的地方。實例方法保存在類的方法列表中,類方法保存在元類的方法列表中

調用實例方法和類方法,接收消息的對象是不同的,實例方法接收消息的對象是實例,類方法接收消息的對象是類,例如:[someObj init][NSObject alloc]。這是由於兩種方法保存在徹底不一樣的地方,實例方法保存在「類」的方法列表中,類方法保存在「元類」的方法列表中。那麼 runtime 響應實例方法和類方法的流程有什麼不同呢?用下圖能夠表示,其中:

  • 子類SubClass包含subClassInstanceMethod實例方法,圖中橘色線表示該消息的響應過程;
  • 根類RootClass包含rootInstanceMethod實例方法,圖中洋紅色線表示該消息的響應過程;
  • 根類RootClass包含rootClassMethod類方法,圖中藍色線表示該消息的響應過程;

基本消息響應鏈.jpg

綜上,runtime 基於繼承結構的基本響應鏈都有相同的結構:一、根據接收消息的對象的isa指針找到對象的類;二、從對象的類開始沿着其superclass串聯起來的繼承鏈,搜索可響應該消息的類,直到根類爲止;三、若繼承鏈上沒有可響應該消息的類,則開始消息轉發流程。

4、屬性、協議、分類

runtime.h頭文件中公佈的屬性、協議、分類等元素的相關信息不多,但這些均可以肯定是類須要保存的元數據。這裏僅簡單收集其中公佈的代碼。

//屬性相關數據結構
typedef struct objc_property *objc_property_t;

typedef struct {
    const char * _Nonnull name;  // 屬性名
    const char * _Nonnull value;  // 特性的值
} objc_property_attribute_t;  // 屬性的特性

//協議相關數據結構
#ifdef __OBJC__
@class Protocol;
#else
typedef struct objc_object Protocol;
#endif

struct objc_protocol_list {
    struct objc_protocol_list * _Nullable next;
    long count;
    __unsafe_unretained Protocol * _Nullable list[1];
};

//分類相關數據結構
typedef struct objc_category *Category;

struct objc_category {
    char * _Nonnull category_name;  //分類名
    char * _Nonnull class_name;  //分類所擴展的類名
    struct objc_method_list * _Nullable instance_methods;  //實例方法列表
    struct objc_method_list * _Nullable class_methods;  //類方法列表
    struct objc_protocol_list * _Nullable protocols; //協議列表
}
複製代碼

5、總結

前文不止一次提到,本文引用的runtime代碼是舊版本的代碼,之因此要從分析舊版本代碼入手,是由於首先新版本代碼在主體架構上仍然沿用了舊版本,只是在部分數據結構和實現細節上作了優化,分析舊版本接口文件已經足以對runtime框架有一個整體的認知,這樣有利於對龐大的runtime源代碼工程,有選擇性、有針對性的學習;另外,從存在缺陷的舊代碼入手,有助於加深新版本之因此要作優化的緣由,並從中借鑑到代碼優化的一些經驗方法。

相關文章
相關標籤/搜索