OC之Method_Swizling一些坑點、KVC原理分析
Method_Swizling
Method_Swizling我們并不陌生,通過交換兩個方法SEL的IMP指向,達到方法交換的目的。一般來說,我們通常寫在cateogry里,在+load()方法里實現(xiàn)方法的交換。
常規(guī)的方法交換
// SSJPerson.h
@interface SSJPerson : NSObject
- (void)person_walk;
@end
// SSJPerson.m
import "SSJPerson.h"
@implementation SSJPerson
- (void)person_walk{
NSLog(@"SSJPerson ---> person_walk");
}
// SSJStudent.h
// SSJStudent 繼承自 SSJPerson
@interface SSJStudent : SSJPerson
- (void)student_sleep;
@end
// SSJStudent.m
import "SSJStudent.h"
@implementation SSJStudent
- (void)student_sleep{
NSLog(@"SSJStudent ---> student_sleep");
}
@end
//SSJStudent+category.m
import "SSJStudent+category.h"
import <objc/runtime.h>
@implementation SSJStudent (category)
- (void)load{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Method originalMethod = class_getInstanceMethod([self class], @selector(student_sleep));
Method swizzlingMethod = class_getInstanceMethod([self class], @selector(student_sleepNew));
method_exchangeImplementations(originalMethod, swizzlingMethod);
});
}
- (void)student_sleepNew{
NSLog(@"替換過的方法 -- > student_sleepNew");
[self student_sleepNew];
}
@end
復制代碼
調用的時候:
SSJStudent *stu = [SSJStudent new];
[stu student_sleep];
復制代碼
運行也沒問題:
image.png
父類實現(xiàn),子類沒有實現(xiàn)
那么替換一個父類的已經(jīng)實現(xiàn)了,但當前cateogry類沒實現(xiàn)的方法呢?
對代碼進行修改:
// SSJStudent+category.m
import "SSJStudent+category.h"
import <objc/runtime.h>
@implementation SSJStudent (category)
- (void)load{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
/// 替換父類方法:person_walk
/// 父類實現(xiàn)了person_walk,子類并沒實現(xiàn)person_walk
Method originalMethod = class_getInstanceMethod([self class], @selector(person_walk));
Method swizzlingMethod = class_getInstanceMethod([self class], @selector(person_walkNew));
method_exchangeImplementations(originalMethod, swizzlingMethod);
});
}
//- (void)student_sleepNew{
// NSLog(@"替換過的方法 -- > student_sleepNew");
// [self student_sleepNew];
//}
- (void)person_walkNew{
NSLog(@"替換過的方法 -- > person_walkNew");
[self person_walkNew];
}
@end
復制代碼
// ViewController.m
@implementation ViewController
-
(void)viewDidLoad {
[super viewDidLoad];SSJStudent *student = [SSJStudent new];
/// 這里換成person_walk
[student person_walk];
}
@end
復制代碼
運行:
image.png
看打印結果,子類調用person_walk都沒問題。
這里對ViewController.m添加兩行代碼:
// ViewController.m
@implementation ViewController
-
(void)viewDidLoad {
[super viewDidLoad];SSJStudent *student = [SSJStudent new];
/// 這里換成person_walk
[student person_walk];
NSLog(@"\n");
/// 添加代碼,父類也調用person_walk
SSJPerson *person = [SSJPerson new];
[person person_walk];
}
@end
復制代碼
再次運行,就發(fā)現(xiàn)提示找不到方法:
image.png
這邊我畫了一張圖:
image.png
由于SSJStudent+category內(nèi)部實現(xiàn)了+load()方法,導致程序在load_images階段,就調用了+load()方法。
而+load()方法里對父類(SSJPerson)的person_walk方法進行了替換,導致父類在調用自己方法person_walk的時候,提示找不到具體的person_walkNew實現(xiàn),因為父類根本就沒這個方法。
在實際多人開發(fā)過程中,提供父類的那個人他不一定知道你交換了父類的方法,當他調用自己父類的方法時,可能就一下子對這個報錯感到莫名其妙:我明明沒有調用這個方法啊,為什么提示這個錯誤?
如何避免這種子類替換了父類的方法,子類自己卻沒有實現(xiàn)父類方法的情況呢?
我們對SSJStudent+category.m的+load()方法進行修改:
// SSJStudent+category.m
import "SSJStudent+category.h"
import <objc/runtime.h>
@implementation SSJStudent (category)
- (void)load{
/// 替換父類方法:person_walk
/// 父類實現(xiàn)了person_walk,子類并沒實現(xiàn)person_walk
Method originalMethod = class_getInstanceMethod([self class], @selector(person_walk));
Method swizzlingMethod = class_getInstanceMethod([self class], @selector(person_walkNew));
//添加一個Method(SEL - person_walk,IMP - person_walkNew)
BOOL isAdded = class_addMethod([self class], method_getName(originalMethod), method_getImplementation(swizzlingMethod), method_getTypeEncoding(originalMethod));
if(isAdded){
/// 添加成功 ,就說明子類沒有實現(xiàn)父類person_walk對應的IMP方法.
/// 經(jīng)過class_addMethod這一步,子類已經(jīng)有了一個person_walk方法,并且IMP指向person_walkNew
/// 接下來,直接添加一個Method(SEL - person_walkNew,IMP - person_walk)
/// 然后子類就實現(xiàn)了有了兩個IMP互相交換的Method,最終效果跟method_exchangeImplementations一樣
class_replaceMethod([self class], method_getName(swizzlingMethod), method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
}else{
///添加不成功,說明子類本身就已經(jīng)實現(xiàn)了person_walk的IMP方法,那就直接交換兩個Method的IMP即可
method_exchangeImplementations(originalMethod, swizzlingMethod);
}
}
復制代碼
運行效果:
image.png
簡單來說,就是:
class_addMethod添加Method(SEL - person_walk,IMP - person_walkNew)
成功 -》則調用class_replaceMethod添加Method(SEL - person_walkNew,IMP - person_walk)。
失敗 -》則調用method_exchangeImplementations交換兩個Method的IMP指向。
為了便于理解,我畫了張圖
image.png
說明:
class_addMethod:只能在SEL沒有IMP指向時才可以添加成功;
class_replaceMethod:不管SEL 有沒有IMP實現(xiàn),都可以添加成功;
父類沒有實現(xiàn),子類也沒有實現(xiàn)
把父類實現(xiàn)部分注釋
image.png
然后再運行:
image.png
提示找不到這個person_walk這個方法實現(xiàn),那就說明category那里出了問題。
我們在class_getInstanceMethod那一行打上斷點:
image.png
由于父類和子類都沒有實現(xiàn)person_walk,導致這邊獲取的originalMethod為空。
我們對category的+load()方法進行修改,添加originalMethod空值處理:
-
(void)load{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{/// 替換父類方法:person_walk /// 父類實現(xiàn)了person_walk,子類并沒實現(xiàn)person_walk Method originalMethod = class_getInstanceMethod([self class], @selector(person_walk)); Method swizzlingMethod = class_getInstanceMethod([self class], @selector(person_walkNew)); if (!originalMethod) { /// 沒有,那就添加一個person_walk方法,并且手動添加一個臨時處理的IMP實現(xiàn) class_addMethod([self class], @selector(person_walk), method_getImplementation(swizzlingMethod), method_getTypeEncoding(swizzlingMethod)); /// originalMethod需要重新獲取一邊,不然依舊是空的 originalMethod = class_getInstanceMethod([self class], @selector(person_walk)); method_setImplementation(originalMethod, imp_implementationWithBlock(^(id self,SEL _cmd){ NSLog(@"臨時方法"); })); } //添加一個Method(SEL - person_walk,IMP - person_walkNew) BOOL isAdded = class_addMethod([self class], method_getName(originalMethod), method_getImplementation(swizzlingMethod), method_getTypeEncoding(originalMethod)); if(isAdded){ /// 添加成功 ,就說明子類沒有實現(xiàn)父類person_walk對應的IMP方法. /// 經(jīng)過class_addMethod這一步,子類已經(jīng)有了一個person_walk方法,并且IMP指向person_walkNew /// 接下來,直接添加一個Method(SEL - person_walkNew,IMP - person_walk) /// 然后子類就實現(xiàn)了有了兩個IMP互相交換的Method,最終效果跟method_exchangeImplementations一樣 class_replaceMethod([self class], method_getName(swizzlingMethod), method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod)); }else{ ///添加不成功,說明子類本身就已經(jīng)實現(xiàn)了person_walk的IMP方法,那就直接交換兩個Method的IMP即可 method_exchangeImplementations(originalMethod, swizzlingMethod); }});
}
復制代碼
運行:
image.png
KVC分析
在講解KVC之前,請先允許我演示一段騷操作,對SSJPerson類的未開放屬性進行讀寫操作:
// SSJPerson.h
import <Foundation/Foundation.h>
@interface SSJPerson : NSObject
@end
// SSJPerson.m
import "SSJPerson.h"
@interface SSJPerson ()
/// 昵稱
@property (nonatomic , strong) NSString *nickName_private;
@end
@implementation SSJPerson
@end
復制代碼
image.png
如圖所示,對于一個未開放出來的屬性,我們無法通過對象.屬性名這種常規(guī)的方式進行訪問。但是我們可以利用setValue:forKey:這方式進行賦值。
這種通過setValue:forKey:方式進行賦值的操作,我們稱之為KVC。
KVC是一種設計模式,那么它的原理又是什么呢?為什么可以對未開放屬性進行直接操作呢?
存在即是真理。帶著探索的思維,我們決定去看一下setValue:forKey:的底層實現(xiàn)。
進入蘋果官方文檔:KVC部分
image.png
大概意思是:
NSObject提供的NSKeyValueCoding協(xié)議,默認實現(xiàn)使用一組明確定義的規(guī)則,將基于密鑰的訪問器調用映射到對象的底層屬性。這些協(xié)議方法使用一個關鍵參數(shù)來搜索它們自己的對象實例,以查找訪問器、實例變量和遵循某些命名約定的相關方法。
Setter
接下來看一下Setter搜索模式: image.png
按照圖上所說,Setter搜索模式分為3步:
找set<Key>: 或 _set<Key>,找到了就調用它;
如果沒找到,就去依次查找_<key>, is<Key>, <key>, 或is<Key>,找到了就用輸入值設置變量(比如找到了查找<key>,那么后面的_is<Key>等就不需要找了)。
如果還是沒找到,就會調用setValue:forUndefinedKey:并引發(fā)異常。
我們來根據(jù)這3個步驟,實操一下:
// SSJPerson.h
@interface SSJPerson : NSObject{
@public
NSString *boddy;
NSString *_boddy;
NSString *isBoddy;
NSString *_isBoddy;
}
@end
// SSJPerson.m
//(根據(jù)第2步,找個要設置為YES)
@implementation SSJPerson
- (BOOL)accessInstanceVariablesDirectly{
return true;
}
@end
// ViewController.m
- (void)viewDidLoad {
[super viewDidLoad];
/// 設置值
[personA setValue:@"足球" forKey:@"boddy"];
/// 打印內(nèi)容,我們要看一下具體賦值給哪個值
NSLog(@"_<key>---%@",personA->_boddy);
NSLog(@"_is<Key>---%@",personA ->_isBoddy);
NSLog(@"<key>---%@",personA ->boddy);
NSLog(@"is<Key>---%@",personA ->isBoddy);
}
復制代碼
運行:
image.png
注釋_boddy:
image.png
注釋_isBoddy:
image.png
這也就驗證了里第2點:當存在多個類似變量,會依次查找_<key>, _is<Key>, <key>, 或is<Key>,找到了就給它賦值,后面的就賦值了。
思考:關于第一點set<Key>:,是不是也有第2點類似的關系呢?
image.png
注釋setBoddy方法:
image.png
注釋_setBoddy方法:
image.png
注釋setIsBoddy方法:
image.png
經(jīng)過4次打印,我們發(fā)現(xiàn),在我們調用setValue:forKey:的時候,會依次查找: set<Key>: > _set<Key> > setIs<Key>。找到后就用輸入值賦值給變量
Getter
接下來看一下Getter:
IMG_0978.JPG
大概意思如下:
1、查找 get<Key>, <key>, is<Key>, or _<key>,找到了就進入步驟5;找不到就進入步驟2;
2、在實例方法中搜索:countOf<Key>和objectIn<Key>AtIndex和<key>AtIndexes:,
countOf<Key>必須實現(xiàn),另外兩個找到了其中一個,就創(chuàng)建集合代理對象,找不到就進入步驟3;
3、改為搜索countOf<Key>、Enumeratorf<Key>和memberOf<Key>:,3個方法都存在才行,否則就進入步驟4;
4、當確定AccessInstanceVariables方法返回YES(默認也是YES),
順序搜索名為 _<key>, _is<Key>, <key>, 或is<Key>的實例變量。
如果找到,直接獲取實例變量的值并繼續(xù)執(zhí)行步驟5。否則,繼續(xù)執(zhí)行步驟6。
5、如果檢索到的屬性值是對象指針,只需返回結果。
如果該值是NSNumber支持的標量類型,請將其存儲在NSNumber實例中并返回該值。
如果結果是NSNumber不支持的標量類型,請轉換為NSValue對象并返回該對象。
6、如果都找不到,調用valueForUndefinedKey:并拋出異常。
復制代碼
針對第1點,我們來進行實操:
// SSJPerson.m
(NSString *)getBoddy{
NSLog(@"%s -->Getter",func);
return boddy;
}(NSString *)boddy{
NSLog(@"%s -->Getter",func);
return boddy;
}(NSString *)isBoddy{
NSLog(@"%s -->Getter",func);
return isBoddy;
}(NSString *)_boddy{
NSLog(@"%s -->Getter",func);
return _boddy;
}
// ViewController.m
- (void)viewDidLoad {
[super viewDidLoad];
SSJPerson *personA = [SSJPerson new];
/// 設置值
[personA setValue:@"足球" forKey:@"boddy"];
NSLog(@"打印---%@",[personA valueForKey:@"boddy"]);
}
復制代碼
運行:
image.png
注釋getBoddy:
image.png
注釋boddy:
image.png
注釋isBoddy: image.png
至于為什么只有boddy方法執(zhí)行之后,valueForKey才打印出內(nèi)容,那是因為默認情況下,setter和getter是一一對應的關系,setter模式優(yōu)先執(zhí)行<key>方法,getter模式對應著_boddy方法。
針對第2點,我們來進行實操
在實例方法中搜索:countOf<Key>和objectIn<Key>AtIndex和<key>AtIndexes:,
countOf<Key>必須實現(xiàn),另外兩個找到了其中一個,就創(chuàng)建集合代理對象,找不到就進入步驟
復制代碼
這里不能使用boddyArray,不然會走第一步的Getter方法:
image.png
把key改為myBoddyArray
image.png
注釋掉objectInMyBoddyArrayAtIndex:方法:
image.png
針對第3點,我們來進行實操
改為搜索countOf<Key>、Enumeratorf<Key>和memberOf<Key>:,3個方法都存在才行,否則就進入步驟4;
復制代碼
image.png
針對第4點,我們來進行實操
當確定AccessInstanceVariables方法返回YES(默認也是YES),
順序搜索名為 _<key>, _is<Key>, <key>, 或is<Key>的實例變量。
如果找到,直接獲取實例變量的值并繼續(xù)執(zhí)行步驟5。否則,繼續(xù)執(zhí)行步驟6。
復制代碼
image.png 注釋_boddy:
image.png
注釋_isBoddy:
image.png
注釋boddy:
image.png
針對第6點,我們來進行實操
如果都找不到,調用valueForUndefinedKey:并拋出異常
復制代碼
在不做處理的情況下,用一個不存在的key去訪問,會報錯:
image.png
我們實現(xiàn)一下valueForUndefinedKey:,然后:
image.png
總結一下KVC的Setter和Getter
Setter:
依次查找set<Key>: 或 _set<Key>,找到了就調用方法;找不到就進入步驟2;
先確定AccessInstanceVariables返回YES,然后依次查找_<key>, _is<Key>, <key>, 或is<Key>,找到了就用輸入值設置變量。找不到就進入步驟3;
(比如找到了查找_<key>,那么后面的_is<Key>等就不需要找了)。
調用setValue:forUndefinedKey:并引發(fā)異常。
Getter:
查找 get<Key>, <key>, is<Key>, or _<key>,找到了就進入步驟5;找不到就進入步驟2;
在實例中搜索:countOf<Key>和objectIn<Key>AtIndex和<key>AtIndexes:, 其中countOf<Key>必須實現(xiàn),另外兩個找到了其中一個,就創(chuàng)建集合代理對象,找不到就進入步驟3;
改為搜索countOf<Key>、enumeratorf<Key>和memberOf<Key>:,3個方法都存在才行,否則就進入步驟4;
當確定AccessInstanceVariables方法返回YES(默認也是YES), 順序搜索名為 _<key>, _is<Key>, <key>, 或is<Key>的實例變量。 如果找到,直接獲取實例變量的值并繼續(xù)執(zhí)行步驟5。否則,繼續(xù)執(zhí)行步驟6。
如果檢索到的屬性值是對象指針,只需返回結果。 如果該值是NSNumber支持的標量類型,請將其存儲在NSNumber實例中并返回該值。 如果結果是NSNumber不支持的標量類型,請轉換為NSValue對象并返回該對象。
如果都找不到,調用valueForUndefinedKey:并拋出異常。
至此,我們就完成了KVC的探索。