10. 在既有類中使用關(guān)聯(lián)對(duì)象存放自定義數(shù)據(jù)
注意關(guān)鍵詞“關(guān)聯(lián)對(duì)象”,就是把兩個(gè)對(duì)象關(guān)聯(lián)起來,例如把對(duì)象B關(guān)聯(lián)到對(duì)象A上面,這樣只要我們知道對(duì)象A,就能通過關(guān)聯(lián)方法拿到對(duì)象B,這是一個(gè)很有用的特性,可以幫助我們攜帶一些數(shù)據(jù),以及一些信息。如果通俗一點(diǎn)理解的話可以把對(duì)象A理解成一個(gè)字典,對(duì)象B是存放在對(duì)象A中的一個(gè)對(duì)象,通過對(duì)應(yīng)的key值就能拿到對(duì)應(yīng)的對(duì)象B。
下面是關(guān)聯(lián)對(duì)象對(duì)應(yīng)的三個(gè)方法(只有三個(gè)方法):
1.通過給定的鍵值和關(guān)聯(lián)策略對(duì)某對(duì)象設(shè)置關(guān)聯(lián)對(duì)象
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
第一個(gè)參數(shù),被關(guān)聯(lián)對(duì)象,對(duì)應(yīng)上面的對(duì)象A。
第二個(gè)參數(shù),鍵值,通過參數(shù)形式我們知道,這是一個(gè)指針,一般我們?cè)诙x這個(gè)指針的時(shí)候使用靜態(tài)全局變量,因?yàn)檫@是一個(gè)“不透明指針”(自行查找什么是“不透明指針”)。
第三個(gè)參數(shù),關(guān)聯(lián)的對(duì)象,對(duì)應(yīng)上面的對(duì)象B。
第四個(gè)參數(shù),關(guān)聯(lián)策略,是一個(gè)枚舉值,對(duì)應(yīng)定義屬性時(shí)候添加的屬性特性,用于維護(hù)內(nèi)存管理,下表列出對(duì)應(yīng)關(guān)系:
| 關(guān)聯(lián)類型 | 等效的屬性特性 |
|---|---|
| OBJC_ASSOCIATION_ASSIGN | assign |
| OBJC_ASSOCIATION_RETAIN_NONATOMIC | nonatomic, retain |
| OBJC_ASSOCIATION_COPY_NONATOMIC | nonatomic, copy |
| OBJC_ASSOCIATION_RETAIN | retain |
| OBJC_ASSOCIATION_COPY | copy |
2.通過給定的鍵值取出相應(yīng)的關(guān)聯(lián)對(duì)象
id objc_getAssociatedObject(id object, const void *key)
第一個(gè)參數(shù),被關(guān)聯(lián)的對(duì)象,對(duì)應(yīng)對(duì)象A。
第二個(gè)參數(shù),鍵值。
返回值,關(guān)聯(lián)對(duì)象,對(duì)應(yīng)對(duì)象B。
3.移除被關(guān)聯(lián)對(duì)象的所有關(guān)聯(lián)對(duì)象
void objc_removeAssociatedObjects(id object)
參數(shù),被關(guān)聯(lián)對(duì)象,對(duì)應(yīng)對(duì)象B。
上面就是關(guān)聯(lián)對(duì)象的所有方法,但是在用的時(shí)候需要注意,關(guān)聯(lián)對(duì)象應(yīng)該被我們列在最后的選擇方案,因?yàn)殛P(guān)聯(lián)對(duì)象之間的關(guān)系沒有正式的定義,其內(nèi)存管理是在設(shè)置關(guān)聯(lián)的時(shí)候才定義的,而不是在接口中預(yù)先設(shè)定好的,有時(shí)會(huì)出現(xiàn)一些不易查找的錯(cuò)誤。
PS:偶爾在代碼中寫點(diǎn)這樣的代碼,會(huì)增加代碼的“氣質(zhì)”,你懂的。
11. 理解objc_msgSend作用
這一小節(jié)的內(nèi)容和我們寫代碼沒有什么關(guān)系,但是我們可以了解一下OC中方法的調(diào)用過程,對(duì)我們的程序調(diào)試很是很有用的。
首先說一下C語言的函數(shù)調(diào)用方式,用以和OC做比較,C語言使用“靜態(tài)綁定”,也就是說,在編譯期就能決定運(yùn)行時(shí)應(yīng)該調(diào)用的函數(shù),而大家都知道,OC是一門動(dòng)態(tài)語言,與之差別的就是OC中有時(shí)候是使用“動(dòng)態(tài)綁定”,就是在運(yùn)行期調(diào)用對(duì)應(yīng)的函數(shù),甚至可以在程序運(yùn)行時(shí)改變。
寫一個(gè)簡(jiǎn)單的方法調(diào)用的例子,解釋一下方法的構(gòu)成:
id returnValue = [someObject messageName:parameter];
在這句調(diào)用語句中,someObject就是類或類的實(shí)例,messageName就是方法名,parameter就是參數(shù),編譯器會(huì)把這條語句編譯成一條標(biāo)準(zhǔn)的C語句,編譯后的語句如下:
id returnValue = objc_msgSend(someObject, @selector(messageName), parameter)
objc_msgSend是一個(gè)可變參數(shù)的函數(shù),對(duì)應(yīng)OC中方法參數(shù)的增加,參數(shù)也會(huì)增加,相信大家都知道這個(gè)方法中參數(shù)的意思。
objc_msgSend函數(shù)會(huì)根據(jù)參數(shù),找到對(duì)應(yīng)類的對(duì)應(yīng)“方法列表”,然后找到對(duì)應(yīng)實(shí)現(xiàn)代碼,若找不到會(huì)沿著繼承關(guān)系向上查找,如果還沒找到,觸發(fā)“消息轉(zhuǎn)發(fā)”機(jī)制(后面會(huì)介紹這個(gè)機(jī)制)。
這樣下來調(diào)用一個(gè)方法大家可能感覺步驟太多,其實(shí)不會(huì),objc_msgSend會(huì)將匹配結(jié)果放到一張“快速映射表”里,每個(gè)類都有一個(gè)這樣的表,加快調(diào)用速度。另外還有一些特殊情況,OC運(yùn)行環(huán)境中還有另外一些相關(guān)的處理函數(shù),例如objc_msgSend_stret、objc_msgSend_fpret、objc_msgSendSuper就不在一一介紹。
另外提一個(gè)點(diǎn),OC對(duì)象的每一個(gè)方法當(dāng)編譯成C語言的時(shí)候可以看成是下面這種的形式的
<returnType> Class_selector(id self, SEL _cmd, ...)
其中的方法名是隨意起的,大家發(fā)現(xiàn)這個(gè)函數(shù)和objc_msgSend的形式很想,這是為了利用“尾調(diào)用優(yōu)化”,是調(diào)用函數(shù)更簡(jiǎn)單、高效。
12. 理解消息轉(zhuǎn)發(fā)機(jī)制
這小節(jié)介紹一下上面提到的消息轉(zhuǎn)發(fā)機(jī)制,大家都知道,觸發(fā)了消息轉(zhuǎn)發(fā)機(jī)制,是因?yàn)槲覀儧]有找到對(duì)應(yīng)的方法,下面看消息轉(zhuǎn)發(fā)機(jī)制怎么處理這個(gè)問題。
介紹一下消息轉(zhuǎn)發(fā)機(jī)制,大致分為三個(gè)階段:
1.第一階段,動(dòng)態(tài)方法解析
對(duì)象在無法解讀方法的時(shí)候,首先會(huì)調(diào)用所屬類下面這個(gè)方法
+ (BOOL)resolveInstanceMethod:(SEL)sel
sel就是方法名,返回值為Boolean類型,表示這個(gè)類是否能新增實(shí)例方法處理這個(gè)方法(如果是類方法會(huì)調(diào)用+ (BOOL)resolveClassMethod:(SEL)sel方法),我們需要自定義一些處理方法,用于動(dòng)態(tài)添加到類中,用以解決問題(可以看后面的例子),如果這一步不能解決問題,轉(zhuǎn)到第二階段。
2.第二階段,備援接收者
來到這一步,我們就要改變解決問題的思路,既然這個(gè)類不能處理這個(gè)方法,我們可不可以找別的類處理,這時(shí)候?qū)?yīng)的處理方法:
- (id)forwardingTargetForSelector:(SEL)aSelector
aSelector是方法名,如果當(dāng)前類能夠找到一個(gè)類幫忙處理這個(gè)方法,就返回這個(gè)類,若找不到就放回nil(通過這個(gè)方法我們可以實(shí)現(xiàn)類似“多繼承”)。
3.第三階段,完整的消息轉(zhuǎn)發(fā)
如果已經(jīng)來到了這一步,我們就要做一個(gè)完整的消息轉(zhuǎn)發(fā)。首先創(chuàng)建一個(gè)NSInvocation對(duì)象,把未處理方法的所有信息封裝在里面,此對(duì)象包含方法名、目標(biāo)、參數(shù),這一步要調(diào)用下面的方法:
- (void)forwardInvocation:(NSInvocation *)anInvocation
這一步處理的方法很簡(jiǎn)單,就是在新的類上調(diào)用方法,如果這樣做的話就和第二階段沒有什么差別了。通常在這一步的時(shí)候會(huì)做一些改進(jìn),會(huì)選擇某種方式改變消息內(nèi)容,例如追加參數(shù),改變方法名等。
對(duì)于消息的處理,越早越好。
下面粘貼一個(gè)利用動(dòng)態(tài)解析方法實(shí)現(xiàn)@dynamic屬性的例子:
這個(gè)例子實(shí)現(xiàn)一個(gè)類,類似字典的功能,只不過寫入和讀取信息的時(shí)候用屬性,而不是像字典一樣用關(guān)鍵字。
.h文件中:
#import <Foundation/Foundation.h>
@interface EOCAutoDictionary : NSObject
@property (nonatomic, strong) NSString *string;
@property (nonatomic, strong) NSNumber *number;
@property (nonatomic, strong) NSData *date;
@property (nonatomic, strong) id opaqueObject;
@end
.m文件中:
#import "EOCAutoDictionary.h"
#import <objc/runtime.h> // 主要頭文件的引用
@interface EOCAutoDictionary ()
@property (nonatomic, strong) NSMutableDictionary *backingStore;
@end
@implementation EOCAutoDictionary
@dynamic string, number, date, opaqueObject;
- (id)init{
if ((self = [super init])) {
_backingStore = [NSMutableDictionary new];
}
return self;
}
+(BOOL)resolveInstanceMethod:(SEL)sel{
NSString *selectorString = NSStringFromSelector(sel);
// 通過是否以“set”開頭判斷方法名
if ([selectorString hasPrefix:@"set"]) {
/**
* 向類中添加一個(gè)方法
* 參數(shù)一 指定類名.
* 參數(shù)二 新添加的方法的方法名.
* 參數(shù)三 函數(shù)指針,指向待添加方法.
* 參數(shù)四 待添加方法的類型編碼.
*/
class_addMethod(self, sel, (IMP)autoDictionarySetter, "v@:@");
} else {
class_addMethod(self, sel, (IMP)autoDictionaryGetter, "@@:");
}
return YES;
}
id autoDictionaryGetter(id self, SEL _cmd){
// 拿到存儲(chǔ)數(shù)據(jù)的字典
EOCAutoDictionary *typedSelf = (EOCAutoDictionary *)self;
NSMutableDictionary *backingStore = typedSelf.backingStore;
// 拿到方法名
NSString *key = NSStringFromSelector(_cmd);
// 返回對(duì)應(yīng)的值
return [backingStore objectForKey:key];
}
void autoDictionarySetter(id self, SEL _cmd, id value){
// 拿到存儲(chǔ)數(shù)據(jù)的字典
EOCAutoDictionary *typedSelf = (EOCAutoDictionary *)self;
NSMutableDictionary *backingStore = typedSelf.backingStore;
// 拿到方法名并對(duì)其進(jìn)行處理
NSString *selectorString = NSStringFromSelector(_cmd);
NSMutableString *key = [selectorString mutableCopy];
// 移除方法名中的“:”
[key deleteCharactersInRange:NSMakeRange(key.length-1, 1)];
// 移除方法名中的“set”
[key deleteCharactersInRange:NSMakeRange(0, 3)];
// 將方法名第一個(gè)字符轉(zhuǎn)為小寫
NSString *lowercaseFirstChar = [[key substringToIndex:1] lowercaseString];
[key replaceCharactersInRange:NSMakeRange(0, 1) withString:lowercaseFirstChar];
// 如果有值,寫入字典中
if (value) {
[backingStore setObject:value forKey:key];
} else {
[backingStore removeObjectForKey:key];
}
}
@end
EOCAutoDictionary的用法也很簡(jiǎn)單,只要直接通過對(duì)應(yīng)的屬性名,就可以進(jìn)行數(shù)據(jù)的存儲(chǔ)。
13. 用“方法調(diào)配技術(shù)”調(diào)試“黑盒方法”
方法調(diào)配技術(shù),簡(jiǎn)言之就是,將方法名和方法實(shí)現(xiàn)分割開來,任意組合。這樣一來我們可以任意改變一個(gè)方法的實(shí)現(xiàn),另外還可以通過這種辦法給原有方法添加功能,對(duì)不知道內(nèi)部實(shí)現(xiàn)的方法添加提示語句(黑盒調(diào)試)等等。
之所以能這么做,主要是因?yàn)榉椒ň灾羔樀男问絹肀硎?,這種指針叫IMP,我們?cè)谡{(diào)用方法的時(shí)候,只要將指針指向改變,就能實(shí)現(xiàn)我們想要的效果,運(yùn)用起來也很簡(jiǎn)單,通過下面的例子大家就會(huì)運(yùn)用(注意運(yùn)行時(shí)頭文件的引用):
Method originalMethod = class_getInstanceMethod([NSString class], @selector(lowercaseString));
Method swappedMethod = class_getInstanceMethod([NSString class], @selector(uppercaseString))
method_exchangeImplementations(originalMethod, swappedMethod);
通過上面的例子,我們就把NSString的lowercaseString方法和uppercaseString方法調(diào)換了,是不是很簡(jiǎn)單。
其實(shí)這樣做并沒有什么意義,因?yàn)榫唧w的方法實(shí)現(xiàn)已經(jīng)都存在了,我們沒必要改變一個(gè)方法實(shí)現(xiàn),但是我們通過這種方法給已知的方法添加功能,例如下面的例子:
.h文件:
@interface NSString (EOCMyAdditions)
- (NSString *)eoc_myLowercaseString; // 在分類中給NSString添加功能
@end
.m文件:
@implementation NSString (EOCMyAdditions)
- (NSString *)eoc_myLowercaseString{
NSString *lowercase = [self eoc_myLowercaseString];
NSLog(@"%@ => %@", self, lowercase);
return lowercase;
}
@end
然后我們使用方法調(diào)配技術(shù),將上面的方法和lowercaseString方法進(jìn)行調(diào)換:
Method originalMethod = class_getInstanceMethod([NSString class], @selector(lowercaseString));
Method swappedMethod = class_getInstanceMethod([NSString class], @selector(eoc_myLowercaseString));
method_exchangeImplementations(originalMethod, swappedMethod);
這樣執(zhí)行完后,當(dāng)我們?cè)僬{(diào)用lowercaseString方法的時(shí)候會(huì)有下面的結(jié)果:
NSString *string = @"This is tHe StRing";
NSString *lowercaseString = [string lowercaseString];
// Output:This is tHe StRing => this is the string
通過這個(gè)方法我們發(fā)現(xiàn),我們可以為那些不知道內(nèi)部實(shí)現(xiàn)的黑盒方法添加日志記錄功能。
一般來說,我們很少用“方法調(diào)配”,只有在調(diào)試程序的時(shí)候才需要在運(yùn)行期修改方法實(shí)現(xiàn)。
14. 理解“類對(duì)象”的用意
首先我們要知道,OC的實(shí)例對(duì)象是指向某塊內(nèi)存數(shù)據(jù)的指針,所以在聲明變量時(shí),要用*號(hào)。同時(shí)我們知道OC中有一種通用對(duì)象類型“id”(id本身已是一個(gè)指針),所以我們?cè)谟谩癷d”聲明變量的時(shí)候可能和平常有點(diǎn)不同:
NSString *aString = @"some string";
id aString = @"some string";
上面兩種定義方式相比,語法意義相同,區(qū)別在于,指定具體類型后,當(dāng)實(shí)例調(diào)用方法的時(shí)候,編輯器會(huì)給我們提示。
下面看一下“id”類型的定義:
typedef struct objc_object *id;
id其實(shí)是objc_object類型的結(jié)構(gòu)體,而objc_object定義如下:
struct objc_object {
Class isa OBJC_ISA_AVAILABILITY;
};
結(jié)構(gòu)體中是一個(gè)Class類型的變量,該變量定義對(duì)象所屬的類。下面我們看一下Class類型是個(gè)什么東西:
typedef struct objc_class *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;
我們看到,這個(gè)結(jié)構(gòu)體存放類的各種信息(元數(shù)據(jù)),例如類有多少個(gè)實(shí)力變量,類名等等信息。
通過上面的關(guān)系,我們知道在objc的runtime中,類是用objc_class結(jié)構(gòu)體表示的,對(duì)象是用objc_object結(jié)構(gòu)體表示的, 對(duì)象的isa用來標(biāo)示這個(gè)對(duì)象是哪個(gè)類的實(shí)例。
這些源碼是屬于objc runtime的,objc runtime的源代碼蘋果已經(jīng)開源了,你可以在這里下載到objc的runtime源代碼。
其實(shí)到這里大家可能會(huì)有一個(gè)疑問,為什么objc_class結(jié)構(gòu)體里面也有一個(gè)isa,那么這個(gè)isa指向誰呢?我們往下看,[NSObject class],這里我們調(diào)用了+ (Class)class這個(gè)類方法,我們?cè)匍_發(fā)中經(jīng)常用到這個(gè)方法,它返回的是這個(gè)類所屬的Class類型。+ (Class)class類方法的實(shí)現(xiàn)源碼是這樣的:
+ (Class)class {
return self;
}
為什么會(huì)返回self,self總是指的自身,而在這里沒有實(shí)例?。∵@時(shí)候看開發(fā)文檔我們會(huì)發(fā)現(xiàn),實(shí)際上函數(shù)的返回值是一個(gè)類對(duì)象class object,所以其本質(zhì)上還是一個(gè)對(duì)象而已。既然是一個(gè)對(duì)象,它擁有一個(gè)self指針也就不奇怪了,所以對(duì)于像NSObject這樣的類來說,它其實(shí)代表的是一個(gè)類對(duì)象,本質(zhì)上還是一個(gè)普通的實(shí)例對(duì)象,那么又會(huì)問了,這個(gè)類對(duì)象是誰的實(shí)例呢?很遺憾,要找到這個(gè)問題的答案,我們?cè)?objc runtime 這一層上已經(jīng)沒辦法辦到了,我們需要到更低層,也就是 objc 語言層去尋找答案了,但是 objc 語言層是不開源的,如果想繼續(xù)學(xué)習(xí),大家可以在網(wǎng)上找模仿OC低層的代碼。
以上了解一下就好,我們只要知道類的繼承體系就行了,下面用一個(gè)例子:有一個(gè)類(暫且叫SomeClass)繼承于NSObject,那么這些類和元類的繼承關(guān)系是,SomeClass實(shí)例有一個(gè)isa指針指向SomeClass類,SomeClass類有一個(gè)isa指針指向SomeClass元類,NSObject類也有一個(gè)isa指針指向NSObject元類,SomeClass的父類是NSObject,SomeClass元類的父類是NSObject元類,通過這種關(guān)系,我們?cè)陬惱^承體系中查詢類型信息,用isMenberOfClass:判斷對(duì)象是否是某個(gè)特定類的實(shí)例,用isKindOfClass:判斷對(duì)象是否為某類或其派生類的實(shí)例。因?yàn)镺C是動(dòng)態(tài)型語言的特性,上面兩個(gè)方法非常有用。
有時(shí)我們可以用比較類對(duì)象是否等同的辦法來進(jìn)行比較,這時(shí)要用==操作符,而不是用isEqual方法,因?yàn)轭悓?duì)象是單利,在應(yīng)用程序中,每個(gè)類的類對(duì)象只有一個(gè)實(shí)例,也就是說另外一種判斷對(duì)象是否為某類實(shí)例的辦法是:
id object = /*...*/
if ([object class] == [SomeClass class]){
}
這一部分基本都是關(guān)于OC運(yùn)行時(shí)的知識(shí),可能我們平時(shí)寫代碼的時(shí)候涉及很少,但是了解這些,對(duì)于我們的開發(fā)是很有幫助的,OC運(yùn)行時(shí)是一個(gè)很強(qiáng)大的東西,有興趣的同學(xué)可以好好研究一下。