iOS——方法互换Method Swizzing

打印 上一主题 下一主题

主题 534|帖子 534|积分 1602

什么是方法互换

Method Swizzing是发生在运行时的,主要用于在运行时将两个Method举行互换,我们可以将Method Swizzling代码写到任何地方,但是只有在这段Method Swilzzling代码执行完毕之后互换才起作用。
利用Objective-C Runtimee的动态绑定特性,将一个方法的实现与另一个方法的实现举行互换。互换两个方法的实现一般写在分类的load方法内里,因为load方法会在程序运行前加载一次,而initialize方法会在类或者子类在 第一次利用的时间调用,当有分类的时间会调用多次。

方法互换的方式


  • 获取方法的 SEL 和 IMP

    • 利用 class_getInstanceMethod 或 class_getClassMethod 函数获取方法的 Method 布局体。
    • 从 Method 布局体中获取 SEL 和 IMP。

  • 互换方法的 IMP

    • 利用 method_exchangeImplementations 函数互换两个方法的实现。
    • 或者利用 class_replaceMethod 函数更换方法的实现。

  1.     // 类中获取oriSEL对应的方法实现
  2.     Method oriMethod = class_getInstanceMethod(cls, oriSEL);
  3.     // 获取swiSEL对应的方法实现
  4.     Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);
  5.     // 将两个方法实现进行交换,
  6.     method_exchangeImplementations(oriMethod, swiMethod);
复制代码
在举行方法互换操作时,发起放在单例下举行,以确保该操作只执行一次,避免重复调用导致互换效果被反转,从而失去互换的目标。
通过上面的方法可以明白,互换的是两者的方法实现。
方法互换的四个风险

直接利用 Runtime 的方法举行方法互换会有很多风险,RSSwizzle库里指出了四个典范的直接利用 Runtime 方法举行方法互换的风险。


  • 第一个风险是,须要在 +load 方法中举行方法互换。因为如果在其他时间举行方法互换,难以包管别的一个线程中不会同时调用被互换的方法,从而导致程序不能按预期执行。而在 +load 方法中执行方法互换,确保互换在类加载时完成,从而避免线程竞争和其他时机相关的问题。
  • 第二个风险是,被互换的方法必须是当前类的方法,不能是父类的方法,直接把父类的实现拷贝过来不会起作用。父类的方法必须在调用的时间利用,而不是方法互换时利用。方法互换只能作用于当前类的方法,不能影响父类的方法。
  • 第三个风险是,互换的方法如果依靠了 cmd,那么互换后,如果 cmd 发生了变革,就会出现各种奇怪问题,而且这些问题还很难排查。特别是互换了系统方法,你无法包管系统方法内部是否依靠了 cmd。(cmd参数表示当前调用的方法)
  • 第四个风险是,方法互换命名辩说。如果出现辩说,可能会导致方法互换失败。
   load方法的特点
+load方法在类加载时调用,确保方法互换在任何实例方法调用之前完成。
一般情况下load方法在每个类中都只会调用一次。
+load方法自动调用,不会被多个线程同时调用,联合 dispatch_once 确保线程安全。
+load方法自动执行,减少开发者的工作量。
  第三个风险详解

第三个风险的意思是,两个方法实现互换后,_cmd却不一定。
   _cmd回首
_cmd 是埋伏的参数,表示当前方法的selector,他和self表示当前方法调用的对象实例。
获取当前被调用方法: NSStringFromSelector(_cmd)
  比如下面这个例子:
我们首先创建了一个ViewController类,在这个类中我们写出将被互换的原方法orimed,然后创建一个swizzled分类,在分类中写出互换后的方法
  1. #import "ViewController.h"
  2. #import "ViewController+swizzled.h"
  3. #import <objc/runtime.h>
  4. @interface ViewController ()
  5. @property (assign, nonatomic) int ticketsCount;
  6. @end
  7. @implementation ViewController
  8. + (void)load {
  9.     static dispatch_once_t onceToken;
  10.     dispatch_once(&onceToken, ^{
  11.         SEL oriSEL = @selector(orimed);
  12.         SEL swiSEL = @selector(swizzledSelector);
  13.         
  14.         Method oriMethod = class_getInstanceMethod([self class], oriSEL);
  15.         Method swiMethod = class_getInstanceMethod([self class], swiSEL);
  16.         method_exchangeImplementations(oriMethod, swiMethod);
  17.     });
  18. }
  19. - (void)viewDidLoad {
  20.     [super viewDidLoad];
  21.     [self orimed];
  22. }
  23. - (void) orimed {
  24.     NSLog(@"交换前的方法");
  25. }
