天天看點

iOS深入學習 - Runtime

SmallTalk 與 C 的融合–Objective-C

三十幾年前,Brad Cox 和 Tom Love在主流且高效的C語言基礎上,借鑒Smalltalk的面向對象與消息機制,想要搞出一個易用且輕量的C語言擴充,但C和Smalltalk的思想和文法格格不入,比如在Smalltalk中一切皆對象,一切調用都是消息:

再比如用一個工廠方法來執行個體化一個對象:

p := Person name: 'sunnyxx' age: 
           

在當時來看,一個具有面向對象功能的C語言真的是非常有吸引力,但必須得解決消息文法的轉換,于是乎他們開發了一個Preprocessor(預編譯程式),去解析Smalltalk風格的文法,再轉換成C語言的代碼,進而和其他C代碼一起編譯。想法很美好,但Smalltalk文法裡又是空格、又是冒号的,萬一遇到個什麼複雜嵌套調用,文法解析多難寫呀,于是乎他們想,把消息兩邊加個中括号吧,這樣Parser寫起來簡單多了:

這就造就了Objective-C奇怪的中括号、冒号四不像文法,這怎麼看都是個臨時的方案,但當時可能是唯一的方法,借用已有的C的編譯器比重造一個成本低多了,而且完全相容C語言。随着這幾年Apple開發的火熱,Objective-C越來越成為Apple不爽的地方,先是恨透了在GCC上給Objective-C加支援,自己重建了個Clang,後是幹脆重新發明了Swift來徹底代替,用了30年的時間終于還完了技術債。

雖然有了個Preprocessor,但隻能做到把Smalltalk風格的代碼分析并轉譯成C,還需要解決兩個問題:

1. C語言上實作一個OOP對象模型

2. 将Smalltalk風格的Message機制轉換成C函數調用

對象模型的設計倒很省事,直接搬照Smalltalk的就好了:如Class/Meta Class/Instance Method/Class Method這些概念,還有一些關鍵字如self/super/nil等全都是Smalltalk的。這步轉換在Preprocessing過程中就可以完成,因為重寫後的Class就是原原本本的C語言的Struct,隻需要按Smalltalk中“類-元類”的模型設定好即可,無需額外的支援。

消息機制就不一樣了,要實作向一個target(class/instance)發送消息名(selector)動态尋找到函數實作位址(IMP)并調用的過程,還要處理消息向父類傳遞、消息轉發(Smallltalk中叫“Message-Not-Understood”)等,這些行為無法在Preprocessing或Build Time實作,需要提供若幹運作時的C函數進行支援,所有這些函數打個包,便形成了最原始的Runtime。

是以最初的Objective-C = C + Preprocessor + Runtime

注:GCC中一開始用預處理器來支援Objective-C,之後作為一個編譯器子產品,再後來都交給了Clang實作。

作為單純的C語言擴充,Runtime中隻要實作幾個最基礎的函數(如objc_msgSend)即可,但為了建構整套Objective-C面向對象的基礎庫(如Foundation),Runtime還需要提供像NSObject這樣的Root Class作為面向對象的起點、提供運作時反射機制以及運作時對Class結構修改的API等。再後來,即便是Objective-C語言本身的不斷發展,新語言特性的加入,也不在乎是擴充Clang和擴充Runtime,比如:

  • ARC:編譯器分析對象引用關系,在合适的位置插入記憶體管理的函數,并需要把這些函數打包加到Runtime中,如 ==objc_storeStrong==,==objc_storeWeak==等。同時還要處理dealloc函數,自動加入對super的調用等,具體可以看這篇文章。
  • Lightweight Generics:叫做“輕量泛型”是因為隻增加了編譯器檢查支援,而泛型資訊并未影響到運作時,是以Runtime庫無需改動。
  • Syntax Sugars:比如Boxed Expr(

    @123

    )、Array Literal(

    @[...]

    )、Dictionary Literal(

    @{...}

    )和輕量泛型一樣,隻是把如

    @123

    在編譯rewrite成

    [NSNumber numberWithInt: 123]

    而已,無需改動Runtime。
  • Non Fragile Ivars: 類執行個體變量的動态調整技術,用于實作Objective-C Binary的相容性,随着Objective-C 2.0出現,需要編譯器和Runtime的共同配合,感興趣的可以看這篇文章。

是以,Runtime的精髓并非在于平日裡很少接觸的那些所謂的“黑魔法”Runtime API、也并非各種Swizzle大法,而是Objective-C語言層面如何處理Type、處理Value、如何設計OOP資料結構和消息機制、如何設計ABI等,去了解這麼一個小而精美的C語言運作時擴充是怎麼設計出來的。

相關的文章:

https://zh.wikipedia.org/wiki/Objective-C

http://web.cecs.pdx.edu/~harry/musings/SmalltalkOverview.html

Runtime簡介

作為一門動态語言,Objective-C會盡可能的将編譯和連結時要做的事情推遲到運作時。隻要有可能,Objective-C總是使用動态的方式來解決問題。這意味着Objective-C語言不僅需要一個編譯環境,同時也需要一個運作時系統來執行編譯好的代碼。運作時系統(runtime)扮演的角色類似于Objective-C語言的作業系統,Objective-C基于該系統來工作的。是以,runtime好比Objective-C的靈魂,很多東西都是在這個基礎上出現的。是以它是值得你花功夫去了解的。

與靜态語言編譯後的差別

1、靜态語言

