簡書上的所有內(nèi)容都可以在我的個人博客上找到(打個廣告??)
在我們知道了 Objective-C 中類的本質(zhì),以及它的消息分發(fā)機制后,我們就可以來看看那些與 runtime 相關(guān)的的函數(shù)了。當(dāng)然,我們只會講比較常見的那些。
關(guān)聯(lián)對象(Associated Object)
關(guān)聯(lián)對象,顧名思義,就是給某對象關(guān)聯(lián)許多其他的對象。這些對象通過 key 來區(qū)分。
與關(guān)聯(lián)對象相關(guān)的函數(shù)有三個:
objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);
objc_getAssociatedObject(id object, const void *key);
objc_removeAssociatedObjects(id object);
從函數(shù)名我們也可以看出來,這三個函數(shù)分別是用來設(shè)置,獲取和移除關(guān)聯(lián)對象的。這里要解釋一下的是他們的參數(shù)。
- 第一個參數(shù) id object 顯然就是你要設(shè)置關(guān)聯(lián)對象的那個對象。
- 第二個參數(shù) const void *key 就是用來區(qū)分不同的關(guān)聯(lián)對象的 key,因為想讓兩個 key 匹配到同一個關(guān)聯(lián)對象就必須是完全相等的指針,所以我們一般用靜態(tài)全局變量來作為 key。
static const void *AssociatedKey = "AssociatedKey";
- 第三個參數(shù) id value 就是要關(guān)聯(lián)的對象了。
- 第四個參數(shù) objc_AssociationPolicy policy 指的是關(guān)聯(lián)對象的存儲策略,它是一個枚舉,可以與 property 的 attribute 相對應(yīng):
typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
OBJC_ASSOCIATION_ASSIGN = 0, // assign
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, // nonatomic, retain
OBJC_ASSOCIATION_COPY_NONATOMIC = 3, // nonatomic, copy
OBJC_ASSOCIATION_RETAIN = 01401, // retain
OBJC_ASSOCIATION_COPY = 01403 // copy
};
大家知道,在 category 中,我們無法添加 property,因為無法添加實例變量。那么,我們現(xiàn)在就可以通過關(guān)聯(lián)對象來實現(xiàn)在 category 中添加屬性的功能了。
我們現(xiàn)在 CYClass 類的拓展中聲明了一個屬性
@interface CYClass (Property)
@property (nonatomic, copy)NSString *aString;
@end
如果這個時候我們直接在外部訪問這個屬性, 那個程序是會 crash 的,不信你可以試試??,編譯器會說:
'-[CYClass setAString:]: unrecognized selector sent to instance 0x1001060a0'
所以我們給它加上 setter 和 getter 方法, 并且在這兩個方法中給它設(shè)置關(guān)聯(lián)對象:
static void *aStringKey = "aStringKey";
@implementation CYClass (Property)
- (void)setAString:(NSString *)newString{
objc_setAssociatedObject(self, aStringKey, newString, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSString *)aString{
return objc_getAssociatedObject(self, aStringKey);
}
@end
現(xiàn)在我們再進行讀寫操作,程序就不會 crash 了。當(dāng)然,沒有必要的情況下,還是不要濫用關(guān)聯(lián)對象, 否則有可能會出現(xiàn)一些難以發(fā)現(xiàn)的bug。
方法調(diào)配(Method Swizzling)
在前一篇博客中我們知道了每個類中的方法是以 objc_method 結(jié)構(gòu)體的形式放在 methodLists 中的。每一個 selector 對應(yīng)了一個實現(xiàn)的函數(shù)的指針 IMP。而 method swizzling 技術(shù)就是通過交換這個函數(shù)指針來實現(xiàn)的。
我們最好在 +load 方法中使用 method swizzling,因為 +load 方法對于加入運行期中的每個類及分類都會調(diào)用且只調(diào)用一次。所以在這里交換方法是最安全的。
我們來看一下蘋果為我們提供了哪些API來實現(xiàn) method swizzling:
IMP class_replaceMethod(Class cls, SEL name, IMP imp, const char *types);
可以直接替換方法,當(dāng)需要的方法不存在時,會先調(diào)用 class_addMethod 來添加一個新的方法。會返回替換前的實現(xiàn)函數(shù)指針 。
Method class_getInstanceMethod(Class cls, SEL name);
根據(jù)類和 selector 得到 method,用來作為下面兩個方法的參數(shù)。
IMP method_setImplementation(Method m, IMP imp);
直接為一個方法設(shè)置它的實現(xiàn),返回之前的實現(xiàn)函數(shù)指針
void method_exchangeImplementations(Method m1, Method m2)
交換兩個方法的實現(xiàn),實際上就是調(diào)用了兩次 method_setImplementation,并且是線程安全的。
我們用 method_exchangeImplementations 來簡單的嘗試一下 method swizzling,我添加了一個 NSString 的分類,用我自己的方法交換了系統(tǒng)的 lowercaseString 方法:
@implementation NSString (Swizzling)
+ (void)load {
Method originalMethod = class_getInstanceMethod([self class], @selector(lowercaseString));
Method swappedMthod = class_getInstanceMethod([self class], @selector(swizzle_lowercaseString));
method_exchangeImplementations(originalMethod, swappedMthod);
}
- (NSString *)swizzle_lowercaseString {
NSString *lowercase = [self swizzle_lowercaseString];
NSLog(@"FROM: %@ TO: %@", self, lowercase);
return lowercase;
}
@end
可能有人會覺得在自己新寫的 swizzle_lowercaseString 方法中又調(diào)用 [self swizzle_lowercaseString] 會導(dǎo)致死循環(huán),其實在交換了方法以后我們調(diào)用原來的 lowercaseString 方法就會進入這個方法的實現(xiàn),而這時候調(diào)用 swizzle_lowercaseString 其實調(diào)用的是系統(tǒng)原來的方法,所以是不會產(chǎn)生死循環(huán)的。這里理解起來可能有點奇怪。
我們在看一下調(diào)用的結(jié)果
2016-03-11 20:01:05.645 Example[4129:101067] FROM: Hello World TO: hello world
當(dāng)然 method swizzling 是一把雙刃劍,我們可以用它來進行黑盒測試,在真正的項目中如果用 method swizzling 一定要格外小心。
消息轉(zhuǎn)發(fā)機制(Message Forwarding)
當(dāng)我們的對象接收到一個無法解讀的消息時,就會進入消息轉(zhuǎn)發(fā)。消息轉(zhuǎn)發(fā)分為兩大階段,第一階段是動態(tài)方法解析,第二階段是完整的消息轉(zhuǎn)發(fā)。
動態(tài)方法解析(dynamic method resolution)
要實現(xiàn)動態(tài)方法解析只要重寫兩個方法:
+ (BOOL)resolveInstanceMethod:(SEL)sel; // 處理無法識別的實例方法
+ (BOOL)resolveClassMethod:(SEL)sel; // 處理無法識別的類方法
這兩個方法傳進來的參數(shù) selector 就是那個無法解析的方法,我們可以根據(jù)這個 selector 來動態(tài)的為這個類添加方法。比如像下面這樣:
void dynamicMethod(id self, SEL _cmd) {
// do something here
}
@implementation CYClass
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(someSelector)) { // 對selector做一些邏輯判斷
class_addMethod([self class], sel, (IMP)dynamicMethod, "v@:"); // 為類添加方法
return YES;
} else {
return NO;
}
}
@end
還要提一下 class_addMethod 函數(shù):
class_addMethod(__unsafe_unretained Class cls, SEL name, IMP imp, const char *types);
它的最后一個參數(shù)是用來描述這個函數(shù)的返回值和參數(shù)類型的,稱之為 類型編碼(Type Encoding)。在前面那個例子里的 "v@:" 中, v 表示返回值為 void, @ 表示第一個參數(shù)是 id, : 表示第二個參數(shù)類型是 SEL 。更多的類型編碼可以看這里
當(dāng) resolveInstanceMethod: 返回 NO 時,就會進入消息轉(zhuǎn)發(fā)的第二階段 完整的消息轉(zhuǎn)發(fā)機制。
完整的消息轉(zhuǎn)發(fā)機制
完整的消息轉(zhuǎn)發(fā)主要涉及兩個方法:
- (id)forwardingTargetForSelector:(SEL)aSelector;
- (void)forwardInvocation:(NSInvocation *)anInvocation;
如果在 + resolveInstanceMethod: 方法中返回了 NO 那么就會執(zhí)行 - forwardingTargetForSelector: 方法。在這個方法內(nèi)我們可以給對象返回一個備援的接受者來處理這個位置的信息。在 CYClass 的實現(xiàn)中我們這么寫:
@implementation CYClass
- (id)forwardingTargetForSelector:(SEL)aSelector {
if (aSelector == @selector(unrecognizedSel)) {
return [AnotherClass new];
}
return [super forwardingTargetForSelector:aSelector];
}
@end
AnotherClass 的實例就是我們用來作為備援接受者的對象,我們在 AnotherClass 中實現(xiàn)了 unrecognizedSel 方法:
@implementation AnotherClass
- (void)unrecognizedSel {
NSLog(@"forwarding target for unrecognized selector in AnotherClass");
}
@end
然后我們再給 CYClass 的實例發(fā)送 unrecognizedSel 的消息就不會 crash 了:
CYClass *c = [CYClass new];
[c performSelector:@selector(unrecognizedSel)];
// 打印結(jié)果
2016-03-12 12:46:30.608 example[1577:19943] forwarding target for unrecognized selector in AnotherClass
如果這一步我們也沒有提供一個備援的接收者,那么就會進入最后一步 - forwardInvocation: 方法,系統(tǒng)會把所有與那條消息相關(guān)的信息全部封裝在一個 NSInvocation 對象中,我們可以在直接改變調(diào)用的目標(biāo), 也可以修改消息的內(nèi)容后再進行轉(zhuǎn)發(fā)。我們把前一個方法去掉,然后重寫一下 - forwardInvocation: 方法:
@implementation CYClass
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
SEL sel = [anInvocation selector];
if (sel == @selector(unrecognizedSel)) {
[anInvocation invokeWithTarget:[AnotherClass new]];
} else {
[super forwardInvocation: anInvocation];
}
}
@end
需要注意的是我們還要重寫- methodSignatureForSelector: 方法,因為生成 NSInvocation 對象會調(diào)用到這個方法,否則會拋出異常。關(guān)于 forwardInvocation 了解的還不是很多,所以例子比較簡單,以后有了更深的理解后會再加上。
消息轉(zhuǎn)發(fā)的全過程
總結(jié)
到這里對于 runtime 的簡單理解與使用就基本結(jié)束了。總的來說,理解了 Objective-C 的運行時會讓我們的代碼更加靈活,當(dāng)然也會增大維護的難度。不過想要學(xué)好 Objective-C 這門語言,runtime 是必不可少的!