016*:方法交换Method-Swizzling?(1:一次性问题:load或者initialize 2: 子类不和父类方法。)
问题
1:一次性问题:load或者initialize
2: 子类不和父类方法。
目录
1:method-swizzling 是什么?
2:注意:
3:method-swizzling - 类方法
4:method-swizzling的应用
预备
HTRuntimeTool
类:负责方法交换的具体操作HTPerson
类: 继承自NSObject
的类,拥有personFunc
方法HTStudent
类: 继承自HTPerson
的类,拥有studentFun
#import <objc/runtime.h//MARK: - HTRuntimeTool@interface HTRuntimeTool : NSObject+ (void)ht_methodSwizzilingWithClass: (Class)cls oriSEL: (SEL)oriSel swizzledSEL: (SEL)swizzledSel; @end @implementation HTRuntimeTool + (void)ht_methodSwizzilingWithClass: (Class)cls oriSEL: (SEL)oriSel swizzledSEL: (SEL)swizzledSel { NSAssert(cls != nil, @"传入的交换类不能为空!"); // 【这是错误实例,下面坑点3讲解】 Method oriMethod = class_getInstanceMethod(cls, oriSel); Method swiMethod = class_getInstanceMethod(cls, swizzledSel); method_exchangeImplementations(oriMethod, swiMethod); } @end //MARK: - HTPerson @interface HTPerson : NSObject - (void)personFunc; @end @implementation HTPerson - (void)personFunc { NSLog(@"HTPerson实例方法: %s", __func__); } @end //MARK: - HTStudent @interface HTStudent : HTPerson - (void)studentFunc; @end @implementation HTStudent //+ (void)load { // static dispatch_once_t onceToken; // dispatch_once(&onceToken, ^{ // [HTRuntimeTool ht_methodSwizzilingWithClass:self oriSEL:@selector(personFunc) swizzledSEL:@selector(studentFunc)]; // }); //} // 避免影响启动时长,方法交换放在initialize中实现 + (void)initialize { if (self == [HTStudent class]) { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ [HTRuntimeTool ht_methodSwizzilingWithClass:self oriSEL:@selector(personFunc) swizzledSEL:@selector(studentFunc)]; }); } } - (void)studentFunc { [self studentFunc]; NSLog(@"HTStudent实例方法: %s", __func__); } @end //MARK: -ViewController @interface ViewController () @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; HTStudent * student = [HTStudent new]; [student studentFunc];
} @end
正文
1:method-swizzling 是什么?
-
method-swizzling
的含义是方法交换
,其主要作用是在运行时将一个方法的实现替换成另一个方法的实现
,这就是我们常说的iOS黑魔法
, -
在OC中就是
利用method-swizzling实现AOP
,其中AOP
(Aspect Oriented Programming,面向切面编程)是一种编程的思想,区别于OOP(面向对象编程)
- OOP和AOP都是一种编程的思想ios_lowLevel
OOP
编程思想更加倾向于对业务模块的封装
,划分出更加清晰的逻辑单元;- 而
AOP
是面向切面进行提取封装,提取各个模块中的公共部分,提高模块的复用率,降低业务之间的耦合性
。
-
每个类都维护着一个
方法列表
,即methodList
,methodList
中有不同的方法
即Method
,每个方法中包含了方法的sel
和IMP
,方法交换就是将sel和imp原本的对应断开,并将sel和新的IMP生成对应关系
如下图所示,交换前后的sel和IMP的对应关系
method-swizzling涉及的相关API
-
通过
sel
获取方法Method
-
class_getInstanceMethod
:获取实例方法 -
class_getClassMethod
:获取类方法
-
-
method_getImplementation
:获取一个方法的实现 -
method_setImplementation
:设置一个方法的实现 -
method_getTypeEncoding
:获取方法实现的编码类型 -
class_addMethod
:添加方法实现 -
class_replaceMethod
:用一个方法的实现,替换另一个方法的实现,即aIMP 指向 bIMP
,但是bIMP不一定指向aIMP -
method_exchangeImplementations
:交换两个方法的实现,即 aIMP -> bIMP, bIMP -> aIMP
2:注意:
坑点1:method-swizzling使用过程中的一次性问题
1.1:可以通过单例设计
原则,使方法交换只执行一次
,在OC中可以通过dispatch_once
实现单例
+ (void)load{ static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ [LGRuntimeTool lg_bestMethodSwizzlingWithClass:self oriSEL:@selector(helloword) swizzledSEL:@selector(lg_studentInstanceMethod)]; }); }
// 避免影响启动时长,方法交换放在initialize中实现 + (void)initialize { if (self == [HTStudent class]) { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ [HTRuntimeTool ht_methodSwizzilingWithClass:self oriSEL:@selector(personFunc) swizzledSEL:@selector(studentFunc)]; }); } }
坑点2:必须提前准备
交换操作
必须提前完成,不然会产生调用混乱
,执行错误会造成crash
或其他业务bug
。
我们可以在+load
方法或+initialize
方法内完成交换操作,保障
交换操作的提前准备
。
-
+load
方法:将懒加载类
变成非懒加载类
,在程序启动前
就完成相应操作。会影响
程序启动时长
。不建议使用。 -
+initialize
方法:系统动态加入
到NSObject
的方法,所有继承
自NSObject
的类,都拥有该方法。
在类
首次被调用
时,首先会执行+initialize
方法。所以既做到了懒加载
,不提前占用资源
。也满足了提前准备
的要求。
坑点3:不可交换父类方法
上面案例,粗看没啥问题,但是当我们创建HTPerson对象
,调用personFunc
函数时,crash
了:
崩溃信息
告诉我们:HTPerson
类没有studentFunc
方法,导致崩溃
。
-
我们进行方法交换时,
HTStudent
中没有找到personFunc
方法,所以会沿着继承链
往上找,在父类HTPerson
中找到了personFunc
方法。所以我们将HTStudent
的studentFunc
方法与HTPerson
的personFunc
方法进行了交换。 -
当使用子类
HTStudent
实例对象进行调用时,一切都正常。但是当使用父类HTPerson
进行调用时,就会找不到交换后的studentFunc
方法,导致崩溃。
解决方法:
将影响范围
,限制
在当前类
中,可借助父类
的IMP
实现,但不可
让交换主体
变成父类
。
具体操作:
方法交换前,先尝试给自己添加待交换
的方法
,再将父类
的IMP
指给swizzle
。
保障交换cls
是当前对象
,不会找到父类
或继承链上层
。
#import <objc/runtime.h> //MARK: - HTRuntimeTool @interface HTRuntimeTool : NSObject + (void)ht_methodSwizzilingWithClass: (Class)cls oriSEL: (SEL)oriSel swizzledSEL: (SEL)swizzledSel; @end @implementation HTRuntimeTool + (void)ht_methodSwizzilingWithClass: (Class)cls oriSEL: (SEL)oriSel swizzledSEL: (SEL)swizzledSel { NSAssert(cls, @"传入的交换类不能为空"); // 1. 分别读取`oriMethod`和`swiMethod`. (此时的oriMethod实现可能来自于`继承链`上的`某个类`。) Method oriMethod = class_getInstanceMethod(cls, oriSel); Method swiMethod = class_getInstanceMethod(cls, swizzledSel); // 2. 被交换的函数必须实现 (想要交换的函数都没实现,就完全没有意义了) NSString * str = [NSString stringWithFormat:@"被交换的函数:[%@]没有实现",NSStringFromSelector(swizzledSel)]; NSAssert(swiMethod, str); // 3. 检查oriMethod是否存在。(不存在表示整个继承链都没有`oriSel`的实现) if (!oriMethod) { // 不存在时,为了避免crash,我们手动添加一个空的Block IMP class_addMethod(cls, oriSel, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod)); method_setImplementation(swiMethod, imp_implementationWithBlock(^{ NSLog(@"[%@]创建新对象%@",NSStringFromClass(cls) ,NSStringFromSelector(oriSel)); })); } // 4. 尝试给cls添加`oriSel`方法。 BOOL addMethod = class_addMethod(cls, oriSel, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod)); // 4.1 添加成功, 表示之前cls没有`oriSel`方法。 if (addMethod) { // `addMethod`时,我们已将`oriSel`的imp实现,并指向了swiMethod, // 所以此时,只需要将`swizzledSel`的imp实现,指向oriMethod即可。 // class_replaceMethod 是替换,覆盖的意思。等于重写绑定了`swizzledSel`的sel和imp关系 class_replaceMethod(cls, swizzledSel, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod)); } // 4.2 添加失败,表示之前cls已经存在`oriSel`方法。 else { // 需要将`oriSel`和`swizzledSel`的Imp进行交换 // method_exchangeImplementations 是交换的意思,等于将`oriMethod`和`swiMethod`的sel和imp的绑定关系进行交叉互换。 //(oriSel -> swiMethod, swizzledSel -> oriMethod) method_exchangeImplementations(oriMethod, swiMethod); } } @end //MARK: - HTPerson @interface HTPerson : NSObject - (void)personFunc; @end @implementation HTPerson - (void)personFunc { NSLog(@"HTPerson实例方法: %s", __func__); } @end //MARK: - HTStudent @interface HTStudent : HTPerson - (void)studentFunc; @end @implementation HTStudent //+ (void)load { // static dispatch_once_t onceToken; // dispatch_once(&onceToken, ^{ // [HTRuntimeTool ht_methodSwizzilingWithClass:self oriSEL:@selector(personFunc) swizzledSEL:@selector(studentFunc)]; // }); //} // 避免影响启动时长,方法交换放在initialize中实现 + (void)initialize { if (self == [HTStudent class]) { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ [HTRuntimeTool ht_methodSwizzilingWithClass:self oriSEL:@selector(personFunc) swizzledSEL:@selector(studentFunc)]; }); } } - (void)studentFunc { // 当我们将studentFunc的实现与personFunc的实现互换后,此处就不是递归调用自己了。 [self studentFunc]; NSLog(@"HTStudent实例方法: %s", __func__); } @end //MARK: -ViewController @interface ViewController () @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; HTStudent * student = [HTStudent new]; [student studentFunc]; [student personFunc]; HTPerson * person = [HTPerson new]; [person personFunc]; } @end
ht_methodSwizzilingWithClass
分析:
1:分别读取oriMethod
和swiMethod
.
(此时的oriMethod
实现可能来自于继承链
上的某个类
。)
2:被交换的函数
(swiMethod
)必须实现
(比如: 你想将HTStudent对象
的studentFunc
与HTPerson
的personFunc
进行交换,你至少得实现studentFunc
方法啊)
3:检查oriMethod
是否存在
。
(不存在表示 整个继承链
都没有oriSel
的实现)
4:如果不存在,为了避免crash
,我们手动添加
一个IMP
(内容是个空的Block
)
尝试给cls
添加oriSel
方法。
4.1 添加成功:
表示之前cls没有oriSel
方法。addMethod
时,我们已将oriSel
的IMP
实现,并指向了swiMethod
,此时只需将swizzledSel
的IMP
实现,指向oriMethod
即可。
(class_replaceMethod:是替换
,覆盖
的意思。等于重绑定
了swizzledSel
的sel
和imp
关系)
4.2 添加失败:
表示之前cls已存在oriSel
方法。
需要将oriSel
和swizzledSel
的SEL
和IMP
进行交换
(method_exchangeImplementations:是交换
的意思,等于将oriMethod
和swiMethod
的SEL
和IMP
的绑定关
系进行交叉互换
。
(oriSel -> swiMethod,swizzledSel -> oriMethod)
3:method-swizzling - 类方法
类方法和实例方法的method-swizzling的原理是类似的,唯一的区别是类方法存在元类中,所以可以做如下操作
LGStudent
中只有类方法sayHello
的声明,没有实现
@interface LGStudent : LGPerson - (void)helloword; + (void)sayHello; @end @implementation LGStudent @end
在LGStudent的分类的load方法中实现类方法的方法交换
+ (void)load{ static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ [LGRuntimeTool lg_bestClassMethodSwizzlingWithClass:self oriSEL:@selector(sayHello) swizzledSEL:@selector(lg_studentClassMethod)]; }); } + (void)lg_studentClassMethod{ NSLog(@"LGStudent分类添加的lg类方法:%s",__func__); [[self class] lg_studentClassMethod]; }
类方法的方法交换
如下
-
需要通过
class_getClassMethod
方法获取类方法
-
在调用
class_addMethod
和class_replaceMethod
方法添加和替换时,需要传入的类是元类
,元类可以通过object_getClass
方法获取类的元类
//封装的method-swizzling方法 + (void)lg_bestClassMethodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL{ if (!cls) NSLog(@"传入的交换类不能为空"); Method oriMethod = class_getClassMethod([cls class], oriSEL); Method swiMethod = class_getClassMethod([cls class], swizzledSEL); if (!oriMethod) { // 避免动作没有意义 // 在oriMethod为nil时,替换后将swizzledSEL复制一个不做任何事的空实现,代码如下: class_addMethod(object_getClass(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(object_getClass(cls), oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod)); if (didAddMethod) { class_replaceMethod(object_getClass(cls), swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod)); }else{ method_exchangeImplementations(oriMethod, swiMethod); } }
调用如下
- (void)viewDidLoad { [super viewDidLoad]; [LGStudent sayHello]; }
运行结果如下,由于符合方法没有实现
,所以会走到空的imp
中
method-swizzling最常用的应用是防止数组、字典等越界崩溃
在iOS中NSNumber
、NSArray
、NSDictionary
等这些类都是类簇,一个NSArray
的实现可能由多个类组成
。所以如果想对NSArray进行Swizzling,必须获取到其“真身”进行Swizzling,直接对NSArray进行操作是无效的
。
下面列举了NSArray和NSDictionary本类的类名,可以通过Runtime函数取出本类。
下面列举了NSArray和NSDictionary本类的类名,可以通过Runtime函数取出本类。
以 NSArray 为例
类名 | 真身 |
---|---|
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
引用
1:OC底层原理二十一:内存平移 & Mothod Swizzling的应用
2:iOS-底层原理 21:Method-Swizzling 方法交换