一個靜态語言程式,如下所示的C程式:

#include <stdio.h>
int main(int argc, const char **argv[]) {
    printf("Hello World");
    return ;
}
           

會經過編譯器的文法分析,優化然後将你最佳化的代碼編譯成彙編語言,然後完全按照你設計的邏輯和你的代碼自上而下執行。

2、Objective-C 動态語言

很常見的一個消息發送語句:

[receiver message]

會被編譯器轉化成

objc_msgSend(receiver, selector)

如果有參數則為

objc_msgSend(receiver, selector, arg1, arg2, …)

消息隻有到運作時才會和函數實作綁定起來,而不是按照編譯好的邏輯一成不變的執行。按照作者的了解,編譯階段隻是确定了要去向receiver對象發送message消息,但是卻沒有發送,真正發送是等到運作的時候進行。是以,編譯階段完全不知道message方法的具體實作,甚至,該方法到底有沒有被實作也不知道。這就有可能導緻運作時奔潰問題。

Objective-C Runtime的幾點說明

1、runtime是開源的

目前Apple公司和GNU公司各自維護一個開源的runtime版本,這兩個版本之間都在努力的保持一緻。其中Apple的版本可以在工程中引用

#import <objc/runtime.h> 點選右鍵jump to definition,進去檢視

2、runtime是由C語言實作的

runtime作為Objective-C最核心的部分,幾乎全部由C語言實作。這裡的“幾乎”所指的例外就包含有的方法(比如下面要說到的objc_msgSend方法)甚至是用彙編實作的

3、runtime的兩個版本

Objective-C運作時系統有兩個已知版本:早期版本(Legacy)和現行版本(Modern)。

在現行版本中,最顯著的新特性就是執行個體變量是“健壯”(non-fragile)的:

在早期版本中,如果你改變類中執行個體變量的布局,你必須重新編譯該類的所有子類。

在現行版本中,如果你改變類中執行個體變量的布局,你無需重新編譯該類的任何子類。

此外,現行版本支援聲明property的synthesis屬性器。

和Runtime system互動的三種方式

1、通過Objective-C源代碼

大部分情況下,運作時系統在背景自動運作,我們隻需要編寫和編譯Objective-C源代碼。

當編譯Objective-C類和方法時,編譯器為實作語言動态特性将自動建立一些資料結構和函數。這些資料結構包含類定義和協定定義中的資訊,如在Objective-C 2.0 程式設計語言中定義類和協定類一節所讨論的類的對象和協定類的對象,方法選标,執行個體變量模闆,以及其他來自于源代碼的資訊。運作時系統的主要功能就是根據源代碼中的表達式發送消息。

2、通過類NSObject的方法

Cocoa程式中絕大部分類都是NSObject類的子類,是以大部分都繼承了NSObject類的方法,因而繼承了NSObject的行為(NSProxy類是個例外)。然而,某些情況下,NSObject類僅僅定義了完成某件事情的模闆,而沒有提供所有需要的代碼。

例如,NSObject類定義了description方法,傳回該類内容的字元串表示。這主要是用來調試程式–GDB中的print-object方法就是直接列印出該方法傳回的字元串。NSObject類中該方法的實作并不知道子類中的内容,是以它隻是傳回類的名字和對象的位址。NSObject的子類可以重新實作該方法以提供更多的資訊。

某些NSObject的方法隻是簡單的從運作時系統中擷取資訊,進而允許對象進行一定程度的自我檢查。

例如,class傳回對象的類;isKindOfClass:和isMemberOfClass:則檢查對象是否在指定的類繼承體系中;respondsToSelector:檢查對象能否響應指定的消息;conformsToProtocol:檢查對象是否實作了指定協定類的方法;methodForSelector:則傳回指定方法實作的位址。

3、通過運作時系統的函數

運作時系統是一個有公開接口的動态庫,由一些資料接口和函數的集合組成,這些資料結構和函數的聲明頭檔案在/usr/include/objc中。這些函數支援用純C的函數來實作和Objective-C同樣的功能。還有一些函數構成了NSObject類方法的基礎。這些函數使得通路運作時系統接口和提供開發工具成為可能。盡管大部分情況下它們在Objective-C程式不是必須的,但是有時候對于Objective-C程式來說某些函數是非常有用的。這些函數的文檔參見Objective-C 2.0運作時系統參考庫。

Runtime的幾個概念

SEL

SEL又叫方法選擇器,這到底是個什麼玩意呢?在objc.h中是這樣定義的:

typedef struct objc_selector *SEL;

這個SEL表示什麼?首先,說白了,方法選擇器僅僅是一個char*指針,僅僅表示它所代表的方法名字罷了。Objective-C在編譯的時候,會根據方法的名字,生成一個用來區分這個方法的唯一的一個ID,這個ID就是SEL類型的。我們需要注意的是,隻要方法的名字相同,那麼它們的ID都是相同的。就是說,不管是超類還是子類,不管有沒有超類和子類的關系,隻要名字相同那麼ID就是一樣的。

而這也就導緻了Objective-C在處理有相同函數名和參數個數但參數類型不同的函數的能力非常的弱,比如當你想在程式中實作下面兩個方法:

-(void)setWidth: (int)width;
-(void)setWidth: (double)width;
           

這樣的函數則被認為是一種編譯錯誤,而這最終導緻了一個非常非常奇怪的Objective-C特色的函數命名:

-(void)setWidthIntValue: (int)width;
-(void)setWidthDoubleValue: (double)width;
           

