ios Runtime

本文轉載自:http://yulingtianxia.com/blog/2014/11/05/objective-c-runtime/#Method-Swizzling

本文詳細整理了 Cocoa 的 Runtime 系統的知識,它使得 Objective-C 如虎添翼,具備了靈活的動態特性,使這門古老的語言煥發生機。主要內容如下:

引言

簡介

與Runtime交互

Runtime術語

消息

動態方法解析

消息轉發

健壯的實例變量(Non Fragile ivars)

Objective-C Associated Objects

Method Swizzling

總結

引言

曾經覺得Objc特別方便上手,面對著 Cocoa 中大量 API,只知道簡單的查文檔和調用。還記得初學 Objective-C 時把[receiver message]當成簡單的方法調用,而無視了“發送消息”這句話的深刻含義。其實[receiver message]會被編譯器轉化為:

1

objc_msgSend(receiver, selector)

如果消息含有參數,則為:

1

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

如果消息的接收者能夠找到對應的selector,那么就相當于直接執行了接收者這個對象的特定方法;否則,消息要么被轉發,或是臨時向接收者動態添加這個selector對應的實現內容,要么就干脆玩完崩潰掉。

現在可以看出[receiver message]真的不是一個簡簡單單的方法調用。因為這只是在編譯階段確定了要向接收者發送message這條消息,而receive將要如何響應這條消息,那就要看運行時發生的情況來決定了。

Objective-C 的 Runtime 鑄就了它動態語言的特性,這些深層次的知識雖然平時寫代碼用的少一些,但是卻是每個 Objc 程序員需要了解的。

簡介

因為Objc是一門動態語言,所以它總是想辦法把一些決定工作從編譯連接推遲到運行時。也就是說只有編譯器是不夠的,還需要一個運行時系統 (runtime system) 來執行編譯后的代碼。這就是 Objective-C Runtime 系統存在的意義,它是整個Objc運行框架的一塊基石。

Runtime其實有兩個版本:“modern”和 “legacy”。我們現在用的 Objective-C 2.0 采用的是現行(Modern)版的Runtime系統,只能運行在 iOS 和 OS X 10.5 之后的64位程序中。而OS X較老的32位程序仍采用 Objective-C 1中的(早期)Legacy 版本的 Runtime 系統。這兩個版本最大的區別在于當你更改一個類的實例變量的布局時,在早期版本中你需要重新編譯它的子類,而現行版就不需要。

Runtime基本是用C和匯編寫的,可見蘋果為了動態系統的高效而作出的努力。你可以在這里下到蘋果維護的開源代碼。蘋果和GNU各自維護一個開源的runtime版本,這兩個版本之間都在努力的保持一致。

與Runtime交互

Objc 從三種不同的層級上與 Runtime 系統進行交互,分別是通過 Objective-C 源代碼,通過 Foundation 框架的NSObject類定義的方法,通過對 runtime 函數的直接調用。

Objective-C源代碼

大部分情況下你就只管寫你的Objc代碼就行,runtime 系統自動在幕后辛勤勞作著。

還記得引言中舉的例子吧,消息的執行會使用到一些編譯器為實現動態語言特性而創建的數據結構和函數,Objc中的類、方法和協議等在 runtime 中都由一些數據結構來定義,這些內容在后面會講到。(比如objc_msgSend函數及其參數列表中的id和SEL都是啥)

NSObject的方法

Cocoa 中大多數類都繼承于NSObject類,也就自然繼承了它的方法。最特殊的例外是NSProxy,它是個抽象超類,它實現了一些消息轉發有關的方法,可以通過繼承它來實現一個其他類的替身類或是虛擬出一個不存在的類,說白了就是領導把自己展現給大家風光無限,但是把活兒都交給幕后小弟去干。

有的NSObject中的方法起到了抽象接口的作用,比如description方法需要你重載它并為你定義的類提供描述內容。NSObject還有些方法能在運行時獲得類的信息,并檢查一些特性,比如class返回對象的類;isKindOfClass:和isMemberOfClass:則檢查對象是否在指定的類繼承體系中;respondsToSelector:檢查對象能否響應指定的消息;conformsToProtocol:檢查對象是否實現了指定協議類的方法;methodForSelector:則返回指定方法實現的地址。

Runtime的函數

Runtime 系統是一個由一系列函數和數據結構組成,具有公共接口的動態共享庫。頭文件存放于/usr/include/objc目錄下。許多函數允許你用純C代碼來重復實現 Objc 中同樣的功能。雖然有一些方法構成了NSObject類的基礎,但是你在寫 Objc 代碼時一般不會直接用到這些函數的,除非是寫一些 Objc 與其他語言的橋接或是底層的debug工作。在Objective-C Runtime Reference中有對 Runtime 函數的詳細文檔。

Runtime術語

還記得引言中的objc_msgSend:方法吧,它的真身是這樣的:

1

idobjc_msgSend (idself, SEL op, ... );

下面將會逐漸展開介紹一些術語,其實它們都對應著數據結構。

SEL

objc_msgSend函數第二個參數類型為SEL,它是selector在Objc中的表示類型(Swift中是Selector類)。selector是方法選擇器,可以理解為區分方法的 ID,而這個 ID 的數據結構是SEL:

1

typedefstructobjc_selector *SEL;

其實它就是個映射到方法的C字符串,你可以用 Objc 編譯器命令@selector()或者 Runtime 系統的sel_registerName函數來獲得一個SEL類型的方法選擇器。

