前言
- MethodSwizzle顧名思義是方法交換,也就是交換方法IMP實現。一般能做很多面向切面的事,但是如果使用不當,就會踩到不少坑。
- 一般是在 + load 中執行方法交換的。因為load方法加載時機較早,基本能確保方法已交換。
- 需要確保交換的方法是本類的方法,而不是父類的。直接交換父類方法,會影響其它子類。
- 方法交換時還需要特別注意類簇,確保交換的是正確的類。
- 實例方法存儲在類對象中,類方法存儲在元類對象中。
-
經典isa流程圖:
isa流程圖.png
開始玩一下
一、首先簡單實現一下方法交換
/**
方法交換
@param origSel 原方法名
@param newSel 新方法名
*/
+(void) methodSwizzleWithOrigSel:(SEL)origSel newSel:(SEL)newSel
{
//類對象(實例方法存儲在類對象中) -- 由于此方法是類方法,所以self是類對象
Class mClass = [self class];
//方法
Method origMethod = class_getInstanceMethod(mClass, origSel);
Method newMethod = class_getInstanceMethod(mClass, newSel);
//imp
IMP origIMP = method_getImplementation(origMethod);
IMP newIMP = method_getImplementation(newMethod);
method_setImplementation(origMethod, newIMP);
method_setImplementation(newMethod, origIMP);
}
- 創建類Person及其子類Student、Student2
Person:
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface Person : NSObject
//名稱
@property (nonatomic,copy) NSString *name;
//年齡
@property (nonatomic,assign) NSInteger age;
-(NSString *)name;
@end
NS_ASSUME_NONNULL_END
#import "Person.h"
@implementation Person
-(NSString *)name
{
return @"person";
}
@end
Student:
#import "Person.h"
NS_ASSUME_NONNULL_BEGIN
@interface Student : Person
//學科
@property (nonatomic,copy) NSString *subject;
@end
NS_ASSUME_NONNULL_END
Student2:
#import "Person.h"
NS_ASSUME_NONNULL_BEGIN
@interface Student2 : Person
//別名
@property (nonatomic,copy) NSString *nickName;
@end
NS_ASSUME_NONNULL_END
- 我們來hook get方法(在Student類中hook name方法)
#import "Student.h"
#import "NSObject+MethodSwizzle.h"
@implementation Student
+(void) load
{
[self methodSwizzleWithOrigSel:@selector(name) newSel:@selector(myName)];
}
-(NSString *) myName
{
return @"學生1";
}
@end
- 在ViewController中測試
#import "ViewController.h"
#import "Student.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
Student *stu = [[Student alloc] init];
NSLog(@"%@",stu.name);
}
@end
輸出結果:
image.png
- 在ViewController中增加Student2的name輸出
#import "ViewController.h"
#import "Student.h"
#import "Student2.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
Student *stu = [[Student alloc] init];
NSLog(@"%@",stu.name);
Student2 *stu2 = [[Student2 alloc] init];
NSLog(@"%@",stu2.name);
}
@end
輸出結果:
image.png
- 結論
由于子類Student交換的是父類Person的name方法,所以影響了其它子類調用父類的name方法,都會變成調用Student的myName方法。
二、修改一下方法交換的實現
- 判斷子類是否有實現需要交換的方法,沒有實現則添加
/**
方法交換
@param origSel 原方法名
@param newSel 新方法名
*/
+(void) methodSwizzleWithOrigSel:(SEL)origSel newSel:(SEL)newSel
{
//類對象(實例方法存儲在類對象中) -- 由于此方法是類方法,所以self是類對象
Class mClass = [self class];
//方法
Method origMethod = class_getInstanceMethod(mClass, origSel);
Method newMethod = class_getInstanceMethod(mClass, newSel);
//imp
IMP origIMP = method_getImplementation(origMethod);
IMP newIMP = method_getImplementation(newMethod);
//方法添加成功代表target中不包含原方法,可能是其父類包含(交換父類方法可能有意想不到的問題)
if(class_addMethod(mClass, origSel, origIMP, method_getTypeEncoding(origMethod))){
//直接替換新添加的方法
class_replaceMethod(mClass, origSel, newIMP, method_getTypeEncoding(newMethod));
}else{
method_setImplementation(origMethod, newIMP);
method_setImplementation(newMethod, origIMP);
}
}
-
再次在ViewController中測試,其代碼不變,輸出結果:
image.png 結論
由此看出,此時交換的本類的方法,不會影響其它子類調用方法。但是還有問題,當父類方法只聲明了,沒有實現的話,而你在交換的方法中又需要調用原方法的時候,會產生死遞歸。
- 上述描述問題重現
在Person類增加say方法但不實現,Student類中交換say方法
@interface Person : NSObject
//名稱
@property (nonatomic,copy) NSString *name;
//年齡
@property (nonatomic,assign) NSInteger age;
-(NSString *)name;
-(void) say;
@end
@implementation Student
+(void) load
{
[self methodSwizzleWithOrigSel:@selector(say) newSel:@selector(mySay)];
}
-(NSString *) myName
{
return @"學生1";
}
-(void) mySay
{
NSLog(@"%@",@"學生1說話");
//調用父類方法
[self mySay];
}
@end
輸出結果:
image.png
三、再次修改一下方法交換的實現
- 判斷原方法是否有實現,沒有實現添加一個空實現
/**
方法交換
@param origSel 原方法名
@param newSel 新方法名
*/
+(void) methodSwizzleWithOrigSel:(SEL)origSel newSel:(SEL)newSel
{
//類對象(實例方法存儲在類對象中) -- 由于此方法是類方法,所以self是類對象
Class mClass = [self class];
//方法
Method origMethod = class_getInstanceMethod(mClass, origSel);
Method newMethod = class_getInstanceMethod(mClass, newSel);
if (!origMethod) {//原方法沒實現
class_addMethod(mClass, origSel, imp_implementationWithBlock(^(id self, SEL _cmd){}), "v16@0:8");
origMethod = class_getInstanceMethod(mClass, origSel);
}
//imp
IMP origIMP = method_getImplementation(origMethod);
IMP newIMP = method_getImplementation(newMethod);
//方法添加成功代表target中不包含原方法,可能是其父類包含(交換父類方法可能有意想不到的問題)
if(class_addMethod(mClass, origSel, origIMP, method_getTypeEncoding(origMethod))){
//直接替換新添加的方法
class_replaceMethod(mClass, origSel, newIMP, method_getTypeEncoding(newMethod));
}else{
method_setImplementation(origMethod, newIMP);
method_setImplementation(newMethod, origIMP);
}
}
輸出結果:
image.png
四、交換類方法
- 類方法交換基本和實例方法交換差不多
- 需要注意的是類方法其實是元類的實例方法,class_getClassMethod實際上內部還是調用class_getInstanceMethod。
/***********************************************************************
* class_getClassMethod. Return the class method for the specified
* class and selector.
**********************************************************************/
Method class_getClassMethod(Class cls, SEL sel)
{
if (!cls || !sel) return nil;
return class_getInstanceMethod(cls->getMeta(), sel);
}
- 所以只要確保class_getInstanceMethod方法中的第一個參數是元類對象,我們就可以直接調用class_getInstanceMethod來獲取類方法,從而減少調用class_getClassMethod時需要的元類判斷。
/**
類方法交換
@param origSel 原類方法名
@param newSel 新類方法名
*/
+(void) methodSwizzleWithOrigClassSel:(SEL)origSel newClassSel:(SEL)newSel
{
//元類(類方法存儲在元類對象中)
Class metaClass = object_getClass([self class]);
//方法
Method origMethod = class_getInstanceMethod(metaClass, origSel);
Method newMethod = class_getInstanceMethod(metaClass, newSel);
if (!origMethod) {//原方法沒實現
class_addMethod(metaClass, origSel, imp_implementationWithBlock(^(id self, SEL _cmd){}), "v16@0:8");
origMethod = class_getInstanceMethod(metaClass, origSel);
}
//imp
IMP origIMP = method_getImplementation(origMethod);
IMP newIMP = method_getImplementation(newMethod);
//方法添加成功代表target中不包含原方法,可能是其父類包含(交換父類方法可能有意想不到的問題)
if(class_addMethod(metaClass, origSel, origIMP, method_getTypeEncoding(origMethod))){
//直接替換新添加的方法
class_replaceMethod(metaClass, origSel, newIMP, method_getTypeEncoding(newMethod));
}else{
method_setImplementation(origMethod, newIMP);
method_setImplementation(newMethod, origIMP);
}
}
五、交換類簇的方法
1、問題
- 創建NSArray的分類,檢測數組越界
#import "NSArray+CheckSize.h"
#import "NSObject+MethodSwizzle.h"
@implementation NSArray (CheckSize)
+(void) load
{
[self methodSwizzleWithOrigSel:@selector(objectAtIndex:) newSel:@selector(myObjectAtIndex:)];
}
- (id)myObjectAtIndex:(NSUInteger)index
{
if (index > [self count] - 1) {
NSLog(@"數組越界了");
return nil;
}
return [self myObjectAtIndex:index];
}
@end
- 在ViewController中測試
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
NSArray *array = @[@"1",@"2",@"3"];
for (int i = 0 ; i < 4; i++) {
NSLog(@"%d-->%@",i,[array objectAtIndex:i]);
}
}
@end
輸出結果:
image.png
-
斷點檢測是否有調用方法交換
image.png 結論
方法交換確實調用了,那就是已經把NSArray的objectAtIndex方法改成分類中的myObjectAtIndex。但是并不管用,一樣奔潰,原因就是因為類簇,可以從崩潰信息中看到實際調用的類是__NSArrayI。
2、解決
- 寫一個新的方法交換方法,支持設置OrigTarget
/**
方法交換
@param origTarget 被交換方法的類
@param origSel 原方法名
@param newSel 新方法名
*/
+(void) methodSwizzleWithOrigTarget:(Class)origTarget OrigSel:(SEL)origSel newSel:(SEL)newSel
{
//類對象(實例方法存儲在類對象中)
Class origClass = origTarget;
if ([origTarget isKindOfClass:[origTarget class]]) {//成立則origTarget為實例對象
origClass = object_getClass(origTarget);
}
//方法
Method origMethod = class_getInstanceMethod(origClass, origSel);
Method newMethod = class_getInstanceMethod(origClass, newSel);
if (!origMethod) {//原方法沒實現
class_addMethod(origClass, origSel, imp_implementationWithBlock(^(id self, SEL _cmd){}), "v16@0:8");
origMethod = class_getInstanceMethod(origClass, origSel);
}
//imp
IMP origIMP = method_getImplementation(origMethod);
IMP newIMP = method_getImplementation(newMethod);
//方法添加成功代表target中不包含原方法,可能是其父類包含(交換父類方法可能有意想不到的問題)
if(class_addMethod(origClass, origSel, origIMP, method_getTypeEncoding(origMethod))){
//直接替換新添加的方法
class_replaceMethod(origClass, origSel, newIMP, method_getTypeEncoding(newMethod));
}else{
method_setImplementation(origMethod, newIMP);
method_setImplementation(newMethod, origIMP);
}
}
- 修改NSArray分類中的交換方法
#import "NSArray+CheckSize.h"
#import "NSObject+MethodSwizzle.h"
@implementation NSArray (CheckSize)
+(void) load
{
[self methodSwizzleWithOrigTarget:NSClassFromString(@"__NSArrayI") OrigSel:@selector(objectAtIndex:) newSel:@selector(myObjectAtIndex:)];
}
- (id)myObjectAtIndex:(NSUInteger)index
{
if (index > [self count] - 1) {
NSLog(@"數組越界了");
return nil;
}
return [self myObjectAtIndex:index];
}
@end
-
再次測試,輸出結果
image.png
六、結論
MethodSwizzle雖然好用,但是一不小心,可能讓你找半天都不知道問題出在哪。特別注意多人開發,同時使用了方法交換相同的方法,會有很多意想不到的問題。