可能有人會問,runtime費了老半天勁,究竟想做什麼?

剛才我們說道,編譯器會根據每個方法的方法名為那個方法生成唯一的SEL,這些SEL組成一個Set集合,這個Set簡單的說就是一個經過了優化過的hash表。而Set的特點就是唯一,也就是SEL是唯一的,是以,如果我們想到這個方法集合中查找某個方法時,隻需要去找到這個方法對應的SEL就行了,SEL實際上就是根據方法名hash化了的一個字元串,而對于字元串的比較僅僅需要比較他們的位址就可以了,犀利,速度上無與倫比。但是,有一個問題,就是數量增多會增大hash沖突而導緻的性能下降(或是沒有沖突,因為也可能用的是perfect hash)。但是不管使用什麼樣的方法加速,如果能夠将總量減少(多個方法可能對應同一個SEL),那将是最犀利的方法。那麼,我們就不難了解,為什麼SEL僅僅是函數名了。

到這裡,我們明白了,本質上,SEL隻是一個指向方法的指針(準确的說,隻是一個根據方法名hash化了的KEY值,能唯一代表一個方法),它的存在隻是為了加快方法的查詢速度。

通過下面三種方法可以擷取SEL:

  1. sel_registerName函數
  2. Objective-C 編譯器提供的@selector()
  3. NSSelectorFromString()方法

IMP,方法實作的指針

IMP在objc.h中是如此定義的:

typedef id(*IMP)(id, SEL, ...);
           

第一個參數:是指向self的指針(如果是執行個體方法,則是類執行個體的記憶體位址;如果是類方法,則是指向元類的指針),這個比SEL要好了解多了,熟悉C語言的同學都知道,這其實是一個函數指針。

第二個參數:是方法選擇器(selector)

接下來的參數:方法的參數清單

前面介紹過的SEL就是為了查找方法的最終實作IMP的,由于每個方法對應唯一的SEL,是以我們可以通過SEL友善快速準确的獲得它所對應的IMP,查找過程将在下面讨論。取得IMP後,我們就獲得了執行這個方法代碼的入口點,此時,我們可以像調用普通的C語言函數一樣來使用這個函數指針了。

下面的例子,介紹了取得函數指針,即函數指針的用法:

void(* performMessage)(id, SEL);//定義一個IMP(函數指針)
performMessage = (void)(*)(id, SEL)[self methodForSelector: @selector(message)];//通過methodForSelector方法根據SEL擷取對應的函數指針
performMessage(self, @selector(message));//通過取到的IMP(函數指針)跳過runtime消息傳遞機制,直接執行message方法
           

用IMP的方式,省去了runtime消息傳遞過程中所做的一系列動作,比直接向對象發送消息效率高效一些。

Method

Method用于表示類定義中的方法,則定義如下:

typedef struct objc_method *Methodstructobjc_method {
    SEL method_name   OBJC2_UNAVAILABLE; //方法名
    char *method_types OBJC2_UNAVAILABLE;
    IMP method_imp OBJC2_UNAVAILABLE; //方法實作
}
           

我們可以看到該結構體中包含一個SEL和IMP,實際上相當于在SEL和IMP之間作了一個映射。有了SEL,我們便可以找到對應的IMP,進而調節方法的實作代碼

元類(Meta Class)

meta-class是一個類對象的類(注意是類對象)。

在上面我們提到,所有的類自身也是一個對象,我們可以向這個對象發送消息(即調用類方法)。

既然是對象,那麼它也是一個objc_object指針,它包含了一個指向其類的一個isa指針。那麼,這個isa指針指向什麼呢?

為了調用類方法,這個類的isa指針必須指向一個包含這些類方法的一個objc_class結構體。這就引出了meta-class的概念,meta-class中存儲着一個類的所有類方法。

是以,調用類方法的這個類對象的isa指針指向的就是meta-class。

當我們向一個對象發送消息時,runtime會在這個對象所屬的這個類的方法清單中查找方法;而向一個類發送消息時,會在這個類的meta-class的方法清單中查找。

再深入一下,meta-class也是一個類,也可以向他發送一個消息,那麼它的isa又是指向什麼呢?為了不讓這種結構無線延伸下去,Objective-C的設計者讓所有的meta-class的isa指向基類的meta-class,以此作為他們的所屬類。即,任何NSObject繼承體系下的meta-class都使用NSObject的meta-class作為自己的所屬類,而基類的meta-class的isa指針是指向它自己。

通過上面的描述,再加上對objc_class結構體中super_class指針的分析,我們就可以描繪出類及相對應meta-class類的一個繼承體系,如下圖:

iOS深入學習 - Runtime

Category

Category是表示一個指向分類的結構體的指針,其定義如下:

typedef struct objc_category* Category {
    char *category_name OBJC2_UNAVAILABLE; //分類名char *cla
    ss_name OBJC2_UNAVAILABLE; //分類所屬的類名structobjc
    _method_list *instance_methods OBJC2_UNAVAILABLE; //執行個體方法清單struc
    tobjc_method_list *class_methods OBJC2_UNAVAILABLE; //類方法清單str
    uctobjc_protocol_list *protocols OBJC2_UNAVAILABLE; //分類所實作的協定清單
}
           

這個結構體主要包含了分類定義的執行個體方法與類方法,其中instance_methods清單是objc_class中方法清單的一個子集,而class_methods清單是元類方法清單的一個子集。