不同類中相同名字的方法所對應的方法選擇器是相同的,即使方法名字相同而變量類型不同也會導致它們具有相同的方法選擇器,于是 Objc 中方法命名有時會帶上參數類型(NSNumber一堆抽象工廠方法拿走不謝),Cocoa 中有好多長長的方法哦。

id

objc_msgSend第一個參數類型為id,大家對它都不陌生,它是一個指向類實例的指針:

1

typedefstructobjc_object *id;

那objc_object又是啥呢:

1

structobjc_object{ Class isa; };

objc_object結構體包含一個isa指針,根據isa指針就可以順藤摸瓜找到對象所屬的類。

PS:isa指針不總是指向實例對象所屬的類,不能依靠它來確定類型,而是應該用class方法來確定實例對象的類。因為KVO的實現機理就是將被觀察對象的isa指針指向一個中間類而不是真實的類,這是一種叫做isa-swizzling的技術,詳見官方文檔

Class

之所以說isa是指針是因為Class其實是一個指向objc_class結構體的指針:

1

typedefstructobjc_class *Class;

而objc_class就是我們摸到的那個瓜,里面的東西多著呢:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

structobjc_class {

Class isa? OBJC_ISA_AVAILABILITY;

#if !__OBJC2__

Class super_class? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? OBJC2_UNAVAILABLE;

constchar*name? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? OBJC2_UNAVAILABLE;

longversion? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? OBJC2_UNAVAILABLE;

longinfo? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? OBJC2_UNAVAILABLE;

longinstance_size? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? OBJC2_UNAVAILABLE;

structobjc_ivar_list *ivars? ? ? ? ? ? ? ? ? ? ? ? ? ? OBJC2_UNAVAILABLE;

structobjc_method_list **methodLists? ? ? ? ? ? ? ? ? ? OBJC2_UNAVAILABLE;

structobjc_cache *cache? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? OBJC2_UNAVAILABLE;

structobjc_protocol_list *protocols? ? ? ? ? ? ? ? ? ? OBJC2_UNAVAILABLE;

#endif

} OBJC2_UNAVAILABLE;

可以看到運行時一個類還關聯了它的超類指針,類名,成員變量,方法,緩存,還有附屬的協議。

PS:OBJC2_UNAVAILABLE之類的宏定義是蘋果在 Objc 中對系統運行版本進行約束的黑魔法,為的是兼容非Objective-C 2.0的遺留邏輯,但我們仍能從中獲得一些有價值的信息,有興趣的可以查看源代碼。

Objective-C 2.0 的頭文件雖然沒暴露出objc_class結構體更詳細的設計,我們依然可以從Objective-C 1.0 的定義中小窺端倪:

在objc_class結構體中:ivars是objc_ivar_list指針;methodLists是指向objc_method_list指針的指針。也就是說可以動態修改*methodLists的值來添加成員方法,這也是Category實現的原理,同樣解釋了Category不能添加屬性的原因。而最新版的 Runtime 源碼對這一塊的描述已經有很大變化,可以參考下美團技術團隊的深入理解Objective-C:Category

PS:任性的話可以在Category中添加@dynamic的屬性,并利用運行期動態提供存取方法或干脆動態轉發;或者干脆使用關聯度對象(AssociatedObject)

其中objc_ivar_list和objc_method_list分別是成員變量列表和方法列表:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

structobjc_ivar_list {

intivar_count? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? OBJC2_UNAVAILABLE;

#ifdef __LP64__

intspace? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? OBJC2_UNAVAILABLE;

#endif

/* variable length structure */

structobjc_ivar ivar_list[1]? ? ? ? ? ? ? ? ? ? ? ? ? ? OBJC2_UNAVAILABLE;

}? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? OBJC2_UNAVAILABLE;

structobjc_method_list {

structobjc_method_list *obsolete? ? ? ? ? ? ? ? ? ? ? ? OBJC2_UNAVAILABLE;

intmethod_count? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? OBJC2_UNAVAILABLE;

#ifdef __LP64__

intspace? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? OBJC2_UNAVAILABLE;

#endif

/* variable length structure */

structobjc_method method_list[1]? ? ? ? ? ? ? ? ? ? ? ? OBJC2_UNAVAILABLE;

}

如果你C語言不是特別好,可以直接理解為objc_ivar_list結構體存儲著objc_ivar數組列表,而objc_ivar結構體存儲了類的單個成員變量的信息;同理objc_method_list結構體存儲著objc_method數組列表,而objc_method結構體存儲了類的某個方法的信息。

最后要提到的還有一個objc_cache,顧名思義它是緩存,它在objc_class的作用很重要,在后面會講到。

不知道你是否注意到了objc_class中也有一個isa對象,這是因為一個 ObjC 類本身同時也是一個對象,為了處理類和對象的關系,runtime 庫創建了一種叫做元類 (Meta Class) 的東西,類對象所屬類型就叫做元類,它用來表述類對象本身所具備的元數據。類方法就定義于此處,因為這些方法可以理解成類對象的實例方法。每個類僅有一個類對象,而每個類對象僅有一個與之相關的元類。當你發出一個類似[NSObject alloc]的消息時,你事實上是把這個消息發給了一個類對象 (Class Object) ,這個類對象必須是一個元類的實例,而這個元類同時也是一個根元類 (root meta class) 的實例。所有的元類最終都指向根元類為其超類。所有的元類的方法列表都有能夠響應消息的類方法。所以當[NSObject alloc]這條消息發給類對象的時候,objc_msgSend()會去它的元類里面去查找能夠響應消息的方法,如果找到了,然后對這個類對象執行方法調用。

