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