可發現,類别中沒有ivar成員變量指針,也就意味着:類别中不能夠添加執行個體變量和屬性

objc_class

Objective-C類是由Class類型來表示的,它實際上是一個指向objc_class結構體的指針。

typedef struct object_class *Class
           

它的定義如下:

檢視objc/runtime.h中objc_class結構體的定義如下:

struct object_class {
    Class isa OBJC_ISA_AVAILABILITY;
    #if!__OBJC2__
    Class super_class  OBJC2_UNAVAILABLE; //父類
    const char *name  OBJC2_UNAVAILABLE; //類名
    long version  OBJC2_UNAVAILABLE; //類的版本資訊,預設0 
    long info  OBJC2_UNAVAILABLE; //類資訊,供運作期使用的一些位辨別
    long  instance_size  OBJC2_UNAVAILABLE; //該類的執行個體變量大小
    struct objc_ivar_list *ivars  OBJC2_UNAVAILABLE; //該類的成員變量連結清單
    struct objc_method_list *methodLists  OBJC2_UNAVAILABLE; //方法定義的連結清單
    struct objc_cache *cache  OBJC2_UNAVAILABLE; //方法緩存
    struct objc_protocol_list *protocol  OBJC2_UNAVAILABLE; //協定連結清單
    #endif
} OBJC2_UNAVAILABLE;
           

objc_object

objc_object是表示一個類的執行個體的結構體

它的定義如下(objc/objc.h):

struct objc_object { Class isa OBJC_ISA_AVAILABILITY;};

typedef struct objc_object *id;

可以看到,這個結構體隻有指向類的isa指針。這樣,當我們向一個Objective-C對象發送消息時,運作時庫會根據執行個體對象的isa指針找到這個執行個體對象所屬的類。runtime庫會在類的方法清單及父類的的方法清單中去尋找與消息對應的selector指向的方法,找到後即運作這個方法。

消息調用流程

傳遞消息所用的幾個runtime方法

前面我們說過,下面的方法:

[receiver message]

objc_msgSend(receiver, selector)

實際上,同objc_msgSend方法類似的還有幾個:

objc_msgSend_stret (傳回值是結構體)
objc_msgSend_fpret (傳回值是浮點型)
objc_msgSendSuper (調用父類方法)
objc_msgSendSuper_stret (調用父類方法,傳回值是結構體)
           

它們的作用都是類似的,為了簡單起見,後續介紹消息和消息傳遞機制都以objc_msgSend方法為例。

消息調用

一切還是從消息表達式[receiver message]開始,在被轉換成objc_msgSend(receiver, SEL)後,在運作時,runtime system會做以下事情:

  1. 檢查忽略的Selector,比如當我們運作在有垃圾回收機制的環境中,将會忽略retain和release消息。
  2. 檢查receiver是否為nil。不像其他語言,nil在Objective-C中是完全合法的,并且這裡有很多原因你也願意這樣,比如,至少我們省去了給一個對象發送消息前檢查對象是否為空的操作。如果receiver為空,則會将selector也設定為空,并且直接傳回到消息調用的地方。如果對象非空,就繼續下一步。
  3. 接下來會根據SEL到目前類中查找對應的IMP,首先會在cache中檢索它,如果找到了就根據函數指針跳轉到這個函數執行,否則進行下一步。
  4. 檢索目前類對象中的方法表(method list),如果找到了,加入cache中,并且跳轉到這個函數執行,否則進行下一步。
  5. 從父類中尋找,直到根類:NSObject類。找到了就将方法加入對應類的cache表中,如果仍未找到,則要進入後文介紹的内容:動态方法決議。
  6. 如果動态方法決議仍不能解決問題,隻能進行最後一次嘗試,進入消息轉發流程。
  7. 如果還不行,會奔潰。

這裡的調用可以分成兩部分

1、調用的方法可以找到(執行步驟1-4)

下面的圖部分展示了這個調用過程:

當消息發送給一個對象時,首先從運作時系統緩存使用過的方法中尋找。如果找到,執行方法,如果沒有找到繼續執行下面的步驟。objc_msgSend通過對象的isa指針擷取到類的結構體,然後在方法分發表裡面查找方法的selector。如果沒有找到selector, objc_msgSend結構體中的指向父類的指針找到其父類,并在父類的分發表裡面查找方法的selector。依此,會一直沿着類的繼承體系到達NSObject類。一旦定位到selector,函數就擷取到了實作的入口點,并傳入相應的參數來執行方法的具體實作,并将該方法添加進入緩存中,如果最後沒有定位到selector,則會走動态解析流程。

iOS深入學習 - Runtime

2、調用的方法找不到(消息轉發機制)

當一個對象能接收一個消息時,就會走正常的方法調用流程。但如果一個對象無法接收指定消息時,又會發生什麼事呢?預設情況下,如果是以[objc message]的方式調用方法,如果object無法響應message消息時,編譯器會報錯。但如果是以perform…的形式來調用,此時編譯器不會報錯,需要等到運作時才能确定object是否能接收message消息。如果不能,則程式奔潰。

通常,我們不能确定一個對象是否能接受某個消息時,會先調用respondsToSelector: 來判斷一下。如下代碼所示:

if (self respondsToSelector: @selector(method)) {
    [self performSelector: @selector(method)];
}
           

不過,我們這邊想讨論下不使用respondsToSelector: 判斷的情況。