复制代码
  1. #import "ViewController.h"
  2. NS_ASSUME_NONNULL_BEGIN
  3. @interface ViewController (swizzled)
  4. - (void) swizzledSelector;
  5. @end
  6. NS_ASSUME_NONNULL_END
复制代码
  1. #import "ViewController+swizzled.h"
  2. @implementation ViewController (swizzled)
  3. - (void)swizzledSelector {
  4.     NSLog(@"方法已交换");
  5.     //然后我们在这个方法中打印当前方法的selector
  6.     NSLog(@"%@", NSStringFromSelector(_cmd));
  7. }
  8. @end
复制代码
打印的效果:

我们的代码明明执行了swizzledSelector中的代码,为什么打印出的_cmd照旧orimed呢?
这是因为方法互换本质上是互换了两个方法的实现,而不是选择器,这段代码实际上是将orimed的SEL指向了swizzleSelector方法的imp,所以执行swizzledSelector的代码实现时,返回的_cmd(方法的selector)为orimed。
因此如果互换的方法依靠于 cmd 来决定行为,可能会导致日志输出的信息不符合实际调用的方法。
方法互换的实际用法

先给要更换的方法的类添加一个Category,然后在Category中的+(void)load方法中添加Method Swizzling方法,我们用来更换的方法也写在这个Category中。就像上面那个例子一样。
由于load类方法是程序运行时这个类被加载到内存中就调用的一个方法,执行比较早,在每个类中都只会调用一次,并且不须要我们手动调用。
留意:


  • Swizzling应该总在+load中执行
  • Swizzling应该总是在dispatch_once中执行
  • Swizzling在+load中执行时,不要调用[super load]。如果多次调用了[super load],可能会出现“Swizzle无效”的假象。
  • 为了避免Swizzling的代码被重复执行,我们可以通过GCD的dispatch_once函数来解决,利用dispatch_once函数内代码只会执行一次的特性。
方法互换的API

方案一:
提供了更精细的控制,可以选择性地添加、更换方法,并能处理方法不存在的情况。
  1. //获取某个类的实例方法。
  2. //cls:目标类。name:方法的选择器(selector)。
  3. class_getInstanceMethod(Class _Nullable cls, SEL _Nonnull name)
复制代码
  1. //获取方法的实现(IMP)
  2. //m:方法(Method)
  3. method_getImplementation(Method _Nonnull m)
复制代码
  1. //向类中添加一个方法及其实现。
  2. //cls:目标类。name:方法的选择器。imp:方法的实现。types:方法的类型编码(Type encoding)。
  3. class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp,
  4.                 const char * _Nullable types)
复制代码
  1. //替换类中的方法实现。如果该方法不存在,则添加这个方法。
  2. //cls:目标类。name:方法的选择器。imp:新的方法实现。types:方法的类型编码。
  3. class_replaceMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp,
  4.                     const char * _Nullable types)
复制代码
方案二:
直接互换两个方法的实现,步调简单,但是少了一些机动性。
  1. //交换两个方法的实现。
  2. method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2)
复制代码
案例分析

案例一:递归调用

我们现在在上面原先代码的底子上修改一下:
  1. #import "ViewController+swizzled.h"
  2. @implementation ViewController (swizzled)
  3. - (void)swizzledSelector {
  4.     NSLog(@"方法已交换");
  5.     //这里递归调用一下swizzledSelector方法
  6.     [self swizzledSelector];
  7. }
  8. @end
复制代码
运行效果:

可以看出,并没有发生递归调用。反而只是打印出了原方法的内容,这是为什么呢?
这是因为举行了方法互换,所以调用方法swizzledSelector,会找到orimed的方法实现,而swizzledSelector中有调用swizzledSelector,而此时它的方法实现已经指向了orimed。见下图:

案例二:互换父类的方法

有如下代码:首先,我们创建一个FatherViewController类,该类有一个fatherMethod方法,然后该类有一个子类SonViewController,子类同样有一个sonMethod方法。在子类的实现中,我们将父类的fatherMethod和子类的sonMethod方法举行互换,然后在ViewController中调用父类的fatherMethod方法:
  1. #import "FatherViewController.h"
  2. @interface FatherViewController ()
  3. @end
  4. @implementation FatherViewController
  5. - (void)viewDidLoad {
  6.     [super viewDidLoad];
  7.     // Do any additional setup after loading the view.
  8. }
  9. - (void)fatherMethod {
  10.     NSLog(@"父类的方法");
  11. }
  12. @end
复制代码
  1. #import "SonViewController.h"
  2. #import <objc/runtime.h>
  3. @interface SonViewController ()
  4. @end
  5. @implementation SonViewController
  6. + (void)load {
  7.     static dispatch_once_t onceToken;
  8.     dispatch_once(&onceToken, ^{
  9.         SEL sonSEL = @selector(sonMethod);
  10.         SEL fatherSEL = @selector(fatherMethod);
  11.         
  12.         Method sonMed = class_getInstanceMethod([self class], sonSEL);
  13.         Method fatherMed = class_getInstanceMethod([self class], fatherSEL);
  14.         
  15.         method_exchangeImplementations(sonMed, fatherMed);
  16.     });
  17. }
  18. - (void)viewDidLoad {
  19.     [super viewDidLoad];
  20.    
  21. }
  22. - (void) sonMethod {
  23.     //递归调用
  24.     [self sonMethod];
  25.     NSLog(@"子类的方法");
  26. }
  27. @end
复制代码
在ViewController中,利用子类的实例对象调用父类的方法:
  1. - (void)viewDidLoad {
  2.     [super viewDidLoad];
  3.     [[[SonViewController alloc] init] fatherMethod];
  4. }
复制代码
执行效果:

可以得出,我们乐成完成了在子类中和父类的方法举行互换。递归调用也没有出错。
但是如果此时我们在ViewController中利用父类的实例对象调用父类的方法呢?
我们现在修改ViewController中的代码:
  1. - (void)viewDidLoad {
  2.     [super viewDidLoad];
  3.     [[[FatherViewController alloc] init] fatherMethod];
  4. }
复制代码
得到的效果却是:

代码运行时发生了错误,这是因为,利用父类的实例对象调用父类的方法时,由于发生了方法互换,因此父类执行的是子类的方法实现。在子类的方法实现中又调用了sonMethod方法,但是问题来了,此时实现子类方法的调用者是父类的实例对象,父类的实例对象中压根没有sonMethod方法的实现,这就导致了找不到sonMethod方法,因而产生了错误。
在开发中,如果举行方法互换,一定要确保方法已经实现,否则会出现本例中啃爹的现象(方法互换,而父类没有方法的实现,导致报错)。所以在举行相关方法互换时,尽量避免涉及到其父类或者其子类的方法。
方法互换的应用

统计ViewController加载次数并打印

  1. #import "UIViewController+Logging.h"
  2. #import <objc/runtime.h>
  3. @implementation UIViewController (Logging)
  4. + (void)load
  5. {
  6.     swizzleMethod([self class], @selector(viewDidAppear:), @selector(swizzled_viewDidAppear:));
  7. }
  8. - (void)swizzled_viewDidAppear:(BOOL)animated
  9. {
  10.     // call original implementation
  11.     [self swizzled_viewDidAppear:animated];
  12.    
  13.     // Logging
  14.     NSLog(@"%@", NSStringFromClass([self class]));
  15. }
  16. void swizzleMethod(Class class, SEL originalSelector, SEL swizzledSelector)
  17. {
  18.     // the method might not exist in the class, but in its superclass
  19.     Method originalMethod = class_getInstanceMethod(class, originalSelector);
  20.     Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
  21.    
  22.     // class_addMethod will fail if original method already exists
  23.     BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
  24.    
  25.     // the method doesn’t exist and we just added one
  26.     if (didAddMethod) {
  27.         class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
  28.     }
  29.     else {
  30.         method_exchangeImplementations(originalMethod, swizzledMethod);
  31.     }
  32.    
  33. }