上圖實線是super_class指針,虛線是isa指針。 有趣的是根元類的超類是NSObject,而isa指向了自己,而NSObject的超類為nil,也就是它沒有超類。

Method

Method是一種代表類中的某個方法的類型。

1

typedef struct objc_method *Method;

而objc_method在上面的方法列表中提到過,它存儲了方法名,方法類型和方法實現:

1

2

3

4

5

structobjc_method {

SEL method_name? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? OBJC2_UNAVAILABLE;

char*method_types? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? OBJC2_UNAVAILABLE;

IMP method_imp? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? OBJC2_UNAVAILABLE;

}? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? OBJC2_UNAVAILABLE;

方法名類型為SEL,前面提到過相同名字的方法即使在不同類中定義,它們的方法選擇器也相同。

方法類型method_types是個char指針,其實存儲著方法的參數類型和返回值類型。

method_imp指向了方法的實現,本質上是一個函數指針,后面會詳細講到。

Ivar

Ivar是一種代表類中實例變量的類型。

1

typedefstructobjc_ivar *Ivar;

而objc_ivar在上面的成員變量列表中也提到過:

1

2

3

4

5

6

7

8

structobjc_ivar {

char*ivar_name? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? OBJC2_UNAVAILABLE;

char*ivar_type? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? OBJC2_UNAVAILABLE;

intivar_offset? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? OBJC2_UNAVAILABLE;

#ifdef __LP64__

intspace? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? OBJC2_UNAVAILABLE;

#endif

}? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? OBJC2_UNAVAILABLE;

可以根據實例查找其在類中的名字,也就是“反射”:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

-(NSString*)nameWithInstance:(id)instance {

unsignedintnumIvars =0;

NSString*key=nil;

Ivar * ivars = class_copyIvarList([selfclass], &numIvars);

for(inti =0; i < numIvars; i++) {

Ivar thisIvar = ivars[i];

constchar*type = ivar_getTypeEncoding(thisIvar);

NSString*stringType =? [NSStringstringWithCString:type encoding:NSUTF8StringEncoding];

if(![stringType hasPrefix:@"@"]) {

continue;

}

if((object_getIvar(self, thisIvar) == instance)) {//此處若 crash 不要慌!

key = [NSStringstringWithUTF8String:ivar_getName(thisIvar)];

break;

}

}

free(ivars);

returnkey;

}

class_copyIvarList函數獲取的不僅有實例變量,還有屬性。但會在原本的屬性名前加上一個下劃線。

IMP

IMP在objc.h中的定義是:

1

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

它就是一個函數指針,這是由編譯器生成的。當你發起一個 ObjC 消息之后,最終它會執行的那段代碼,就是由這個函數指針指定的。而IMP這個函數指針就指向了這個方法的實現。既然得到了執行某個實例某個方法的入口,我們就可以繞開消息傳遞階段,直接執行方法,這在后面會提到。

你會發現IMP指向的方法與objc_msgSend函數類型相同,參數都包含id和SEL類型。每個方法名都對應一個SEL類型的方法選擇器,而每個實例對象中的SEL對應的方法實現肯定是唯一的,通過一組id和SEL參數就能確定唯一的方法實現地址;反之亦然。

Cache

在runtime.h中Cache的定義如下:

1

typedefstructobjc_cache *Cache

還記得之前objc_class結構體中有一個struct objc_cache *cache吧,它到底是緩存啥的呢,先看看objc_cache的實現:

1

2

3

4

5

structobjc_cache {

unsignedintmask/* total = mask + 1 */OBJC2_UNAVAILABLE;

unsignedintoccupied? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? OBJC2_UNAVAILABLE;

Method buckets[1]? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? OBJC2_UNAVAILABLE;

};

Cache為方法調用的性能進行優化,通俗地講,每當實例對象接收到一個消息時,它不會直接在isa指向的類的方法列表中遍歷查找能夠響應消息的方法,因為這樣效率太低了,而是優先在Cache中查找。Runtime 系統會把被調用的方法存到Cache中(理論上講一個方法如果被調用,那么它有可能今后還會被調用),下次查找的時候效率更高。這根計算機組成原理中學過的 CPU 繞過主存先訪問Cache的道理挺像,而我猜蘋果為提高Cache命中率應該也做了努力吧。

Property

@property標記了類中的屬性,這個不必多說大家都很熟悉,它是一個指向objc_property結構體的指針:

1

2

typedefstructobjc_property *Property;

typedefstructobjc_property *objc_property_t;//這個更常用

可以通過class_copyPropertyList和protocol_copyPropertyList方法來獲取類和協議中的屬性:

1

2

objc_property_t*class_copyPropertyList(Class cls,unsignedint*outCount)

objc_property_t*protocol_copyPropertyList(Protocol *proto,unsignedint*outCount)

返回類型為指向指針的指針,哈哈,因為屬性列表是個數組,每個元素內容都是一個objc_property_t指針,而這兩個函數返回的值是指向這個數組的指針。

舉個栗子,先聲明一個類:

1

2

3

4

5

@interfaceLender: NSObject {

float alone;

}

@propertyfloat alone;

@end

你可以用下面的代碼獲取屬性列表:

1

2

3

id LenderClass = objc_getClass("Lender");

unsigned int outCount;

objc_property_t *properties = class_copyPropertyList(LenderClass,&outCount);

