专栏名称: Adrenine
iOS开发
目录
相关文章推荐
51好读  ›  专栏  ›  Adrenine

Method Swizzling的各种姿势

Adrenine  · 掘金  ·  · 2017-12-13 08:46

正文

因为Objective-C的runtime机制, Method Swizzling这个黑魔法解决了我们实际开发中诸多常规手段所无法解决的问题, 比如代码的插桩,Hook,Patch等等. 我们首先看看常规的Method Swizzling是怎样用的, NSHipster有一篇介绍基本用法的文章 Method Swizzling , 我们就先以这篇文章中的示例开始说起吧:

#import 
@implementation UIViewController (Tracking)
+ (void)load {
  staticdispatch_once_t onceToken;
  dispatch_once(&onceToken, ^{
    Classclass= [self class];
    SEL originalSelector = @selector(viewWillAppear:);
    SEL swizzledSelector = @selector(xby_viewWillAppear:);
    
    Method originalMethod = class_getInstanceMethod(class, originalSelector);
    Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
    
    BOOL didAddMethod = class_addMethod(class, originalSelector,
method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
    
    if(didAddMethod) {
      class_replaceMethod(class, swizzledSelector,
method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
    }else{
      method_exchangeImplementations(originalMethod, swizzledMethod);
    }
  });
}

#pragma mark - Method Swizzling
- (void)xby_viewWillAppear:(BOOL)animated {
  [self xby_viewWillAppear:animated];//因为方法替换,这里相当于调用的是系统的viewWillAppear,所以自然也就不会造成循环调用的问题
  NSLog(@"viewWillAppear: %@", self);
}
@end

简要说明一下以上代码的几个重点: 通过在Category的+ (void)load方法中添加Method Swizzling的代码,在类初始加载时自动被调用,load方法按照父类到子类,类自身到Category的顺序被调用. 在dispatch_once中执行Method Swizzling是一种防护措施,以保证代码块只会被执行一次并且线程安全,不过此处并不需要,因为当前Category中的load方法并不会被多次调用. 尝试先调用class_addMethod方法,以保证即便originalSelector只在父类中实现,也能达到Method Swizzling的目的. xby_viewWillAppear:方法中 [self xby_viewWillAppear:animated]; 代码并不会造成死循环,因为Method Swizzling之后,调用xby_viewWillAppear:实际执行的代码已经是原来viewWillAppear中的代码了. 其实以上的代码也可以简写为以下:

+ (void)load {
  Classclass= [self class];
  SEL originalSelector = @selector(viewWillAppear:);
  SEL swizzledSelector = @selector(xby_viewWillAppear:);
  
  Method originalMethod = class_getInstanceMethod(class, originalSelector);
  Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
  
  if(!originalMethod || !swizzledMethod) {
    return;
  }

  IMP originalIMP = method_getImplementation(originalMethod);
  IMP swizzledIMP = method_getImplementation(swizzledMethod);

  const char *originalType = method_getTypeEncoding(originalMethod);
  const char *swizzledType = method_getTypeEncoding(swizzledMethod);

  // 这儿的先后顺序是有讲究的,如果先执行后一句,那么在执行完瞬间方法被调用容易引发死循环
  class_replaceMethod(class,swizzledSelector,originalIMP,originalType);
  class_replaceMethod(class,originalSelector,swizzledIMP,swizzledType);
}

这是因为class_replaceMethod方法其实能够覆盖到class_addMethod和method_setImplementation两种场景, 对于第一个class_replaceMethod来说, 如果viewWillAppear:实现在父类, 则执行class_addMethod, 否则就执行method_setImplementation将原方法的IMP指定新的代码块; 而第二个class_replaceMethod完成的工作便只是将新方法的IMP指向原来的代码. 但此处需要特别注意交换的顺序,应该优化把新的方法指定原IMP,再修改原有的方法的IMP. 除了以上的场景之外,其它场景下我们如何使用Method Swizzling呢?


1.在不同类之间实现Method Swizzling 上面示例是通过Category来新增一个方法然后实现Method Swizzling的, 但有一些场景可能并不适合使用Category(比如私有的类,未获取到该类的声明), 此时我们应该如何来做Method Swizzling呢? 例如已知一个className为Car的类中有一个实例方法- (void)run:(double)speed, 目前需要Hook该方法对速度小于120才执行run的代码, 按照方法交换的流程, 代码应该是这样的:

#import 
@interface MyCar : NSObject
@end
@implementation MyCar
+ (void)load {
  Class originalClass = NSClassFromString(@"Car");
  Class swizzledClass = [self class];

  SEL originalSelector = NSSelectorFromString(@"run:");
  SEL swizzledSelector = @selector(xby_run:);

  Method originalMethod = class_getInstanceMethod(originalClass, originalSelector);
  Method swizzledMethod = class_getInstanceMethod(swizzledClass, swizzledSelector);

  // 向Car类中新添加一个xby_run:的方法
  BOOL registerMethod = class_addMethod(originalClass, swizzledSelector,
method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
  if(!registerMethod) {
    return;
  }
  
  // 需要更新swizzledMethod变量,获取当前Car类中xby_run:的Method指针
  swizzledMethod = class_getInstanceMethod(originalClass, swizzledSelector);
  if(!swizzledMethod) {
    return;
  }
  // 后续流程与之前的一致
  BOOL didAddMethod = class_addMethod(originalClass, originalSelector,
method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));

  if(didAddMethod) {
    class_replaceMethod(originalClass, swizzledSelector,
method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
  }else{
    method_exchangeImplementations(originalMethod, swizzledMethod);
  }
}

- (void)xby_run:(double)speed {
  if(speed < 120) {
    [self xby_run:speed];
  }
}
@end

与之前的流程相比,在前面添加了两个逻辑: 利用runtime向目标类Car动态添加了一个新的方法,此时Car类与MyCar类一样具备了xby_run:这个方法,MyCar的利用价值便结束了; 为了完成后续Car类中run:与xby_run:的方法交换,此时需要更新swizzledMethod变量为Car中的xby_run:方法所对应的Method. 以上所有的逻辑也可以合并简化为以下:

+ (void)load {
  Class originalClass = NSClassFromString(@"Car");
  Class swizzledClass = [selfclass];

  SEL originalSelector = NSSelectorFromString(@"run:");
  SEL swizzledSelector = @selector(xby_run:);

  Method originalMethod = class_getInstanceMethod(originalClass, originalSelector);
  Method swizzledMethod = class_getInstanceMethod(swizzledClass, swizzledSelector);

  IMP originalIMP = method_getImplementation(originalMethod);
  IMP swizzledIMP = method_getImplementation(swizzledMethod);

  const char *originalType = method_getTypeEncoding(originalMethod);
  const char *swizzledType = method_getTypeEncoding(swizzledMethod);

  class_replaceMethod(originalClass,swizzledSelector,originalIMP,originalType);
  class_replaceMethod(originalClass,originalSelector,swizzledIMP,swizzledType);
}

简化后的代码便与之前使用Category的方式并没有什么差异, 这样代码就很容易覆盖到这两种场景了, 但我们需要明确此时class_replaceMethod所完成的工作却是不一样的. 第一个class_replaceMethod直接在Car类中注册了xby_run:方法,并且指定的IMP为当前run:方法的IMP; 第二个class_replaceMethod与之前的逻辑一致,当run:方法是实现在Car类或Car的父类,分别执行method_setImplementation或class_addMethod;

2.如何实现类方法的Method Swizzling 以上的代码都是实现的对实例方法的交换, 那如何来实现对类方法的交换呢, 依旧直接贴代码吧:

@interface NSDictionary (Test)
@end

@implementation NSDictionary (Test)
+ (void)load {
  Class cls = [self class];
  SEL originalSelector = @selector(dictionary);
  SEL swizzledSelector = @selector(xby_dictionary);

  // 使用class_getClassMethod来获取类方法的Method
  Method originalMethod = class_getClassMethod(cls, originalSelector);
  Method swizzledMethod = class_getClassMethod(cls, swizzledSelector);
  if(!originalMethod || !swizzledMethod) {
    return;
  }

  IMP originalIMP = method_getImplementation(originalMethod);
  IMP swizzledIMP = method_getImplementation(swizzledMethod);
  const char *originalType = method_getTypeEncoding(originalMethod);
  const char *swizzledType = method_getTypeEncoding(swizzledMethod);

  // 类方法添加,需要将方法添加到MetaClass中
  Class metaClass = objc_getMetaClass(class_getName(cls));
  class_replaceMethod(metaClass,swizzledSelector,originalIMP,originalType);
  class_replaceMethod(metaClass,originalSelector,swizzledIMP,swizzledType);
}

+ (id)xby_dictionary {
  id result = [self xby_dictionary];
  return result;
}
@end

相比实例方法的Method Swizzling,流程有两点差异: 获取Method的方法变更为class_getClassMethod(Class cls, SEL name),从函数命名便直观体现了和class_getInstanceMethod(Class cls, SEL name)的差别; 对于类方法的动态添加,需要将方法添加到MetaClass中,因为实例方法记录在class的method-list中,类方法是记录在meta-class中的method-list中的.







请到「今天看啥」查看全文