10. 在既有類(lèi)中使用關(guān)聯(lián)對(duì)象存放自定義數(shù)據(jù)
注意關(guān)鍵詞“關(guān)聯(lián)對(duì)象”钞啸,就是把兩個(gè)對(duì)象關(guān)聯(lián)起來(lái)遥巴,例如把對(duì)象B關(guān)聯(lián)到對(duì)象A上面伍玖,這樣只要我們知道對(duì)象A婿失,就能通過(guò)關(guān)聯(lián)方法拿到對(duì)象B钞艇,這是一個(gè)很有用的特性,可以幫助我們攜帶一些數(shù)據(jù)豪硅,以及一些信息哩照。如果通俗一點(diǎn)理解的話可以把對(duì)象A理解成一個(gè)字典,對(duì)象B是存放在對(duì)象A中的一個(gè)對(duì)象懒浮,通過(guò)對(duì)應(yīng)的key值就能拿到對(duì)應(yīng)的對(duì)象B飘弧。
下面是關(guān)聯(lián)對(duì)象對(duì)應(yīng)的三個(gè)方法(只有三個(gè)方法):
1.通過(guò)給定的鍵值和關(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ù),鍵值稽穆,通過(guò)參數(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)類(lèi)型 | 等效的屬性特性 |
---|---|
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.通過(guò)給定的鍵值取出相應(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)系沒(méi)有正式的定義学搜,其內(nèi)存管理是在設(shè)置關(guān)聯(lián)的時(shí)候才定義的娃善,而不是在接口中預(yù)先設(shè)定好的,有時(shí)會(huì)出現(xiàn)一些不易查找的錯(cuò)誤瑞佩。
PS:偶爾在代碼中寫(xiě)點(diǎn)這樣的代碼聚磺,會(huì)增加代碼的“氣質(zhì)”,你懂的炬丸。
11. 理解objc_msgSend作用
這一小節(jié)的內(nèi)容和我們寫(xiě)代碼沒(méi)有什么關(guān)系瘫寝,但是我們可以了解一下OC中方法的調(diào)用過(guò)程,對(duì)我們的程序調(diào)試很是很有用的稠炬。
首先說(shuō)一下C語(yǔ)言的函數(shù)調(diào)用方式焕阿,用以和OC做比較,C語(yǔ)言使用“靜態(tài)綁定”酸纲,也就是說(shuō)捣鲸,在編譯期就能決定運(yùn)行時(shí)應(yīng)該調(diào)用的函數(shù),而大家都知道闽坡,OC是一門(mén)動(dòng)態(tài)語(yǔ)言栽惶,與之差別的就是OC中有時(shí)候是使用“動(dòng)態(tài)綁定”,就是在運(yùn)行期調(diào)用對(duì)應(yīng)的函數(shù)疾嗅,甚至可以在程序運(yùn)行時(shí)改變外厂。
寫(xiě)一個(gè)簡(jiǎn)單的方法調(diào)用的例子,解釋一下方法的構(gòu)成:
id returnValue = [someObject messageName:parameter];
在這句調(diào)用語(yǔ)句中代承,someObject就是類(lèi)或類(lèi)的實(shí)例汁蝶,messageName就是方法名,parameter就是參數(shù)论悴,編譯器會(huì)把這條語(yǔ)句編譯成一條標(biāo)準(zhǔn)的C語(yǔ)句掖棉,編譯后的語(yǔ)句如下:
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)類(lèi)的對(duì)應(yīng)“方法列表”帕棉,然后找到對(duì)應(yīng)實(shí)現(xiàn)代碼针肥,若找不到會(huì)沿著繼承關(guān)系向上查找,如果還沒(méi)找到香伴,觸發(fā)“消息轉(zhuǎn)發(fā)”機(jī)制(后面會(huì)介紹這個(gè)機(jī)制)慰枕。
這樣下來(lái)調(diào)用一個(gè)方法大家可能感覺(jué)步驟太多,其實(shí)不會(huì)即纲,objc_msgSend會(huì)將匹配結(jié)果放到一張“快速映射表”里具帮,每個(gè)類(lèi)都有一個(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語(yǔ)言的時(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)槲覀儧](méi)有找到對(duì)應(yīng)的方法奸远,下面看消息轉(zhuǎn)發(fā)機(jī)制怎么處理這個(gè)問(wèn)題既棺。
介紹一下消息轉(zhuǎn)發(fā)機(jī)制,大致分為三個(gè)階段:
1.第一階段懒叛,動(dòng)態(tài)方法解析
對(duì)象在無(wú)法解讀方法的時(shí)候丸冕,首先會(huì)調(diào)用所屬類(lèi)下面這個(gè)方法
+ (BOOL)resolveInstanceMethod:(SEL)sel
sel
就是方法名,返回值為Boolean類(lèi)型薛窥,表示這個(gè)類(lèi)是否能新增實(shí)例方法處理這個(gè)方法(如果是類(lèi)方法會(huì)調(diào)用+ (BOOL)resolveClassMethod:(SEL)sel
方法)胖烛,我們需要自定義一些處理方法,用于動(dòng)態(tài)添加到類(lèi)中诅迷,用以解決問(wèn)題(可以看后面的例子)佩番,如果這一步不能解決問(wèn)題,轉(zhuǎn)到第二階段罢杉。
2.第二階段答捕,備援接收者
來(lái)到這一步,我們就要改變解決問(wèn)題的思路屑那,既然這個(gè)類(lèi)不能處理這個(gè)方法拱镐,我們可不可以找別的類(lèi)處理,這時(shí)候?qū)?yīng)的處理方法:
- (id)forwardingTargetForSelector:(SEL)aSelector
aSelector是方法名持际,如果當(dāng)前類(lèi)能夠找到一個(gè)類(lèi)幫忙處理這個(gè)方法沃琅,就返回這個(gè)類(lèi),若找不到就放回nil(通過(guò)這個(gè)方法我們可以實(shí)現(xiàn)類(lèi)似“多繼承”)蜘欲。
3.第三階段益眉,完整的消息轉(zhuǎn)發(fā)
如果已經(jīng)來(lái)到了這一步,我們就要做一個(gè)完整的消息轉(zhuǎn)發(fā)姥份。首先創(chuàng)建一個(gè)NSInvocation對(duì)象郭脂,把未處理方法的所有信息封裝在里面,此對(duì)象包含方法名澈歉、目標(biāo)展鸡、參數(shù),這一步要調(diào)用下面的方法:
- (void)forwardInvocation:(NSInvocation *)anInvocation
這一步處理的方法很簡(jiǎn)單埃难,就是在新的類(lèi)上調(diào)用方法莹弊,如果這樣做的話就和第二階段沒(méi)有什么差別了。通常在這一步的時(shí)候會(huì)做一些改進(jìn)涡尘,會(huì)選擇某種方式改變消息內(nèi)容忍弛,例如追加參數(shù),改變方法名等考抄。
對(duì)于消息的處理细疚,越早越好。
下面粘貼一個(gè)利用動(dòng)態(tài)解析方法實(shí)現(xiàn)@dynamic屬性的例子:
這個(gè)例子實(shí)現(xiàn)一個(gè)類(lèi)川梅,類(lèi)似字典的功能疯兼,只不過(guò)寫(xiě)入和讀取信息的時(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);
// 通過(guò)是否以“set”開(kāi)頭判斷方法名
if ([selectorString hasPrefix:@"set"]) {
/**
* 向類(lèi)中添加一個(gè)方法
* 參數(shù)一 指定類(lèi)名.
* 參數(shù)二 新添加的方法的方法名.
* 參數(shù)三 函數(shù)指針镇防,指向待添加方法.
* 參數(shù)四 待添加方法的類(lèi)型編碼.
*/
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)為小寫(xiě)
NSString *lowercaseFirstChar = [[key substringToIndex:1] lowercaseString];
[key replaceCharactersInRange:NSMakeRange(0, 1) withString:lowercaseFirstChar];
// 如果有值,寫(xiě)入字典中
if (value) {
[backingStore setObject:value forKey:key];
} else {
[backingStore removeObjectForKey:key];
}
}
@end
EOCAutoDictionary的用法也很簡(jiǎn)單潮饱,只要直接通過(guò)對(duì)應(yīng)的屬性名来氧,就可以進(jìn)行數(shù)據(jù)的存儲(chǔ)。
13. 用“方法調(diào)配技術(shù)”調(diào)試“黑盒方法”
方法調(diào)配技術(shù)香拉,簡(jiǎn)言之就是啦扬,將方法名和方法實(shí)現(xiàn)分割開(kāi)來(lái),任意組合凫碌。這樣一來(lái)我們可以任意改變一個(gè)方法的實(shí)現(xiàn)扑毡,另外還可以通過(guò)這種辦法給原有方法添加功能,對(duì)不知道內(nèi)部實(shí)現(xiàn)的方法添加提示語(yǔ)句(黑盒調(diào)試)等等盛险。
之所以能這么做瞄摊,主要是因?yàn)榉椒ň灾羔樀男问絹?lái)表示勋又,這種指針叫IMP,我們?cè)谡{(diào)用方法的時(shí)候换帜,只要將指針指向改變楔壤,就能實(shí)現(xiàn)我們想要的效果,運(yùn)用起來(lái)也很簡(jiǎn)單惯驼,通過(guò)下面的例子大家就會(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);
通過(guò)上面的例子蹲嚣,我們就把NSString的lowercaseString方法和uppercaseString方法調(diào)換了羽氮,是不是很簡(jiǎn)單净嘀。
其實(shí)這樣做并沒(méi)有什么意義,因?yàn)榫唧w的方法實(shí)現(xiàn)已經(jīng)都存在了诺凡,我們沒(méi)必要改變一個(gè)方法實(shí)現(xiàn)说贝,但是我們通過(guò)這種方法給已知的方法添加功能议惰,例如下面的例子:
.h文件:
@interface NSString (EOCMyAdditions)
- (NSString *)eoc_myLowercaseString; // 在分類(lèi)中給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
通過(guò)這個(gè)方法我們發(fā)現(xiàn)换淆,我們可以為那些不知道內(nèi)部實(shí)現(xiàn)的黑盒方法添加日志記錄功能。
一般來(lái)說(shuō)几颜,我們很少用“方法調(diào)配”倍试,只有在調(diào)試程序的時(shí)候才需要在運(yùn)行期修改方法實(shí)現(xiàn)。
14. 理解“類(lèi)對(duì)象”的用意
首先我們要知道蛋哭,OC的實(shí)例對(duì)象是指向某塊內(nèi)存數(shù)據(jù)的指針县习,所以在聲明變量時(shí),要用*號(hào)谆趾。同時(shí)我們知道OC中有一種通用對(duì)象類(lèi)型“id”(id本身已是一個(gè)指針)躁愿,所以我們?cè)谟谩癷d”聲明變量的時(shí)候可能和平常有點(diǎn)不同:
NSString *aString = @"some string";
id aString = @"some string";
上面兩種定義方式相比,語(yǔ)法意義相同沪蓬,區(qū)別在于彤钟,指定具體類(lèi)型后,當(dāng)實(shí)例調(diào)用方法的時(shí)候跷叉,編輯器會(huì)給我們提示逸雹。
下面看一下“id”類(lèi)型的定義:
typedef struct objc_object *id;
id其實(shí)是objc_object類(lèi)型的結(jié)構(gòu)體,而objc_object定義如下:
struct objc_object {
Class isa OBJC_ISA_AVAILABILITY;
};
結(jié)構(gòu)體中是一個(gè)Class類(lèi)型的變量云挟,該變量定義對(duì)象所屬的類(lèi)梆砸。下面我們看一下Class類(lèi)型是個(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)體存放類(lèi)的各種信息(元數(shù)據(jù)),例如類(lèi)有多少個(gè)實(shí)力變量园欣,類(lèi)名等等信息帖世。
通過(guò)上面的關(guān)系,我們知道在objc的runtime中沸枯,類(lèi)是用objc_class結(jié)構(gòu)體表示的日矫,對(duì)象是用objc_object結(jié)構(gòu)體表示的赂弓, 對(duì)象的isa用來(lái)標(biāo)示這個(gè)對(duì)象是哪個(gè)類(lèi)的實(shí)例。
這些源碼是屬于objc runtime的搬男,objc runtime的源代碼蘋(píng)果已經(jīng)開(kāi)源了拣展,你可以在這里下載到objc的runtime源代碼。
其實(shí)到這里大家可能會(huì)有一個(gè)疑問(wèn)缔逛,為什么objc_class結(jié)構(gòu)體里面也有一個(gè)isa,那么這個(gè)isa指向誰(shuí)呢姓惑?我們往下看褐奴,[NSObject class],這里我們調(diào)用了+ (Class)class這個(gè)類(lèi)方法于毙,我們?cè)匍_(kāi)發(fā)中經(jīng)常用到這個(gè)方法敦冬,它返回的是這個(gè)類(lèi)所屬的Class類(lèi)型。+ (Class)class類(lèi)方法的實(shí)現(xiàn)源碼是這樣的:
+ (Class)class {
return self;
}
為什么會(huì)返回self唯沮,self總是指的自身脖旱,而在這里沒(méi)有實(shí)例啊介蛉!這時(shí)候看開(kāi)發(fā)文檔我們會(huì)發(fā)現(xiàn)萌庆,實(shí)際上函數(shù)的返回值是一個(gè)類(lèi)對(duì)象class object,所以其本質(zhì)上還是一個(gè)對(duì)象而已币旧。既然是一個(gè)對(duì)象践险,它擁有一個(gè)self指針也就不奇怪了,所以對(duì)于像NSObject這樣的類(lèi)來(lái)說(shuō)吹菱,它其實(shí)代表的是一個(gè)類(lèi)對(duì)象巍虫,本質(zhì)上還是一個(gè)普通的實(shí)例對(duì)象,那么又會(huì)問(wèn)了鳍刷,這個(gè)類(lèi)對(duì)象是誰(shuí)的實(shí)例呢占遥?很遺憾,要找到這個(gè)問(wèn)題的答案输瓜,我們?cè)?objc runtime 這一層上已經(jīng)沒(méi)辦法辦到了瓦胎,我們需要到更低層,也就是 objc 語(yǔ)言層去尋找答案了前痘,但是 objc 語(yǔ)言層是不開(kāi)源的凛捏,如果想繼續(xù)學(xué)習(xí),大家可以在網(wǎng)上找模仿OC低層的代碼芹缔。
以上了解一下就好坯癣,我們只要知道類(lèi)的繼承體系就行了,下面用一個(gè)例子:有一個(gè)類(lèi)(暫且叫SomeClass)繼承于NSObject,那么這些類(lèi)和元類(lèi)的繼承關(guān)系是最欠,SomeClass實(shí)例有一個(gè)isa指針指向SomeClass類(lèi)示罗,SomeClass類(lèi)有一個(gè)isa指針指向SomeClass元類(lèi)惩猫,NSObject類(lèi)也有一個(gè)isa指針指向NSObject元類(lèi),SomeClass的父類(lèi)是NSObject蚜点,SomeClass元類(lèi)的父類(lèi)是NSObject元類(lèi)轧房,通過(guò)這種關(guān)系,我們?cè)陬?lèi)繼承體系中查詢(xún)類(lèi)型信息绍绘,用isMenberOfClass:
判斷對(duì)象是否是某個(gè)特定類(lèi)的實(shí)例奶镶,用isKindOfClass:
判斷對(duì)象是否為某類(lèi)或其派生類(lèi)的實(shí)例。因?yàn)镺C是動(dòng)態(tài)型語(yǔ)言的特性陪拘,上面兩個(gè)方法非常有用厂镇。
有時(shí)我們可以用比較類(lèi)對(duì)象是否等同的辦法來(lái)進(jìn)行比較,這時(shí)要用==
操作符左刽,而不是用isEqual方法捺信,因?yàn)轭?lèi)對(duì)象是單利,在應(yīng)用程序中欠痴,每個(gè)類(lèi)的類(lèi)對(duì)象只有一個(gè)實(shí)例迄靠,也就是說(shuō)另外一種判斷對(duì)象是否為某類(lèi)實(shí)例的辦法是:
id object = /*...*/
if ([object class] == [SomeClass class]){
}
這一部分基本都是關(guān)于OC運(yùn)行時(shí)的知識(shí),可能我們平時(shí)寫(xiě)代碼的時(shí)候涉及很少喇辽,但是了解這些掌挚,對(duì)于我們的開(kāi)發(fā)是很有幫助的,OC運(yùn)行時(shí)是一個(gè)很強(qiáng)大的東西茵臭,有興趣的同學(xué)可以好好研究一下疫诽。