【IOS開發(fā)高級系列】Objective-c Runtime專題總結(jié)

主要參考鏈接:

????http://yulingtianxia.com/blog/2014/11/05/objective-c-runtime/

(Good)刨根問底Objective-C Runtime

http://www.cocoachina.com/ios/20141224/10740.html


1 OC與Runtime的交互方式

? ? ? ?OC從三種不同的層級上與Runtime系統(tǒng)進行交互,分別是通過Objective-C源代碼,通過Foundation框架的NSObject類定義的方法,通過對runtime函數(shù)的直接調(diào)用。

1.1 Objective-C源代碼

? ? ? ? 大部分情況下你就只管寫你的OC代碼就行,runtime系統(tǒng)自動在幕后辛勤勞作著。

1.2 NSObject的方法

? ? ? ? Cocoa中大多數(shù)類都繼承于NSObject類,也就自然繼承了它的方法。最特殊的例外是NSProxy,它是個抽象超類,它實現(xiàn)了一些消息轉(zhuǎn)發(fā)有關(guān)的方法,可以通過繼承它來實現(xiàn)一個其他類的替身類或是虛擬出一個不存在的類。

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

1.3 Runtime的函數(shù)

????????其實[receiver message]會被編譯器轉(zhuǎn)化為:

objc_msgSend(receiver, selector)

????????如果消息含有參數(shù),則為:

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

????????如果消息的接收者能夠找到對應(yīng)的selector,那么就相當(dāng)于直接執(zhí)行了接收者這個對象的特定方法;否則,消息要么被轉(zhuǎn)發(fā),或是臨時向接收者動態(tài)添加這個selector對應(yīng)的實現(xiàn)內(nèi)容,要么就干脆玩完崩潰掉。

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

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

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

2 Runtime術(shù)語

id objc_msgSend ( id self, SEL op, ... );

2.1 SEL

? ? ? ? objc_msgSend函數(shù)第二個參數(shù)類型為SEL,它是selector在OC中的表示類型(Swift中是Selector類)。 selector是方法選擇器,可以理解為區(qū)分方法的ID,而這個ID的數(shù)據(jù)結(jié)構(gòu)是SEL :

typedef struct objc_selector *SEL;

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

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

2.2 Id

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

typedef struct objc_object *id;

????????id 在 objc.h 中定義如下:

///?A?pointer?to?an?instance?of?a?class.

typedef?struct?objc_object?*id;

????????就像注釋中所說的這樣 id 是指向一個 objc_object 結(jié)構(gòu)體的指針。

????????id 這個struct的定義本身就帶了一個 *, 所以我們在使用其他NSObject類型的實例時需要在前面加上 *, 而使用 id 時卻不用。

2.3 objc_object結(jié)構(gòu)體

????????那?objc_object?又是啥呢:

struct objc_object { Class isa; };

????????objc_object?結(jié)構(gòu)體包含一個?isa?指針,根據(jù)?isa?指針就可以順藤摸瓜找到對象所屬的類。

????????這個時候我們知道Objective-C中的object在最后會被轉(zhuǎn)換成C的結(jié)構(gòu)體Class,而在這個struct中有一個 isa 指針,指向它的類別 Class(PS:這里還不是Meta Class)。

2.4 Class、objc_class

????????在 objc.h 中定義如下:

///?An?opaque?type?that?represents?an?Objective-C?class.

typedef?struct?objc_class?*Class;

????????我們可以看到Class本身指向的也是一個C的struct objc_class。

????????繼續(xù)看在runtime.h中objc_class定義如下:

struct?objc_class?{

????Class?isa??OBJC_ISA_AVAILABILITY;

????#if?!__OBJC2__

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

????const?char?*name?????????????????????????????????????????OBJC2_UNAVAILABLE;

????long?version?????????????????????????????????????????????OBJC2_UNAVAILABLE;

????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?*protocols?????????????????????OBJC2_UNAVAILABLE;

????#endif

}?OBJC2_UNAVAILABLE;

????????該結(jié)構(gòu)體中,isa指向所屬Class, super_class指向父類別。另外,還關(guān)聯(lián)了它的類名,成員變量列表,方法列表,緩存,還有附屬的協(xié)議。

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

struct objc_ivar_list {????

? ??int ivar_count OBJC2_UNAVAILABLE;?

#ifdef __LP64__?????

? ??int?space OBJC2_UNAVAILABLE;

#endif???? /* variable length structure */????

struct objc_ivar ivar_list[1] OBJC2_UNAVAILABLE;

}? OBJC2_UNAVAILABLE;?

struct objc_method_list {????

? ??struct objc_method_list *obsolete OBJC2_UNAVAILABLE;?

? ??int method_count OBJC2_UNAVAILABLE;

#ifdef __LP64__????

? ??int space OBJC2_UNAVAILABLE;

#endif????

/* variable length structure */????

struct objc_method method_list[1] OBJC2_UNAVAILABLE;

}

2.5 Isa指針

????????Isa指針有兩類:

? ? 1、在oc對象轉(zhuǎn)化為c結(jié)構(gòu)體時,用于指向?qū)?yīng)Class結(jié)構(gòu)體的isa指針;

????2、在Class結(jié)構(gòu)體內(nèi)部實現(xiàn)里,isa指針作為一個成員變量,指向該class的MetaClass;

????????情況一:

????????情況二:

? ? ? ? Class在設(shè)計中本身也是一個對象。而這個Class對象的對應(yīng)的類,我們叫它 Meta Class。即Class結(jié)構(gòu)體中的isa指向的就是它的Meta Class。

2.6 元類(MetaClass)

????????一個ObjC類同時也是一個對象,為了處理類和對象的關(guān)系,runtime 庫創(chuàng)建了一種叫做元類 (Meta Class) 的東西。當(dāng)你發(fā)出一個類似[NSObject alloc]的消息時,你事實上是把這個消息發(fā)給了一個類對象(Class Object),這個類對象必須是一個元類的實例,而這個元類同時也是一個根元類(root meta class)的實例。你會說NSObject的子類時,你的類就會指向NSObject做為其超類。但是所有的元類都指向根元類為其超類。所有的元類的方法列表都有能夠響應(yīng)消息的類方法。所以當(dāng)[NSObject alloc]這條消息發(fā)給類對象的時候,objc_msgSend()會去它的元類里面去查找能夠響應(yīng)消息的方法,如果找到了,然后對這個類對象執(zhí)行方法調(diào)用。

????????簡單理解:

????? objc_class中存放的是實例方法列表,而Meta Class中存放的是類方法列表;可以把Meta Class理解為一個Class對象的Class。簡單的說:

????1、當(dāng)我們發(fā)送一個消息給一個NSObject對象實例時,這條消息會在對象的類的方法列表里查找;

????2、當(dāng)我們發(fā)送一個消息給一個類時,這條消息會在類的Meta?Class的方法列表(即類方法)里查找,而 Meta Class本身也是一個Class,它跟其他Class一樣也有自己的isa 和 super_class 指針。看下圖:

? ? ? ? 注意最左邊son(Instance)的isa指針,其實與其他isa指針不同。

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

????· 每個Class都有一個isa指針指向一個唯一的MetaClass;

????· 每一個Meta Class的isa指針都指向最上層的Meta Class(圖中的NSObject的Meta Class);

