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面向切面进行提取封装,提取各个模块中的公共部分,提高模块的复用率,降低业务之间的耦合性
  • 每个类都维护着一个方法列表,即methodListmethodList中有不同的方法Method,每个方法中包含了方法的selIMP,方法交换就是将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方法。所以我们将HTStudentstudentFunc方法与HTPersonpersonFunc方法进行了交换。

  • 当使用子类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:分别读取oriMethodswiMethod.

(此时的oriMethod实现可能来自于继承链上的某个类。)

2:被交换的函数(swiMethod)必须实现

(比如: 你想将HTStudent对象studentFuncHTPersonpersonFunc进行交换,你至少得实现studentFunc方法啊)

3:检查oriMethod是否存在

(不存在表示 整个继承链都没有oriSel的实现)

4:如果不存在,为了避免crash,我们手动添加一个IMP(内容是个空的Block)

尝试给cls添加oriSel方法。 

4.1 添加成功:
表示之前cls没有oriSel方法。
addMethod时,我们已将oriSelIMP实现,并指向了swiMethod,此时只需将swizzledSelIMP实现,指向oriMethod即可。
(class_replaceMethod:替换覆盖的意思。等于重绑定swizzledSelselimp关系)

4.2 添加失败:
表示之前cls已存在oriSel方法。
需要将oriSelswizzledSelSELIMP进行交换
(method_exchangeImplementations:交换的意思,等于将oriMethodswiMethodSELIMP绑定关系进行交叉互换
(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_addMethodclass_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

4:method-swizzling的应用

method-swizzling最常用的应用是防止数组、字典等越界崩溃

在iOS中NSNumberNSArrayNSDictionary等这些类都是类簇,一个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 方法交换

3:OC基础知识点之-Method Swizzling黑魔法(方法交换)

4:iOS - 方法交换Method-Swizzling 

posted on 2020-12-02 23:50  风zk  阅读(202)  评论(0编辑  收藏  举报

导航