iOS-OC底层16:Method-Swizzling 方法交换
2020-10-26 本文已影响0人
MonKey_Money
Method swizzling是什么
Method swizzling 用于改变一个已经存在的 selector 的实现。这项技术使得在运行时通过改变 selector 在类的消息分发列表中的映射从而改变方法的掉用成为可能。Method swizzling也就是我们常说的“黑魔法”
一般定义一个工具类如下
@interface LGRuntimeTool : NSObject
/**
交换方法
@param cls 交换对象
@param oriSEL 原始方法编号
@param swizzledSEL 交换的方法编号
*/
+ (void)lg_methodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL;
@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);
}
@end
我们定义一个类如下
@interface LGPerson : NSObject
- (void)personInstanceMethod;
-(void)swizzlingMethod;
+ (void)personClassMethod;
@end
@implementation LGPerson
+(void)load {
[LGRuntimeTool lg_methodSwizzlingWithClass:[self class] oriSEL:@selector(personInstanceMethod) swizzledSEL:@selector(swizzlingMethod)];
}
- (void)personInstanceMethod{
NSLog(@"person对象方法:%s",__func__);
}
-(void)swizzlingMethod {
NSLog(@"person交换后的方法对象方法:%s",__func__);
}
@end
LGPerson *s = [[LGPerson alloc] init];
[s personInstanceMethod];
[s swizzlingMethod];
我们调用之后打印如下
person对象方法:-[LGPerson personInstanceMethod]
person交换后的方法对象方法:-[LGPerson swizzlingMethod]
被重复调用很多次
我们交换方法写到load方法里,但是有可能load方法被手动调用,
LGPerson *s = [[LGPerson alloc] init];
[s personInstanceMethod];
[LGPerson load];
[s swizzlingMethod];
person对象方法:-[LGPerson personInstanceMethod]
person交换后的方法对象方法:-[LGPerson swizzlingMethod]
如果方法交换方法被多次调用,我们就无法控制方法到底调用情况
解决方案单利
+(void)load {
static dispatch_once_t dispachOnce;
dispatch_once(&dispachOnce, ^{
[LGRuntimeTool lg_methodSwizzlingWithClass:[self class] oriSEL:@selector(personInstanceMethod) swizzledSEL:@selector(swizzlingMethod)];
});
}
子类中交换父类方法,父类调用该方法
@interface LGStudent : LGPerson
@end
@implementation LGStudent
+ (void)load {
static dispatch_once_t studenDispatchonce;
dispatch_once(&studenDispatchonce, ^{
[LGRuntimeTool lg_methodSwizzlingWithClass:[self class] oriSEL:@selector(personInstanceMethod) swizzledSEL:@selector(studentInstanceMethod)];
});
}
-(void)studentInstanceMethod {
[self studentInstanceMethod];
NSLog(@"student对象方法%s",__func__);
}
@end
LGPerson *person = [[LGPerson alloc] init];
[person personInstanceMethod];
崩溃日志
[LGPerson studentInstanceMethod]: unrecognized selector sent to instance 0x6000036cc420'
这是因为studentInstanceMethod内调用[self studentInstanceMethod],而[self studentInstanceMethod]在子类中能访问到。
解决方案,把需要更改的方法加到本类中,防止污染父类
LGRuntimeTool添加方法
+ (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));
/**
personInstanceMethod(sel) - lg_studentInstanceMethod(imp)
lg_studentInstanceMethod (swizzledSEL) - personInstanceMethod(imp)
*/
if (success) {// 自己没有 - 交换 - 没有父类进行处理 (重写一个)
class_replaceMethod(cls, swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
}else{ // 自己有
method_exchangeImplementations(oriMethod, swiMethod);
}
}
打印日志
person对象方法:-[LGPerson personInstanceMethod]
交换未实现的的方法
我们把LGPerson方法中personInstanceMethod的实现注释调
运行
warning: `self' is not accessible (substituting 0)
死循环解决方案
我们在交换方法里对要交换的方法进行判断,如果未实现我们可以添加一个空实现
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");
}));
}
// 一般交换方法: 交换自己有的方法 -- 走下面 因为自己有意味添加方法失败
// 交换自己没有实现的方法:
// 首先第一步:会先尝试给自己添加要交换的方法 :personInstanceMethod (SEL) -> swiMethod(IMP)
// 然后再将父类的IMP给swizzle personInstanceMethod(imp) -> swizzledSEL
//oriSEL:personInstanceMethod
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);
}
类方法交换
//给LGStudent添加类方法
+(void)firstClassMethod {
NSLog(@"---%s---%@-----%@",__func__,self,NSStringFromSelector(_cmd));
}
+(void)secondClassMethod{
NSLog(@"--%s---%@-----%@",__func__,self,NSStringFromSelector(_cmd));
}
//实现方法的交换
[LGRuntimeTool lg_bestClassMethodSwizzlingWithClass:self oriSEL:@selector(firstClassMethod) swizzledSEL:@selector(secondClassMethod)];
+(void)lg_bestClassMethodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL {
Method oriMethod = class_getClassMethod([cls class], oriSEL);
Method swiMethod = class_getClassMethod([cls class], swizzledSEL);
Class metalClass = objc_getMetaClass(NSStringFromClass(cls).UTF8String);
if (!oriMethod) { // 避免动作没有意义
// 在oriMethod为nil时,替换后将swizzledSEL复制一个不做任何事的空实现,代码如下:
class_addMethod(metalClass, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
method_setImplementation(swiMethod, imp_implementationWithBlock(^(id self, SEL _cmd){
NSLog(@"来了一个空的 imp");
}));
}
// 一般交换方法: 交换自己有的方法 -- 走下面 因为自己有意味添加方法失败
// 交换自己没有实现的方法:
// 首先第一步:会先尝试给自己添加要交换的方法 :personInstanceMethod (SEL) -> swiMethod(IMP)
// 然后再将父类的IMP给swizzle personInstanceMethod(imp) -> swizzledSEL
//oriSEL:personInstanceMethod
BOOL didAddMethod = class_addMethod(metalClass, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
if (didAddMethod) {
class_replaceMethod(metalClass, swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
}else{
method_exchangeImplementations(oriMethod, swiMethod);
}
}
//调用
[LGStudent firstClassMethod];
打印结果
[LGStudent(LG) secondClassMethod]---LGStudent-----firstClassMethod
method-swizzling的应用
method-swizzling最常用的应用是防止数组、字典等越界崩溃
在iOS中NSNumber、NSArray、NSDictionary等这些类都是类簇,一个NSArray的实现可能由多个类组成。所以如果想对NSArray进行Swizzling,必须获取到其“真身”进行Swizzling,直接对NSArray进行操作是无效的。
下面列举了NSArray和NSDictionary本类的类名,可以通过Runtime函数取出本类。
image.png@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