Runtime整理(一)——Runtime的介紹和知識點

Runtime整理(一)——Runtime的介紹和知識點


前言

    本篇文章是runtime知識點的整理,以便於從此學習和快速查找。html

本篇文章分爲2個章節:ios

  • (一)Runtime的介紹和知識點
  • (二)Runtime包含的全部函數

目錄

  • 介紹
  • runtime.h
  • 消息發送和轉發
  • 常見問題
  • 使用案例

介紹

在講runtime以前,咱們先來明白什麼是動態語言。面試

動態語言:是指程序在 運行時能夠改變其結構:新的函數能夠被引進,已有的函數能夠被刪除等在結構上的變化,類型的檢查是在運行時作的,優勢爲方便閱讀,清晰明瞭,缺點爲不方便調試。 動態語言-百度百科

咱們都知道OC是一門動態語言,由於它objective-c

1.能夠在運行時新增方法(使用class_addMethod爲類新增方法)

2.能夠改變類的結構(使用class_replaceMethod替換方法的實現等)編程

3.運行時檢查類型(運行時多態,id類型)segmentfault

動態語言特性意味着OC不只須要一個編譯器,還須要一個運行時系統Objc Runtime來實現上述的操做。Objc Runtime實際上是一組API,它基本上是用C和彙編寫的,是它讓C語言得到了面向對象的能力而蛻變爲OC,並使OC語言擁有了動態語言特性。它主要功能是下面兩點:緩存

1.構建和改變類的結構(在objc/runtime.h文件中定義)

2.處理運行時消息的發送(在objc/message.h文件中定義)app

總結:OC中的runtime是OC語言中實現面向對象和動態語言特性的一組API。ide


runtime.h

咱們來看一下runtime.h文件,看它是如何構建出OC中的類,並經過哪些方法來改變類結構的。函數

OC類的構建

若要構建出類,咱們要先了解類,咱們知道類的概念是面向對象設計實現封裝的基礎,面向對象編程的三大特性是:封裝、繼承、多態

1.封裝:抽象出數據類型和數據操做構成一個總體。

那麼咱們若是想要構建出類,就須要類中有這些:成員變量、方法

2.繼承:類之間的父子關係,例如A is a B(A屬於B,B是父類,A是子類)isa的由來

這就要求咱們創建起的類之間的父子關係

3.多態:引用變量指向的具體類型和經過該引用變量發出的方法調用在編程時不肯定。通俗點舉例說明就是A類型指針能夠指向B、C、D類型的對象,在編程時你沒法知道這個指針指向的具體是哪一個對象,這種A可以指向B、C、D的特性就是多態。

這就要求咱們的類和實例可以正確的響應消息。這點會在後面的message.h中講

咱們來看runtime中關於對象(Object)和類(Class)的定義與結構:

// 如下刪去了不重要的代碼

<objc/objc.h>
typedef struct objc_class *Class;
struct objc_object {
    Class _Nonnull isa;
};

<objc/runtime.h>
struct objc_class {
    // 指向元類的指針
    Class _Nonnull isa;
    
    // 父類
    Class _Nullable super_class;
    // 類名
    const char * _Nonnull name;
    // 類的版本信息,默認爲0
    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; 
};

對於封裝特性來講:咱們能夠看到OC中的類實際上是結構體,而類Class其實是一個objc_class結構體指針,成員變量存放在ivars鏈表中,方法存放在methodLists鏈表中,此外還有包含了類名、協議鏈表等其餘內容。

對於繼承特性來講:咱們能夠看到OC中的類中存在一個isa指針和super_class指針,經過他們創建起了類之間的父子關係。

如此,OC中類的雛形就初步構建出來了。下面分別來詳細介紹他們

類和實例和父類之間的關係

先上經典的圖,如下內容請結合圖來一塊兒食用。
clipboard.png

經過方法調用的過程咱們能夠了解到類和實例和其父類之間的關係。

實例的方法調用過程:

1.當一個實例調用方法時,運行時庫會根據實例對象的isa指針找到這個實例對象所屬的類,而後會在類的methodLists方法鏈表中搜尋是否存在有這個方法。

2.若是在類的methodLists中並未搜索到須要執行的方法,會經過super_class指針找到其父類,並在父類的methodLists中搜尋,而後一直向上尋找一直到根類(也就是NSObject),在過程當中若是找到即運行這個方法。

過程爲:

實例 --(isa)--> 類 --(super_class)--> 父類  --(super_class)--> ...  --(super_class)--> 根類(NSObject) --(super_class)--> nil

能夠說:isa指針創建起了實例與他所屬的類之間的關係,super_class指針創建起了類與其父類之間的關係。

類方法的調用過程:

1.當一個類調用方法時,運行時庫會根據類對象的isa指針找到這個類的元類(metaClass),而後會在元類的methodLists方法鏈表中搜尋是否存在有這個方法。

2.若是在元類的methodLists中並未搜索到須要執行的方法,會經過super_class指針找到這個元類的父類,並在它的methodLists中搜尋,而後一直向上尋找一直到根元類(也就是NSObject的元類),在過程當中若是找到即運行這個方法。

過程爲:

類 --(isa)--> 元類 --(super_class)--> 父類  --(super_class)--> ...  --(super_class)--> 根元類(NSObject的元類) --(super_class)--> 根元類的父類(NSObject)--(super_class)--> nil

能夠說:isa指針創建起了類與其元類之間的關係,super_class指針創建起了元類與其父類之間的關係。

那麼元類(metaClass)是什麼:

咱們知道類其實也是一個對象,他存放了實例的信息(成員變量、實例方法等),既然是對象就會有他所屬的類,元類(metaClass)就是類對象的類,它存儲了類變量的信息(類方法等)。
其實元類也是類對象,你又會問了那麼元類的類是什麼?全部元類的類都是根元類(也就是NSObject的元類),根元類自己也不例外它的類是其自身,以此來造成閉環。

objc_class中objc_cache的做用:

一個類每每大部分的方法都不會被調用到,可是每次調用方法都須要遍歷一次 objc_method_list,這種方式不太合理效率低。若是把常常被調用的函數緩存下來,那能夠大大提升函數查詢的效率。這也就是 objc_cache 作的事情,調用方法時會將 method_name 做爲 key ,method_imp 做爲 value 存起來。當再次調用該方法時,能夠直接在 cache 裏找到,避免去遍歷 objc_method_list。

變量的結構

成員變量的定義:
struct objc_ivar {
    // 變量名
    char * _Nullable ivar_name;
    // 變量類型
    char * _Nullable ivar_type;
    // 基地址偏移字節
    int ivar_offset;
#ifdef __LP64__
    int space;
} 

成員變量列表的定義:
struct objc_ivar_list {
    int ivar_count;
#ifdef __LP64__
    int space;
#endif
    /* variable length structure */
    struct objc_ivar ivar_list[1];
}

方法的結構

// 方法的定義
struct objc_method {
    // 方法名
    SEL _Nonnull method_name;
    // 方法返回值和參數描述字符串
    char * _Nullable method_types;
    // 方法實現
    IMP _Nonnull method_imp;
};

// 方法列表的定義
struct objc_method_list {
    struct objc_method_list * _Nullable obsolete;
    
    int method_count;
#ifdef __LP64__
    int space;
#endif
    /* variable length structure */
    struct objc_method method_list[1];
}

對於方法幾個類型的解釋:

SEL:方法選擇器。不一樣類中相同名字的方法所對應的 selector 是相同的。
IMP:方法實現。是一個函數指針,指向方法的實現。
Method:方法的結構體,其中保存了方法的名字,實現和類型描述字符串

對於method_types的解釋:
TypeEncoding

//編碼值   含意
//c     表明char類型
//i     表明int類型
//s     表明short類型
//l     表明long類型,在64位處理器上也是按照32位處理
//q     表明long long類型
//C     表明unsigned char類型
//I     表明unsigned int類型
//S     表明unsigned short類型
//L     表明unsigned long類型
//Q     表明unsigned long long類型
//f     表明float類型
//d     表明double類型
//B     表明C++中的bool或者C99中的_Bool
//v     表明void類型
//*     表明char *類型
//@     表明對象類型
//#     表明類對象 (Class)
//:     表明方法selector (SEL)
//[array type]  表明array
//{name=type…}  表明結構體
//(name=type…)  表明union
//bnum  A bit field of num bits
//^type     A pointer to type
//?     An unknown type (among other things, this code is used for function pointers)

總結

runtime.h文件包含了不少內容,總結來講有如下幾點:

1.定義:
對象、類、父類、元類、方法、屬性、協議、分類的定義。

