Runtime常用场景之黑魔法Method Swizzling

什么是Method Swizzling 

Method Swizzling是改变一个已存在的selector的实现的技术。可以使用它来在利用iOS的runtime机制通过修改类的分发表中selector对应的函数,来修改selector的实现。也就是说它可以修改系统的方法,本质上就是对IMP和SEL进行交换。

SEL:系统在编译过程中,会根据方法的名字以及参数序列生成一个用来区分这个方法的唯一ID编号,这个 ID 就是 SEL 类型的。我们需要注意的是,只要方法的名字和参数序列完全相同,那么它们的 ID编号就是相同的。

IMP:即Implementation,为指向函数实现的指针,如果我们能够获取到这个指针,则可以直接调用该方法,充分证实了它就是一个函数的指针。

额外说下Method:字面上一看就是方法的意思。Method其实就是 objc_method的结构体指针。

struct objc_object {  
    Class isa  OBJC_ISA_AVAILABILITY;
};

struct objc_class {  
    Class isa  OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
    Class super_class;
    const char *name; long version; long info; long instance_size; struct objc_ivar_list *ivars; **struct objc_method_list **methodLists**; **struct objc_cache *cache**; struct objc_protocol_list *protocols; #endif }; struct objc_method_list { struct objc_method_list *obsolete; int method_count; #ifdef __LP64__ int space; #endif /* variable length structure */ struct objc_method method_list[1]; }; struct objc_method { SEL method_name; char *method_types; /* a string representing argument/return types */ IMP method_imp; };

objc_method_list实际上就是有着objc_method元素的可变数组,一个objc_method结构体里面有SEL也就是函数名,有表示函数类型的字符串,以及函数的实现IMP。
从这些定义中可以看出发送一条消息也就是objc_msgSend实际上做了什么事情:
1.首先通过obj的isa指针找到它的class。
2.然后在class的method_list列表中找我们要实现的函数。
3.如果没有找到,那就继续去它的superClass中找。
4.一旦找到我们要实现的函数,就去执行它的实现IMP。

Method Swizzling原理

Method Swizzing是发生在运行时的,主要用于在运行时将两个Method进行交换,我们可以将Method Swizzling代码写到任何地方,但是只有在这段Method Swilzzling代码执行完毕之后互换才起作用。

在OC语言的runtime特性中,调用一个对象的方法就是给这个对象发送消息。是通过查找接收消息对象的方法列表,从方法列表中查找对应的SEL,这个SEL对应着一个IMP(一个IMP可以对应多个SEL),通过这个IMP找到对应的方法调用,归根到底是函数指针的调用,我们可以改变Method结构体中IMP函数实现指针的指向:

以实例方法为例上两张原理图:

交换前:

交换后:

在每个类中都有一个Dispatch Table,这个Dispatch Table本质是将类中的SEL和IMP(可以理解为函数指针)进行对应。而我们的Method Swizzling就是对这个table进行了操作,让SEL对应另一个IMP。

 

Method Swizzling使用

 

1.页面添加统计功能:

因为Method Swizzling的实现模式比较固定,所以将其抽象为一个分类,可以直接方便调用。

#import <Foundation/Foundation.h> 
    
@interface NSObject (Swizzling)
    
+(void)methodSwizzlingWithOriginalSelector:(SEL)originalSelector bySwizzledSelector:(SEL)swizzledSelector;
    
@end
#import "NSObject+Swizzling.h" 
#import <objc/runtime.h> 
    
@implementation NSObject (Swizzling)
+(void)methodSwizzlingWithOriginalSelector:(SEL)originalSelector bySwizzledSelector:(SEL)swizzledSelector
{
    Class class = [self class];
    Method originalMethod = class_getInstanceMethod(class, originalSelector);
    Method swizzleMethod = class_getInstanceMethod(class, swizzledSelector);
    BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzleMethod), method_getTypeEncoding(swizzleMethod));
    if (didAddMethod) {
        class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
    } else{
        method_exchangeImplementations(originalMethod, swizzleMethod);
    }
}
@end
  • class_addMethod:实现会覆盖父类的方法实现。但不会取代本类中已存在的实现,如果本类中包含一个同名的实现,则函数返回NO。这里为源SEL添加IMP是为了避免源SEL没有实现IMP的情况。若添加成功则说明源SEL没有实现IMP,将源SEL的IMP替换到交换SEL的IMP;若添加失败则说明源SEL已经有IMP了,直接将两个SEL的IMP交换就可以了。
  • class_replaceMethod:该函数的行为可以分为两种:如果类中不存在name指定的方法,则类似于clss_addMethod函数一样会添加方法;如果类中已存在name指定的方法,则类似于method_setImplementation一样代替原方法的实现。