當一個對象無法接收某一消息時,就會啟動所謂”消息轉發(message forwarding)”機制,通過這一機制,我們可以告訴對象如何處理位置消息。預設情況下,對象接收到未知的消息,會導緻程式奔潰,通過控制台,我麼可以看到以下異常資訊:

這段異常資訊實際上是由NSObject的“doesNotRecongnizeSelector”方法抛出的。不過,我們可以采取一些措施,在程式奔潰前執行特定的邏輯,而避免程式奔潰。

消息轉發機制基本上分為三個步驟:

  1. 動态方法解析
  2. 備用接收者
  3. 完整轉發

消息的轉發流程圖:

iOS深入學習 - Runtime

動态方法解析

對象在接收到未知消息時,首先會調用所屬類的類方法

+ resolveInstanceMethod: (執行個體方法) 或者
+ resolveClassMethod: (類方法)
           

讓我們可以在程式運作時動态的為一個selector提供實作,如果我們添加了函數的實作,并傳回YES,運作時系統會重新開機一次消息的放松過程,調用動态添加的方法。例如,下面的例子:

+ (BOOL)resolveInstanceMethod: (SEL)sel {
    if (sel == @selector(foo)) {
        class_addMethod([self class], sel, (IMP)dynamicMethodIMP, "[email protected]:");
        return YES;
    }

    return [super resolveInstanceMethod: sel];
}

void dynamicMethodIMP(id self, SEL _cmd) {
    NSLog(@"%s", __PRETTY_FUNCTION__);
}
           

在這個方法中,我們有機會為該未知消息新增一個“處理方法”,通過運作時class_addMethod函數動态的添加到類裡面就可以了。

這種方案更多的是為了實作@dynamic屬性。注:@dynamic 關鍵字就是告訴編譯器不要做這些事,同時在使用了存儲方法時也不要報錯,即讓編譯器相信存儲方法會在運作時找到。

備用接收者

如果在上一步無法處理消息,則runtime會繼續調用以下方法:

如果一個對象實作了這個方法,并傳回一個非nil的結果,則這個對象會作為消息的新接收者,且消息會被分發到這個對象。當然這個對象不能是self自身,否則就是出現無限循環。當然,如果我們沒有指定相應的對象來處理aSelector,則應該調用父類的實作來傳回結果。

這一步合适于我們隻想将消息轉發到另一個能處理該消息的對象上。但這一步無法對消息進行處理,如操作消息的參數和傳回值。

完整消息轉發

如果在上一步還不能處理未知消息,則唯一能做的就是啟用完整的消息轉發機制。我們首先要通過,指定方法簽名,若傳回nil,則表示不處理。

如下代碼:

- (NSMethodSignature *)methodSignatureForSelector: (SEL)aSelector {
    if ([NSStringFromSelector(aSelector) isEqualToString: @"testInstanceMethod"]) {
        return [NSMethodSignature signatureWithObjcTypes: "[email protected]:"];
    }

    return [super methodSignatureForSelector];
}
           

若傳回方法簽名,則會進入下一步調用以下方法,對象會建立一個表示消息的NSInvocation對象,把與尚未處理的消息有關的全部細節都封裝在Invocation中,包括selector,目标(target)和參數。

我們可以在forwardInvocation方法中選擇将消息轉發給其它對象。我們可以通過Invocation做很多處理,比如修改實作方法,修改響應對象等。

- (void)forwardInvaocation: (NSInvocation)anInvocation {
    [anInvocation invokeWithTarget: _helper];
    [anInvocation setSelector: @selector(run)];
    [anInvocation invokeWithTarget: self];
}
           

函數檢索優化措施

通過SEL進行IMP比對

先來看看類對象中儲存的方法清單和方法的資料結構:

typedef struct method_list_t {
    uint32_t entsize_NEVER_USE;
    uint32_t count;
    struct method_t first; 
} method_list_t;

typedef struct method_t {
    SEL name;
    const char *types; //參數類型和傳回值類型
    IMP imp;
} method_t;
           

在前面介紹SEL的時候,我們已經說過了蘋果在通過SEL檢索IMP時做的努力,這裡不再累述。

cache緩存

cache的原則就是緩存那些可能要執行的函數位址,那麼下次調用的時候,速度就可以快速很多。這個和CPU的各種緩存原理相同。說了這麼多,再來認識幾個名詞:

struct objc_cache {
    uintptr_tmask;
    uintptr_toccupied;
    cache_entry *buckets[];
};

typedef struct {
    SEL name;
    void *unused;
    IMP imp;
} cache_entry;
           

看這個結構,還是hash table。

objc_msgSend 首先在cache list中找SEL,沒有找到就在class method中找,super class method中找(當然super class 也有cache list)。而cache的緩存機制則非常複雜了,由于Objective-C是動态語言。是以,這裡面還有很多的多線程同步問題,而這些鎖又是效率的大敵,相關的内容已經遠遠超過文本讨論的範圍。

如果在緩存中已經有了需要的方法選标,則消息僅僅比函數調用慢一點。如果程式運作了足夠長的時間,幾乎每個消息都能在緩存中找到方法實作。程式運作時,緩存也講随着新的消息的增加而增加。據牛人說(沒有親測過),蘋果通過這些優化,在消息傳遞和直接的函數調用上的差距已經相當的小了。

方法調用中的隐藏參數

在進行面向對象程式設計的時候,在執行個體方法中都是用過self關鍵字,可是你有沒有想過,為什麼在一個執行個體方法中,通過self關鍵字就能調取到目前方法的對象呢?這就要歸功于runtime system消息的隐藏參數了。