????· 最上層的Meta Class的isa指針指向自己,形成一個回路;

????· 每一個Meta Class的super class指針指向它原本Class的 Super Class的Meta Class。但是最上層的Meta Class的 Super Class指向NSObject Class本身;

????· 最上層的NSObject Class的super class指向nil;

2.7 SEL與objc_selector

????????打開objc.h文件,看下SEL的定義如下:

typedef?struct?objc_selector?*SEL;

????????SEL是一個指向objc_selector結(jié)構(gòu)體的指針。而objc_selector 的定義并沒有在runtime.h中給出定義。我們可以嘗試運行如下代碼:

SEL?sel?=?@selector(foo);

NSLog(@"%s",?(char?*)sel);

NSLog(@"%p",?sel);

const?char?*selName?=?[@"foo"?UTF8String];

SEL?sel2?=?sel_registerName(selName);

NSLog(@"%s",?(char?*)sel2);

NSLog(@"%p",?sel2);

輸出如下:

2014-11-06?13:46:08.058?Test[15053:1132268]?foo

2014-11-06?13:46:08.058?Test[15053:1132268]?0x7fff8fde5114

2014-11-06?13:46:08.058?Test[15053:1132268]?foo

2014-11-06?13:46:08.058?Test[15053:1132268]?0x7fff8fde5114

????????Objective-C在編譯時,會根據(jù)方法的名字生成一個用來區(qū)分這個方法的唯一的一個ID。只要方法名稱相同,那么它們的ID就是相同的。

????????兩個類之間,不管它們是父類與子類的關(guān)系,還是之間沒有這種關(guān)系,只要方法名相同,那么它的SEL就是一樣的。每一個方法都對應(yīng)著一個SEL。編譯器會根據(jù)每個方法的方法名為那個方法生成唯一的SEL。這些SEL組成了一個Set集合,當(dāng)我們在這個集合中查找某個方法時,只需要去找這個方法對應(yīng)的SEL即可。而SEL本質(zhì)是一個字符串,所以直接比較它們的地址即可。

????????當(dāng)然,不同的類可以擁有相同的selector。不同類的實例對象執(zhí)行相同的selector時,會在各自的方法列表中去根據(jù)selector去尋找自己對應(yīng)的IMP。

2.8 Method

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

typedef struct objc_method *Method;

????????而?objc_method?在上面的方法列表中提到過,它存儲了方法名、方法類型和方法實現(xiàn):

struct objc_method {? ???

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

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

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

} OBJC2_UNAVAILABLE;

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

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

????· method_imp指向了方法的實現(xiàn),本質(zhì)上是一個函數(shù)指針,后面會詳細講到。

2.9 Ivar

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

typedef struct objc_ivar *Ivar;

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

struct objc_ivar {????

? ??char *ivar_name OBJC2_UNAVAILABLE;

? ??char *ivar_type OBJC2_UNAVAILABLE;

? ??int ivar_offset OBJC2_UNAVAILABLE;

#ifdef __LP64__????

? ??int space OBJC2_UNAVAILABLE;

#endif

} OBJC2_UNAVAILABLE;

????????PS:?OBJC2_UNAVAILABLE之類的宏定義是蘋果在OC中對系統(tǒng)運行版本進行約束的黑魔法,有興趣的可以查看源代碼。

????????這里我們注意第三個成員ivar_offset。它表示基地址偏移字節(jié)。

2.10 IMP函數(shù)指針

????????IMP?在?objc.h?中的定義是:

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

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

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

2.11 Cache

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

typedef struct objc_cache *Cache

????????還記得之前?objc_class?結(jié)構(gòu)體中有一個?struct objc_cache *cache?吧,它到底是緩存啥的呢,先看看?objc_cache?的實現(xiàn):

struct objc_cache {????

? ??unsigned int mask /* total = mask + 1 */?? OBJC2_UNAVAILABLE;

? ??unsigned int occupied OBJC2_UNAVAILABLE;

????Method buckets[1] OBJC2_UNAVAILABLE;

};

????· mask: 指定分配cache buckets的總數(shù)。在方法查找中,Runtime使用這個字段確定數(shù)組的索引位置;

????· occupied: 實際占用cache buckets的總數(shù);

????· buckets: 指定Method數(shù)據(jù)結(jié)構(gòu)指針的數(shù)組。這個數(shù)組可能包含不超過mask+1個元素。需要注意的是,指針可能是NULL,表示這個緩存bucket沒有被占用,另外被占用的bucket可能是不連續(xù)的。這個數(shù)組可能會隨著時間而增長。

????????Cache?為方法調(diào)用的性能進行優(yōu)化,通俗地講,每當(dāng)實例對象接收到一個消息時,它不會直接在?isa?指向的類的方法列表中遍歷查找能夠響應(yīng)消息的方法,因為這樣效率太低了,而是優(yōu)先在Cache中查找。Runtime 系統(tǒng) 會把 被調(diào)用的 方法存到Cache?中(理論上講一個方法如果被調(diào)用,那么它有可能今后還會被調(diào)用),下次查找的時候效率更高。

2.12 方法列表

????????objc_method_list就是用來存儲當(dāng)前類的方法鏈表,objc_method存儲了類的某個方法的信息。

3 消息

????????OC中發(fā)送消息是用中括號([ ])把接收者和消息括起來,而直到運行時才會把消息與方法實現(xiàn)綁定。

3.1 objc_msgSend消息發(fā)送機制

? ? ? ?看起來像是?objc_msgSend返回了數(shù)據(jù),其實?objc_msgSend?從不返回數(shù)據(jù)而是你的方法被調(diào)用后返回了數(shù)據(jù)。下面詳細敘述下消息發(fā)送步驟:

????1. 檢測這個selector是不是要忽略的。比如 Mac OS X 開發(fā),有了垃圾回收就不理會retain、release這些函數(shù)了。

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

????3. 如果上面兩個都過了,那就開始查找這個類的IMP,先從cache里面找,完了找得到就跳到對應(yīng)的函數(shù)去執(zhí)行。

????4. 如果cache找不到就找一下方法分發(fā)表。(Class中的方法列表)

????5. 如果分發(fā)表找不到就到超類的分發(fā)表去找,一直找,直到找到NSObject類為止。

? ? ? ? 如果還找不到就要開始進入動態(tài)方法解析和消息轉(zhuǎn)發(fā)的機制,后面會提到。

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

3.2 方法中的隱藏參數(shù)

????????我們經(jīng)常在方法中使用?self?關(guān)鍵字來引用實例本身,但從沒有想過為什么?self就能取到調(diào)用當(dāng)前方法的對象吧。其實?self?的內(nèi)容是在方法運行時被偷偷地動態(tài)傳入的。

????????當(dāng)?objc_msgSend?找到方法對應(yīng)的實現(xiàn)時,它將直接調(diào)用該方法實現(xiàn),并將消息中所有的參數(shù)都傳遞給方法實現(xiàn),同時,它還將傳遞兩個隱藏的參數(shù):

????– 接收消息的對象(也就是?self?指向的內(nèi)容)

????– 方法選擇器(?_cmd?指向的內(nèi)容)

