methodswizzling面试题
method-swizzling
的含义是方法交换
,其主要作用是在运行时将一个方法的实现替换成另一个方法的实现
。iOS中每个类都维护着一个方法列表
即methodList
,methodList中有不同的方法即Method
,每个方法中包含了方法的sel和IMP
,方法交换就是将sel和imp原本的对应断开
,并将sel和新的IMP
生成对应关系。
交换前后的sel和IMP
的对应关系如下图

method-swizzling
使用过程中的一些坑点
坑点一 method-swizzling
使用过程中的一次性问题
一次性是指:mehod-swizzling
写在load方法中,而load方法会主动调用多次,这样会导致方法的重复交换
,使方法sel的指向又恢复成原来的imp
解决方案
可以通过单例设计原则,使方法交换只执行一次,在OC中可以通过dispatch_once
实现单例
+ (void)load{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[LGRuntimeTool lg_betterMethodSwizzlingWithClass:self oriSEL:@selector(personInstanceMethod) swizzledSEL:@selector(lg_studentInstanceMethod)];
});
}
坑点二 子类没有实现,父类实现了
调试一
创建LGPerson
、LGStudent
类,其中LGPerson
中实现了personInstanceMethod
,而LGStudent
继承自LGPerson
,没有实现personInstanceMethod
,运行代码会出现什么问题?
//*********LGPerson类*********
@interface LGPerson : NSObject
- (void)personInstanceMethod;
@end
@implementation LGPerson
- (void)personInstanceMethod{
NSLog(@"person对象方法:%s",__func__);
}
@end
//*********LGStudent类*********
@interface LGStudent : LGPerson
@end
@implementation LGStudent
+ (void)load{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[LGRuntimeTool lg_methodSwizzlingWithClass:self oriSEL:@selector(personInstanceMethod) swizzledSEL:@selector(lg_studentInstanceMethod)];
});
}
- (void)lg_studentInstanceMethod{
[self lg_studentInstanceMethod]; //lg_studentInstanceMethod -/-> personInstanceMethod
NSLog(@"LGStudent分类添加的lg对象方法:%s",__func__);
}
@end
//封装好的method-swizzling方法
@implementation LGRuntimeTool
+ (void)lg_methodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL{
if (!cls) NSLog(@"传入的交换类不能为空");
Method oriMethod = class_getInstanceMethod(cls, oriSEL);
Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);
method_exchangeImplementations(oriMethod, swiMethod);
}
//*********调用*********
- (void)viewDidLoad {
[super viewDidLoad];
LGStudent *s = [[LGStudent alloc] init];
[s personInstanceMethod];
LGPerson *p = [[LGPerson alloc] init];
[p personInstanceMethod];
}

-
[s personInstanceMethod];
中不报错是因为student中的imp
交换成了lg_studentInstanceMethod
,而LGStudent
中有这个方法,所以不会报错 - 崩溃的点在于
[p personInstanceMethod];
,其本质原因:LGStudent
中进行了方法交换,将person中imp
交换成了LGStudent中的lg_studentInstanceMethod
,然后需要去LGPerson
中的找lg_studentInstanceMethod
,但是LGPerson
中没有lg_studentInstanceMethod
方法,即相关的imp找不到就崩溃了
优化:通过class_addMethod
尝试添加你要交换的方法,避免imp找不到
- 如果添加成功即
类中没有这个方法
,则通过class_replaceMethod
进行替换,其内部会调用class_addMethod
进行添加 - 如果添加不成功即
类中有这个方法
,则通过method_exchangeImplementations
进行交换
+ (void)lg_betterMethodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL{
if (!cls) NSLog(@"传入的交换类不能为空");
Method oriMethod = class_getInstanceMethod(cls, oriSEL);
Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);
// 一般交换方法: 交换自己有的方法 -- 走下面 因为自己有意味添加方法失败
// 交换自己没有实现的方法:
// 首先第一步:会先尝试给自己添加要交换的方法 :personInstanceMethod (SEL) -> swiMethod(IMP)
// 然后再将父类的IMP给swizzle personInstanceMethod(imp) -> swizzledSEL
BOOL success = class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(oriMethod));
if (success) {// 自己没有 - 交换 - 没有父类进行处理 (重写一个)
class_replaceMethod(cls, swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
}else{ // 自己有
method_exchangeImplementations(oriMethod, swiMethod);
}
}
调试二
删除LGPerson
中的personInstanceMethod
方法实现
//*********LGPerson类*********
@interface LGPerson : NSObject
- (void)personInstanceMethod;
@end
@implementation LGPerson
@end
//*********LGStudent类*********
@implementation LGStudent
+ (void)load{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// 使用上面优化过的方法
[LGRuntimeTool lg_betterMethodSwizzlingWithClass:self oriSEL:@selector(personInstanceMethod) swizzledSEL:@selector(lg_studentInstanceMethod)];
});
}