當objc_msgSend找到方法對應的實作時,它将直接調用該方法實作,并将消息中所有的參數都傳遞給方法實作,同時,它還将傳遞兩個隐藏的參數:

  • 接收消息的對象(也就是self指向的内容)
  • 方法選标(_cmd指向的内容)

這些參數幫助方法實作獲得了消息表達式的資訊。它們被認為是“隐藏”的,是因為它們并沒有在在定義方法的源碼中聲明,而是在代碼編譯時是插入方法的實作中的。盡管這些參數沒有被顯示聲明,但在源碼中仍然可以引用它們(就像可以引用消息接收者對象的執行個體變量一樣)。在方法中可以通過self來引用消息接收者,通過标選_cmd來引用方法本身。下面的例子很好的說明了這個問題:

- (void)message {
    self.name = @"James"; //通過self關鍵字給目前對象的屬性指派
    SEL currentSel = _cmd; //通過_cmd關鍵字取到目前函數對應的SEL
    NSLog(@"currentSel is: %s", (char *)currentSel);
}
列印結果:ObjcRuntime[:] currentSel is: message 
           

當然,在這兩參數中,self更有用,更常用一些。實際上,它是在方法實作中通路消息接收者對象的執行個體變量的途徑。

方法交換Swizzling

使用場景:系統自帶的方法功能不夠,給系統自帶的方法擴充一些功能,并且保持原有的功能。

方式一:繼承系統的類,重寫方法。

方式二:使用runtime,交換方法。

在Objective-C中調用一個方法,其實是向一個對象發送消息,而查找消息的唯一依據是selector的名字。是以,我們可以利用Objective-C的runtime機制,實作在運作時交換selector對應的方法實作以達到我們的目的。每個類都有一個方法清單,存放着selector的名字和方法實作的映射關系。IMP有點類似函數指針,指向具體的Method實作。我們先看看SEL與IMP之間的關系圖:

iOS深入學習 - Runtime

從上圖可以看出來,每一個SEL與一個IMP一一對應,正常情況下通過SEL可以查找到對應消息的IMP實作。但是,現在我們要做的就是把連結線解開,然後連結到我們自定義的函數的IMP上。當然,交換了兩個SEL的IMP,還是可以再次交換回來了。交換後變成這樣的,如下圖

iOS深入學習 - Runtime

從圖中可以看出,我們通過swizzling特性,将selectorC的方法實作IMPc與selectorN的方法實作IMPn交換了,當我們調用selectorC,也就是給對象發送selectorC消息時,所查找到的對應的方法實作就是IMPn而不是IMPc了。

#import "UIViewController+swizzling.h"
@implementation UIViewController(swizzling)
//load方法會在類第一次加載的時候被調用,調用的時間比較靠前,适合在這個方法裡做方法交換
+ (void)load {
    //方法交換應該被保證,在程式中隻會執行一次
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        //獲得viewController的生命周期方法的selector
        SEL systemSel = @selector(viewWillAppear:);
        //自己實作的将要被交換的方法的selector
        SEL swizzSel = @selector(swiz_viewWillAppear:);
        //兩個方法的Method
        Method systemMethod = class_getInstanceMethod([self class], systemSel);
        Method swizzMethod = class_getInstanceMethod([self class], swizzSel);
        //首先動态添加方法,實作是被交換的方法,傳回值表示添加成功還是失敗
        BOOL isAdd = class_addMethod(self, swizzMethod, method_getImplementation(swizzMethod), method_getTypeEncoding(swizzMethod));

        if (isAdd) {
            //如果成功,說明類中不存在這個方法的實作
            //将被交換方法的實作替換到這個并不存在的實作
            class_replaceMethod(self, swizzMethod, method_getImplementation(systemMethod), method_getTypeEncoding(systemMethod));
        } else {
            //否則,交換兩個方法的實作
            method_exchangeImplementations(systemMethod, swizzMethod);
        }
    });
}

- (void)swiz_viewWillApper: (BOOL)animated {
    //這時候調用自己,看起來像死循環
    //但是其實自己的實作已經被替換了
    [self swiz_ViewWillAppear: animated];
    NSLog(@"swizzle");
}

@end
           

在一個自己定義的viewController中重寫viewWillAppear

- (void)viewWillAppear: (BOOL)animated {
    [super viewWillAppear: animated];
    NSLog(@"viewWillAppear");
}
           

設定關聯值

使用場景:現在你準備一個系統的類,但是系統的類并不能滿足你的需求,你需要額外添加一個屬性。給一個類聲明屬性,其實本質就是給這個類添加關聯,并不會直接把這個值的記憶體空間添加到類存儲空間。分類隻能添加方法。

設定關聯值

這種情況的一般解決辦法就是繼承。但是隻增加一個屬性,就去繼承一個類,總覺得太麻煩。這個時候,runtime的關聯屬性就發揮它的作用了。

添加關聯對象