????????之所以說它們是隱藏的是因為在源代碼方法的定義中并沒有聲明這兩個參數(shù)。它們是在代碼被編譯時被插入實現(xiàn)中的。盡管這些參數(shù)沒有被明確聲明,在源代碼中我們?nèi)匀豢梢砸盟鼈?。在下面的例子中?self?引用了接收者對象,而?_cmd?引用了方法本身的選擇器:

- strange {????

????id? target =? getTheReceiver();????

????SEL method = getTheMethod();?????

? ??if( target ==self?|| method == _cmd )?? return?nil;?

? ??return [target performSelector: method];????

}

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

????????而當(dāng)方法中的?super?關(guān)鍵字接收到消息時,編譯器會創(chuàng)建一個?objc_super?結(jié)構(gòu)體:

struct objc_super { id receiver;?Class class; };

????????這個結(jié)構(gòu)體指明了消息應(yīng)該被傳遞給特定超類的定義。

3.3 獲取方法地址

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

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

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);

????????PS:?methodForSelector:?方法是由Cocoa的Runtime系統(tǒng)提供的,而不是OC自身的特性。

3.4 Category處理機制

3.4.1 objc_category

????????在runtime.h中查看定義:

typedef?struct?objc_category?*Category;

????????同樣也是指向一個 objc_category 的C結(jié)構(gòu)體,定義如下:

struct?objc_category?{

????char?*category_name OBJC2_UNAVAILABLE;

????char?*class_name OBJC2_UNAVAILABLE;

????struct?objc_method_list?*instance_methods OBJC2_UNAVAILABLE;

????struct?objc_method_list?*class_methods OBJC2_UNAVAILABLE;

????struct?objc_protocol_list?*protocols OBJC2_UNAVAILABLE;

}? OBJC2_UNAVAILABLE;

????????通過上面的結(jié)構(gòu)體,大家可以很清楚的看出存儲的內(nèi)容。我們繼續(xù)往下看,打開objc源代碼,在objc-runtime-new.h中我們可以發(fā)現(xiàn)如下定義:

struct?category_t?{

????const?char?*name;

????classref_t?cls;

????struct?method_list_t?*instanceMethods;

????struct?method_list_t?*classMethods;

????struct?protocol_list_t?*protocols;

????struct?property_list_t?*instanceProperties;

};

????????上面的定義需要提到的地方有三點:

????1. name 是指class_name 而不是category_name;

????2. cls是要擴展的類對象,編譯期間是不會定義的,而是在Runtime階段通過name對應(yīng)到對應(yīng)的類對象;

????3. instanceProperties表示Category里所有的properties,這就是我們可以通過objc_setAssociatedObject和objc_getAssociatedObject增加實例變量的原因,不過這個和一般的實例變量是不一樣的;

3.4.2 Category加載機制

????1. 打開objc源代碼,找到objc-os.mm, 函數(shù)_objc_init為runtime的加載入口,由libSystem調(diào)用,進行初始化操作。

????2. 之后調(diào)用objc-runtime-new.mm -> map_images加載map到內(nèi)存;

????3. 之后調(diào)用objc-runtime-new.mm ->_read_images初始化內(nèi)存中的map, 這個時候?qū)oad所有的類、協(xié)議還有Category。NSObject的+load方法就是這個時候調(diào)用的;

????????這里貼上Category被加載的代碼:

// Discover categories.

for (EACH_HEADER) {

??? category_t **catlist = _getObjc2CategoryList(hi, &count);

??? for (i = 0; i< count; i++) {

??????? category_t*cat = catlist[i];

??????? Class cls = remapClass(cat->cls);

??????? if (!cls) {

??????????? //Category's target class is missing (probably weak-linked).

??????????? //Disavow any knowledge of this category.

???????????catlist[i] = nil;

??????????? if(PrintConnecting) {

???????????????_objc_inform("CLASS: IGNORING category \?\?\?(%s) %p with missing weak-linked target class", cat->name, cat);

??????????? }

???????????continue;

??????? }

??????? // Process this category.

??????? // First, register the category with its target class.

??????? // Then, rebuild the class's method lists (etc) if

??????? // the class is realized.

??????? BOOL classExists = NO;

??????? if(cat->instanceMethods ||?cat->protocols ||? cat->instanceProperties)

??????? {

???????? ???addUnattachedCategoryForClass(cat, cls, hi);

??????????? if(cls->isRealized()) {

???????????????remethodizeClass(cls);

???????????????classExists = YES;

??????????? }

??????????? if(PrintConnecting) {

???????????????_objc_inform("CLASS: found category -%s(%s) %s",

????????????????????????????cls->nameForLogging(), cat->name,

???????????????????????????? classExists ? "on existing class" : "");

??????????? }

??????? }

??????? if(cat->classMethods? ||? cat->protocols?

??????????? /*||? cat->classProperties */)

??????? {

? ? ? ? ? ? addUnattachedCategoryForClass(cat, cls->ISA(), hi);

??????????? if(cls->ISA()->isRealized()) {

???????????????remethodizeClass(cls->ISA());

??????????? }

??????????? if(PrintConnecting) {

???????????????_objc_inform("CLASS: found category +%s(%s)", cls->nameForLogging(), cat->name);

??????????? }

??????? }

??? }

}

????????1) 循環(huán)調(diào)用了_getObjc2CategoryList方法,這個方法的實現(xiàn)是:

GETSECT(_getObjc2CategoryList,? category_t *,?"__objc_catlist");

????????方法中最后一個參數(shù)__objc_catlist就是編譯器剛剛生成的category數(shù)組。

????????2) load完所有的categories之后,開始對Category進行處理。

????????從上面的代碼中我們可以發(fā)現(xiàn):實例方法被加入到了當(dāng)前的類對象中, 類方法被加入到了當(dāng)前類的Meta Class中(cls->ISA);

????Step 1. 調(diào)用addUnattachedCategoryForClass方法

????Step 2. 調(diào)用remethodizeClass方法, 在remethodizeClass的實現(xiàn)里調(diào)用attachCategoryMethods

static void attachCategoryMethods(Class cls, category_list *cats, bool flushCaches)

{

???if (!cats) return;

???if (PrintReplacedMethods) printReplacements(cls, cats);

???bool isMeta = cls->isMetaClass();

???method_list_t **mlists = (method_list_t **)_malloc_internal(cats->count * sizeof(*mlists));

???// Count backwards through cats to get newest categories first

???int mcount = 0;

???int i = cats->count;

???BOOL fromBundle = NO;

???while (i--) {

???????method_list_t *mlist = cat_method_list(cats->list[i].cat, isMeta);

???????if (mlist) {

???????????mlists[mcount++] = mlist;

???????????fromBundle |= cats->list[i].fromBundle;

???????}

??? }

???attachMethodLists(cls, mlists, mcount, NO, fromBundle, flushCaches);

???_free_internal(mlists);

}

????????這里把一個類的category_list的所有方法取出來生成了method list。這里是倒序添加的,也就是說,新生成的category的方法會先于舊的category的方法插入。

????????之后調(diào)用attachMethodLists將所有方法前序添加進類的method list中,如果原來類的方法列表是a,b,Category的方法列表是c,d。那么插入之后的方法列表將會是c,d,a,b。

3.4.3 小發(fā)現(xiàn)

