IOS 黑魔法(方法交換) -- MethodSwizzle

前言

  • 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雖然好用,但是一不小心,可能讓你找半天都不知道問題出在哪。特別注意多人開發,同時使用了方法交換相同的方法,會有很多意想不到的問題。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。