原因是[self lg_studentInstanceMethod];
出现了死递归
,其中交换方法method_exchangeImplementations(oriMethod, swiMethod);
中的oriMethod
为nil,交换失败
再次优化:
+ (void)lg_bestMethodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL{
if (!cls) NSLog(@"传入的交换类不能为空");
Method oriMethod = class_getInstanceMethod(cls, oriSEL);
Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);
if (!oriMethod) { // 避免动作没有意义
// 在oriMethod为nil时,替换后将swizzledSEL复制一个不做任何事的空实现,代码如下:
class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
method_setImplementation(swiMethod, imp_implementationWithBlock(^(id self, SEL _cmd){
NSLog(@"来了一个空的 imp");
}));
}
BOOL didAddMethod = class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
if (didAddMethod) {
class_replaceMethod(cls, swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
}else{
method_exchangeImplementations(oriMethod, swiMethod);
}
}
KVC简介
KVC全称Key-Value Coding
,也称键值编码
。是由NSKeyValueCoding
非正式协议启用的一种机制,对象采用该协议来间接访问其属性。既可以通过字符串key
访问某个属性,也可以通过字符串路径
的形式访问。
相关API
- 常用API
//直接通过Key来取值
- (nullable id)valueForKey:(NSString *)key;
//通过Key来设值
- (void)setValue:(nullable id)value forKey:(NSString *)key;
//通过KeyPath来取值
- (nullable id)valueForKeyPath:(NSString *)keyPath;
//通过KeyPath来设值
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;
- 其他方法
//默认返回YES,表示如果没有找到Set<Key>方法的话,会按照_key,_iskey,key,iskey的顺序搜索成员,设置成NO就不这样搜索
+ (BOOL)accessInstanceVariablesDirectly;
//KVC提供属性值正确性验证的API,它可以用来检查set的值是否正确、为不正确的值做一个替换值或者拒绝设置新值并返回错误原因。
- (BOOL)validateValue:(inout id __nullable * __nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;
//这是集合操作的API,里面还有一系列这样的API,如果属性是一个NSMutableArray,那么可以用这个方法来返回。
- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;
//如果Key不存在,且KVC无法搜索到任何和Key有关的字段或者属性,则会调用这个方法,默认是抛出异常。
- (nullable id)valueForUndefinedKey:(NSString *)key;
//和上一个方法一样,但这个方法是设值。
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;
//如果你在SetValue方法时面给Value传nil,则会调用这个方法
- (void)setNilValueForKey:(NSString *)key;
//输入一组key,返回该组key对应的Value,再转成字典返回,用于将Model转到字典。
- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
苹果官方文档解释KVC


KVC设值和取值过程
设值过程
我们通过Key-Value Coding Programming Guide苹果官方文档来探索setValue:forKey
的原理
//*********LGPerson类*********
//MARK: - setKey. 的流程分析
- (void)setName:(NSString *)name{
NSLog(@"%s - %@",__func__,name);
}
- (void)_setName:(NSString *)name{
NSLog(@"%s - %@",__func__,name);
}
//*********调用*********
- (void)viewDidLoad {
[super viewDidLoad];
LGPerson *person = [[LGPerson alloc] init];
// 1: KVC - 设置值的过程 setValue 分析调用过程
[person setValue:@"LG_Cooci" forKey:@"name"];
}
// 控制台只调用了setName方法,并不会调用_setName方法
2021-09-02 23:17:20.423229+0800 002-KVC取值&赋值过程[3549:12869048] -[LGPerson setName:] - LG_Cooci
- 首先查找是否有这三种setter方法,按照查找顺序为
set<Key>:-> _set<Key> -> setIs<Key>
但凡找到其中任意一个setter方法,便直接设置属性的value
如果没有,进入下一步
//*********LGPerson.h*********
@interface LGPerson : NSObject{
@public
NSString *_isName;
NSString *name;
NSString *isName;
NSString *_name;
}
//*********LGPerson.m*********
#pragma mark - 关闭或开启实例变量赋值
+ (BOOL)accessInstanceVariablesDirectly{
return YES;
}
//*********调用*********
- (void)viewDidLoad {
[super viewDidLoad];
LGPerson *person = [[LGPerson alloc] init];
// 1: KVC - 设置值的过程 setValue 分析调用过程
[person setValue:@"LG_Cooci" forKey:@"name"];
NSLog(@"取值:%@",[person valueForKey:@"name"]);
}
2021-09-02 23:21:49.262497+0800 002-KVC取值&赋值过程[3575:12872987] 取值:LG_Cooci
//************************************************************
//*********LGPerson.h*********
@interface LGPerson : NSObject{
@public
NSString *_isName;
// NSString *name;
NSString *isName;
NSString *_name;
}
//*********调用*********
- (void)viewDidLoad {
[super viewDidLoad];
LGPerson *person = [[LGPerson alloc] init];
// 1: KVC - 设置值的过程 setValue 分析调用过程
[person setValue:@"LG_Cooci" forKey:@"name"];
NSLog(@"取值:%@",[person valueForKey:@"_name"]);
}
// _name同样能取到值
2021-09-02 23:26:41.563610+0800 002-KVC取值&赋值过程[3627:12878756] 取值:LG_Cooci
- 接下来查找
accessInstanceVariablesDirectly
是否返回YES,返回NO直接进入下一步
如果返回YES,则间接访问实例变量进行赋值,顺序为_<key> -> _is<Key> -> <key> -> is<Key>
如果都没有进入下一步 - 系统会执行
setValue:forUndefinedKey:
方法,默认抛出NSUndefinedKeyException
类型的异常

取值过程
//*********LGPerson.m*********
//MARK: - valueForKey 流程分析 - get<Key>, <key>, is<Key>, or _<key>,
- (NSString *)getName{
return NSStringFromSelector(_cmd);
}
- (NSString *)name{
return NSStringFromSelector(_cmd);
}
- (NSString *)isName{
return NSStringFromSelector(_cmd);
}
- (NSString *)_name{
return NSStringFromSelector(_cmd);
}
//*********调用*********
- (void)viewDidLoad {
[super viewDidLoad];
LGPerson *person = [[LGPerson alloc] init];
// 1: KVC - 设置值的过程 setValue 分析调用过程
[person setValue:@"LG_Cooci" forKey:@"name"];
// 2: KVC - 取值的过程
// _name
person->_name = @"_name";
person->_isName = @"_isName";
person->name = @"name";
person->isName = @"isName";
NSLog(@"取值:%@",[person valueForKey:@"name"]);
}
// 调用的是getName方法
2021-09-02 23:42:50.114460+0800 002-KVC取值&赋值过程[3717:12891729] 取值:getName
我们通过Key-Value Coding Programming Guide苹果官方文档来探索setValue:forKey
的原理
同样的通过官方文档探索valueForKey
的原理
- 1.首先查找getter方法,按照
get<Key> -> <key> -> is<Key> -> _<key>
的方法顺序查找
如果找到,则进入第五步
如果没有找到,则进入第二步
- 2.如果第一步中的
getter方法
没有找到,KVC会查找countOf <Key>
和objectIn <Key> AtIndex :
和<key> AtIndexes :
如果找到countOf <Key>
和其他两个中的一个,则会创建一个响应所有NSArray方法的集合代理对象,并返回该对象,即NSKeyValueArray
,是NSArray的子类。代理对象随后将接收到的所有NSArray消息转换为countOf<Key>
,objectIn<Key> AtIndex:
和<key>AtIndexes:
消息的某种组合,用来创建键值编码对象。如果原始对象还实现了一个名为get<Key>:range:
之类的可选方法,则代理对象也将在适当时使用该方法(注意:方法名的命名规则要符合KVC的标准命名方法,包括方法签名。)
如果没有找到这三个访问数组的,请继续进入第三步
- 3.如果没有找到上面的几种方法,则会同时查找
countOf <Key>
,enumeratorOf<Key>
和memberOf<Key>
这三个方法
如果这三个方法都找到,则会创建一个响应所有NSSet方法
的集合代理对象,并返回该对象,此代理对象随后将其收到的所有NSSet消息转换为countOf<Key>
,enumeratorOf<Key>
和memberOf<Key>:
消息的某种组合,用于创建它的对象
如果还是没有找到,则进入第四步
- 4.如果还没有找到,检查类方法
InstanceVariablesDirectly是否YES
,依次搜索_<key>,_is<Key>
,<key>
或is<Key>
的实例变量
如果搜到,直接获取实例变量的值,进入第五步
- 5.根据搜索到的属性值的类型,返回不同的结果
如果是对象指针
,则直接返回结果
如果是NSNumber
支持的标量类型,则将其存储在NSNumber
实例中并返回它
如果是NSNumber
不支持的标量类型,请转换为NSValue
对象并返回该对象 - 6.如果上面5步的方法均失败,系统会执行该对象的
valueForUndefinedKey:
方法,默认抛出NSUndefinedKeyException
类型的异常
KVC通过valueForKey:
方法取值的流程,以设置LGPerson对象person
的属性name为例,如下图所示

KVC自定义实现
通过给NSObject
添加分类LGKVC
,实现自定义的lg_setValue: forKey:
和lg_valueForKey:
方法,根据苹果官方文档提供的查找规则进行实现
@interface NSObject (LGKVC)
//设值
- (void)lg_setValue:(nullable id)value forKey:(NSString *)key;
//取值
- (nullable id)lg_valueForKey:(NSString *)key;
@end
自定义KVC设值流程如下:
- 1、判断
key
非空 - 2、查找
setter
方法,顺序是:set<key>
、_set<key>
、setIs<key>
- 3、判断是否响应
accessInstanceVariablesDirectly
方法,即间接访问实例变量,
返回YES,继续下一步设值,
如果是NO,则崩溃 - 4、间接访问变量赋值(只会走一次),顺序是:
_key、_isKey、key、isKey
4.1 定义一个收集实例变量的可变数组
4.2 通过class_getInstanceVariable
方法,获取相应的ivar
4.3 通过object_setIvar
方法,对相应的ivar
设置值 - 5、如果找不到相关实例变量,则
抛出异常
- (void)lg_setValue:(nullable id)value forKey:(NSString *)key{
// KVC 自定义
// 1: 判断什么 key
if (key == nil || key.length == 0) {
return;
}
// 2: setter set<Key>: or _set<Key>,
// key 要大写
NSString *Key = key.capitalizedString;
// 拼接方法
NSString *setKey = [NSString stringWithFormat:@"set%@:",Key];
NSString *_setKey = [NSString stringWithFormat:@"_set%@:",Key];
NSString *setIsKey = [NSString stringWithFormat:@"setIs%@:",Key];
if ([self lg_performSelectorWithMethodName:setKey value:value]) {
NSLog(@"*********%@**********",setKey);
return;
}else if ([self lg_performSelectorWithMethodName:_setKey value:value]) {
NSLog(@"*********%@**********",_setKey);
return;
}else if ([self lg_performSelectorWithMethodName:setIsKey value:value]) {
NSLog(@"*********%@**********",setIsKey);
return;
}
// 3: 判断是否响应 accessInstanceVariablesDirectly 返回YES NO 奔溃
// 3:判断是否能够直接赋值实例变量
if (![self.class accessInstanceVariablesDirectly] ) {
@throw [NSException exceptionWithName:@"LGUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****",self] userInfo:nil];
}
// 4: 间接变量
// 获取 ivar -> 遍历 containsObjct -
// 4.1 定义一个收集实例变量的可变数组
NSMutableArray *mArray = [self getIvarListName];
// _<key> _is<Key> <key> is<Key>
NSString *_key = [NSString stringWithFormat:@"_%@",key];
NSString *_isKey = [NSString stringWithFormat:@"_is%@",Key];
NSString *isKey = [NSString stringWithFormat:@"is%@",Key];
if ([mArray containsObject:_key]) {
// 4.2 获取相应的 ivar
Ivar ivar = class_getInstanceVariable([self class], _key.UTF8String);
// 4.3 对相应的 ivar 设置值
object_setIvar(self , ivar, value);
return;
}else if ([mArray containsObject:_isKey]) {
Ivar ivar = class_getInstanceVariable([self class], _isKey.UTF8String);
object_setIvar(self , ivar, value);
return;
}else if ([mArray containsObject:key]) {
Ivar ivar = class_getInstanceVariable([self class], key.UTF8String);
object_setIvar(self , ivar, value);
return;
}else if ([mArray containsObject:isKey]) {
Ivar ivar = class_getInstanceVariable([self class], isKey.UTF8String);
object_setIvar(self , ivar, value);
return;
}
// 5:如果找不到相关实例
@throw [NSException exceptionWithName:@"LGUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ %@]: this class is not key value coding-compliant for the key name.****",self,NSStringFromSelector(_cmd)] userInfo:nil];
}
自定义KVC取值流程如下:
- 1、判断
key
非空 - 2、查找相应方法,顺序是:
get<Key>
、<key>
、countOf<Key>
、objectIn<Key>AtIndex
- 3、判断是否能够直接赋值实例变量,即判断是否响应
accessInstanceVariablesDirectly
方法
返回YES,继续下一步取值
如果是NO,则崩溃 - 4、间接访问实例变量,顺序是:
_<key> _is<Key> <key> is<Key>
4.1 定义一个收集实例变量的可变数组
4.2 通过class_getInstanceVariable
方法,获取相应的ivar
4.3 通过object_getIvar
方法,返回相应的ivar
的值
- (nullable id)lg_valueForKey:(NSString *)key{
// 1:刷选key 判断非空
if (key == nil || key.length == 0) {
return nil;
}
// 2:找到相关方法 get<Key> <key> countOf<Key> objectIn<Key>AtIndex
// key 要大写
NSString *Key = key.capitalizedString;
// 拼接方法
NSString *getKey = [NSString stringWithFormat:@"get%@",Key];
NSString *countOfKey = [NSString stringWithFormat:@"countOf%@",Key];
NSString *objectInKeyAtIndex = [NSString stringWithFormat:@"objectIn%@AtIndex:",Key];
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
if ([self respondsToSelector:NSSelectorFromString(getKey)]) {
return [self performSelector:NSSelectorFromString(getKey)];
}else if ([self respondsToSelector:NSSelectorFromString(key)]){
return [self performSelector:NSSelectorFromString(key)];
}else if ([self respondsToSelector:NSSelectorFromString(countOfKey)]){
if ([self respondsToSelector:NSSelectorFromString(objectInKeyAtIndex)]) {
int num = (int)[self performSelector:NSSelectorFromString(countOfKey)];
NSMutableArray *mArray = [NSMutableArray arrayWithCapacity:1];
for (int i = 0; i<num-1; i++) {
num = (int)[self performSelector:NSSelectorFromString(countOfKey)];
}
for (int j = 0; j<num; j++) {
id objc = [self performSelector:NSSelectorFromString(objectInKeyAtIndex) withObject:@(num)];
[mArray addObject:objc];
}
return mArray;
}
}
#pragma clang diagnostic pop
// 3:判断是否能够直接赋值实例变量
if (![self.class accessInstanceVariablesDirectly] ) {
@throw [NSException exceptionWithName:@"LGUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****",self] userInfo:nil];
}
// 4.找相关实例变量进行赋值
// 4.1 定义一个收集实例变量的可变数组
NSMutableArray *mArray = [self getIvarListName];
// _<key> _is<Key> <key> is<Key>
// _name -> _isName -> name -> isName
NSString *_key = [NSString stringWithFormat:@"_%@",key];
NSString *_isKey = [NSString stringWithFormat:@"_is%@",Key];
NSString *isKey = [NSString stringWithFormat:@"is%@",Key];
if ([mArray containsObject:_key]) {
Ivar ivar = class_getInstanceVariable([self class], _key.UTF8String);
return object_getIvar(self, ivar);;
}else if ([mArray containsObject:_isKey]) {
Ivar ivar = class_getInstanceVariable([self class], _isKey.UTF8String);
return object_getIvar(self, ivar);;
}else if ([mArray containsObject:key]) {
Ivar ivar = class_getInstanceVariable([self class], key.UTF8String);
return object_getIvar(self, ivar);;
}else if ([mArray containsObject:isKey]) {
Ivar ivar = class_getInstanceVariable([self class], isKey.UTF8String);
return object_getIvar(self, ivar);;
}
return @"";
}
网友评论