和谐学习!不急不躁!!我是你们的老朋友小青龙~
Method_Swizling
Method_Swizling我们并不陌生,通过交换两个方法SEL的IMP指向,达到方法交换的目的。一般来说,我们通常写在cateogry里,在+load()方法里实现方法的交换。
常规的方法交换
// 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];
运行也没问题:
父类实现,子类没有实现
那么替换一个父类的已经实现了,但当前cateogry类没实现的方法呢?
对代码进行修改:
// 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
/// 父类实现了person_walk,子类并没实现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
运行:
看打印结果,子类调用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
再次运行,就发现提示找不到方法:
这边我画了一张图:
-
由于SSJStudent+category内部实现了
+load()
方法,导致程序在load_images
阶段,就调用了+load()
方法。 -
而
+load()
方法里对父类(SSJPerson)的person_walk
方法进行了替换
,导致父类在调用自己方法person_walk
的时候,提示找不到具体的person_walkNew
实现,因为父类根本就没这个方法。
在实际多人开发过程中,提供父类的那个人他不一定知道你交换了父类的方法,当他调用自己父类的方法时,可能就一下子对这个报错感到莫名其妙:我明明没有调用这个方法啊,为什么提示这个错误?
如何避免这种子类替换了父类的方法,子类自己却没有实现父类方法的情况呢?
我们对SSJStudent+category.m
的+load()
方法进行修改:
// SSJStudent+category.m
#import "SSJStudent+category.h"
#import <objc/runtime.h>
@implementation SSJStudent (category)
+ (void)load{
/// 替换父类方法:person_walk
/// 父类实现了person_walk,子类并没实现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){
/// 添加成功 ,就说明子类没有实现父类person_walk对应的IMP方法.
/// 经过class_addMethod这一步,子类已经有了一个person_walk方法,并且IMP指向person_walkNew
/// 接下来,直接添加一个Method(SEL - person_walkNew,IMP - person_walk)
/// 然后子类就实现了有了两个IMP互相交换的Method,最终效果跟method_exchangeImplementations一样
class_replaceMethod([self class], method_getName(swizzlingMethod), method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
}else{
///添加不成功,说明子类本身就已经实现了person_walk的IMP方法,那就直接交换两个Method的IMP即可
method_exchangeImplementations(originalMethod, swizzlingMethod);
}
}
运行效果:
简单来说,就是:
-
class_addMethod
添加Method(SEL - person_walk,IMP - person_walkNew) -
成功
-》则调用class_replaceMethod
添加Method(SEL - person_walkNew,IMP - person_walk)。 -
失败
-》则调用method_exchangeImplementations
交换两个Method的IMP指向。
为了便于理解,我画了张图
说明:
class_addMethod
:只能在SEL
没有IMP
指向时才可以添加成功;class_replaceMethod
:不管SEL
有没有IMP实现,都可以添加成功
;
父类没有实现,子类也没有实现
把父类实现部分注释
然后再运行:
提示找不到这个person_walk
这个方法实现,那就说明category那里出了问题。
我们在class_getInstanceMethod那一行打上断点:
由于父类和子类都没有实现person_walk
,导致这边获取的originalMethod
为空。
我们对category的+load()
方法进行修改,添加originalMethod空值处理
:
+ (void)load{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
/// 替换父类方法:person_walk
/// 父类实现了person_walk,子类并没实现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实现
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){
/// 添加成功 ,就说明子类没有实现父类person_walk对应的IMP方法.
/// 经过class_addMethod这一步,子类已经有了一个person_walk方法,并且IMP指向person_walkNew
/// 接下来,直接添加一个Method(SEL - person_walkNew,IMP - person_walk)
/// 然后子类就实现了有了两个IMP互相交换的Method,最终效果跟method_exchangeImplementations一样
class_replaceMethod([self class], method_getName(swizzlingMethod), method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
}else{
///添加不成功,说明子类本身就已经实现了person_walk的IMP方法,那就直接交换两个Method的IMP即可
method_exchangeImplementations(originalMethod, swizzlingMethod);
}
});
}
运行:
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
如图所示,对于一个未开放出来的属性,我们无法通过对象.属性名
这种常规的方式进行访问。但是我们可以利用setValue:forKey:
这方式进行赋值。
- 这种通过
setValue:forKey:
方式进行赋值的操作,我们称之为KVC
。
KVC
是一种设计模式,那么它的原理又是什么呢?为什么可以对未开放属性进行直接操作呢?
存在即是真理。带着探索的思维,我们决定去看一下setValue:forKey:
的底层实现。
进入苹果官方文档:KVC部分
大概意思是:
NSObject
提供的NSKeyValueCoding
协议,默认实现使用一组明确定义的规则,将基于密钥的访问器调用映射到对象的底层属性。这些协议方法使用一个关键参数
来搜索它们自己的对象实例,以查找访问器、实例变量和遵循某些命名约定的相关方法。
Setter
接下来看一下Setter搜索模式:
按照图上所说,Setter搜索模式分为3步:
-
找
set<Key>:
或_set<Key>
,找到了就调用它; -
如果没找到,就去依次查找
_<key>
,_is<Key>
,<key>
, 或is<Key>
,找到了就用输入值设置变量(比如找到了查找_<key>
,那么后面的_is<Key>
等就不需要找了)。 -
如果还是没找到,就会调用
setValue:forUndefinedKey:
并引发异常
。
我们来根据这3个步骤,实操一下:
// SSJPerson.h
@interface SSJPerson : NSObject{
@public
NSString *boddy;
NSString *_boddy;
NSString *isBoddy;
NSString *_isBoddy;
}
@end
// SSJPerson.m
//(根据第2步,找个要设置为YES)
@implementation SSJPerson
+ (BOOL)accessInstanceVariablesDirectly{
return true;
}
@end
// ViewController.m
- (void)viewDidLoad {
[super viewDidLoad];
/// 设置值
[personA setValue:@"足球" forKey:@"boddy"];
/// 打印内容,我们要看一下具体赋值给哪个值
NSLog(@"_<key>---%@",personA->_boddy);
NSLog(@"_is<Key>---%@",personA ->_isBoddy);
NSLog(@"<key>---%@",personA ->boddy);
NSLog(@"is<Key>---%@",personA ->isBoddy);
}
运行:
注释_boddy:
注释_isBoddy:
- 这也就验证了里第2点:当存在多个类似变量,会依次查找
_<key>
,_is<Key>
,<key>
, 或is<Key>
,找到了就给它赋值,后面的就赋值了。
思考:关于第一点set<Key>:
,是不是也有第2点类似的关系呢?
注释setBoddy方法:
注释_setBoddy方法:
注释setIsBoddy方法:
经过4次打印,我们发现,在我们调用setValue:forKey:
的时候,会依次查找:
set<Key>:
> _set<Key>
> setIs<Key>
。找到后就用输入值赋值给变量
Getter
接下来看一下Getter:
大概意思如下:
1、查找 get<Key>, <key>, is<Key>, or _<key>,找到了就进入步骤5;找不到就进入步骤2;
2、在实例方法中搜索:countOf<Key>和objectIn<Key>AtIndex和<key>AtIndexes:,
countOf<Key>必须实现,另外两个找到了其中一个,就创建集合代理对象,找不到就进入步骤3;
3、改为搜索countOf<Key>、Enumeratorf<Key>和memberOf<Key>:,3个方法都存在才行,否则就进入步骤4;
4、当确定AccessInstanceVariables方法返回YES(默认也是YES),
顺序搜索名为 _<key>, _is<Key>, <key>, 或is<Key>的实例变量。
如果找到,直接获取实例变量的值并继续执行步骤5。否则,继续执行步骤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"]);
}
运行:
注释getBoddy:
注释boddy:
注释isBoddy:
至于为什么只有_boddy方法执行之后,valueForKey才打印出内容,那是因为默认情况下,setter和getter是一一对应的关系,setter模式
优先执行_<key>
方法,getter模式
对应着_boddy方法。
针对第2点
,我们来进行实操
在实例方法中搜索:countOf<Key>和objectIn<Key>AtIndex和<key>AtIndexes:,
countOf<Key>必须实现,另外两个找到了其中一个,就创建集合代理对象,找不到就进入步骤
这里不能使用boddyArray,不然会走第一步的Getter方法:
把key改为myBoddyArray
注释掉objectInMyBoddyArrayAtIndex:
方法:
针对第3点
,我们来进行实操
改为搜索countOf<Key>、Enumeratorf<Key>和memberOf<Key>:,3个方法都存在才行,否则就进入步骤4;
针对第4点
,我们来进行实操
当确定AccessInstanceVariables方法返回YES(默认也是YES),
顺序搜索名为 _<key>, _is<Key>, <key>, 或is<Key>的实例变量。
如果找到,直接获取实例变量的值并继续执行步骤5。否则,继续执行步骤6。
注释
_boddy
:
注释_isBoddy
:
注释boddy
:
针对第6点
,我们来进行实操
如果都找不到,调用valueForUndefinedKey:并抛出异常
在不做处理的情况下,用一个不存在的key去访问,会报错:
我们实现一下valueForUndefinedKey:
,然后:
总结一下KVC的Setter和Getter
Setter:
-
依次查找
set<Key>:
或_set<Key>
,找到了就调用方法;找不到就进入步骤2; -
先确定AccessInstanceVariables返回YES,然后依次查找
_<key>
,_is<Key>
,<key>
, 或is<Key>
,找到了就用输入值设置变量。找不到就进入步骤3; (比如找到了查找_<key>
,那么后面的_is<Key>
等就不需要找了)。 -
调用
setValue:forUndefinedKey:
并引发异常
。
Getter:
-
查找
get<Key>
,<key>
,is<Key>
, or_<key>
,找到了就进入步骤5;找不到就进入步骤2; -
在实例中搜索:
countOf<Key>
和objectIn<Key>AtIndex
和<key>AtIndexes
:, 其中countOf<Key>
必须实现,另外两个找到了其中一个,就创建集合代理对象,找不到就进入步骤3; -
改为搜索
countOf<Key>
、enumeratorf<Key>
和memberOf<Key>:
,3个方法都存在才行,否则就进入步骤4; -
当确定
AccessInstanceVariables
方法返回YES
(默认也是YES), 顺序搜索名为_<key>
,_is<Key>
,<key>
, 或is<Key>
的实例变量。 如果找到,直接获取实例变量的值并继续执行步骤5。否则,继续执行步骤6。 -
如果检索到的属性值是对象指针,只需返回结果。 如果该值是NSNumber支持的标量类型,请将其存储在NSNumber实例中并返回该值。 如果结果是NSNumber不支持的标量类型,请转换为NSValue对象并返回该对象。
-
如果都找不到,调用
valueForUndefinedKey:
并抛出异常。
至此,我们就完成了KVC的探索。
代码
链接: pan.baidu.com/s/17oeOIPE2…
密码: 0ira
--来自百度网盘超级会员V2的分享
预告
KVC的实现原理,我们已经知道了。下篇文章,我将尝试自定义一个KVC。敬请期待~
文章已更新: OC之自定义KVC