Method Swizzling
在OC
语言的runtime
特性中,调用一个对象的方法就是给这个对象发送消息。是通过查找接收消息对象的方法列表,从方法列表中查找对应的SEL
,这个SEL
对应着一个IMP
(一个IMP可以对应多个SEL),通过这个IMP
找到对应的方法调用。
在每个类中都有一个Dispatch Table
,这个Dispatch Table
本质是将类中的SEL
和IMP
(可以理解为函数指针)进行对应。而我们的Method Swizzling
就是对这个table
进行了操作,让SEL
对应另一个IMP
。
作者:刘小壮
链接:https://www.jianshu.com/p/ff19c04b34d0
来源:
著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。
Method Swizzling
页面统计的需求来说吧,我们先给UIViewController
添加一个Category
,然后在Category
中的+(void)load
方法中添加Method Swizzling
方法,我们用来替换的方法也写在这个Category
中。由于+ load
类方法是程序运行时这个类被加载到内存中就调用的一个方法,执行比较早,并且不需要我们手动调用。而且这个方法具有唯一性,也就是只会被调用一次,不用担心资源抢夺的问题。
定义Method Swizzling中我们自定义的方法时,需要注意尽量加前缀,以防止和其他地方命名冲突,Method Swizzling的替换方法命名一定要是唯一的,至少在被替换的类中必须是唯一的。
#import "UIViewController+swizzling.h"
#import
@implementation UIViewController (swizzling)
+ (void)load {
// 通过class_getInstanceMethod()函数从当前对象中的method list获取method结构体,如果是类方法就使用class_getClassMethod()函数获取。
Method fromMethod = class_getInstanceMethod([self class], @selector(viewDidLoad));
Method toMethod = class_getInstanceMethod([self class], @selector(swizzlingViewDidLoad));
/**
我们在这里使用class_addMethod()函数对Method Swizzling做了一层验证,如果self没有实现被交换的方法,会导致失败。
而且self没有交换的方法实现,但是父类有这个方法,这样就会调用父类的方法,结果就不是我们想要的结果了。
所以我们在这里通过class_addMethod()的验证,如果self实现了这个方法,class_addMethod()函数将会返回NO,我们就可以对其进行交换了。
*/
if (!class_addMethod([self class], @selector(swizzlingViewDidLoad), method_getImplementation(toMethod), method_getTypeEncoding(toMethod))) {
method_exchangeImplementations(fromMethod, toMethod);
}
}
// 我们自己实现的方法,也就是和self的viewDidLoad方法进行交换的方法。
- (void)swizzlingViewDidLoad {
NSString *str = [NSString stringWithFormat:@"%@", self.class];
// 我们在这里加一个判断,将系统的UIViewController的对象剔除掉
if(![str containsString:@"UI"]){
NSLog(@"统计打点 : %@", self.class);
}
[self swizzlingViewDidLoad];
}
@end
NSArray、NSMutableArray、NSDictionary、NSMutableDictionary
等类进行Method Swizzling
,实现方式还是按照上面的例子来做。但是....你发现Method Swizzling根本就不起作用,代码也没写错啊,到底是什么鬼?
这是因为Method Swizzling
对NSArray
这些的类簇是不起作用的。因为这些类簇类,其实是一种抽象工厂的设计模式。抽象工厂内部有很多其它继承自当前类的子类,抽象工厂类会根据不同情况,创建不同的抽象对象来进行使用。例如我们调用NSArray
的objectAtIndex:
方法,这个类会在方法内部判断,内部创建不同抽象类进行操作。
所以也就是我们对NSArray
类进行操作其实只是对父类进行了操作,在NSArray
内部会创建其他子类来执行操作,真正执行操作的并不是NSArray
自身,所以我们应该对其“真身”进行操作。
#import "NSArray+LXZArray.h"
#import "objc/runtime.h"
@implementation NSArray (LXZArray)
+ (void)load {
Method fromMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(objectAtIndex:));
Method toMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(lxz_objectAtIndex:));
method_exchangeImplementations(fromMethod, toMethod);
}
- (id)lxz_objectAtIndex:(NSUInteger)index {
if (self.count-1 < index) {
// 这里做一下异常处理,不然都不知道出错了。
@try {
return [self lxz_objectAtIndex:index];
}
@catch (NSException *exception) {
// 在崩溃后会打印崩溃信息,方便我们调试。
NSLog(@"---------- %s Crash Because Method %s ----------\n", class_getName(self.class), __func__);
NSLog(@"%@", [exception callStackSymbols]);
return nil;
}
@finally {}
} else {
return [self lxz_objectAtIndex:index];
}
}
@end
Method Swizzling 错误剖析
在上面的例子中,如果只是单独对NSArray
或NSMutableArray
中的单个类进行Method Swizzling
,是可以正常使用并且不会发生异常的。如果进行Method Swizzling
的类中,有两个类有继承关系的,并且Swizzling
了同一个方法。例如同时对NSArray
和NSMutableArray
中的objectAtIndex:
方法都进行了Swizzling,这样可能会导致父类
Swizzling`失效的问题。
对于这种问题主要是两个原因导致的,
- 首先是不要在
+ (void)load
方法中调用[super load]
方法,这会导致父类的Swizzling
被重复执行两次,这样父类的Swizzling
就会失效。例如下面的两张图片,你会发现由于NSMutableArray
调用了[super load]导致父类NSArray
的Swizzling
代码被执行了两次。
#import "NSMutableArray+LXZArrayM.h"
@implementation NSMutableArray (LXZArrayM)
+ (void)load {
// 这里不应该调用super,会导致父类被重复Swizzling
[super load];
Method fromMethod = class_getInstanceMethod(objc_getClass("__NSArrayM"), @selector(objectAtIndex:));
Method toMethod = class_getInstanceMethod(objc_getClass("__NSArrayM"), @selector(lxz_objectAtIndexM:));
method_exchangeImplementations(fromMethod, toMethod);
}
这样就会导致程序运行过程中,子类调用Swizzling
的方法是没有问题的,父类调用同一个方法就会发现Swizzling
失效了.....具体原因我们后面讲!
还有一个原因就是因为代码逻辑导致Swizzling
代码被执行了多次,这也会导致Swizzling
失效,其实原理和上面的问题是一样的,我们下面讲讲为什么会出现这个问题。
问题原因
我们上面提到过Method Swizzling
的实现原理就是对类的Dispatch Table
进行操作,每进行一次Swizzling
就交换一次SEL
和IMP
(可以理解为函数指针),如果Swizzling
被执行了多次,就相当于SEL
和IMP
被交换了多次。这就会导致第一次执行成功交换了、第二次执行又换回去了、第三次执行.....这样换来换去的结果,能不能成功就看运气了,这也是好多人说Method Swizzling
不好用的原因之一。
解决方案
Swizzling
的代码被重复执行,为了避免这样的原因出现,我们可以通过GCD
的dispatch_once
函数来解决,利用dispatch_once
函数内代码只会执行一次的特性。
Method Swizzling源码分析
下面是Method Swizzling
的实现源码,从源码来看,其实内部实现很简单。核心代码就是交换两个Method的imp函数指针,这也就是方法被swizzling多次,可能会被换回去的原因,因为每次调用都会执行一次交换操作。
void method_exchangeImplementations(Method m1, Method m2)
{
if (!m1 || !m2) return;
rwlock_writer_t lock(runtimeLock);
IMP m1_imp = m1->imp;
m1->imp = m2->imp;
m2->imp = m1_imp;
flushCaches(nil);
updateCustomRR_AWZ(nil, m1);
updateCustomRR_AWZ(nil, m2);
}
二、实现分类添加新属性
我们在开发中常常使用类目Category
为一些已有的类扩展功能。虽然继承也能够为已有类增加新的方法,而且相比类目更是具有增加属性的优势,但是继承毕竟是一个重量级的操作,添加不必要的继承关系无疑增加了代码的复杂度。
遗憾的是,OC的分类并不支持直接添加属性,如果我们直接在分类的声明中写入Property属性,那么只能为其生成set与get方法声明,却不能生成成员变量,直接调用这些属性还会造成崩溃。
所以为了实现给分类添加属性,我们还需借助Runtime的关联对象(Associated Objects)特性,它能够帮助我们在运行阶段将任意的属性关联到一个对象上,下面是相关的三个方法:
/**
1.给对象设置关联属性
@param object 需要设置关联属性的对象,即给哪个对象关联属性
@param key 关联属性对应的key,可通过key获取这个属性,
@param value 给关联属性设置的值
@param policy 关联属性的存储策略(对应Property属性中的assign,copy,retain等)
OBJC_ASSOCIATION_ASSIGN @property(assign)。
OBJC_ASSOCIATION_RETAIN_NONATOMIC @property(strong, nonatomic)。
OBJC_ASSOCIATION_COPY_NONATOMIC @property(copy, nonatomic)。
OBJC_ASSOCIATION_RETAIN @property(strong,atomic)。
OBJC_ASSOCIATION_COPY @property(copy, atomic)。
*/
void objc_setAssociatedObject(id _Nonnull object,
const void * _Nonnull key,
id _Nullable value,
objc_AssociationPolicy policy)
/**
2.通过key获取关联的属性
@param object 从哪个对象中获取关联属性
@param key 关联属性对应的key
@return 返回关联属性的值
*/
id _Nullable objc_getAssociatedObject(id _Nonnull object,
const void * _Nonnull key)
/**
3.移除对象所关联的属性
@param object 移除某个对象的所有关联属性
*/
void objc_removeAssociatedObjects(id _Nonnull object)
注意:key
与关联属性一一对应,我们必须确保其全局唯一性,常用我们使用@selector(methodName)
作为key
。
现在演示一个代码示例:为UIImage
增加一个分类:UIImage+Tools
,并为其设置关联属性urlString
(图片网络链接属性),相关代码如下:
//UIImage+Tools.h文件中
UIImage+Tools.m
@interface UIImage (Tools)
//添加一个新属性:图片网络链接
@property(nonatomic,copy)NSString *urlString;
@end
//UIImage+Tools.m文件中
#import "UIImage+Tools.h"
#import
@implementation UIImage (Tools)
//set方法
- (void)setUrlString:(NSString *)urlString{
objc_setAssociatedObject(self,
@selector(urlString),
urlString,
OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
//get方法
- (NSString *)urlString{
return objc_getAssociatedObject(self,
@selector(urlString));
}
//添加一个自定义方法,用于清除所有关联属性
- (void)clearAssociatedObjcet{
objc_removeAssociatedObjects(self);
}
@end
测试文件中:
UIImage *image = [[UIImage alloc] init];
image.urlString = @"http://www.image.png";
NSLog(@"获取关联属性:%@",image.urlString);
[image clearAssociatedObjcet];
NSLog(@"获取关联属性:%@",image.urlString);
//打印:
//获取关联属性:http://www.image.png
// 获取关联属性:(null)
三、获取类的详细信息
1.获取属性列表
unsigned int count;
objc_property_t *propertyList = class_copyPropertyList([self class], &count);
for (unsigned int i = 0; i
2.获取所有成员变量
Ivar *ivarList = class_copyIvarList([self class], &count);
for (int i= 0; i
3.获取所有方法
Method *methodList = class_copyMethodList([self class], &count);
for (unsigned int i = 0; i
4.获取当前遵循的所有协议
__unsafe_unretained Protocol **protocolList = class_copyProtocolList([self class], &count);
for (int i=0; i
五、方法动态解析与消息转发
1.动态方法解析:动态添加方法
Runtime足够强大,能够让我们在运行时动态添加一个未实现的方法,这个功能主要有两个应用场景:
场景1:动态添加未实现方法,解决代码中因为方法未找到而报错的问题;
场景2:利用懒加载思路,若一个类有很多个方法,同时加载到内存中会耗费资源,可以使用动态解析添加方法。方法动态解析主要用到的方法如下:
//OC方法:
//类方法未找到时调起,可于此添加类方法实现
+ (BOOL)resolveClassMethod:(SEL)sel
//实例方法未找到时调起,可于此添加实例方法实现
+ (BOOL)resolveInstanceMethod:(SEL)sel
//Runtime方法:
/**
运行时方法:向指定类中添加特定方法实现的操作
@param cls 被添加方法的类
@param name selector方法名
@param imp 指向实现方法的函数指针
@param types imp函数实现的返回值与参数类型
@return 添加方法是否成功
*/
BOOL class_addMethod(Class _Nullable cls,
SEL _Nonnull name,
IMP _Nonnull imp,
const char * _Nullable types)
2.解决方法无响应崩溃问题
执行OC
方法其实就是一个发送消息的过程,若方法未实现,我们可以利用方法动态解析与消息转发来避免程序崩溃,这主要涉及下面一个处理未实现消息的过程:
除了上述的方法动态解析,还使用到的相关方法如下:
消息接收者重定向
//重定向类方法的消息接收者,返回一个类
- (id)forwardingTargetForSelector:(SEL)aSelector
//重定向实例方法的消息接受者,返回一个实例对象
- (id)forwardingTargetForSelector:(SEL)aSelector
消息重定向
- (void)forwardInvocation:(NSInvocation *)anInvocation;
- (NSMethodSignature*)methodSignatureForSelector:(SEL)aSelector;
六、动态操作属性
1.动态修改属性变量
现在假设这样一个情况:我们使用第三方框架里的Person类,在特殊需求下想要更改其私有属性nickName
,这样的操作我们就可以使用Runtime可以动态修改对象属性。
基本思路:首先使用Runtime获取Peson对象的所有属性,找到nickName,然后使用ivar的方法修改其值。具体的代码示例如下:
Person *ps = [[Person alloc] init];
NSLog(@"ps-nickName: %@",[ps valueForKey:@"nickName"]); //null
//第一步:遍历对象的所有属性
unsigned int count;
Ivar *ivarList = class_copyIvarList([ps class], &count);
for (int i= 0; i
总结:此过程类似KVC的取值和赋值
2.实现NSCoding
的自动归档和解档
归档是一种常用的轻量型文件存储方式,但是它有个弊端:在归档过程中,若一个Model有多个属性,我们不得不对每个属性进行处理,非常繁琐。
归档操作主要涉及两个方法:encodeObject
和 decodeObjectForKey
,现在,我们可以利用Runtime
来改进它们,关键的代码示例如下:
//原理:使用Runtime动态获取所有属性
//解档操作
- (instancetype)initWithCoder:(NSCoder *)aDecoder{
self = [super init];
if (self) {
unsigned int count = 0;
Ivar *ivarList = class_copyIvarList([self class], &count);
for (int i = 0; i < count; i++) {
Ivar ivar = ivarList[I];
const char *ivarName = ivar_getName(ivar);
NSString *key = [NSString stringWithUTF8String:ivarName];
id value = [aDecoder decodeObjectForKey:key];
[self setValue:value forKey:key];
}
free(ivarList); //释放指针
}
return self;
}
//归档操作
- (void)encodeWithCoder:(NSCoder *)aCoder{
unsigned int count = 0;
Ivar *ivarList = class_copyIvarList([self class], &count);
for (NSInteger i = 0; i < count; i++) {
Ivar ivar = ivarList[I];
NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];
id value = [self valueForKey:key];
[aCoder encodeObject:value forKey:key];
}
free(ivarList); //释放指针
}
下面是有关归档的测试代码:
//--测试归档
Person *ps = [[Person alloc] init];
ps.name = @"梧雨北辰";
ps.age = 18;
NSString *temp = NSTemporaryDirectory();
NSString *fileTemp = [temp stringByAppendingString:@"person.archive"];
[NSKeyedArchiver archiveRootObject:ps toFile:fileTemp];
//--测试解档
NSString *temp = NSTemporaryDirectory();
NSString *fileTemp = [temp stringByAppendingString:@"person.henry"];
Person *person = [NSKeyedUnarchiver unarchiveObjectWithFile:fileTemp];
NSLog(@"person-name:%@,person-age:%ld",person.name,person.age);
//person-name:梧雨北辰,person-age:18
3.实现字典与模型的转换
字典数据转模型的操作在项目开发中很常见,通常我们会选择第三方如YYModel;其实我们也可以自己来实现这一功能,主要的思路有两种:KVC、Runtime,总结字典转化模型过程中需要解决的问题如下:
现在,我们使用Runtime来实现字典转模型的操作,大致的思路是这样:
借助Runtime可以动态获取成员列表的特性,遍历模型中所有属性,然后以获取到的属性名为key,在JSON字典中寻找对应的值value;再将每一个对应Value赋值给模型,就完成了字典转模型的目的。