对一个知识点的深入学习,有利于我们加深对代码的理解,更重要的是要学会使用它们,以提高日常开发的效率。下面是几个在平时开发中会使用到的场景:
Method Swizzling
- 给分类增加属性
- 实现字典转模型
Method Swizzling
Method Swizzling
是一项很强大的改变现有方法实现的技术,比子类化替换方法更加灵活。再来看下Method
的数据结构:
typedef struct objc_method *Method;
struct objc_method {
SEL method_name;//方法名称
char *method_types;
IMP method_imp;//方法的具体实现
}
复制代码
从结构中可以看出方法的名称method_name
与方法的实现method_imp
是一一对应的,Method Swizzling
的原理就是动态地改变它们的对应关系,以达到替换方法实现的目的。通常我们利用这项技术把系统的某个方法替换成自定义的方法,根据具体的需求添加一些功能。
下面通过一个示例,看看Method Swizzling
到底有什么用?假设现在项目中需要进行页面统计,我们可以怎么做?
- 直接修改项目中的每一个
ViewController
; - 子类化
ViewController
,让项目中的ViewController
都继承自这些子类。
第一种方式会产生大量重复代码,而且很容易遗漏一些界面,非常难维护;第二种方式比第一种方式好一点,但是需要子类化系统提供的所有类型的ViewController
,假如后续开发的新界面忘记继承这些子类,一样会导致遗漏。Method Swizzling
就是解决这类问题的最佳方式。
下面通过个人在实际开发中的一个使用场景,来看看Method Swizzling
的具体实现。我们在维护一个项目的时候,为了解决特定界面上的bug
首先要找到对应的ViewController
,此时可以通过搜索界面上的关键字来做到快速定位目标界面,但是如果项目比较大又比较复杂该方法有时就不是很靠谱了,我的做法是利用Method Swizzling
直接在控制台打印出当前界面的名称,代码如下:
@interface UIViewController (Log)
@end
复制代码
@implementation UIViewController (Log)
+ (void)load {
#ifdef DEBUG
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class cls = [self class];
SEL originalSel = @selector(viewWillAppear:);
SEL swizzledSel = @selector(rl_viewWillAppear:);
Method originalMethod = class_getInstanceMethod(cls, originalSel);
Method swizzledMethod = class_getInstanceMethod(cls, swizzledSel);
BOOL success = class_addMethod(cls, originalSel, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
if (success) {
class_replaceMethod(cls, swizzledSel, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
});
#endif
}
- (void)rl_viewWillAppear:(BOOL)animated {
NSString *clsName = NSStringFromClass([self class]);
NSLog(@"当前界面是:%@", clsName);
[self rl_viewWillAppear:animated];
}
@end
复制代码
Method Swizzling
的写法比较固定,就代码而言,有几点需要说明:
#ifdef DEBUG
这个条件编译不是必须的,这里是因为我的这个需求,我只需要开发阶段在控制台打印,发布阶段是不需要的。- 通常,我们将代码放在
+load
方法中,runtime
会自动调用+load
方法,且分类中的+load
方法不会覆盖主类中的+load
方法。 - 我们通过
dispatch_once
来保证Method Swizzling
的逻辑只会处理一次,因为runtime
只会调用一次+load
方法,但是你不能保证+load
方法不会被人为触发,虽然一般没人会这么干。 - 我们使用
Method Swizzling
是为了给程序增加功能,而不是完全替换掉某个功能,所以我们需要在自定义的实现中调用原始的实现。代码中调用了class_addMethod
方法,并且依据它的结果处理了两种情况:- 一个类如果包含了特定方法名对应的实现,那么
class_addMethod
方法会返回NO
,这种情况下只需要简单的通过method_exchangeImplementations
交换两个方法的实现就可以了。 - 如果
class_addMethod
方法返回YES
,说明了主类没有实现要被替换的方法,而是继承了父类的实现。这种情况下,一开始通过class_getInstanceMethod
获取到的originalSel
指向的就是父类的实现。由于add_classMethod
方法现在已经成功地将originalSel
指向了自定义方法的实现,所以接下来就是通过class_replaceMethod
将自定义方法指向原先父类的实现,已达到交换实现的目的。
- 一个类如果包含了特定方法名对应的实现,那么
给分类增加属性
通常在主类中,如下这行代码,系统会自动生成成员变量_rlName
,以及与之对应的getter/setter
方法。
@property (nonatomic, copy) NSString *rlName;
复制代码
但是如果在分类中,我们写上同样的代码,然后在程序中调用getter/setter
方法,编译正常,而程序运行时会抛出unrecognized selector sent to instance ...
的异常并crash
,这是为什么呢?因为分类中没法添加成员变量,所以在分类中通过@property
声明的属性,编译器既不会生成对应的成员变量,也不会生成getter/setter
方法。我们来看看分类的结构:
typedef struct objc_category *Category;
struct objc_category {
char *category_name;//分类名称
char *class_name;//类名
struct objc_method_list *instance_methods;//实例方法列表
struct objc_method_list *class_methods;//类方法列表
struct objc_protocol_list *protocols;//协议列表
}
复制代码
从结构可以看出分类中没有成员变量列表,这也就解释了为什么说分类中不能添加成员变量。我们所说的给分类添加属性,其实是通过runtime
来给对象关联一个对象,注意关联对象不是成员变量!
runtime
提供了3个关联对象的操作函数:
/**
* 设置关联对象
*
* object 关联对象的源对象
* key 关联对象的key,将来通过这个key取出关联对象
* value 关联对象的值,可以通过传nil来清空一个key对应的关联对象
* policy 关联对象的策略
*/
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);
/**
* 取出关联对象
*/
id objc_getAssociatedObject(id object, const void *key);
/**
* 清空源对象的所有关联对象
*/
void objc_removeAssociatedObjects(id object);
复制代码
关联对象涉及到了5种策略:
OBJC_ASSOCIATION_ASSIGN //对关联对象进行弱引用
OBJC_ASSOCIATION_RETAIN_NONATOMIC //对关联对象进行强引用(非原子的)
OBJC_ASSOCIATION_COPY_NONATOMIC //对关联对象进行copy引用(非原子的)
OBJC_ASSOCIATION_RETAIN //对关联对象进行强引用
OBJC_ASSOCIATION_COPY //对关联对象进行copy引用
复制代码
所以我们实现给分类添加属性的原理就是:通过关联对象,手动实现getter/setter
方法。以下是实现的示例代码:
@interface NSObject (Associate)
@property (nonatomic, copy) NSString *rlName;
@end
复制代码
@implementation NSObject (Associate)
- (void)setRlName:(NSString *)rlName {
objc_setAssociatedObject(self, @selector(rlName), rlName, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSString *)rlName {
return objc_getAssociatedObject(self, _cmd);
}
复制代码
实现字典转模型
我们可以通过两种方式实现字典转模型:1.KVC
、2.runtime
,其实本质上都是通过调用-setValue:forKey:
方法,但是两种方式的思路是不一样的。KVC
的方式是遍历需要转换的字典,然后赋值到对象中的同名成员变量,如果字典很大,就会有很多我们不需要的键值,自然效率就低了;runtime
的方式则是遍历对象的成员列表,以成员变量名称为键到字典中取出对应的值赋给对象,这种方式只取出自己需要的数据,效率相对更高。这里着重介绍通过runtime
方式实现字典转模型。 我们自定义三个类RLCar
、RLBook
、RLPerson
:
//.h
@interface RLCar : NSObject
@property (copy, nonatomic) NSString *brand;
@property (assign, nonatomic) CGFloat price;
@end
//.m
@implementation RLCar
@end
复制代码
//.h
@interface RLBook : NSObject
@property (copy, nonatomic) NSString *bookName;
@property (assign, nonatomic) CGFloat bookPrice;
@end
//.m
@implementation RLBook
@end
复制代码
//.h
@class RLBook;
typedef NS_ENUM(NSInteger, RLSex) {
RLSexUnknow = 0,
RLSexMale,
RLSexFemale
};
@interface RLPerson : NSObject
@property (copy, nonatomic) NSString *name;
@property (assign, nonatomic) RLSex sex;
@property (assign, nonatomic) NSInteger age;
@property (strong, nonatomic) RLBook *book;
@property (strong, nonatomic) NSArray *cars;
@end
//.m
@implementation RLPerson
@end
复制代码
我们自定义的三个类之间的关系,涉及到了字典转模型的核心内容:
- 对象包含普通类型的成员变量;
- 对象包含类类型的成员变量;
- 对象包含一个数组成员变量:数组中存放其他类类型的对象。
字典转模型,最关键的是通过runtime
获取对象的成员列表,先通过下面的关键代码,来看看上面的RLPerson
类成员变量的情况:
unsigned int count = 0;
Ivar *ivarList = class_copyIvarList(self, &count);
for (int i = 0; i < count; i ++) {
Ivar ivar = ivarList[i];
// 获取属性名称
NSString *propertyName = [NSString stringWithUTF8String:ivar_getName(ivar)];
// 获取属性类型
NSString *propertyType = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];
NSLog(@"%@ %@", propertyName, propertyType);
}
复制代码
_name @"NSString"
_sex q
_age q
_book @"RLBook"
_cars @"NSArray"
复制代码
我们来对比分析一下打印结果:
- 获取到的属性名称是带下划线的(
_
),如果我们要利用属性名称作为key
到字典中取值,首先得去掉下划线; _book
属性对应的类型是RLBook
,正如我们在RLPerson
中声明的一样。其实这种情况下我们通过book
从字典中取出来的值也是一个字典,结合属性类型,我们就能解析出RLBook
了,有点递归的意思。这就是解决对象包含类类型的成员变量
的思路;_cars
属性对应的类型是NSArray
,我们通过RLPerson
可以知道,这里数组中存放的是RLCar
类型的数据,但是runtime
在这里是无法知道数组中存放的数据是什么类型的。我们要想办法告诉它,于是我们可以声明一个协议方法,遇到这种情况,通过实现这个协议方法,来让runtime
知道数组中数据的类型。这是解决对象包含一个数组成员变量
的思路。
下面展示完整的字典转模型的代码,为了让所有的继承NSObject
的子类都可以有转模型的方法,我们给NSObject
添加分类:
//.h
@protocol RLModel <NSObject>
@optional
+ (NSDictionary *)rl_objectClassInArray;
@end
@interface NSObject (RLModel) <RLModel>
+ (instancetype)rl_modelWithDictionary:(NSDictionary *)dataDic;
@end
//.m
@implementation NSObject (RLModel)
+ (instancetype)rl_modelWithDictionary:(NSDictionary *)dataDic {
id obj = [[self alloc] init];
// 通过runtime获取属性列表
unsigned int count = 0;
Ivar *ivarList = class_copyIvarList(self, &count);
for (int i = 0; i < count; i ++) {
Ivar ivar = ivarList[i];
// 获取属性名称 (带有下划线,如:_name)
NSString *propertyName = [NSString stringWithUTF8String:ivar_getName(ivar)];
// 获取属性类型
NSString *propertyType = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];
//NSLog(@"%@ %@", propertyName, propertyType);
NSString *key = [propertyName substringFromIndex:1];
id value = dataDic[key];
// 二级转换,值为字典并且属性类型不是字典则需要字典转模型
if ([value isKindOfClass:[NSDictionary class]] && ![propertyType containsString:@"NS"]) {
// 根据属性类型创建对象 @"RLBook"
NSRange range = [propertyType rangeOfString:@"\""];
propertyType = [propertyType substringFromIndex: range.location + range.length];
range = [propertyType rangeOfString:@"\""];
propertyType = [propertyType substringToIndex:range.location];
Class modelClass = NSClassFromString(propertyType);
if (modelClass) {
value = [modelClass rl_modelWithDictionary:value];
}
}
// 三级转换,值为数组
if ([value isKindOfClass:[NSArray class]]) {
// 判断类有没有实现rl_ObjectClassInArray方法
if ([self respondsToSelector:@selector(rl_objectClassInArray)]) {
NSString *clazzStr = [self rl_objectClassInArray][key];
NSMutableArray *temp = [NSMutableArray array];
for (NSDictionary *dic in value) {
Class clazz = NSClassFromString(clazzStr);
if (clazz) {
[temp addObject:[clazz rl_modelWithDictionary:dic]];
}
}
value = temp;
}
}
if (value) {
[obj setValue:value forKey:key];
}
}
return obj;
}
@end
复制代码
当然在RLPerson.m
中我们需要实现+rl_objectClassInArray
方法:
+ (NSDictionary *)rl_objectClassInArray {
return @{@"cars" : @"RLCar"};
}
复制代码