你可以用property_getName函數來查找屬性名稱:

1

const char *property_getName(objc_property_tproperty)

你可以用class_getProperty和protocol_getProperty通過給出的名稱來在類和協議中獲取屬性的引用:

1

2

objc_property_tclass_getProperty(Class cls,constchar*name)

objc_property_tprotocol_getProperty(Protocol *proto,constchar*name, BOOL isRequiredProperty, BOOL isInstanceProperty)

你可以用property_getAttributes函數來發掘屬性的名稱和@encode類型字符串:

1

const char *property_getAttributes(objc_property_tproperty)

把上面的代碼放一起,你就能從一個類中獲取它的屬性啦:

1

2

3

4

5

6

7

id LenderClass = objc_getClass("Lender");

unsignedintoutCount, i;

objc_property_t *properties = class_copyPropertyList(LenderClass, &outCount);

for(i =0; i < outCount; i++) {

objc_property_tproperty= properties[i];

fprintf(stdout,"%s %s\n", property_getName(property), property_getAttributes(property));

}

對比下class_copyIvarList函數,使用class_copyPropertyList函數只能獲取類的屬性,而不包含成員變量。但此時獲取的屬性名是不帶下劃線的。

消息

前面做了這么多鋪墊,現在終于說到了消息了。Objc 中發送消息是用中括號([])把接收者和消息括起來,而直到運行時才會把消息與方法實現綁定。

有關消息發送和消息轉發機制的原理,可以查看這篇文章

objc_msgSend函數

在引言中已經對objc_msgSend進行了一點介紹,看起來像是objc_msgSend返回了數據,其實objc_msgSend從不返回數據而是你的方法被調用后返回了數據。下面詳細敘述下消息發送步驟:

檢測這個selector是不是要忽略的。比如 Mac OS X 開發,有了垃圾回收就不理會retain,release這些函數了。

檢測這個 target 是不是nil對象。ObjC 的特性是允許對一個nil對象執行任何一個方法不會 Crash,因為會被忽略掉。

如果上面兩個都過了,那就開始查找這個類的IMP,先從cache里面找,完了找得到就跳到對應的函數去執行。

如果cache找不到就找一下方法分發表。

如果分發表找不到就到超類的分發表去找,一直找,直到找到NSObject類為止。

如果還找不到就要開始進入動態方法解析了,后面會提到。

PS:這里說的分發表其實就是Class中的方法列表,它將方法選擇器和方法實現地址聯系起來。

其實編譯器會根據情況在objc_msgSend,objc_msgSend_stret,objc_msgSendSuper, 或objc_msgSendSuper_stret四個方法中選擇一個來調用。如果消息是傳遞給超類,那么會調用名字帶有”Super”的函數;如果消息返回值是數據結構而不是簡單值時,那么會調用名字帶有”stret”的函數。排列組合正好四個方法。

值得一提的是在 i386 平臺處理返回類型為浮點數的消息時,需要用到objc_msgSend_fpret函數來進行處理,這是因為返回類型為浮點數的函數對應的 ABI(Application Binary Interface) 與返回整型的函數的 ABI 不兼容。此時objc_msgSend不再適用,于是objc_msgSend_fpret被派上用場,它會對浮點數寄存器做特殊處理。不過在 PPC 或 PPC64 平臺是不需要麻煩它的。

PS:有木有發現這些函數的命名規律哦?帶“Super”的是消息傳遞給超類;“stret”可分為“st”+“ret”兩部分,分別代表“struct”和“return”;“fpret”就是“fp”+“ret”,分別代表“floating-point”和“return”。

方法中的隱藏參數

我們經常在方法中使用self關鍵字來引用實例本身,但從沒有想過為什么self就能取到調用當前方法的對象吧。其實self的內容是在方法運行時被偷偷的動態傳入的。

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

接收消息的對象(也就是self指向的內容)

方法選擇器(_cmd指向的內容)

之所以說它們是隱藏的是因為在源代碼方法的定義中并沒有聲明這兩個參數。它們是在代碼被編譯時被插入實現中的。盡管這些參數沒有被明確聲明,在源代碼中我們仍然可以引用它們。在下面的例子中,self引用了接收者對象,而_cmd引用了方法本身的選擇器:

1

2

3

4

5

6

7

8

9

- strange

{

id? target = getTheReceiver();

SELmethod=getTheMethod();

if( target ==self||method== _cmd )

returnnil;

return[target performSelector:method];

}

在這兩個參數中,self更有用。實際上,它是在方法實現中訪問消息接收者對象的實例變量的途徑。

而當方法中的super關鍵字接收到消息時,編譯器會創建一個objc_super結構體:

1

structobjc_super{idreceiver;Classclass;};

這個結構體指明了消息應該被傳遞給特定超類的定義。但receiver仍然是self本身,這點需要注意,因為當我們想通過[super class]獲取超類時,編譯器只是將指向self的id指針和class的SEL傳遞給了objc_msgSendSuper函數,因為只有在NSObject類才能找到class方法,然后class方法調用object_getClass(),接著調用objc_msgSend(objc_super->receiver, @selector(class)),傳入的第一個參數是指向self的id指針,與調用[self class]相同,所以我們得到的永遠都是self的類型。

獲取方法地址

在IMP那節提到過可以避開消息綁定而直接獲取方法的地址并調用方法。這種做法很少用,除非是需要持續大量重復調用某方法的極端情況,避開消息發送泛濫而直接調用該方法會更高效。