????????看上面被編譯器轉(zhuǎn)換的代碼,我們發(fā)現(xiàn)Category頭文件被注釋掉了,結(jié)合上面category的加載過程。這就是我們即使沒有import category的頭文件,都能夠成功調(diào)用到Category方法的原因。

????????runtime加載完成后,Category的原始信息在類結(jié)構(gòu)中將不會存在。

4 objc runtime的成員變量和屬性

4.1 objc_ivar類型

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

typedef struct objc_ivar *Ivar;

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

struct objc_ivar {????

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

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

? ??int ivar_offset OBJC2_UNAVAILABLE;

#ifdef __LP64__????

? ??int space OBJC2_UNAVAILABLE;

#endif

} OBJC2_UNAVAILABLE;

????????PS:?OBJC2_UNAVAILABLE?之類的宏定義是蘋果在 OC 中對系統(tǒng)運行版本進行約束的黑魔法,有興趣的可以查看源代碼。

????????這里我們注意第三個成員ivar_offset。它表示基地址偏移字節(jié)。

????????在編譯我們的類時,編譯器生成了一個 ivar布局,顯示了在類中從哪可以訪問我們的ivars 。看下圖:

????????上圖中,左側(cè)的數(shù)據(jù)就是地址偏移字節(jié),我們對 ivar 的訪問就可以通過 對象地址 + ivar偏移字節(jié)的方法。但是這又引發(fā)一個問題,看下圖:

????????我們增加了父類的ivar,這個時候布局就出錯了,我們就不得不重新編譯子類來恢復(fù)兼容性。而Objective-C Runtime中使用了Non Fragile ivars,看下圖:

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

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

4.2 偏移示例

????????我們來看一個例子:

@interface?Student?:?NSObject

{

????@private NSInteger?age;

}

@end

@implementation?Student

-?(NSString?*)description

{

????return?[NSString?stringWithFormat: @"age?=?%d",?age];

}

@end

int?main(int?argc,?const?char?*?argv[])?{

????@autoreleasepool?{

????????Student?*student?=?[[Student?alloc]?init];

????????student->age?=?24;

????}

????return?0;

}

????????上述代碼,Student有兩個被標記為private的ivar,這個時候當(dāng)我們使用 -> 訪問時,編譯器會報錯。那么我們?nèi)绾卧O(shè)置一個被標記為private的ivar的值呢?

????????通過上面的描述,我們知道ivar是通過計算字節(jié)偏量來確定地址,并訪問的。我們可以改成這樣:

@interface?Student?:?NSObject

{

????@private int?age;

}

@end

@implementation?Student

-?(NSString?*)description

{

????NSLog(@"current?pointer?=?%p",?self);

????NSLog(@"age?pointer?=?%p",?&age);

????return?[NSString?stringWithFormat: @"age?=?%d",?age];

}

@end

int?main(int?argc,?const?char?*?argv[])?{

? @autoreleasepool?{

??? Student?*student?=?[[Student?alloc]?init];

????Ivar?age_ivar?=?class_getInstanceVariable(object_getClass(student),?"age");

????int?*age_pointer?=?(int?*)((__bridge?void?*)(student)?+ ivar_getOffset(age_ivar));

????NSLog(@"age?ivar?offset?=?%td",?ivar_getOffset(age_ivar));

????*age_pointer?=?10;

????NSLog(@"%@",?student);

? }

? return?0;

}

上述代碼的輸出結(jié)果為:

2014-11-08?18:24:38.892?Test[4143:466864]?age?ivar?offset?=?8

2014-11-08?18:24:38.893?Test[4143:466864]?current?pointer?=?0x1001002d0

2014-11-08?18:24:38.893?Test[4143:466864]?age?pointer?=?0x1001002d8

2014-11-08?18:24:38.894?Test[4143:466864]?age?=?10

????????我們可以清晰的看到指針地址的變化和偏移量,和我們上述描述一致。

4.3 Property實現(xiàn)機制

????????使用clang -rewrite-objc main.m重寫題目中的代碼,我們發(fā)現(xiàn)Sark類中的name屬性被轉(zhuǎn)換成了如下代碼:

struct Sark_IMPL{

??? struct NSObject_IMPL NSObject_IVARS;

??? NSString *_name;

};

// @property(nonatomic, copy) NSString *name;

/* @end */

// @implementationSark

static NSString*_I_Sark_name(Sark *self, SEL _cmd)

{

????return (*(NSString**)((char*)self + OBJC_IVAR_$_Sark$_name));

}

static void_I_Sark_setName_(Sark *self, SEL_cmd, NSString *name)

{

????objc_setProperty(self, _cmd, __OFFSETOFIVAR__(struct Sark,_name), (id)name,0,1);

}

??? 類中的Property屬性被編譯器轉(zhuǎn)換成了Ivar,并且自動添加了我們熟悉的Set和Get方法。

??? 我們這個時候回頭看一下objc_class結(jié)構(gòu)體中的內(nèi)容,并沒有發(fā)現(xiàn)用來專門記錄Property的list。我們翻開objc源代碼,在objc-runtime-new.h中,發(fā)現(xiàn)最終還是會通過在class_ro_t結(jié)構(gòu)體中使用property_list_t存儲對應(yīng)的propertyies。

????????而在剛剛重寫的代碼中,我們可以找到這個property_list_t:

static struct/*_prop_list_t*/{

??? unsigned int entsize; //sizeof(struct _prop_t)

??? unsigned int count_of_properties;

??? struct _prop_t prop_list[1];

}_OBJC_$_PROP_LIST_Sark__attribute__ ((used, section("__DATA,__objc_const"))) = {

??? sizeof(_prop_t),1,name

};

static struct_class_ro_t _OBJC_CLASS_RO_$_Sark __attribute__((used, section ("__DATA,__objc_const")))={

0, __OFFSETOFIVAR__(structSark,_name),sizeof(struct Sark_IMPL),

?(unsigned int)0,0,"Sark",

?(const struct _method_list_t*)&_OBJC_$_INSTANCE_METHODS_Sark,0,

?(const struct _ivar_list_t*)&_OBJC_$_INSTANCE_VARIABLES_Sark,0,

?(const struct _prop_list_t*)&_OBJC_$_PROP_LIST_Sark,

};

解惑

1)為什么能夠正常運行,并調(diào)用到speak方法?

id?cls?=?[Sark?class];

void?*obj?=?&cls;

[(__bridge?id)obj?speak];

????????obj被轉(zhuǎn)換成了一個指向Sark Class的指針,然后使用id轉(zhuǎn)換成了objc_object類型。這個時候的obj已經(jīng)相當(dāng)于一個Sark的實例對象(但是和使用[Sark new]生成的對象還是不一樣的),我們回想下Runtime的第二篇博文中objc_object結(jié)構(gòu)體的構(gòu)成就是一個指向Class的isa指針。

????????這個時候我們再回想下上一篇博文中objc_msgSend的工作流程,在代碼中的obj指向的Sark Class中能夠找到speak方法,所以代碼能夠正常運行。

2) 為什么self.name的輸出為?

????????我們在測試代碼中加入一些調(diào)試代碼和Log如下:

-?(void)speak

