一、Method Swizzling(方法交换)
Method Swizzling
本质上就是对方法的IMP
和SEL
进行交换,也是我们常说的黑魔法
之一。
1.1 方法交换的原理
-
Method Swizzing
是发生在运行时
的,主要用于在运行时将两个Method进行交换。 - 每个类都维护着一个
方法列表
,即methodList
,methodList
中有不同的方法即Method,每个方法中包含了方法的SEL
和IMP
,方法交换就是将原本的SEL
和IMP
对应断开,并将SEL
和新的IMP
生成对应关系。 - 而且
Method Swizzling
也是iOS中AOP(面相切面编程)
的一种实现方式,我们可以利用苹果这一特性来实现AOP编程。
方法的SEL
和IMP
的交换如下图👇
1.2使用时需注意的坑点
1.2.1method-swizzling 多次调用的混乱问题
我们都知道,mehod-swizzling写在load方法中,而load方法会被系统主动调用多次,这样会导致方法的重复交换,第1次交换,第2次又还原,第3次又交换,这不就乱套了吗?所以,我们得保证方法交换的触发有且仅有1次。
解决方案
当然是单例模式
。👇
+ (void)load{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
//TODO:这里进行你的方法交换
});
}
1.2.2 子类没有实现,父类实现了
父类实现了方法交换,但是其子类却没有,例如👇
//*********LGPerson类*********
@interface LGPerson : NSObject
- (void)personInstanceMethod;
@end
@implementation LGPerson
- (void)personInstanceMethod{
NSLog(@"person对象方法:%s",__func__);
}
@end
//*********LGStudent类*********
@interface LGStudent : LGPerson
@end
@implementation LGStudent
@end
//*********调用*********
- (void)viewDidLoad {
[super viewDidLoad];
// 黑魔法坑点二: 子类没有实现 - 父类实现
LGStudent *s = [[LGStudent alloc] init];
[s personInstanceMethod];
LGPerson *p = [[LGPerson alloc] init];
[p personInstanceMethod];
}
然后,方法交换的代码👇
@implementation LGStudent (LG)
+ (void)load{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[LGRuntimeTool lg_methodSwizzlingWithClass:self oriSEL:@selector(personInstanceMethod) swizzledSEL:@selector(lg_studentInstanceMethod)];
});
}
// personInstanceMethod 我需要父类的这个方法的一些东西
// 给你加一个personInstanceMethod 方法
// imp
- (void)lg_studentInstanceMethod{
////是否会产生递归?--不会产生递归,原因是lg_studentInstanceMethod 会走 oriIMP,即personInstanceMethod的实现中去
[self lg_studentInstanceMethod];
NSLog(@"LGStudent分类添加的lg对象方法:%s",__func__);
}
@end
其中,封装的方法交换代码👇
@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);
}
run运行项目
红框处刻制,在
类LGPerson
找不到方法lg_studentInstanceMethod
的实现。原因在于,
LGStudent的分类
中进行了方法交换,将LGPerson类
中的方法personInstanceMethod
的实现imp交换成了LGStudent类
中的lg_studentIndtanceMethod
,然后[p personInstanceMethod];
实际是去LGPerson类
中找lg_studentInstanceMethod
的imp,但是没有,就引起崩溃。
解决方案
上述崩溃的根本原因是找不到方法的实现imp,假如我们在方法交换时,先判断交换的方法是否有实现imp,未实现则先实现,这样不就解决问题了。那么,优化交换方法
,改为替换方法
👇
+ (void)lg_betterMethodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL{
if (!cls) NSLog(@"传入的交换类不能为空");
// oriSEL personInstanceMethod
// swizzledSEL lg_studentInstanceMethod
Method oriMethod = class_getInstanceMethod(cls, oriSEL);
Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);
// 尝试添加你要交换的方法 - lg_studentInstanceMethod
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);
}
}
在交换的load方法处调用👇
+ (void)load{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[LGRuntimeTool lg_betterMethodSwizzlingWithClass:self oriSEL:@selector(personInstanceMethod) swizzledSEL:@selector(lg_studentInstanceMethod)];
});
}
这样就完美解决了!
1.2.3 子类没有实现,父类也没有实现
将父类的personInstanceMethod
方法注释
//*********LGPerson类*********
@interface LGPerson : NSObject
- (void)personInstanceMethod;
@end
@implementation LGPerson
//- (void)personInstanceMethod{
// NSLog(@"person对象方法:%s",__func__);
//}
@end
run运行项目
很明显
递归死循环
导致栈溢出,那么为什么会发生递归呢?主要是因为
personInstanceMethod
没有实现,然后在方法交换时,始终都找不到oriMethod,然后交换了寂寞,即交换失败,当我们调用personInstanceMethod(oriMethod)
时,也就是oriMethod会进入LG中lg_studentInstanceMethod
方法,然后这个方法中又调用了lg_studentInstanceMethod
,此时的lg_studentInstanceMethod
并没有指向personInstanceMethod(oriMethod)
,然后导致了自己调自己,即递归死循环。
解决方案
避免递归死循环 -->增加判断:如果oriMethod
为空,可通过class_addMethod
给oriSEL
添加swiMethod
方法,然后通过method_setImplementation
将swiMethod
的实现IMP
指向不做任何事的空实现。
例如:将上述方法lg_bestMethodSwizzlingWithClass
改成👇
+ (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){ }));
}
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);
}
}
1.2.4 Method Swizzling类簇
在我们平时项目开发过程中,经常碰到NSArray数组越界或者NSDictionary的key或者value值为nil等问题导致的crash,对于这些问题苹果爸爸并不会报一个警告,而是直接崩溃,不给你任何机会补救。
因此,我们可以根据这个方法交换
,对NSArray、NSMutableArray、NSDictionary、NSMutableDictionary等类
进行Method Swizzling
,发现越界或值为nil时,做一些补救措施,实现方式还是按照上面的例子来做。但是,你发现Method Swizzling
根本就不起作用,代码也没写错啊,到底是什么鬼?
因为它们是
类簇
,Method Swizzling
对类簇
不起作用!
那什么是类簇
呢?-->是一种抽象工厂的设计模式。抽象工厂内部有很多其它继承自当前类的子类,抽象工厂类会根据不同情况,创建不同的抽象对象来进行使用。例如我们调用NSArray的objectAtIndex:方法,这个类会在方法内部判断,内部创建不同抽象类进行操作。
类名 | 真身 |
---|---|
NSArray | __NSArrayI |
NSMutableArray | __NSArrayM |
NSDictionary | __NSDictionaryI |
NSMutableDictionary | __NSDictionaryM |
示例代码
@implementation NSArray (CJLArray)
//如果下面代码不起作用,造成这个问题的原因大多都是其调用了super load方法。在下面的load方法中,不应该调用父类的load方法。这样会导致方法交换无效
+ (void)load{
Method fromMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(objectAtIndex:));
Method toMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(cjl_objectAtIndex:));
method_exchangeImplementations(fromMethod, toMethod);
}
//如果下面代码不起作用,造成这个问题的原因大多都是其调用了super load方法。在下面的load方法中,不应该调用父类的load方法。这样会导致方法交换无效
- (id)cjl_objectAtIndex:(NSUInteger)index{
//判断下标是否越界,如果越界就进入异常拦截
if (self.count-1 < index) {
// 这里做一下异常处理,不然都不知道出错了。
#ifdef DEBUG // 调试阶段
return [self cjl_objectAtIndex:index];
#else // 发布阶段
@try {
return [self cjl_objectAtIndex:index];
} @catch (NSException *exception) {
// 在崩溃后会打印崩溃信息,方便我们调试。
NSLog(@"---------- %s Crash Because Method %s ----------\n", class_getName(self.class), __func__);
NSLog(@"%@", [exception callStackSymbols]);
return nil;
} @finally {
}
#endif
}else{ // 如果没有问题,则正常进行方法调用
return [self cjl_objectAtIndex:index];
}
}
@end
二、KVC
2.1KVC概念
Key Value Coding
也即 KVC
,是 iOS 开发中一个很重要的知识点,中文翻译过来是 键值对编码
,关于这个概念的具体定义可以在苹果的官方文档处找到。
2.2KVC基本用法
2.2.1 直接对属性或者成员变量进行取值和赋值
@interface LGPerson : NSObject
@property (nonatomic, copy) NSString *name;
@end
// 调用
LGPerson *person = [[LGPerson alloc] init];
// 一般setter情况
person.name = @"LG_Cooci";
// 非正式协议 - 间接访问
[person setValue:@"KC" forKey:@"name"];
2.2.2 针对集合属性,可以通过mutableArrayValueForKey
对其进行操作更改
@interface LGPerson : NSObject
@property (nonatomic, strong) NSArray *array;
@end
// 调用
person.array = @[@"1",@"2",@"3"];
// 第一种:搞一个新的数组 - KVC 赋值就OK
NSArray *array = [person valueForKey:@"array"];
array = @[@"100",@"2",@"3"];
[person setValue:array forKey:@"array"];
NSLog(@"%@",[person valueForKey:@"array"]);
// 第二种
NSMutableArray *mArray = [person mutableArrayValueForKey:@"array"];
mArray[0] = @"200";
NSLog(@"%@",[person valueForKey:@"array"]);
2.2.3 针对结构体,此时需将结构体转成NSValue类型
typedef struct {
float x, y, z;
} ThreeFloats;
@interface LGPerson : NSObject
@property (nonatomic) ThreeFloats threeFloats;
@end
// 调用
ThreeFloats floats = {1.,2.,3.};
NSValue *value = [NSValue valueWithBytes:&floats objCType:@encode(ThreeFloats)];
[person setValue:value forKey:@"threeFloats"];
NSValue *value1 = [person valueForKey:@"threeFloats"];
NSLog(@"%@",value1);
ThreeFloats th;
[value1 getValue:&th];
NSLog(@"%f-%f-%f",th.x,th.y,th.z);
2.2.4 对象的属性也是对象,可以通过keyPath来操作
新建一个类LGStudent
,继承LGPerson
@interface LGStudent : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *subject;
@property (nonatomic, copy) NSString *nick;
@property (nonatomic, assign) int age;
@property (nonatomic, assign) int length;
@property (nonatomic, strong) NSMutableArray *penArr;
@end
@implementation LGStudent
@end
LGPerson
中声明对象属性:
@interface LGPerson : NSObject
@property (nonatomic, strong) LGStudent *student;
@end
// 调用
LGStudent *student = [LGStudent alloc];
student.subject = @"大师班";
person.student = student;
[person setValue:@"Swift" forKeyPath:@"student.subject"];
NSLog(@"%@",[person valueForKeyPath:@"student.subject"]);
2.2.5 setValuesForKeysWithDictionary
& dictionaryWithValuesForKeys
NSDictionary* dict = @{
@"name":@"Cooci",
@"nick":@"KC",
@"subject":@"iOS",
@"age":@18,
@"length":@180
};
LGStudent *p = [[LGStudent alloc] init];
// 字典转模型
[p setValuesForKeysWithDictionary:dict];
NSLog(@"%@",p);
// 键数组转模型到字典
NSArray *array = @[@"name",@"age"];
NSDictionary *dic = [p dictionaryWithValuesForKeys:array];
NSLog(@"%@",dic);
2.2.6 数组相关的特殊api
NSArray *array = @[@"Hank",@"Cooci",@"Kody",@"CC"];
NSArray *lenStr= [array valueForKeyPath:@"length"];
NSLog(@"%@",lenStr);// 消息从array传递给了string
NSArray *lowStr= [array valueForKeyPath:@"lowercaseString"];
NSLog(@"%@",lowStr);
2.2.7 聚合操作符
// @avg、@count、@max、@min、@sum
- (void)aggregationOperator{
NSMutableArray *personArray = [NSMutableArray array];
for (int i = 0; i < 6; i++) {
LGStudent *p = [LGStudent new];
NSDictionary* dict = @{
@"name":@"Tom",
@"age":@(18+i),
@"nick":@"Cat",
@"length":@(175 + 2*arc4random_uniform(6)),
};
[p setValuesForKeysWithDictionary:dict];
[personArray addObject:p];
}
NSLog(@"%@", [personArray valueForKey:@"length"]);
/// 平均身高
float avg = [[personArray valueForKeyPath:@"@avg.length"] floatValue];
NSLog(@"%f", avg);
int count = [[personArray valueForKeyPath:@"@count.length"] intValue];
NSLog(@"%d", count);
int sum = [[personArray valueForKeyPath:@"@sum.length"] intValue];
NSLog(@"%d", sum);
int max = [[personArray valueForKeyPath:@"@max.length"] intValue];
NSLog(@"%d", max);
int min = [[personArray valueForKeyPath:@"@min.length"] intValue];
NSLog(@"%d", min);
}
// 数组操作符 @distinctUnionOfObjects @unionOfObjects
- (void)arrayOperator{
NSMutableArray *personArray = [NSMutableArray array];
for (int i = 0; i < 6; i++) {
LGStudent *p = [LGStudent new];
NSDictionary* dict = @{
@"name":@"Tom",
@"age":@(18+i),
@"nick":@"Cat",
@"length":@(175 + 2*arc4random_uniform(6)),
};
[p setValuesForKeysWithDictionary:dict];
[personArray addObject:p];
}
NSLog(@"%@", [personArray valueForKey:@"length"]);
// 返回操作对象指定属性的集合
NSArray* arr1 = [personArray valueForKeyPath:@"@unionOfObjects.length"];
NSLog(@"arr1 = %@", arr1);
// 返回操作对象指定属性的集合 -- 去重
NSArray* arr2 = [personArray valueForKeyPath:@"@distinctUnionOfObjects.length"];
NSLog(@"arr2 = %@", arr2);
}
// 嵌套集合(array&set)操作 @distinctUnionOfArrays @unionOfArrays @distinctUnionOfSets
- (void)arrayNesting{
NSMutableArray *personArray1 = [NSMutableArray array];
for (int i = 0; i < 6; i++) {
LGStudent *student = [LGStudent new];
NSDictionary* dict = @{
@"name":@"Tom",
@"age":@(18+i),
@"nick":@"Cat",
@"length":@(175 + 2*arc4random_uniform(6)),
};
[student setValuesForKeysWithDictionary:dict];
[personArray1 addObject:student];
}
NSMutableArray *personArray2 = [NSMutableArray array];
for (int i = 0; i < 6; i++) {
LGPerson *person = [LGPerson new];
NSDictionary* dict = @{
@"name":@"Tom",
@"age":@(18+i),
@"nick":@"Cat",
@"length":@(175 + 2*arc4random_uniform(6)),
};
[person setValuesForKeysWithDictionary:dict];
[personArray2 addObject:person];
}
// 嵌套数组
NSArray* nestArr = @[personArray1, personArray2];
NSArray* arr = [nestArr valueForKeyPath:@"@distinctUnionOfArrays.length"];
NSLog(@"arr = %@", arr);
NSArray* arr1 = [nestArr valueForKeyPath:@"@unionOfArrays.length"];
NSLog(@"arr1 = %@", arr1);
}
- (void)setNesting{
NSMutableSet *personSet1 = [NSMutableSet set];
for (int i = 0; i < 6; i++) {
LGStudent *person = [LGStudent new];
NSDictionary* dict = @{
@"name":@"Tom",
@"age":@(18+i),
@"nick":@"Cat",
@"length":@(175 + 2*arc4random_uniform(6)),
};
[person setValuesForKeysWithDictionary:dict];
[personSet1 addObject:person];
}
NSLog(@"personSet1 = %@", [personSet1 valueForKey:@"length"]);
NSMutableSet *personSet2 = [NSMutableSet set];
for (int i = 0; i < 6; i++) {
LGPerson *person = [LGPerson new];
NSDictionary* dict = @{
@"name":@"Tom",
@"age":@(18+i),
@"nick":@"Cat",
@"length":@(175 + 2*arc4random_uniform(6)),
};
[person setValuesForKeysWithDictionary:dict];
[personSet2 addObject:person];
}
NSLog(@"personSet2 = %@", [personSet2 valueForKey:@"length"]);
// 嵌套set
NSSet* nestSet = [NSSet setWithObjects:personSet1, personSet2, nil];
// 交集
NSArray* arr1 = [nestSet valueForKeyPath:@"@distinctUnionOfSets.length"];
NSLog(@"arr1 = %@", arr1);
}
2.3 KVC设值 & 取值原理
KVC
在底层库Foundation
中,由于Foundation
库是闭源的,无法查看其源码,我们只有通过官方文档来看看KVC设值 & 取值的一个大致流程。
2.3.1 KVC设值原理
翻译出来,大致意思是:
- 先依次查询有没有相关的方法:set< Key>:、_set< Key>:、setIs< Key>: 找到直接进行调用赋值。
- 若没有相关方法时,会查看类方法accessInstanceVariablesDirectly是否为YES时进入下一步。否则进入步骤4
- 为YES时,可以直接访问成员变量的来进行赋值,依次寻找变量 _< key >、 _is< Key>、 < key>、 is< Key>。找到则直接赋值,否则进入下一步。
- 将会调用setValue:forUndefinedKey:方法进行抛出异常。可以自定义的实现为未找到的场景来避免抛出异常的行为。
2.3.2 KVC取值原理
上图是取值流程,大致分为6步:
- 以 get<Key>, <key>, is<Key> 以及 _<key> 的顺序查找对象中是否有对应的方法。
- 如果找到了,将方法返回值带上跳转到第 5 步
- 如果没有找到,跳转到第 2 步
- 查找是否有 countOf<Key> 和 objectIn<Key>AtIndex: 方法(对应于 NSArray 类定义的原始方法)以及 <key>AtIndexes: 方法(对应于 NSArray 方法 objectsAtIndexes:)
- 如果找到其中的第一个(countOf<Key>),再找到其他两个中的至少一个,则创建一个响应所有 NSArray 方法的代理集合对象,并返回该对象。(翻译过来就是要么是 countOf<Key> + objectIn<Key>AtIndex:,要么是 countOf<Key> + <key>AtIndexes:,要么是 countOf<Key> + objectIn<Key>AtIndex: + <key>AtIndexes:)
- 如果没有找到,跳转到第 3 步
- 查找名为 countOf<Key>,enumeratorOf<Key> 和 memberOf<Key> 这三个方法(对应于NSSet类定义的原始方法)
- 如果找到这三个方法,则创建一个响应所有 NSSet 方法的代理集合对象,并返回该对象
- 如果没有找到,跳转到第 4 步
- 判断类方法 accessInstanceVariablesDirectly 结果
- 如果返回 YES,则以 _<key>, _is<Key>, <key>, is<Key> 的顺序查找成员变量,如果找到了,将成员变量带上跳转到第 5 步,如果没有找到则跳转到第 6 步
- 如果返回 NO,跳转到第 6 步
- 判断取出的属性值
- 如果属性值是对象,直接返回
- 如果属性值不是对象,但是可以转化为 NSNumber 类型,则将属性值转化为 NSNumber 类型返回
- 如果属性值不是对象,也不能转化为 NSNumber 类型,则将属性值转化为 NSValue 类型返回
- 调用 valueForUndefinedKey:。 默认情况下,这会引发一个异常,但是 NSObject 的子类可以提供特定于 key 的行为。
2.4自定义KVC
根据上面官方文档的设值和取值流程,我们可以尝试实现一个自定义KVC,大致代码如下:
XFCustomKVC
总结
KVC的功能点有很多,我们在实际的开发过程中用到的点很少。所有的功能点总结如下图👇
网友评论