NSObject類中有個methodForSelector:實例方法,你可以用它來獲取某個方法選擇器對應的IMP,舉個栗子:

1

2

3

4

5

6

7

void(*setter)(id, SEL, BOOL);

int i;

setter = (void (*)(id, SEL, BOOL))[target

methodForSelector:@selector(setFilled:)];

for( i =0; i <1000; i++ )

setter(targetList[i], @selector(setFilled:), YES);

當方法被當做函數調用時,上節提到的兩個隱藏參數就需要我們明確給出了。上面的例子調用了1000次函數,你可以試試直接給target發送1000次setFilled:消息會花多久。

PS:methodForSelector:方法是由 Cocoa 的 Runtime 系統提供的,而不是 Objc 自身的特性。

動態方法解析

你可以動態地提供一個方法的實現。例如我們可以用@dynamic關鍵字在類的實現文件中修飾一個屬性:

1

@dynamicpropertyName;

這表明我們會為這個屬性動態提供存取方法,也就是說編譯器不會再默認為我們生成setPropertyName:和propertyName方法,而需要我們動態提供。我們可以通過分別重載resolveInstanceMethod:和resolveClassMethod:方法分別添加實例方法實現和類方法實現。因為當 Runtime 系統在Cache和方法分發表中(包括超類)找不到要執行的方法時,Runtime會調用resolveInstanceMethod:或resolveClassMethod:來給程序員一次動態添加方法實現的機會。我們需要用class_addMethod函數完成向特定類添加特定方法實現的操作:

1

2

3

4

5

6

7

8

9

10

11

12

13

void dynamicMethodIMP(id self, SEL _cmd) {

// implementation ....

}

@implementationMyClass

+ (BOOL)resolveInstanceMethod:(SEL)aSEL

{

if(aSEL ==@selector(resolveThisMethodDynamically)) {

class_addMethod([selfclass],aSEL,(IMP) dynamicMethodIMP,"v@:");

returnYES;

}

return[superresolveInstanceMethod:aSEL];

}

@end

上面的例子為resolveThisMethodDynamically方法添加了實現內容,也就是dynamicMethodIMP方法中的代碼。其中 “v@:” 表示返回值和參數,這個符號涉及Type Encoding

PS:動態方法解析會在消息轉發機制浸入前執行。如果respondsToSelector:或instancesRespondToSelector:方法被執行,動態方法解析器將會被首先給予一個提供該方法選擇器對應的IMP的機會。如果你想讓該方法選擇器被傳送到轉發機制,那么就讓resolveInstanceMethod:返回NO。

評論區有人問如何用resolveClassMethod:解析類方法,我將他貼出有問題的代碼做了糾正和優化后如下,可以順便將實例方法和類方法的動態方法解析對比下:

頭文件:

1

2

3

4

5

6

#import

@interfaceStudent:NSObject

+ (void)learnClass:(NSString*) string;

- (void)goToSchool:(NSString*) name;

@end

m 文件:

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

#import"Student.h"

#import

@implementationStudent

+ (BOOL)resolveClassMethod:(SEL)sel {

if(sel ==@selector(learnClass:)) {

class_addMethod(object_getClass(self), sel, class_getMethodImplementation(object_getClass(self),@selector(myClassMethod:)),"v@:");

returnYES;

}

return[class_getSuperclass(self) resolveClassMethod:sel];

}

+ (BOOL)resolveInstanceMethod:(SEL)aSEL

{

if(aSEL ==@selector(goToSchool:)) {

class_addMethod([selfclass], aSEL, class_getMethodImplementation([selfclass],@selector(myInstanceMethod:)),"v@:");

returnYES;

}

return[superresolveInstanceMethod:aSEL];

}

+ (void)myClassMethod:(NSString*)string {

NSLog(@"myClassMethod = %@", string);

}

- (void)myInstanceMethod:(NSString*)string {

NSLog(@"myInstanceMethod = %@", string);

}

@end

需要深刻理解[self class]與object_getClass(self)甚至object_getClass([self class])的關系,其實并不難,重點在于self的類型:

當self為實例對象時,[self class]與object_getClass(self)等價,因為前者會調用后者。object_getClass([self class])得到元類。

當self為類對象時,[self class]返回值為自身,還是self。object_getClass(self)與object_getClass([self class])等價。

凡是涉及到類方法時,一定要弄清楚元類、selector、IMP 等概念,這樣才能做到舉一反三,隨機應變。

消息轉發

重定向

在消息轉發機制執行前,Runtime 系統會再給我們一次偷梁換柱的機會,即通過重載- (id)forwardingTargetForSelector:(SEL)aSelector方法替換消息的接受者為其他對象:

1

2

3

4

5

6

7

-(id)forwardingTargetForSelector:(SEL)aSelector

{

if(aSelector ==@selector(mysteriousMethod:)){

return alternateObject;

}

return[super forwardingTargetForSelector:aSelector];

}

畢竟消息轉發要耗費更多時間,抓住這次機會將消息重定向給別人是個不錯的選擇,不過千萬別返回self,因為那樣會死循環。如果此方法返回nil或self,則會進入消息轉發機制(forwardInvocation:);否則將向返回的對象重新發送消息。

如果想替換類方法的接受者,需要覆寫+ (id)forwardingTargetForSelector:(SEL)aSelector方法,并返回類對象

1

2

3

4

5

6

+ (id)forwardingTargetForSelector:(SEL)aSelector {

if(aSelector ==@selector(xxx)) {

returnNSClassFromString(@"Class name");

}

return[superforwardingTargetForSelector:aSelector];

}