{

??? unsigned?int?numberOfIvars?=?0;

????Ivar?*ivars?=?class_copyIvarList([self?class],?&numberOfIvars);

????for(const?Ivar?*p?=?ivars;?p?<?ivars+numberOfIvars;?p++)?{

?? ????Ivar?const?ivar?=?*p;

????????ptrdiff_t?offset?=?ivar_getOffset(ivar);

????????const?char?*name?=?ivar_getName(ivar);

????????NSLog(@"Sark? ivar name = %s, offset = %td", name, offset);

????}

????NSLog(@"my?name?is?%p",?&_name);

????NSLog(@"my?name?is?%@",?*(&_name));

}

@implementation?Test

-?(instancetype)init

{

????self?=?[super?init];

????if?(self)?{

????????NSLog(@"Test?instance?=?%@",?self);

????????void?*self2?=?(__bridge?void?*)self;

????????NSLog(@"Test?instance?pointer?=?%p",?&self2);

????????id?cls?=?[Sark?class];

????????NSLog(@"Class?instance?address?=?%p",?cls);

????????void?*obj?=?&cls;

????????NSLog(@"Void?*obj?=?%@",?obj);

????????[(__bridge?id)obj?speak];

????}

????return?self;

}

@end

????????輸出結(jié)果如下:

2014-11-11?00:56:02.464?Test[10475:1071029]?Test?instance?=

2014-11-11?00:56:02.464 Test[10475:1071029]?Test?instance?pointer?=?0x7fff5fbff7c8

2014-11-11?00:56:02.465?Test[10475:1071029]?Class?instance?address?=?0x1000023c8

2014-11-11?00:56:02.465?Test[10475:1071029]?Void?*obj?=?

2014-11-11?00:56:02.465 Test[10475:1071029]?Sark?ivar?name?=?_name,?offset?=?8

2014-11-11?00:56:02.465?Test[10475:1071029]?my?name?is?0x7fff5fbff7c8

2014-11-11?00:56:02.465?Test[10475:1071029]?my?name?is

????????Sark中Propertyname最終被轉(zhuǎn)換成了Ivar加入到了類的結(jié)構(gòu)中,Runtime通過計算成員變量的地址偏移來尋找最終Ivar的地址,我們通過上述輸出結(jié)果,可以看到 Sark的對象指針地址加上Ivar的偏移量之后剛好指向的是Test對象指針地址。

? ? ? ? 這里的原因主要是因為在C中,局部變量是存儲到內(nèi)存的棧區(qū),程序運行時棧的生長規(guī)律是從地址高到地址低。C語言到頭來講是一個順序運行的語言,隨著程序運行,棧中的地址依次往下走。

? ? ? ? 看下圖,可以清楚的展示整個計算的過程:

? ? ? ? 我們可以做一個另外的實驗,把Test Class 的init方法改為如下代碼:

@interface?Father?:?NSObject

@end

@implementation?Father

@end

@implementation?Test

-?(instancetype)init

{

????self?=?[super?init];

????if?(self)?{

????????NSLog(@"Test?instance?=?%@",?self);

????????id?fatherCls?=?[Father?class];

????????void?*father;

????????father?=?(void?*)&fatherCls;

????????id?cls?=?[Sark?class];

????????void?*obj;

????????obj?=?(void?*)&cls;

????????[(__bridge?id)obj?speak];

????}

????return?self;

}

@end

????????你會發(fā)現(xiàn)這個時候的輸出變成了:

2014-11-08 21:40:36.724 Test[4845:543231] Test instance? =?

2014-11-08 21:40:36.725 Test[4845:543231]?ivar?name?=?_name,?offset?=?8

2014-11-08 21:40:36.726 Test[4845:543231] Sark instance?=?0x7fff5fbff7b8

2014-11-08 21:40:36.726 Test[4845:543231] my name is?0x7fff5fbff7c0

2014-11-08 21:40:36.726 Test[4845:543231] my name is

關(guān)于C語言內(nèi)存分配和使用的問題可參考這篇文章http://www.th7.cn/Program/c/201212/114923.shtml。

5 動態(tài)方法解析

? ? ? ? 你可以動態(tài)地提供一個方法的實現(xiàn)。例如我們可以用?@dynamic?關(guān)鍵字在類的實現(xiàn)文件中修飾一個屬性:

@dynamic propertyName;

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

void dynamicMethodIMP(id?self, SEL _cmd) {????

??? //implementation ....

}

@implementation MyClass

+ (BOOL)resolveInstanceMethod:(SEL)aSEL {????

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

????? ??class_addMethod([self?class],? aSEL, (IMP) dynamicMethodIMP, "v@:");?

??????? return?YES;

??? }

? ??return?[super?resolveInstanceMethod: aSEL];

}

@end

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

? ? ? ? PS:動態(tài)方法解析會在消息轉(zhuǎn)發(fā)機制浸入前執(zhí)行。如果?respondsToSelector:或?instancesRespondToSelector:?方法被執(zhí)行,動態(tài)方法解析器將會被首先給予一個提供該方法選擇器對應(yīng)的IMP的機會。如果你想讓該方法選擇器被傳送到轉(zhuǎn)發(fā)機制,那么就讓?resolveInstanceMethod:?返回?NO?。

6 消息轉(zhuǎn)發(fā)

6.1 重定向

????????在消息轉(zhuǎn)發(fā)機制執(zhí)行前,Runtime 系統(tǒng)會再給我們一次偷梁換柱的機會,即通過重載?-

(id)forwardingTargetForSelector:(SEL)aSelector?方法替換消息的接受者為其他對象:

- (id)forwardingTargetForSelector:(SEL)aSelector {????

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

??? ????return?alternateObject;????

????}????

??? return?[super?forwardingTargetForSelector: aSelector];

}

????????畢竟消息轉(zhuǎn)發(fā)要耗費更多時間,抓住這次機會將消息重定向給別人是個不錯的選擇,不過千萬別返回?self?,因為那樣會死循環(huán)。

6.2 轉(zhuǎn)發(fā)

????????當(dāng)動態(tài)方法解析不作處理返回?NO?時,消息轉(zhuǎn)發(fā)機制會被觸發(fā),這時forwardInvocation:?方法會被執(zhí)行,我們可以重載這個方法來定義我們的轉(zhuǎn)發(fā)邏輯:

- (void) forwardInvocation: (NSInvocation *)anInvocation {????

? ??if?([someOtherObject respondsToSelector: [anInvocation selector]])

??? ????[anInvocation invokeWithTarget: someOtherObject];????

? ??else?[super?forwardInvocation: anInvocation];

}

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

? ? ? ? 當(dāng)一個對象由于沒有相應(yīng)的方法實現(xiàn)而無法響應(yīng)某消息時,運行時系統(tǒng)將通過forwardInvocation:?消息通知該對象。每個對象都從?NSObject?類中繼承了forwardInvocation:?方法。然而,NSObject中的方法實現(xiàn)只是簡單地調(diào)用了doesNotRecognizeSelector:?。通過實現(xiàn)我們自己的?forwardInvocation:方法,我們可以在該方法實現(xiàn)中將消息轉(zhuǎn)發(fā)給其它對象。

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

????????注意:?forwardInvocation:?方法只有在消息接收對象中無法正常響應(yīng)消息時才會被調(diào)用。 所以,如果我們希望一個對象將?negotiate?消息轉(zhuǎn)發(fā)給其它對象,則這個對象不能有?negotiate?方法。否則,?forwardInvocation:?將不可能會被調(diào)用。

