簡(jiǎn)書(shū)上的所有內(nèi)容都可以在我的個(gè)人博客上找到(打個(gè)廣告??)
在我們知道了 Objective-C 中類的本質(zhì)续扔,以及它的消息分發(fā)機(jī)制后,我們就可以來(lái)看看那些與 runtime 相關(guān)的的函數(shù)了案疲。當(dāng)然芥喇,我們只會(huì)講比較常見(jiàn)的那些权逗。
關(guān)聯(lián)對(duì)象(Associated Object)
關(guān)聯(lián)對(duì)象,顧名思義捣染,就是給某對(duì)象關(guān)聯(lián)許多其他的對(duì)象骄瓣。這些對(duì)象通過(guò) key 來(lái)區(qū)分。
與關(guān)聯(lián)對(duì)象相關(guān)的函數(shù)有三個(gè):
objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);
objc_getAssociatedObject(id object, const void *key);
objc_removeAssociatedObjects(id object);
從函數(shù)名我們也可以看出來(lái)耍攘,這三個(gè)函數(shù)分別是用來(lái)設(shè)置榕栏,獲取和移除關(guān)聯(lián)對(duì)象的。這里要解釋一下的是他們的參數(shù)蕾各。
- 第一個(gè)參數(shù) id object 顯然就是你要設(shè)置關(guān)聯(lián)對(duì)象的那個(gè)對(duì)象扒磁。
- 第二個(gè)參數(shù) const void *key 就是用來(lái)區(qū)分不同的關(guān)聯(lián)對(duì)象的 key,因?yàn)橄胱寖蓚€(gè) key 匹配到同一個(gè)關(guān)聯(lián)對(duì)象就必須是完全相等的指針式曲,所以我們一般用靜態(tài)全局變量來(lái)作為 key妨托。
static const void *AssociatedKey = "AssociatedKey";
- 第三個(gè)參數(shù) id value 就是要關(guān)聯(lián)的對(duì)象了。
- 第四個(gè)參數(shù) objc_AssociationPolicy policy 指的是關(guān)聯(lián)對(duì)象的存儲(chǔ)策略吝羞,它是一個(gè)枚舉兰伤,可以與 property 的 attribute 相對(duì)應(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 中钧排,我們無(wú)法添加 property敦腔,因?yàn)闊o(wú)法添加實(shí)例變量。那么恨溜,我們現(xiàn)在就可以通過(guò)關(guān)聯(lián)對(duì)象來(lái)實(shí)現(xiàn)在 category 中添加屬性的功能了符衔。
我們現(xiàn)在 CYClass 類的拓展中聲明了一個(gè)屬性
@interface CYClass (Property)
@property (nonatomic, copy)NSString *aString;
@end
如果這個(gè)時(shí)候我們直接在外部訪問(wèn)這個(gè)屬性, 那個(gè)程序是會(huì) crash 的糟袁,不信你可以試試??判族,編譯器會(huì)說(shuō):
'-[CYClass setAString:]: unrecognized selector sent to instance 0x1001060a0'
所以我們給它加上 setter 和 getter 方法, 并且在這兩個(gè)方法中給它設(shè)置關(guān)聯(lián)對(duì)象:
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)在我們?cè)龠M(jìn)行讀寫(xiě)操作项戴,程序就不會(huì) crash 了形帮。當(dāng)然,沒(méi)有必要的情況下肯尺,還是不要濫用關(guān)聯(lián)對(duì)象, 否則有可能會(huì)出現(xiàn)一些難以發(fā)現(xiàn)的bug躯枢。
方法調(diào)配(Method Swizzling)
在前一篇博客中我們知道了每個(gè)類中的方法是以 objc_method 結(jié)構(gòu)體的形式放在 methodLists 中的则吟。每一個(gè) selector 對(duì)應(yīng)了一個(gè)實(shí)現(xiàn)的函數(shù)的指針 IMP。而 method swizzling 技術(shù)就是通過(guò)交換這個(gè)函數(shù)指針來(lái)實(shí)現(xiàn)的锄蹂。
我們最好在 +load 方法中使用 method swizzling氓仲,因?yàn)?+load 方法對(duì)于加入運(yùn)行期中的每個(gè)類及分類都會(huì)調(diào)用且只調(diào)用一次。所以在這里交換方法是最安全的。
我們來(lái)看一下蘋果為我們提供了哪些API來(lái)實(shí)現(xiàn) method swizzling:
IMP class_replaceMethod(Class cls, SEL name, IMP imp, const char *types);
可以直接替換方法敬扛,當(dāng)需要的方法不存在時(shí)晰洒,會(huì)先調(diào)用 class_addMethod 來(lái)添加一個(gè)新的方法。會(huì)返回替換前的實(shí)現(xiàn)函數(shù)指針 啥箭。
Method class_getInstanceMethod(Class cls, SEL name);
根據(jù)類和 selector 得到 method谍珊,用來(lái)作為下面兩個(gè)方法的參數(shù)。
IMP method_setImplementation(Method m, IMP imp);
直接為一個(gè)方法設(shè)置它的實(shí)現(xiàn)急侥,返回之前的實(shí)現(xiàn)函數(shù)指針
void method_exchangeImplementations(Method m1, Method m2)
交換兩個(gè)方法的實(shí)現(xiàn)砌滞,實(shí)際上就是調(diào)用了兩次 method_setImplementation,并且是線程安全的坏怪。
我們用 method_exchangeImplementations 來(lái)簡(jiǎn)單的嘗試一下 method swizzling贝润,我添加了一個(gè) 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
可能有人會(huì)覺(jué)得在自己新寫(xiě)的 swizzle_lowercaseString 方法中又調(diào)用 [self swizzle_lowercaseString] 會(huì)導(dǎo)致死循環(huán)铝宵,其實(shí)在交換了方法以后我們調(diào)用原來(lái)的 lowercaseString 方法就會(huì)進(jìn)入這個(gè)方法的實(shí)現(xiàn)打掘,而這時(shí)候調(diào)用 swizzle_lowercaseString 其實(shí)調(diào)用的是系統(tǒng)原來(lái)的方法,所以是不會(huì)產(chǎn)生死循環(huán)的鹏秋。這里理解起來(lái)可能有點(diǎn)奇怪尊蚁。
我們?cè)诳匆幌抡{(diào)用的結(jié)果
2016-03-11 20:01:05.645 Example[4129:101067] FROM: Hello World TO: hello world
當(dāng)然 method swizzling 是一把雙刃劍,我們可以用它來(lái)進(jìn)行黑盒測(cè)試拼岳,在真正的項(xiàng)目中如果用 method swizzling 一定要格外小心枝誊。
消息轉(zhuǎn)發(fā)機(jī)制(Message Forwarding)
當(dāng)我們的對(duì)象接收到一個(gè)無(wú)法解讀的消息時(shí),就會(huì)進(jìn)入消息轉(zhuǎn)發(fā)惜纸。消息轉(zhuǎn)發(fā)分為兩大階段叶撒,第一階段是動(dòng)態(tài)方法解析,第二階段是完整的消息轉(zhuǎn)發(fā)耐版。
動(dòng)態(tài)方法解析(dynamic method resolution)
要實(shí)現(xiàn)動(dòng)態(tài)方法解析只要重寫(xiě)兩個(gè)方法:
+ (BOOL)resolveInstanceMethod:(SEL)sel; // 處理無(wú)法識(shí)別的實(shí)例方法
+ (BOOL)resolveClassMethod:(SEL)sel; // 處理無(wú)法識(shí)別的類方法
這兩個(gè)方法傳進(jìn)來(lái)的參數(shù) selector 就是那個(gè)無(wú)法解析的方法祠够,我們可以根據(jù)這個(gè) selector 來(lái)動(dòng)態(tài)的為這個(gè)類添加方法。比如像下面這樣:
void dynamicMethod(id self, SEL _cmd) {
// do something here
}
@implementation CYClass
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(someSelector)) { // 對(duì)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);
它的最后一個(gè)參數(shù)是用來(lái)描述這個(gè)函數(shù)的返回值和參數(shù)類型的粪牲,稱之為 類型編碼(Type Encoding)古瓤。在前面那個(gè)例子里的 "v@:" 中, v 表示返回值為 void腺阳, @ 表示第一個(gè)參數(shù)是 id落君, : 表示第二個(gè)參數(shù)類型是 SEL 。更多的類型編碼可以看這里
當(dāng) resolveInstanceMethod: 返回 NO 時(shí)亭引,就會(huì)進(jìn)入消息轉(zhuǎn)發(fā)的第二階段 完整的消息轉(zhuǎn)發(fā)機(jī)制绎速。
完整的消息轉(zhuǎn)發(fā)機(jī)制
完整的消息轉(zhuǎn)發(fā)主要涉及兩個(gè)方法:
- (id)forwardingTargetForSelector:(SEL)aSelector;
- (void)forwardInvocation:(NSInvocation *)anInvocation;
如果在 + resolveInstanceMethod: 方法中返回了 NO 那么就會(huì)執(zhí)行 - forwardingTargetForSelector: 方法。在這個(gè)方法內(nèi)我們可以給對(duì)象返回一個(gè)備援的接受者來(lái)處理這個(gè)位置的信息焙蚓。在 CYClass 的實(shí)現(xiàn)中我們這么寫(xiě):
@implementation CYClass
- (id)forwardingTargetForSelector:(SEL)aSelector {
if (aSelector == @selector(unrecognizedSel)) {
return [AnotherClass new];
}
return [super forwardingTargetForSelector:aSelector];
}
@end
AnotherClass 的實(shí)例就是我們用來(lái)作為備援接受者的對(duì)象纹冤,我們?cè)?AnotherClass 中實(shí)現(xiàn)了 unrecognizedSel 方法:
@implementation AnotherClass
- (void)unrecognizedSel {
NSLog(@"forwarding target for unrecognized selector in AnotherClass");
}
@end
然后我們?cè)俳o CYClass 的實(shí)例發(fā)送 unrecognizedSel 的消息就不會(huì) 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
如果這一步我們也沒(méi)有提供一個(gè)備援的接收者洒宝,那么就會(huì)進(jìn)入最后一步 - forwardInvocation: 方法,系統(tǒng)會(huì)把所有與那條消息相關(guān)的信息全部封裝在一個(gè) NSInvocation 對(duì)象中萌京,我們可以在直接改變調(diào)用的目標(biāo)雁歌, 也可以修改消息的內(nèi)容后再進(jìn)行轉(zhuǎn)發(fā)。我們把前一個(gè)方法去掉知残,然后重寫(xiě)一下 - 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
需要注意的是我們還要重寫(xiě)- methodSignatureForSelector: 方法靠瞎,因?yàn)樯?NSInvocation 對(duì)象會(huì)調(diào)用到這個(gè)方法,否則會(huì)拋出異常橡庞。關(guān)于 forwardInvocation 了解的還不是很多较坛,所以例子比較簡(jiǎn)單,以后有了更深的理解后會(huì)再加上扒最。
消息轉(zhuǎn)發(fā)的全過(guò)程
總結(jié)
到這里對(duì)于 runtime 的簡(jiǎn)單理解與使用就基本結(jié)束了丑勤。總的來(lái)說(shuō)吧趣,理解了 Objective-C 的運(yùn)行時(shí)會(huì)讓我們的代碼更加靈活法竞,當(dāng)然也會(huì)增大維護(hù)的難度。不過(guò)想要學(xué)好 Objective-C 這門語(yǔ)言强挫,runtime 是必不可少的岔霸!