统计页面展示次数,我们可以先给UIViewController添加一个Category,然后再Category中的+(void)load方法中添加Method Swizzling方法。我们用来天的方法也可以写在这个分类中。

#import <UIKit/UIKit.h> 
@interface UIViewController (Swizzling)

@end
#import "UIViewController+Swizzling.h" 
#import "NSObject+Swizzling.h" 
    
@implementation UIViewController (Swizzling)
    
+(void)load
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [UIViewController methodSwizzlingWithOriginalSelector:@selector(viewDidLoad) bySwizzledSelector:@selector(my_ViewDidLoad)];
    });
}

-(void) my_ViewDidLoad
{
    [self my_ViewDidLoad];
    NSString *str = [NSString stringWithFormat:@"%@", self.class];
    // 我们在这里加一个判断,将系统的UIViewController的对象剔除掉
    if(![str containsString:@"UI"]){
        NSLog(@"统计打点 : %@", self.class);
    }
}
    
@end

有几点需要注意下:

1.Swizzling应该总在+load中执行

Runtime会在类被加载到内存时调用+load方法,在类第一次被调用时实现+initialize方法。由于Method Swizzling会影响到类的全局状态,所以要尽量避免在并发处理中出现竞争情况。+load方法能保证在类的初始化过程中被加载,并保证这种改变应用级的行为的一致性。

2.要使用dispatch_once执行方法交换

方法交换要求线程安全,而且保证任何情况下只交换一次。

3.可能会有人有疑问,在my_ViewDidLoad中又调用了一次my_ViewDidLoad不会引起递归调用吗

实际上Method Swizzling实现原理可以理解为“方法互换”,当调用viewDidLoad的时候实际上调用的是my_ViewDidLoad,而在my_ViewDidLoad中再次调用my_ViewDidLoad的时候实际上调用的是viewDidLoad。

 

2.对于通用模块基于系统方法的扩展,比如我们要统计页面停留时长,那么我们需要在 - (void)viewDidAppear:(BOOL)animated;以及 - (void)viewDidDisappear:(BOOL)animated;具体实现和上面类同。

 

Method Swizzling类簇

 

当你交换NSArray等类簇的方法的时候,因为这些类簇类,其实是一种抽象工厂的设计模式。抽象工厂内部有很多其它继承自当前类的子类,抽象工厂类会根据不同情况,创建不同的抽象对象来进行调用。所以对NSArray、NSMutableArray、NSDictionary、NSMutableDictionary等类进行Method Swizzling,实现方式还是按照上面的例子来做。但是....你发现Method Swizzling根本就不起作用。所以也就是我们对NSArray类进行操作其实只是对父类进行了操作,在NSArray内部会创建其他子类来执行操作,真正执行操作的并不是NSArray自身,所以我们应该对其“真身”进行操作。

下面我们实现了防止NSArray因为调用objectAtIndex:方法,取下标时数组越界导致的崩溃:

 

#import "NSArray+SwizzArray.h"
#import "objc/runtime.h"
@implementation NSArray (SwizzArray)
+ (void)load {
    [super load];
    Method fromMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(objectAtIndex:));
    Method toMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(lxz_objectAtIndex:));
    method_exchangeImplementations(fromMethod, toMethod);
}
 
- (id)lxz_objectAtIndex:(NSUInteger)index {
    if (self.count-1 < index) {
        // 这里做一下异常处理,不然都不知道出错了。
        @try {
            return [self lxz_objectAtIndex:index];
        }
        @catch (NSException *exception) {
            // 在崩溃后会打印崩溃信息,方便我们调试。
            NSLog(@"---------- %s Crash Because Method %s  ----------\n", class_getName(self.class), __func__);
            NSLog(@"%@", [exception callStackSymbols]);
            return nil;
    }
        @finally {}
    } else {
        return [self lxz_objectAtIndex:index];
    }
}
@end

由此可见,实际上__NSArrayI才是NSArray真正的类。

关于Method Swizzling类簇这块,可以直接使用AvoidCrash -- 远离常见的崩溃,但是往往快刀都是一把双刃剑,具体使用还要看情况。

 

参考文章:

Method Swizzling 和 AOP 实践

iOS黑魔法-Method Swizzling

Runtime奇技淫巧之method_exchangeImplementations

Runtime深度解析以及实用技巧(不扯淡,不套路)

 

 

 

转载于:https://www.cnblogs.com/byxixiblogs/p/7477758.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值