6.3 轉(zhuǎn)發(fā)和多繼承

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

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

????????消息轉(zhuǎn)發(fā)彌補了 OC 不支持多繼承的性質(zhì),也避免了因為多繼承導(dǎo)致單個類變得臃腫復(fù)雜。它將問題分解得很細,只針對想要借鑒的方法才轉(zhuǎn)發(fā),而且轉(zhuǎn)發(fā)機制是透明的。

6.4 替代者對象(Surrogate Objects)

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

6.5 轉(zhuǎn)發(fā)與繼承

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

if ( [aWarrior respondsToSelector:@selector(negotiate)] )???? ...

????????結(jié)果是?NO?,盡管它能夠接受?negotiate?消息而不報錯,因為它靠轉(zhuǎn)發(fā)消息給Diplomat?類來響應(yīng)消息。

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

- (BOOL)respondsToSelector:(SEL)aSelector {?????

? ??if ( [super respondsToSelector:aSelector] )?????????

? ??????return YES;?????

? ??else {?????????

? ??????/* Here, test whether the aSelector message can???? *?????????

? ??????* be forwarded to another object and whether that? * ?????????

? ??????* object can respond to it. Return YES if it can.? */?????

????}?????

? ??return NO;?

}

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

- (NSMethodSignature*)methodSignatureForSelector: (SEL)selector {

????NSMethodSignature* signature = [super methodSignatureForSelector: selector];

? ??if (!signature) {

????????signature = [surrogate methodSignatureForSelector: selector];

????}

? ??return signature;

}

7 動態(tài)關(guān)聯(lián)屬性——Objective-CAssociated Objects

7.1 關(guān)聯(lián)的概念

????????關(guān)聯(lián)是指把兩個對象相互關(guān)聯(lián)起來,使得其中的一個對象作為另外一個對象的一部分。關(guān)聯(lián)特性只有在Mac OS X V10.6以及以后的版本上才是可用的。

????????在 OS X 10.6 之后,Runtime系統(tǒng)讓OC支持向?qū)ο髣討B(tài)添加變量。涉及到的函數(shù)有以下三個:

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

id objc_getAssociatedObject ( id object, const void *key );

void objc_removeAssociatedObjects ( id object );

????????這些方法以鍵值對的形式動態(tài)地向?qū)ο筇砑印@取或刪除關(guān)聯(lián)值。其中關(guān)聯(lián)政策是一組枚舉常量:

enum {???

????OBJC_ASSOCIATION_ASSIGN? = 0,

????OBJC_ASSOCIATION_RETAIN_NONATOMIC? = 1,

????OBJC_ASSOCIATION_COPY_NONATOMIC? = 3,

????OBJC_ASSOCIATION_RETAIN? = 01401,

????OBJC_ASSOCIATION_COPY? = 01403

};

????????這些常量對應(yīng)著引用關(guān)聯(lián)值的政策,也就是 OC 內(nèi)存管理的引用計數(shù)機制。

7.2 作用

? ??????在類的定義之外為類增加額外的存儲空間

? ? ????使用關(guān)聯(lián),我們可以不用修改類的定義而為其對象增加存儲空間。這在我們無法訪問到類的源碼的時候或者是考慮到二進制兼容性的時候是非常有用。

????????關(guān)聯(lián)是基于關(guān)鍵字的,因此,我們可以為任何對象增加任意多的關(guān)聯(lián),每個都使用不同的關(guān)鍵字即可。關(guān)聯(lián)是可以保證被關(guān)聯(lián)的對象在關(guān)聯(lián)對象的整個生命周期都是可用的(在垃圾自動回收環(huán)境下也不會導(dǎo)致資源不可回收)。

7.3 具體使用

7.3.1 創(chuàng)建關(guān)聯(lián)

????????創(chuàng)建關(guān)聯(lián)要使用到Objective-C的運行時函數(shù):?

????????objc_setAssociatedObject來把一個對象與另外一個對象進行關(guān)聯(lián)。該函數(shù)需要四個參數(shù):源對象,關(guān)鍵字,關(guān)聯(lián)的對象和一個關(guān)聯(lián)策略。當(dāng)然,此處的關(guān)鍵字和關(guān)聯(lián)策略是需要進一步討論的。

????? ■ ?關(guān)鍵字是一個void類型的指針。每一個關(guān)聯(lián)的關(guān)鍵字必須是唯一的。通常都是會采用靜態(tài)變量來作為關(guān)鍵字。

????? ■ ?關(guān)聯(lián)策略表明了相關(guān)的對象是通過賦值,保留引用還是復(fù)制的方式進行關(guān)聯(lián)的;還有這種關(guān)聯(lián)是原子的還是非原子的。這里的關(guān)聯(lián)策略和聲明屬性時的很類似。這種關(guān)聯(lián)策略是通過使用預(yù)先定義好的常量來表示的。

? ? ? ? 下面的代碼展示了如何把一個字符串關(guān)聯(lián)到一個數(shù)組上。

????????列表7-1 把一個字符串關(guān)聯(lián)到一個數(shù)組

static?char?overviewKey;

NSArray?*?array?= [[NSArray?alloc]?initWidthObjects: @"One",?@"Two",?@"Three",?nil];

//為了演示的目的,這里使用initWithFormat:來確保字符串可以被銷毀

NSString?*?overview?=?[[NSString?alloc]?initWithFormat: @"@", @"First?three?numbers"];

objc_setAssociatedObject(array,?&overviewKey,?overview,?OBJC_ASSOCIATION_RETAIN);

[overview?release];

//(1)?overview仍然是可用的

[array?release];

//(2)overview?不可用?

????????在(1)處,字符串overview仍然是可用的,這是因為OBJC_ASSOCIATION_RETAIN策略指明了數(shù)組要保有相關(guān)的對象。當(dāng)數(shù)組array被銷毀的時候,也就是在(2)處overview也就會被釋放,因此而被銷毀。如果此時還想使用overview,例如想通過log來輸出overview的值,則會出現(xiàn)運行時異常。

7.3.2 獲取相關(guān)聯(lián)的對象

????????獲取相關(guān)聯(lián)的對象時使用Objective-C函數(shù)objc_getAssociatedObject。接著上面列表7-1的代碼,我們可以使用如下代碼來獲取與array相關(guān)聯(lián)的字符串:

NSString?*?associatedObject?=?(NSString?*)objc_getAssociatedObject(array,?&oveviewKey);

7.3.3 斷開關(guān)聯(lián)

????????斷開關(guān)聯(lián)是使用objc_setAssociatedObject函數(shù),傳入nil值即可。

? ? ????接著列表7-1中的程序,我們可以使用如下的代碼來斷開字符串overview和arry之間的關(guān)聯(lián):

objc_setAssociatedObject(array,?&overviewKey,?nil,?OBJC_ASSOCIATION_ASSIGN);

? ? ? ? 其中,被關(guān)聯(lián)的對象為nil,此時關(guān)聯(lián)策略也就無關(guān)緊要了。