复制代码
防止UI控件短时间多次激活事件

偶然候会有这种需求,项目中的写好的按钮要求不能连续点击,这时间最方便的方法就是利用方法互换将系统的sendAction:to:forEvent:方法更换为自定义的swizzled_sendAction:to:forEvent:方法。在自定义方法中判断是否须要拦截点击事件。
UIControl+Limit.m:
  1. #import "UIControl+Limit.h"
  2. #import <objc/runtime.h>
  3. static const char *UIControl_acceptEventInterval="UIControl_acceptEventInterval";
  4. static const char *UIControl_ignoreEvent="UIControl_ignoreEvent";
  5. @implementation UIControl (Limit)
  6. #pragma mark - acceptEventInterval
  7. - (void)setAcceptEventInterval:(NSTimeInterval)acceptEventInterval
  8. {
  9.     objc_setAssociatedObject(self,UIControl_acceptEventInterval, @(acceptEventInterval), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
  10. }
  11. -(NSTimeInterval)acceptEventInterval {
  12.     return [objc_getAssociatedObject(self,UIControl_acceptEventInterval) doubleValue];
  13. }
  14. #pragma mark - ignoreEvent
  15. -(void)setIgnoreEvent:(BOOL)ignoreEvent{
  16.     objc_setAssociatedObject(self,UIControl_ignoreEvent, @(ignoreEvent), OBJC_ASSOCIATION_ASSIGN);
  17. }
  18. -(BOOL)ignoreEvent{
  19.     return [objc_getAssociatedObject(self,UIControl_ignoreEvent) boolValue];
  20. }
  21. #pragma mark - Swizzling
  22. +(void)load {
  23.     Method a = class_getInstanceMethod(self,@selector(sendAction:to:forEvent:));
  24.     Method b = class_getInstanceMethod(self,@selector(swizzled_sendAction:to:forEvent:));
  25.     method_exchangeImplementations(a, b);//交换方法
  26. }
  27. - (void)swizzled_sendAction:(SEL)action to:(id)target forEvent:(UIEvent*)event
  28. {
  29.     if(self.ignoreEvent){
  30.         NSLog(@"btnAction is intercepted");
  31.         return;}
  32.     if(self.acceptEventInterval>0){
  33.         self.ignoreEvent=YES;
  34.         [self performSelector:@selector(setIgnoreEventWithNo)  withObject:nil afterDelay:self.acceptEventInterval];
  35.     }
  36.     [self swizzled_sendAction:action to:target forEvent:event];
  37. }
  38. -(void)setIgnoreEventWithNo{
  39.     self.ignoreEvent=NO;
  40. }
  41. @end
复制代码
ViewController.m:
  1. -(void)setupSubViews{
  2.    
  3.     UIButton *btn = [UIButton new];
  4.     btn =[[UIButton alloc]initWithFrame:CGRectMake(100,100,100,40)];
  5.     [btn setTitle:@"btnTest"forState:UIControlStateNormal];
  6.     [btn setTitleColor:[UIColor redColor]forState:UIControlStateNormal];
  7.     btn.acceptEventInterval = 3;
  8.     [self.view addSubview:btn];
  9.     [btn addTarget:self action:@selector(btnAction)forControlEvents:UIControlEventTouchUpInside];
  10. }
  11. - (void)btnAction{
  12.     NSLog(@"btnAction is executed");
  13. }
复制代码
防奔溃处理:数组越界问题

在实际项目中,偶然会因为数组越界导致瓦解,须要一个解决方案来防止这种情况,纵然数组越界也不会瓦解。
通过方法互换(Swizzling)更换NSArray的objectAtIndex:方法,添加防越界处理逻辑。
无痕埋点


免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

海哥

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表