2.結構:
isa指針,super_class指針。對象的的isa指針指向它的類,類的isa指針指向它的元類,類和元類的super_class指針指向他們的父類。

3.註冊和建立:
建立新類、銷燬類、註冊新類、爲新類添加方法、變量、屬性、協議等;
建立協議、註冊協議、爲協議添加方法。

4.獲取和設置:
獲取Object中的信息:object所屬的類
獲取Class中的信息:類名、父類、元類、成員變量列表、屬性列表、方法列表、協議列表
獲取Ivar中的信息:變量名、類型
獲取Method中的信息:方法名、方法實現、返回值和參數描述字符串
獲取Protocol的信息:獲取協議名
獲取屬性的信息:屬性名、屬性列表、屬性值

5.功能方法:
方法交換、方法替換、獲取和設置實例的關聯對象等


消息發送和轉發

message頭文件中定義了一組消息發送和轉發相關的函數。

OC中的消息發送

在不少語言,好比 C ,調用一個方法其實就是跳到內存中的某一點並開始執行一段代碼。沒有任何動態的特性,由於這在編譯時就決定好了。而OC中全部對象或類的方法調用都是以消息發送的形式進行的。方法調用會被編譯器轉化爲objc_msgSend(receiver, selector)函數的調用,而後開始消息發送的過程。過程以下圖:

clipboard.png

類和實例和父類之間的關係小結中提到了方法的調用過程,這裏再也不贅述。

OC中的消息轉發

若是從本類到父類一層層的找也沒有找到對應方法的話,就會走消息轉發。

1.方法解決:在消息轉發前會先走本類的方法+resolveInstanceMethod:(處理找不到的實例方法)或+resolveClassMethod:(處理找不到的類方法)。在這個方法裏面可使用class_addMethod函數向實例添加方法,使得消息發送可以正常進行。

第一步主要是爲了讓咱們給對象添加方法。該方法的返回值不管爲YES仍是NO,只要沒有正確處理都會接着走轉發流程第二步
若是是類方法未正確處理,則不會再走後面的轉發流程,會直接crash。

2.快速轉發:該步會響應forwardingTargetForSelector:方法,返回一個指定的接收者

返回nil,走轉發流程第三步
返回非nil對象,走返回對象的消息發送流程,本次消息轉發至此結束
若是返回的對象可處理該方法,哪怕是他本身沒有該方法可是父類有,則ok(其實就是消息發送流程)
若是返回的對象沒法處理該方法,接下來走返回對象的消息轉發流程

3.完整轉發:若是上一步沒有處理者,那麼會響應最後這兩個方法處理轉發,methodSignatureForSelector:返回一個方法簽名,forwardInvocation:處理消息執行

methodSignatureForSelector若是返回nil,直接crash
methodSignatureForSelector返回只要是非nil且是NSMethodSignature類型的任何值都ok,
forwardInvocation的參數anInvocation中的signature即爲上一步返回的方法簽名,
forwardInvocation的參數anInvocation中的selector爲致使crash的方法,target爲致使crash的對象
forwardInvocation方法能夠啥都不處理,或者作任何不會出問題的事,至此本次消息轉發結束,也不會crash。


常見問題

[super class]和[self class]
解釋:主要是由於class方法的實如今NSObject,子類也都沒有重寫class方法,而且class方法的內部實現代碼是return objc_getClass(self)而不是return objc_getClass("NSObject")。若是把class方法重寫掉一切就都會變了。

iOS元類面試一題
解釋:因爲NSObject的元類的父類是NSObject,因此[NSObject foo]這裏調用的是- (void)foo;方法而不是+ (void)foo;方法。能夠將類方法改成+ (int)foo;,這個時候會發現NSObject只能調用void foo。而後再將類方法改成+ (int)foo:(int)a;,這個時候就會發現NSObject中能夠調用兩個方法:void foo、int foo:(int),若是此處這樣寫[NSObject foo:1],那麼結果會由於找不到方法實現而crash。NSObject的元類的父類是NSObject是最主要的緣由,若是換個類這樣搞就不行了。


使用案例

Runtime 10種用法
給分類(Category)添加屬性


後續

Runtime整理(二)——Runtime包含的全部函數


參考文章

Objective-C Runtime 運行時之一:類與對象
iOS-runtime通篇詳解-上
Objective-C Runtime
iOS開發-Runtime詳解(簡書)

相關文章
相關標籤/搜索