????????使用函數(shù)objc_removeAssociatedObjects可以斷開所有關(guān)聯(lián)。通常情況下不建議使用這個函數(shù),因為他會斷開所有關(guān)聯(lián)。只有在需要把對象恢復(fù)到“原始狀態(tài)”的時候才會使用這個函數(shù)。

7.3.4 一個完整的實例程序

-?(void)viewDidLoad?{

????[super?viewDidLoad];

????// static?const?char?associated Button key;

????UIButton?*btn?=?[UIButton?buttonWithType: UIButtonTypeCustom];

????[btn?setTitle: @"點我" forState: UIControlStateNormal];

????[self.view?addSubview: btn];

????[btn?setFrame: CGRectMake(50,?50,?50,?50)];

????btn.backgroundColor?=?[UIColor?redColor];

????[btn?addTarget: self?action: @selector(click:)?forControlEvents: UIControlEventTouchUpInside];


????//?Do?any?additional?setup?after?loading?the?view,?typically?from?a?nib.

}


-(void)click: (UIButton?*)sender

{

????NSString?*message?=?@"你是誰";

????UIAlertView?*alert?=?[[UIAlertView?alloc] initWithTitle: @"提示"?message: @"我要傳值·"?delegate: self?cancelButtonTitle: @"確定" otherButtonTitles: nil];

????alert.delegate?=?self;

????[alert?show];


????//#import?頭文件

????//objc_setAssociatedObject需要四個參數(shù):源對象,關(guān)鍵字,關(guān)聯(lián)的對象和一個關(guān)聯(lián)策略。


????//1?源對象alert

????//2?關(guān)鍵字?唯一靜態(tài)變量key?associatedkey

????//3?關(guān)聯(lián)的對象?sender

????//4?關(guān)鍵策略??OBJC_ASSOCIATION_ASSIGN

????//????enum?{

????// OBJC_ASSOCIATION_ASSIGN?=?0, 若引用/**<?Specifies?a?weak?reference?to?the?associated?object.?*/

????// OBJC_ASSOCIATION_RETAIN_NONATOMIC?=?1,?/**<?Specifies?a?strong?reference?to?the?associated?object.

????// *???The?association?is?not?made?atomically.?*/

????// OBJC_ASSOCIATION_COPY_NONATOMIC?=?3,???/**<?Specifies?that?the?associated?object?is?copied.

????// *???The?association?is?not?made?atomically.?*/

????// OBJC_ASSOCIATION_RETAIN?=?01401,???????/**<?Specifies?a?strong?reference?to?the?associated?object.

????// *???The?association?is?made?atomically.?*/

????// OBJC_ASSOCIATION_COPY?=?01403??????????/**<?Specifies?that?the?associated?object?is?copied.

????// *???The?association?is?made?atomically.?*/

????//????};


????objc_setAssociatedObject(alert,?@"msgstr",?message, OBJC_ASSOCIATION_ASSIGN);

????//把alert和message字符串關(guān)聯(lián)起來,作為alertview的一部分,關(guān)鍵詞就是msgstr,之后可以使用objc_getAssociatedObject從alertview中獲取到所關(guān)聯(lián)的對象,便可以訪問message或者btn了

????//????即實現(xiàn)了關(guān)聯(lián)傳值

????objc_setAssociatedObject(alert,?@"btn?property", sender, OBJC_ASSOCIATION_ASSIGN);

}


-(void)alertView:(UIAlertView?*)alertView?clickedButtonAtIndex:(NSInteger)buttonIndex

{

????//通過?objc_getAssociatedObject獲取關(guān)聯(lián)對象

????NSString *messageString?= objc_getAssociatedObject(alertView,?@"msgstr");

????UIButton?*sender?=?objc_getAssociatedObject(alertView,?@"btn?property");

????NSLog(@"%ld", buttonIndex);

????NSLog(@"%@", messageString);

????NSLog(@"%@", [[sender?titleLabel]?text]);

????//使用函數(shù)objc_removeAssociatedObjects可以斷開所有關(guān)聯(lián)。通常情況下不建議使用這個函數(shù),因為他會斷開所有關(guān)聯(lián)。只有在需要把對象恢復(fù)到“原始狀態(tài)”的時候才會使用這個函數(shù)。

}

終端打?。?/p>

2015-07-22?16:18:35.294?test[5174:144121]?0

2015-07-22?16:18:35.295?test[5174:144121]?你是誰

2015-07-22?16:18:35.295?test[5174:144121]?點我

8 Objc源碼分析

Objc4源碼下載地址

http://www.opensource.apple.com/tarballs/objc4/

9 總結(jié)

????????我們之所以讓自己的類繼承?NSObject?不僅僅因為蘋果幫我們完成了復(fù)雜的內(nèi)存分配問題,更是因為這使得我們能夠用上Runtime系統(tǒng)帶來的便利??赡芪覀兤綍r寫代碼時可能很少會考慮一句簡單的[receiver message]?背后發(fā)生了什么,而只是當(dāng)做方法或函數(shù)調(diào)用。深入理解 Runtime 系統(tǒng)的細節(jié)更有利于我們利用消息機制寫出功能更強大的代碼,比如 Method Swizzling 等。

9.1 類本質(zhì)

????????OC中類的本質(zhì)其實就是“結(jié)構(gòu)體+函數(shù)指針”的組合,當(dāng)然,對于NSObject而言,首先它只是包含一個objc_class類型的成員isa,而objc_class是一個結(jié)構(gòu)體類型,繼承自objc_object,

9.2 方法執(zhí)行流程

????1. 檢測這個selector是不是要忽略的。比如 Mac OS X 開發(fā),有了垃圾回收就不理會retain、release這些函數(shù)了。

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

????3. 如果上面兩個都過了,那就開始查找這個類的IMP,先從cache里面找,完了找得到就跳到對應(yīng)的函數(shù)去執(zhí)行。

????4. 如果cache找不到就找一下方法分發(fā)表。(Class中的方法列表)

????5. 如果分發(fā)表找不到就到超類的分發(fā)表去找,一直找,直到找到NSObject類為止。

? ? 6. 如果還找不到就要開始進入?動態(tài)方法解析和消息轉(zhuǎn)發(fā)的機制,后面會提到。

? ? 7.在消息轉(zhuǎn)發(fā)機制執(zhí)行前,Runtime 系統(tǒng)會再給我們一次偷梁換柱的機會,即通過重載?-

(id)forwardingTargetForSelector:(SEL)aSelector?方法替換消息的接受者為其他對象

9.3 Method Swizzling

參考鏈接:Method Swizzling和AOP實踐

http://tech.glowing.com/cn/method-swizzling-aop/

????????Method Swizzling 利用 Runtime 特性把一個方法的實現(xiàn)與另一個方法的實現(xiàn)進行替換。

????????上一篇文章有講到每個類里都有一個 Dispatch Table ,將方法的名字(SEL)跟方法的實現(xiàn)(IMP,指向 C 函數(shù)的指針)一一對應(yīng)。Swizzle 一個方法其實就是在程序運行時在 Dispatch Table 里做點改動,讓這個方法的名字(SEL)對應(yīng)到另個 IMP 。

????????首先定義一個類別,添加將要 Swizzled 的方法:

@implementation UIViewController (Logging)

- (void)swizzled_viewDidAppear:(BOOL)animated