轉發

當動態方法解析不作處理返回NO時,消息轉發機制會被觸發。在這時forwardInvocation:方法會被執行,我們可以重寫這個方法來定義我們的轉發邏輯:

1

2

3

4

5

6

7

8

- (void)forwardInvocation:(NSInvocation *)anInvocation

{

if([someOtherObjectrespondsToSelector:

[anInvocation selector]])

[anInvocationinvokeWithTarget:someOtherObject];

else

[superforwardInvocation:anInvocation];

}

該消息的唯一參數是個NSInvocation類型的對象——該對象封裝了原始的消息和消息的參數。我們可以實現forwardInvocation:方法來對不能處理的消息做一些默認的處理,也可以將消息轉發給其他對象來處理,而不拋出錯誤。

這里需要注意的是參數anInvocation是從哪的來的呢?其實在forwardInvocation:消息發送前,Runtime系統會向對象發送methodSignatureForSelector:消息,并取到返回的方法簽名用于生成NSInvocation對象。所以我們在重寫forwardInvocation:的同時也要重寫methodSignatureForSelector:方法,否則會拋異常。

當一個對象由于沒有相應的方法實現而無法響應某消息時,運行時系統將通過forwardInvocation:消息通知該對象。每個對象都從NSObject類中繼承了forwardInvocation:方法。然而,NSObject中的方法實現只是簡單地調用了doesNotRecognizeSelector:。通過實現我們自己的forwardInvocation:方法,我們可以在該方法實現中將消息轉發給其它對象。

forwardInvocation:方法就像一個不能識別的消息的分發中心,將這些消息轉發給不同接收對象。或者它也可以象一個運輸站將所有的消息都發送給同一個接收對象。它可以將一個消息翻譯成另外一個消息,或者簡單的”吃掉“某些消息,因此沒有響應也沒有錯誤。forwardInvocation:方法也可以對不同的消息提供同樣的響應,這一切都取決于方法的具體實現。該方法所提供是將不同的對象鏈接到消息鏈的能力。

注意:forwardInvocation:方法只有在消息接收對象中無法正常響應消息時才會被調用。 所以,如果我們希望一個對象將negotiate消息轉發給其它對象,則這個對象不能有negotiate方法。否則,forwardInvocation:將不可能會被調用。

轉發和多繼承

轉發和繼承相似,可以用于為Objc編程添加一些多繼承的效果。就像下圖那樣,一個對象把消息轉發出去,就好似它把另一個對象中的方法借過來或是“繼承”過來一樣。

這使得不同繼承體系分支下的兩個類可以“繼承”對方的方法,在上圖中Warrior和Diplomat沒有繼承關系,但是Warrior將negotiate消息轉發給了Diplomat后,就好似Diplomat是Warrior的超類一樣。

消息轉發彌補了 Objc 不支持多繼承的性質,也避免了因為多繼承導致單個類變得臃腫復雜。它將問題分解得很細,只針對想要借鑒的方法才轉發,而且轉發機制是透明的。

替代者對象(Surrogate Objects)

轉發不僅能模擬多繼承,也能使輕量級對象代表重量級對象。弱小的女人背后是強大的男人,畢竟女人遇到難題都把它們轉發給男人來做了。這里有一些適用案例,可以參看官方文檔

轉發與繼承

盡管轉發很像繼承,但是NSObject類不會將兩者混淆。像respondsToSelector:和isKindOfClass:這類方法只會考慮繼承體系,不會考慮轉發鏈。比如上圖中一個Warrior對象如果被問到是否能響應negotiate消息:

1

2

if( [aWarriorrespondsToSelector:@selector(negotiate)] )

...

結果是NO,盡管它能夠接受negotiate消息而不報錯,因為它靠轉發消息給Diplomat類來響應消息。

如果你為了某些意圖偏要“弄虛作假”讓別人以為Warrior繼承到了Diplomat的negotiate方法,你得重新實現respondsToSelector:和isKindOfClass:來加入你的轉發算法:

1

2

3

4

5

6

7

8

9

10

11

- (BOOL)respondsToSelector:(SEL)aSelector

{

if( [super respondsToSelector:aSelector] )

returnYES;

else{

/* Here, test whethertheaSelector message can? ? *

* be forwardedtoanother objectandwhetherthat*

* object can respondtoit. Return YESifitcan.? */

}

returnNO;

}

除了respondsToSelector:和isKindOfClass:之外,instancesRespondToSelector:中也應該寫一份轉發算法。如果使用了協議,conformsToProtocol:同樣也要加入到這一行列中。類似地,如果一個對象轉發它接受的任何遠程消息,它得給出一個methodSignatureForSelector:來返回準確的方法描述,這個方法會最終響應被轉發的消息。比如一個對象能給它的替代者對象轉發消息,它需要像下面這樣實現methodSignatureForSelector::

1

2

3

4

5

6

7

8

- (NSMethodSignature*)methodSignatureForSelector:(SEL)selector

{

NSMethodSignature* signature = [supermethodSignatureForSelector:selector];

if(!signature) {

signature = [surrogatemethodSignatureForSelector:selector];

}

returnsignature;

}

健壯的實例變量(Non Fragile ivars)

在 Runtime 的現行版本中,最大的特點就是健壯的實例變量。當一個類被編譯時,實例變量的布局也就形成了,它表明訪問類的實例變量的位置。從對象頭部開始,實例變量依次根據自己所占空間而產生位移:

上圖左邊是NSObject類的實例變量布局,右邊是我們寫的類的布局,也就是在超類后面加上我們自己類的實例變量,看起來不錯。但試想如果哪天蘋果更新了NSObject類,發布新版本的系統的話,那就悲劇了:

我們自定義的類被劃了兩道線,那是因為那塊區域跟超類重疊了。唯有蘋果將超類改為以前的布局才能拯救我們,但這樣也導致它們不能再拓展它們的框架了,因為成員變量布局被死死地固定了。在脆弱的實例變量(Fragile ivars) 環境下我們需要重新編譯繼承自 Apple 的類來恢復兼容性。那么在健壯的實例變量下會發生什么呢?

在健壯的實例變量下編譯器生成的實例變量布局跟以前一樣,但是當 runtime 系統檢測到與超類有部分重疊時它會調整你新添加的實例變量的位移,那樣你在子類中新添加的成員就被保護起來了。

需要注意的是在健壯的實例變量下,不要使用sizeof(SomeClass),而是用class_getInstanceSize([SomeClass class])代替;也不要使用offsetof(SomeClass, SomeIvar),而要用ivar_getOffset(class_getInstanceVariable([SomeClass class], "SomeIvar"))來代替。

Objective-C Associated Objects

在 OS X 10.6 之后,Runtime系統讓Objc支持向對象動態添加變量。涉及到的函數有以下三個:

1

2

3

voidobjc_setAssociatedObject(idobject,constvoid*key, idvalue, objc_AssociationPolicy policy);

idobjc_getAssociatedObject(idobject,constvoid*key);

voidobjc_removeAssociatedObjects(idobject);

這些方法以鍵值對的形式動態地向對象添加、獲取或刪除關聯值。其中關聯政策是一組枚舉常量:

1

2

3

4

5

6

7

enum{

OBJC_ASSOCIATION_ASSIGN? =0,

OBJC_ASSOCIATION_RETAIN_NONATOMIC? =1,

OBJC_ASSOCIATION_COPY_NONATOMIC? =3,

OBJC_ASSOCIATION_RETAIN? =01401,

OBJC_ASSOCIATION_COPY? =01403

};

這些常量對應著引用關聯值的政策,也就是 Objc 內存管理的引用計數機制。有關 Objective-C 引用計數機制的原理,可以查看這篇文章

Method Swizzling

之前所說的消息轉發雖然功能強大,但需要我們了解并且能更改對應類的源代碼,因為我們需要實現自己的轉發邏輯。當我們無法觸碰到某個類的源代碼,卻想更改這個類某個方法的實現時,該怎么辦呢?可能繼承類并重寫方法是一種想法,但是有時無法達到目的。這里介紹的是 Method Swizzling ,它通過重新映射方法對應的實現來達到“偷天換日”的目的。跟消息轉發相比,Method Swizzling 的做法更為隱蔽,甚至有些冒險,也增大了debug的難度。

這里摘抄一個 NSHipster 的例子:

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

#import

@implementationUIViewController(Tracking)

+ (void)load {

staticdispatch_once_tonceToken;

dispatch_once(&onceToken, ^{

Class aClass = [selfclass];

SEL originalSelector =@selector(viewWillAppear:);

SEL swizzledSelector =@selector(xxx_viewWillAppear:);

Method originalMethod = class_getInstanceMethod(aClass, originalSelector);

Method swizzledMethod = class_getInstanceMethod(aClass, swizzledSelector);

// When swizzling a class method, use the following:

// Class aClass = object_getClass((id)self);

// ...

// Method originalMethod = class_getClassMethod(aClass, originalSelector);

// Method swizzledMethod = class_getClassMethod(aClass, swizzledSelector);

BOOLdidAddMethod =

class_addMethod(aClass,

originalSelector,

method_getImplementation(swizzledMethod),

method_getTypeEncoding(swizzledMethod));

if(didAddMethod) {

class_replaceMethod(aClass,

swizzledSelector,

method_getImplementation(originalMethod),

method_getTypeEncoding(originalMethod));

}else{

method_exchangeImplementations(originalMethod, swizzledMethod);

}

});

}

#pragma mark - Method Swizzling

- (void)xxx_viewWillAppear:(BOOL)animated {

[selfxxx_viewWillAppear:animated];

NSLog(@"viewWillAppear: %@",self);

}

@end

上面的代碼通過添加一個Tracking類別到UIViewController類中,將UIViewController類的viewWillAppear:方法和Tracking類別中xxx_viewWillAppear:方法的實現相互調換。Swizzling 應該在+load方法中實現,因為+load是在一個類最開始加載時調用。dispatch_once是GCD中的一個方法,它保證了代碼塊只執行一次,并讓其為一個原子操作,線程安全是很重要的。

如果類中不存在要替換的方法,那就先用class_addMethod和class_replaceMethod函數添加和替換兩個方法的實現;如果類中已經有了想要替換的方法,那么就調用method_exchangeImplementations函數交換了兩個方法的IMP,這是蘋果提供給我們用于實現 Method Swizzling 的便捷方法。

可能有人注意到了這行:

1

2

3

4

5

// When swizzling aclassmethod,usethefollowing:

//ClassaClass=object_getClass((id)self);

// ...

//MethodoriginalMethod=class_getClassMethod(aClass, originalSelector);

//MethodswizzledMethod=class_getClassMethod(aClass, swizzledSelector);