- (void)addAssociatedObject: (id)object {
    objc_setAssociatedObject(self, @selector(getAssociatedObject), object, OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
//擷取關聯對象
- (id)getAssociatedObject {
    return objc_getAssociatedObject(self, _cmd);
}
           

注意,這裡面我們把getAssociatedObject方法的位址作為唯一的key,_cmd代表目前調用方法的位址。

參數說明:

object:與誰關聯,通常是傳self

key:唯一鍵,在擷取值時通過該鍵擷取,通常是使用static const void * 來聲明

value:關聯所設定的值

policy:記憶體管理政策,比如使用copy

void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
           

擷取關聯值

參數說明:

object:與誰關聯,通常是傳self,在設定關聯時所指定的與哪個對象關聯的那個對象

key:唯一鍵,在設定關聯時所指定的鍵

id objc_getAssociatedObject(id object, const void *key)
           

取消關聯

void objc_removeAssociatedObjects(id object)
           

關聯政策

typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
    OBJC_ASSOCIATION_ASSIGN = , //表示弱引用關聯,通常是基本資料類型
    OBJC_ASSOCIATION_RETAIN_NONATOMIC = , //表示強引用關聯對象,是線程安全的
    OBJC_ASSOCIATION_COPY_NONATOMIC = , //表示關聯對象copy,是線程安全的
    OBJC_ASSOCIATION_RETAIN = , //表示強引用關聯對象,不是線程安全的
    OBJC_ASSOCIATION_COPY =  //表示關聯對象copy,不是線程安全的
};

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    //給系統NSObject類動态添加屬性name
    NSObject *objc = [[NSObject alloc] init];
    objc.name = @"123";
    NSLog(@"%@", objc.name);
}
@end

//定義關聯的key 
static const char *key = "name"; 

@implementation NSObject(Property)
- (NSString *)name {
    //根據關聯的key,擷取關聯的值。
    return objc_getAssociatedObject(self, key);
}

- (void)setName: (NSString *)name {
    //第一個參數:給對象添加關聯
    //第二個參數:關聯的key,通過這個key擷取
    //第三個參數:關聯的value
    //第四個參數:關聯的政策
    objc_setAssociatedObject(self, key, name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
@end
           

動态添加方法

使用場景:如果一個類方法非常多,加載類到記憶體的時候也比較耗費資源,需要給每個方法生成映射表,可以使用動态給某個類添加方法解決。

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    Person *p = [[Person alloc] init]; //預設person,沒有實作eat方法,可以通過performSelector調用,但是會報錯。
    //動态添加方法就不會報錯
    [p performSelector: @selector(eat)];
}
@end

@implementation Person 

void(*)() 
//預設方法都有兩個隐式參數
void eat(id self, SEL sel) {
    NSLog(@"%@ %@", self, NSStringFromSelector(sel));
}
// 當一個對象調用未實作的方法,會調用這個方法處理,并且會吧對應的方法清單傳過來
// 剛好可以用來判斷,未實作的方法是不是我們想要動态添加的方法

+ (BOOL)resolveInstanceMethod: (SEL)sel {
    if (sel == @selector(eat)) {
        //動态添加eat方法
        //第一個參數:給哪個類添加方法
        //第二個參數:添加方法的方法編号
        //第三個參數:添加方法的函數實作(函數位址)
        //第四個參數:函數的類型(傳回值+參數類型)v:void  @:對象->self  :表示SEL->_cmd
        class_addMethod(self, @selector(eat), eat, "[email protected]:");
    }

    return [super resolveInstanceMethod];
}
@end
           

字典轉模型

設計模式

模型屬性,通常需要跟字典中的key一一對應

問題:一個一個的生成模型屬性,很慢?

需求:能不能自動根據一個字典,生成對應的屬性。

解決:提供一個分類,專門根據字典生成對應的屬性字元串

@implementation NSObject(Log)
//自動列印屬性字元串
+ (void)resolveDict: (NSDictionary *)dict {
    //拼接屬性字元串代碼
    NSMutableString *strM = [NSMutableString string];
    //1、周遊字典,把字典中的所有key取出來,生成對應的屬性代碼
    [dict enumerateKeysAndObjectsUsingBlock:^(id_Nonnull key, id_Nonnull obj, BOOL_Nonnull stop){
    //類型經常變,抽出來
        NSString *type;
        if ([obj isKindOfClass:NSClassFromString(@"__NSCFString")]) {
            type = @"NSString";
        } else if ([obj isKindOfClass:NSClassFromString(@"__NSCFArray")]) {
            type = @"NSArray";
        } else if ([obj isKindOfClass:NSClassFromString(@"__NSCFNumber")]) {
            type = @"NSNumber";
        } else if ([obj isKindOfClass:NSClassFromString(@"__NSCFDictionary")]) {
            type = @"NSDictionary";
        }

        //屬性字元串
        NSString *str;
        if ([type containsString: @"NS"]) {
            str = [NSString stringWithFormat: @"@property(nonatomic, strong) %@ *%@;", type, key];
        } else {
            str = [NSString stringWithFormat: @"@property(nonatomic, assign) %@ %@;", type, key];
        }
          //每生成屬性字元串,就自動換行。
          [strM appendFormat: @"\n%@\n", str];

    }];

    //把拼接好的字元串列印出來
    NSLog(@"%@", strM);
}
@end
           

字典轉模型的方式一:KVC

@implementation Status
+ (instancetype)statusWithDict: (NSDictionary *)dict {
    Status *status = [[self alloc] init];
    [status setValuesForKeysWithDictionary: dict];
    return status;
}
@end
           

KVC字典轉模型的弊端:必須保證,模型中的屬性和字典中的key一一對應。如果不一緻,就會調用[setValue:forUndefinedKey:],報key找不到的錯。

分析:模型中的屬性和字典的ke不一一對應,系統就會調用[setValue:forUndefinedKey:]報錯。

解決:重寫對象的[setValue:forUndefinedKey:],把系統方法覆寫,就能繼續使用KVC,字典轉模型了。

字典轉模型的方式二:Runtime

思路:利用運作時,周遊模型中所有屬性,根據模型的屬性名,去字典中查找key,取出對應的值,給模型的屬性指派。

步驟:提供一個NSObject類,專門字典轉模型,以後所有的模型都可以通過這個分類轉。

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    //解析plist檔案
    NSString *filePath = [[NSBundle mainBundle] pathForResource:@"status.plist" ofType:nil];
    NSDictionary *statusDic = [NSDictionary dictionaryWithContentsOfFile:filePath];
    //擷取字典數組
    NSArray *dictArr = statusDict[@"statuses"];
    //自動生成模型的屬性字元串
    [NSObject resolveDict:dictArr[][@"user"]];
    _statuses = [NSMutableArray array];
    //周遊字典數組
    for(NSDictionary *dict in dictArr) {
        Status *status = [Status modelWithDict: dict];
        [_statuses addObject:status];
    }
}
@end

