iOS学习之Runtime(二)
前面已經(jīng)介紹了Runtime系統(tǒng)的概念、作用、部分技術(shù)點和應(yīng)用場景,這篇將會繼續(xù)學(xué)習(xí)Runtime的其他知識。
一、Runtime技術(shù)點之類/對象的關(guān)聯(lián)對象
關(guān)聯(lián)對象不是為類/對象添加屬性或者成員變量(因為在設(shè)置關(guān)聯(lián)后也無法通過copyIvarList或者copyPropertyList取得),而是為類添加一個相關(guān)的對象,通常用于存儲類信息,例如存儲類的屬性列表數(shù)組,方便以后字典轉(zhuǎn)模型的操作。
Runtime為我們提供了三個函數(shù)進行關(guān)聯(lián)對象的相關(guān)操作:
1 /** 2 * 為某個類關(guān)聯(lián)某個對象 3 * 4 * id object,當(dāng)前對象 5 * const void *key,關(guān)聯(lián)的key,是C字符串 6 * id value,被關(guān)聯(lián)的對象 7 * objc_AssociationPolicy policy,關(guān)聯(lián)引用的規(guī)則,取值有以下幾種: 8 * enum { 9 * OBJC_ASSOCIATION_ASSIGN = 0, 10 * OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, 11 * OBJC_ASSOCIATION_COPY_NONATOMIC = 3, 12 * OBJC_ASSOCIATION_RETAIN = 01401, 13 * OBJC_ASSOCIATION_COPY = 01403 14 * }; 15 * 16 */ 17 void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy) 18 19 /** 20 * 獲取到某個類的某個關(guān)聯(lián)對象 21 * 22 */ 23 id objc_getAssociatedObject(id object, const void *key) 24 25 /** 26 * 移除已經(jīng)關(guān)聯(lián)的對象 27 * 28 */ 29 void objc_removeAssociatedObjects(id object) View Code我們可以將關(guān)聯(lián)對象的設(shè)置與獲取封裝起來,用于方便獲取類的屬性列表。
1 @implementation Person 2 3 const char *propertiesKey = "propertiesKey"; 4 5 + (NSArray *)properties 6 { 7 // 通過key取出關(guān)聯(lián)對象 8 NSArray *pList = objc_getAssociatedObject(self, propertiesKey); 9 if (pList != nil) 10 { 11 return pList; 12 } 13 14 // 如果沒有關(guān)聯(lián)對象,則取出成員變量和屬性,存入數(shù)組 15 unsigned int outCount; 16 Ivar *ivarList = class_copyIvarList(self, &outCount); 17 18 NSMutableArray *array = [NSMutableArray arrayWithCapacity:outCount]; 19 20 for (int i = 0; i < outCount; i++) 21 { 22 Ivar *ivar = &ivarList[i]; 23 NSString *name = [NSString stringWithUTF8String:ivar_getName(*ivar)]; 24 NSString *key = [name substringFromIndex:1]; 25 [array addObject:key]; 26 } 27 28 // 釋放ivarList 29 free(ivarList); 30 31 // 設(shè)置關(guān)聯(lián)對象 32 objc_setAssociatedObject(self, propertiesKey, array, OBJC_ASSOCIATION_COPY_NONATOMIC); 33 34 return array.copy; 35 } 36 37 - (NSString *)description 38 { 39 NSLog(@"name: %@---------age: %d---------height: %f", self.name, self.age, _height); 40 return nil; 41 } 42 43 @end View Code這樣的話,我們只需在外部調(diào)用這個類方法,即可獲得該類的所有屬性列表。
不過我在網(wǎng)上也看到有人將關(guān)聯(lián)對象應(yīng)用到分類中,目前來說我還不能確定這種做法是否恰當(dāng),不過倒是可以提供一種思路。這種用法的初衷是在不使用繼承的方式下給系統(tǒng)類添加一個公共變量。我們都知道,分類只能為類添加方法,而延展里面為類添加的變量或方法都是私有的(這里簡單介紹一下延展的作用,延展其實就是C語言中的前向聲明,不過現(xiàn)在蘋果已經(jīng)彌補了這個缺陷,所以這里不再細(xì)述)。那么怎樣才能在不使用繼承的方式下給系統(tǒng)類添加一個公共變量呢?這里就用的了關(guān)聯(lián)對象。
我們可以在NSDictionary的分類MyDict.h中新增一個屬性property1,一般情況下如果我們只聲明了這些變量,在外面使用的時候就會報錯,因為分類是不允許你這么做的。那么我們就需要通過設(shè)置關(guān)聯(lián)對象來實現(xiàn)property1的set、get方法,其實原理很簡單,就是在set方法中,通過一個key將property1的值關(guān)聯(lián)到類中;在get方法中,再通過這個key將property1的值取出即可。
1 const char *property1Key = "property1Key";2 3 - (void)setProperty1:(NSString *)property14 {5 // 通過key設(shè)置關(guān)聯(lián)對象6 objc_setAssociatedObject(self, property1Key, property1, OBJC_ASSOCIATION_COPY_NONATOMIC);7 }8 9 - (NSString *)property1 10 { 11 // 通過key獲取關(guān)聯(lián)對象 12 return objc_getAssociatedObject(self, property1Key); 13 } View Code這樣我們就可以在外部使用這個分類的新增屬性了。同樣的,我們也可以為其設(shè)置block,原理都是一樣的,這里就不再累述了。
二、Runtime技術(shù)點之消息轉(zhuǎn)發(fā)
在學(xué)習(xí)消息轉(zhuǎn)發(fā)知識之前,我們需要知道幾個概念:
1、OC中調(diào)用方法就是向?qū)ο蟀l(fā)送消息。比如[person walk];實際上是給person對象發(fā)送了walk這個消息。調(diào)用類方法也一樣,類實際上也是一個對象,是元類的實例。方法調(diào)用的流程如下:
(1)系統(tǒng)會查看這個對象能否接收這個消息(查看這個類有沒有這個方法,或者有沒有實現(xiàn)這個方法);
(2)如果不能接收這個消息,就會調(diào)用下面這幾個方法,給出“補救”的機會;
(3)如果在這幾個方法中都沒有做處理,那么程序就會報錯;
需要注意的是,下面這幾個方法調(diào)用是有先后順序的,并且如果前一個方法做出相應(yīng)處理了,就不會再調(diào)用后面的方法了。
1 1 + resolveInstanceMethod:(SEL)sel // 實例方法沒有實現(xiàn)時會調(diào)用這個方法 2 + resolveClassMethod:(SEL)sel // 類方法沒有實現(xiàn)時會調(diào)用這個方法 3 2 - (id)forwardingTargetForSelector:(SEL)aSelector 4 3 - (void)forwardInvocation:(NSInvocation *)anInvocation View Code其中:- (void)forwardInvocation:(NSInvocation *)anInvocation; 需要跟 - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector; 結(jié)合使用才能實現(xiàn)消息轉(zhuǎn)發(fā),methodSignatureForSelector的作用是為方法創(chuàng)建一個有效的簽名。如果沒有找到方法的對應(yīng)實現(xiàn),則會返回一個空的方法簽名,最終導(dǎo)致程序崩潰。關(guān)于怎樣使用它們實現(xiàn)消息轉(zhuǎn)發(fā),下面會介紹。
2、SEL的概念。
SEL就是對方法的一種包裝。包裝的SEL類型數(shù)據(jù),它對應(yīng)相應(yīng)的方法地址,找到方法地址就可以調(diào)用方法。在內(nèi)存中每個類的方法都存儲在類對象中,每個方法都有一個與之對應(yīng)的SEL類型的數(shù)據(jù),根據(jù)一個SEL數(shù)據(jù)就可以找到對應(yīng)的方法地址,進而調(diào)用方法。
每個類都有一個包含SEL和對應(yīng)的IMP的Method列表,也就是說一個Method包含著一個SEL和一個對應(yīng)的IMP,而消息轉(zhuǎn)發(fā)就是將原本的SEL和IMP的這種對應(yīng)關(guān)系給分開,跟其他的Method重新組合。
SEL類型的定義:typedef struct objc_selector *SEL
3、OC中的方法默認(rèn)被隱藏了兩個參數(shù):self和_cmd。self指向?qū)ο蟊旧?#xff0c;_cmd指向方法本身。比如- (void)walk; 這個方法實際有兩個參數(shù):self和_cmd。再比如- (void)walk:(NSString *)address; 這個方法實際有三個參數(shù):self、_cmd和address。而且self的類型必須是id,_cmd的類型必須是SEL,這也就解釋了為什么_cmd能夠指向方法本身了,因為_cmd的類型就是SEL,而SEL就是對方法的一種包裝。
有了對上面這些概念的認(rèn)知,我們才能更好的理解消息轉(zhuǎn)發(fā)的原理與實現(xiàn)。
(一)、動態(tài)添加方法實現(xiàn)消息轉(zhuǎn)發(fā)
根據(jù)概念1我們知道,假如一個方法沒有對應(yīng)的實現(xiàn),那么系統(tǒng)首先會調(diào)用+ (BOOL)resolveInstanceMethod:(SEL)sel; 來進行“補救”,那我們是否可以在這里手動添加一個該方法對應(yīng)的實現(xiàn)呢?答案是肯定的,這也就是有些文檔中提到的動態(tài)添加方法。現(xiàn)在假設(shè)Person.h中有一個- (void)walk; 方法,但是Person.m中并沒有實現(xiàn)它,現(xiàn)在我們需要重寫+ (BOOL)resolveInstanceMethod:(SEL)sel; 來實現(xiàn)動態(tài)添加方法。
1 @implementation Person 2 3 + (BOOL)resolveInstanceMethod:(SEL)sel 4 { 5 NSString *selString = NSStringFromSelector(sel); 6 if ([selString isEqualToString:@"walk"]) 7 { 8 class_addMethod(self, @selector(walk), (IMP)goTo, "v@:"); 9 } 10 return [super resolveInstanceMethod:sel]; 11 } 12 13 // 這是C語言的語法 14 void goTo(id self, SEL sel) 15 { 16 NSLog(@"Person walk."); 17 } 18 19 @end View Code這里有幾點需要解釋:
(1)根據(jù)概念2我們知道,SEL是對方法的封裝,那么通過SEL我們可以獲取到方法名,只有在walk被調(diào)用時,我們才動態(tài)添加這個方法的實現(xiàn);
(2)我們再來分析一下class_addMethod。官方的解釋是這樣的:Adds a new method to a class with a given name and implementation.?直接可以理解為給類添加一個新的方法。
第一個參數(shù):The class to which to add a method. 要添加方法的類。
第二個參數(shù):A selector that specifies the name of the method being added.?可以理解為沒有實現(xiàn)的方法名稱。
第三個參數(shù):A function which is the implementation of the new method. The function must take at least two arguments—self?and?_cmd. 要添加的方法實現(xiàn)。注意,這個方法最少要有兩個參數(shù):self和_cmd。
第四個參數(shù):An array of characters that describe the types of the arguments to the method. 描述要添加的方法的參數(shù)類型的數(shù)組。Since the function must take at least two arguments—self?and?_cmd, the second and third characters must be “@:” (the first character is the return type). 因此這個方法最少要有兩個參數(shù):self和_cmd,并且第二個字符和第三個字符必須是“@:”,第一個字符是這個方法的返回值類型。
(3)根據(jù)概念3我們知道,OC中的方法默認(rèn)被隱藏了兩個參數(shù),但是C語言并非如此,而Runtime又是基于C語言和匯編的,所以也就很好理解為什么這個方法的實現(xiàn)必須要有self和_cmd這兩個參數(shù)了。但是“@:”又是什么東西?還記得上一篇中我們提到的類型編碼嗎?具體可以看這里。“@”代表的就是對象,也就是對應(yīng)這里的self;“:”代表的就是SEL,也就是對應(yīng)這里的_cmd;而上面的“v”則是代表這個方法的返回值是void類型。
這樣一來,當(dāng)我們調(diào)用[person walk]; 時,實際上調(diào)用的就是goTo方法,所以最終打印結(jié)果為:
1 2016-04-07 15:41:54.073 RunTimeTest[9162:345046] Person walk. View Code(二)、切換消息接收者實現(xiàn)消息轉(zhuǎn)發(fā)
消息轉(zhuǎn)發(fā)的另一種形式相比起來更容易理解,直接將消息轉(zhuǎn)發(fā)給其他對象,相當(dāng)于調(diào)用其他對象的同名方法,這就用到了- (id)forwardingTargetForSelector:(SEL)aSelector;
現(xiàn)在我們再創(chuàng)建一個Car類,同樣在Car.h中聲明一個方法- (void)walk; 并且在Car.m中實現(xiàn)它,然后重寫Person類的- (id)forwardingTargetForSelector:(SEL)aSelector; 注意,此時不要在+ (BOOL)resolveInstanceMethod:(SEL)sel 做任何處理。
1 - (id)forwardingTargetForSelector:(SEL)aSelector 2 { 3 NSString *selString = NSStringFromSelector(aSelector); 4 if ([selString isEqualToString:@"walk"]) 5 { 6 // 將消息轉(zhuǎn)發(fā)給Car 7 return [[Car alloc] init]; 8 } 9 10 return [super forwardingTargetForSelector:aSelector]; 11 } View Code外部同樣是調(diào)用[person walk]; 這樣就實現(xiàn)了將消息由Person轉(zhuǎn)發(fā)到Car中了。
我們還可以利用- (void)forwardInvocation:(NSInvocation *)anInvocation; 和- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector; 結(jié)合來實現(xiàn)消息轉(zhuǎn)發(fā)。如果一個對象收到一條無法處理的消息,運行時系統(tǒng)會在拋出錯誤前,給該對象發(fā)送一條forwardInvocation:消息,該消息的唯一參數(shù)是個NSInvocation類型的對象,該對象封裝了原始的消息和消息的參數(shù)。
1 - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector 2 { 3 NSMethodSignature *methodSign = [super methodSignatureForSelector:aSelector]; 4 if (!methodSign) 5 { 6 // 手動設(shè)置方法的有效簽名 7 methodSign = [Car instanceMethodSignatureForSelector:aSelector]; 8 } 9 10 return methodSign; 11 } 12 13 - (void)forwardInvocation:(NSInvocation *)anInvocation 14 { 15 SEL selector = [anInvocation selector]; 16 NSString *selString = NSStringFromSelector(selector); 17 if ([selString isEqualToString:@"walk"]) 18 { 19 if ([Car instancesRespondToSelector:selector]) 20 { 21 // 消息調(diào)用 22 [anInvocation invokeWithTarget:[[Car alloc] init]]; 23 } 24 } 25 } View Code三、Runtime技術(shù)點之交換方法實現(xiàn)
交換方法實現(xiàn)的需求場景還是比較多的,假設(shè)我們寫了一個功能性的方法,該方法在整個項目中被多次調(diào)用,當(dāng)需求更改時,要求使用另一種功能代替現(xiàn)有的這個功能,這個時候我們通常有以下幾種做法:
(1)將這個方法的現(xiàn)有實現(xiàn)刪掉,重新實現(xiàn)新的功能;
(2)重新實現(xiàn)一個方法,將項目中所有調(diào)用現(xiàn)有方法的地方,都改成調(diào)用新的方法;
......
這兩種做法無疑都存在一定的缺陷,第(1)中方案,假設(shè)需求又要再改成之前的功能呢?這種現(xiàn)象是很常見的。第(2)種方案,耗時耗力,實施起來太麻煩。
那利用Runtime該怎么操作呢?我們確實還是需要重新實現(xiàn)一個方法的,因為是一個新的功能需求嘛,但是原來的方法我們不去動它,只需在Runtime時將它們的實現(xiàn)交換一下即可,聽起來是不是很簡單呢?那就直接上代碼吧。
1 @implementation Person 2 3 - (void)walk 4 { 5 NSLog(@"Person walk."); 6 } 7 8 - (void)eat 9 { 10 NSLog(@"Person eat."); 11 } 12 13 + (void)load 14 { 15 Method methodOne = class_getInstanceMethod(self, @selector(walk)); 16 Method methodTwo = class_getInstanceMethod(self, @selector(eat)); 17 18 method_exchangeImplementations(methodOne, methodTwo); 19 } 20 21 @end View Code交換兩個方法的實現(xiàn)一般寫在類的load方法里面,因為load方法會在程序運行前加載一次,而initialize方法會在類或者子類在第一次使用的時候調(diào)用,當(dāng)有分類的時候會調(diào)用多次。通過method_exchangeImplementations我們將walk和eat方法的實現(xiàn)進行了交換,這樣在外邊調(diào)用[person walk]; 時,實際上執(zhí)行的是eat中的實現(xiàn)。
有兩點是需要注意一下的:
(1)如果兩個方法都是有參數(shù)的,那么參數(shù)的類型必須是匹配的,也即參數(shù)的類型必須一致;但是如果一個有參數(shù),一個沒有參數(shù),經(jīng)過測試,也是可以執(zhí)行成功的。
(2)如果方法一調(diào)用了方法二,就像這樣:
1 - (void)walk 2 { 3 NSLog(@"Person walk."); 4 } 5 6 - (void)eat 7 { 8 NSLog(@"Person eat."); 9 10 [self walk]; 11 } View Code那么在執(zhí)行交換方法實現(xiàn)之后,需要將調(diào)用方法二的地方改成調(diào)用方法一,就像這樣:
1 - (void)walk 2 { 3 NSLog(@"Person walk."); 4 } 5 6 - (void)eat 7 { 8 NSLog(@"Person eat."); 9 10 [self eat]; 11 } View Code否則會造成死循環(huán)。其實很好理解,交換之后,walk方法的實現(xiàn)實際已經(jīng)變成了eat的實現(xiàn),再去調(diào)用walk時相當(dāng)于調(diào)用的eat,所以會一直調(diào)用下去。
如果明白了下面這個原理,上面的這個技術(shù)點很好理解:
任何一個方法都有兩個重要的屬性:SEL是方法的編號,IMP是方法的實現(xiàn),方法的調(diào)用過程實際上去根據(jù)SEL去尋找IMP。
?
ps:好了,關(guān)于iOS的Runtime學(xué)習(xí),就先整理到這吧,有一些東西只是停留在原理上,還沒有實際應(yīng)用到具體場景,所以還是有些地方是不太透徹的,歡迎大家評論交流,共同進步。
代碼地址仍然是上一篇中的地址:GitHub,依然是每一個知識點對應(yīng)一個版本,需要的小伙伴可以下載查看。
轉(zhuǎn)載于:https://www.cnblogs.com/diesel/p/5359250.html
總結(jié)
以上是生活随笔為你收集整理的iOS学习之Runtime(二)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。