object_getClass((id)self)與[self class]返回的結果類型都是Class,但前者為元類,后者為其本身,因為此時self為Class而不是實例.注意[NSObject class]與[object class]的區別:

1

2

3

4

5

6

7

+ (Class)class{

returnself;

}

- (Class)class{

returnobject_getClass(self);

}

PS:如果類中沒有想被替換實現的原方法時,class_replaceMethod相當于直接調用class_addMethod向類中添加該方法的實現;否則調用method_setImplementation方法,types參數會被忽略。method_exchangeImplementations方法做的事情與如下的原子操作等價:

1

2

3

4

IMP imp1 = method_getImplementation(m1);

IMP imp2 = method_getImplementation(m2);

method_setImplementation(m1, imp2);

method_setImplementation(m2, imp1);

最后xxx_viewWillAppear:方法的定義看似是遞歸調用引發死循環,其實不會的。因為[self xxx_viewWillAppear:animated]消息會動態找到xxx_viewWillAppear:方法的實現,而它的實現已經被我們與viewWillAppear:方法實現進行了互換,所以這段代碼不僅不會死循環,如果你把[self xxx_viewWillAppear:animated]換成[self viewWillAppear:animated]反而會引發死循環。

看到有人說+load方法本身就是線程安全的,因為它在程序剛開始就被調用,很少會碰到并發問題,于是 stackoverflow 上也有大神給出了另一個 Method Swizzling 的實現:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

- (void)replacementReceiveMessage:(const structBInstantMessage*)arg1 {

NSLog(@"arg1 is %@", arg1);

[self replacementReceiveMessage:arg1];

}

+ (void)load {

SELoriginalSelector= @selector(ReceiveMessage:);

SEL overrideSelector = @selector(replacementReceiveMessage:);

MethodoriginalMethod= class_getInstanceMethod(self,originalSelector);

Method overrideMethod = class_getInstanceMethod(self, overrideSelector);

if (class_addMethod(self,originalSelector,method_getImplementation(overrideMethod), method_getTypeEncoding(overrideMethod))) {

class_replaceMethod(self, overrideSelector, method_getImplementation(originalMethod),method_getTypeEncoding(originalMethod));

} else {

method_exchangeImplementations(originalMethod,overrideMethod);

}

}

上面的代碼同樣要添加在某個類的類別中,相比第一個種實現,只是去掉了dispatch_once部分。

Method Swizzling 的確是一個值得深入研究的話題,Method Swizzling 的最佳實現是什么呢?小弟才疏學淺理解的不深刻,找了幾篇不錯的資源推薦給大家:

Objective-C的hook方案(一): Method Swizzling

Method Swizzling

How do I implement method swizzling?

What are the Dangers of Method Swizzling in Objective C?

JRSwizzle

在用 SpriteKit 寫游戲的時候,因為 API 本身有一些缺陷(增刪節點時不考慮父節點是否存在啊,很容易崩潰啊有木有!),我在 Swift 上使用 Method Swizzling彌補這個缺陷:

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

extensionSKNode{

classfuncyxy_swizzleAddChild(){

letcls =SKNode.self

letoriginalSelector =Selector("addChild:")

letswizzledSelector =Selector("yxy_addChild:")

letoriginalMethod = class_getInstanceMethod(cls, originalSelector)

letswizzledMethod = class_getInstanceMethod(cls, swizzledSelector)

method_exchangeImplementations(originalMethod, swizzledMethod)

}

classfuncyxy_swizzleRemoveFromParent(){

letcls =SKNode.self

letoriginalSelector =Selector("removeFromParent")

letswizzledSelector =Selector("yxy_removeFromParent")

letoriginalMethod = class_getInstanceMethod(cls, originalSelector)

letswizzledMethod = class_getInstanceMethod(cls, swizzledSelector)

method_exchangeImplementations(originalMethod, swizzledMethod)

}

funcyxy_addChild(node: SKNode){

ifnode.parent ==nil{

self.yxy_addChild(node)

}

else{

println("This node has already a parent!\(node.name)")

}

}

funcyxy_removeFromParent(){

ifparent !=nil{

dispatch_async(dispatch_get_main_queue(), { () ->Voidin

self.yxy_removeFromParent()

})

}

else{

println("This node has no parent!\(name)")

}

}

}

然后其他地方調用那兩個類方法:

1

2

SKNode.yxy_swizzleAddChild()

SKNode.yxy_swizzleRemoveFromParent()

因為 Swift 中的 extension 的特殊性,最好在某個類的load()方法中調用上面的兩個方法.我是在AppDelegate 中調用的,于是保證了應用啟動時能夠執行上面兩個方法.

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • runtime 和 runloop 作為一個程序員進階是必須的,也是非常重要的, 在面試過程中是經常會被問到的, ...
    SOI閱讀 21,848評論 3 63
  • 轉至元數據結尾創建: 董瀟偉,最新修改于: 十二月 23, 2016 轉至元數據起始第一章:isa和Class一....
    40c0490e5268閱讀 1,768評論 0 9
  • 轉載:http://yulingtianxia.com/blog/2014/11/05/objective-c-r...
    F麥子閱讀 761評論 0 2
  • 特別佩服古人的智慧,可以根據晝夜交替、四季變化來將一年劃分成二十四個節氣,對應每個節氣,都有各自的詩詞俗語規律來提...
    風沙獨舞閱讀 717評論 6 9
  • Fail to log on weibo(forget password),so log in here. It'...
    ORANGE_JUN閱讀 130評論 0 0