@implementation NSObject(Model)
+ (instancetype)modelWithDict:(NSDictionary *)dict {
    // 思路:周遊模型中所有屬性 ->使用運作時
    // 0、建立對應的對象
    id objc = [[self alloc] init];
    // 1、利用runtime給對象中成員屬性指派
    // class_copyIvarList: 擷取類中的所有成員屬性
    // Ivar: 成員屬性的意思
    // 第一個參數:表示擷取哪個類中的成員屬性
    // 第二個參數:表示這個類有多少成員屬性,傳入一個int變量位址,會自動給這個變量指派
    // 傳回值Ivar *:指的是一個ivar數組,會把所有的成員屬性放在一個數組中,通過傳回的數組就能全部擷取到。
    /*
    Ivar ivar;
    Ivar ivar1;
    Ivar ivar2;
    //定義一個ivar的數組a
    Ivar a[] = {ivar, ivar1, ivar2};
    //用一個Ivar *指針指向數組的第一個元素
    Ivar *ivarList = a;
    //根據指針通路數組的第一個元素
    ivarList[0];
    */

    unsigned int count;
    //擷取類中的所有成員屬性 
    Ivar *ivarList = class_copyIvarList(self, &count);
    for (int i = ; i < count; i++) {
        //根據角标,從數組取出對應的成員屬性
        Ivar ivar = ivarList[i];
        //擷取成員屬性名
        NSString *name = [NSString stringWithUTF8String:ivar_getName(ivar)];
        //處理成員屬性名->字典中的key
        //從第一個角标開始截取
        NSString *key = [name substringFromIndex: ];
        //根據成員屬性名去字典中查找對應的value
        id value = dict[key];
        //二級轉換:如果字典中還有字典,也需要把對應的字典轉成模型
        //判斷下value是否是字典
        if ([value isKindOfClass: [NSDictionary class]]) {
            //字典轉模型
            //擷取模型的類對象,調用modelWithDict
            //模型的類名已知,就是成員屬性的類型
            //擷取成員屬性類型
            NSString *type = [NSString stringWithUTF8String: ivar_getTypeEncoding(ivar)];
            //生成的是這種"@\"User\""類型 -> @"User"  在OC字元串中\" -> \是轉義的意思,不占用字元串
            //裁剪類型字元串
            NSRanger ranger = [type rangeOfString:@"\""];
            type = [type substringFromIndex:ranger.location + ranger.length];
            range = [type rangeOfString:@"\""];
            //裁剪到哪個角标,不包括目前角标
            type = [type substringToIndex:range.location];
            //根據字元串類名生成類對象
            Class modelClass = NSClassFromString(type);
            if (modelClass) {
                //有對應的模型才需要轉
                //把字典轉模型
                value = [modelClass modelWithDict:value];
            }
        }
        //三級轉換:NSArray中也是字典,把數組中的字典轉換成模型
        //判斷值是否是數組
        if ([value isKindOfClass:[NSArray class]]) {
            //判斷對應類有沒有實作字典數組轉模型數組的協定
            if ([self respondsToSelector:@selector(arrayContainModelClass)]) {
                //轉換成id類型,就能調用任何對象的方法id
                id Self = self;
                //擷取數組中字典對應的模型
                NSString *type = [Self arrayContainModelClass][key];
                //生成模型
                Class classModel = NSClassFromString(type);
                NSMutableArray *arrM = [NSMutableArray array];
                //周遊字典數組,生成模型數組
                for (NSDictionary *dict in value) {
                    //字典轉模型 
                    id model = [classModel modelWithDict: dict];
                    [arrM addObject: model];
                }
                //把模型數組指派給value
                value = arrM;
            }
        }

        if (value) {
            //有值,才需要給模型的屬性指派
            //利用KVC給模型中的屬性指派
            [objc setValue:value forKey:key];    
        }

    }
    return objc;
}

@end
           

參考文章

http://www.jianshu.com/p/e071206103a4

http://www.jianshu.com/p/adf0d566c887

http://www.jianshu.com/p/927c8384855a

http://chun.tips/2014/11/05/objc-runtime-1/#more

http://blog.sunnyxx.com/2016/08/13/reunderstanding-runtime-0/

http://blog.csdn.net/wzzvictory/article/details/8624057

http://www.cocoachina.com/ios/20151208/14595.html

http://www.jianshu.com/p/46dd81402f63

繼續閱讀