{

??? // call original implementation

??? [self swizzled_viewDidAppear: animated];


??? // Logging

???[Logging logWithEventName: NSStringFromClass([selfclass])];

}

????????代碼看起來可能有點奇怪,像遞歸不是么。當(dāng)然不會是遞歸,因為在 runtime 的時候,函數(shù)實現(xiàn)已經(jīng)被交換了。調(diào)用 viewDidAppear: 會調(diào)用你實現(xiàn)的 swizzled_viewDidAppear:,而在 swizzled_viewDidAppear: 里調(diào)用 swizzled_viewDidAppear: 實際上調(diào)用的是原來的 viewDidAppear: 。

接下來實現(xiàn) swizzle 的方法 :

@implementation UIViewController (Logging)

void swizzleMethod(Class class, SEL originalSelector, SEL swizzledSelector)?

{

??? // the method might not exist in the class, but in its superclass

???Method originalMethod = class_getInstanceMethod(class, originalSelector);

???Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);


??? // class_addMethod will fail if original method already exists

???BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));


??? // the method doesn’t exist and we just added one

??? if(didAddMethod) {

???????class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));

??? }

??? else{

???????method_exchangeImplementations(originalMethod, swizzledMethod);

??? }

}

????????這里唯一可能需要解釋的是 class_addMethod 。要先嘗試添加原 selector 是為了做一層保護,因為如果這個類沒有實現(xiàn) originalSelector ,但其父類實現(xiàn)了,那 class_getInstanceMethod 會返回父類的方法。這樣 method_exchangeImplementations 替換的是父類的那個方法,這當(dāng)然不是你想要的。所以我們先嘗試添加 orginalSelector ,如果已經(jīng)存在,再用 method_exchangeImplementations 把原方法的實現(xiàn)跟新的方法實現(xiàn)給交換掉。

????????最后,我們只需要確保在程序啟動的時候調(diào)用 swizzleMethod 方法。比如,我們可以在之前 UIViewController 的 Logging 類別里添加 +load: 方法,然后在 +load: 里把 viewDidAppear 給替換掉:

@implementation UIViewController (Logging)

+ (void)load

{

???swizzleMethod([self class], @selector(viewDidAppear:), @selector(swizzled_viewDidAppear:));

}

????????一般情況下,類別里的方法會重寫掉主類里相同命名的方法。如果有兩個類別實現(xiàn)了相同命名的方法,只有一個方法會被調(diào)用。但 +load: 是個特例,當(dāng)一個類被讀到內(nèi)存的時候, runtime 會給這個類及它的每一個類別都發(fā)送一個 +load: 消息。

????????其實,這里還可以更簡化點:直接用新的 IMP 取代原 IMP ,而不是替換。只需要有全局的函數(shù)指針指向原 IMP 就可以。

void (gOriginalViewDidAppear)(id, SEL, BOOL);

void newViewDidAppear(UIViewController *self, SEL _cmd, BOOLanimated)?

{

??? // call original implementation

???gOriginalViewDidAppear(self, _cmd, animated);


??? // Logging

??? [Logging logWithEventName: NSStringFromClass([selfclass])];

}


+ (void)load

{

???Method originalMethod = class_getInstanceMethod(self, @selector(viewDidAppear:));

???gOriginalViewDidAppear = (void*)method_getImplementation(originalMethod);


??? if(!class_addMethod(self, @selector(viewDidAppear:),(IMP) newViewDidAppear, method_getTypeEncoding(originalMethod))) {

???????method_setImplementation(originalMethod, (IMP) newViewDidAppear);

??? }

}

????????通過 Method Swizzling ,我們成功把邏輯代碼跟處理事件記錄的代碼解耦。當(dāng)然除了 Logging ,還有很多類似的事務(wù),如 Authentication 和 Caching。這些事務(wù)瑣碎,跟主要業(yè)務(wù)邏輯無關(guān),在很多地方都有,又很難抽象出來單獨的模塊。這種程序設(shè)計問題,業(yè)界也給了他們一個名字-Cross Cutting Concerns

????????而像上面例子用 Method Swizzling 動態(tài)給指定的方法添加代碼,以解決 Cross Cutting Concerns 的編程方式叫:Aspect Oriented Programming

10 參考鏈接

–?Objective-C Runtime Programming Guide

–?Objective-C runtime之運行時的基本特點

–?Understanding the Objective-C Runtime


(Good)刨根問底Objective-C Runtime

http://www.cocoachina.com/ios/20141224/10740.html


Objective-C Runtime Programming Guide

https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Introduction/Introduction.html


深入理解Objective-C的Runtime機制

http://www.csdn.net/article/2015-07-06/2825133-objective-c-runtime/1


Objective-C Runtime運行時之一:類與對象

http://southpeak.github.io/blog/2014/10/25/objective-c-runtime-yun-xing-shi-zhi-lei-yu-dui-xiang/


Objective-C Runtime運行時之二:成員變量與屬性

http://southpeak.github.io/blog/2014/10/30/objective-c-runtime-yun-xing-shi-zhi-er-:cheng-yuan-bian-liang-yu-shu-xing/


Objective-C Runtime運行時之三:方法與消息

http://southpeak.github.io/blog/2014/11/03/objective-c-runtime-yun-xing-shi-zhi-san-:fang-fa-yu-xiao-xi-zhuan-fa/


Objective-C Runtime運行時之四:MethodSwizzling

http://southpeak.github.io/blog/2014/11/06/objective-c-runtime-yun-xing-shi-zhi-si-:method-swizzling/


Objective-C Runtime運行時之五:協(xié)議與分類

http://southpeak.github.io/blog/2014/11/08/objective-c-runtime-yun-xing-shi-zhi-wu-:xie-yi-yu-fen-lei/


Objective-C Runtime運行時之六:拾遺

http://southpeak.github.io/blog/2014/11/09/objective-c-runtime-yun-xing-shi-zhi-liu-:shi-yi/


iOS用Runtime解決服務(wù)器返回NSNull問題

http://blog.csdn.net/uxyheaven/article/details/48299599


[Objective-C]關(guān)聯(lián)(objc_setAssociatedObject、objc_getAssociatedObject、objc_removeAssociatedObjects)

http://blog.csdn.net/onlyou930/article/details/9299169


objc_setAssociatedObject使用

http://my.oschina.net/wupengnash/blog/482377


ios專題- objc runtime動態(tài)增加屬性

http://www.cnblogs.com/luoguoqiang1985/p/3551966.html


objc_getAssociatedObject, objc_setAssociatedObject

http://zhy584520.iteye.com/blog/1742493


IOS中延時執(zhí)行的幾種方式的比較和匯總

http://blog.csdn.net/chenyong05314/article/details/24695897


IOS關(guān)于取消延遲執(zhí)行函數(shù)的種種。performSelector與cancelPreviousPerformRequestsWithTarget

http://blog.csdn.net/samuelltk/article/details/8994313


iOS設(shè)置 延遲執(zhí)行 與 取消延遲執(zhí)行 方法 以及對runloop初步認識

http://www.cnblogs.com/someonelikeyou/p/5509878.html


Method Swizzling和AOP實踐

http://tech.glowing.com/cn/method-swizzling-aop/


Objective-C Runtime

http://tech.glowing.com/cn/objective-c-runtime/

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容