Method Swizzling的各种姿势

因为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中的.

3.在类簇中如何实现Method Swizzling
在上面的代码中我们实现了对NSDictionary中的+ (id)dictionary方法的交换,但如果我们用类似代码尝试对- (id)objectForKey:(id)key方法进行交换后, 你便会发现这似乎并没有什么用.
这是为什么呢? 平常我们在Xcode调试时,在下方Debug区域左侧的Variables View中,常常会发现如__NSArrayI或是__NSCFConstantString这样的Class类型, 这便是在Foundation框架中被广泛使用的类簇, 详情请参看Apple文档class cluster的内容.
所以针对类簇的Method Swizzling问题就转变为如何对这些类簇中的私有类做Method Swizzling, 在上面介绍的不同类之间做Method Swizzling便已经能解决该问题, 下面一个简单的示例通过交换NSMutableDictionary的setObject:forKey:方法,让调用这个方法时当参数object或key为空的不会抛出异常:

@interface MySafeDictionary : NSObject
@end

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

  SEL originalSelector = @selector(setObject:forKey:);
  SEL swizzledSelector = @selector(safe_setObject:forKey:);

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

  IMP originalIMP = method_getImplementation(originalMethod);
  IMP swizzledIMP = method_getImplementation(swizzledMethod);
constchar*originalType = method_getTypeEncoding(originalMethod);

  const char *swizzledType = method_getTypeEncoding(swizzledMethod);
  class_replaceMethod(originalClass,swizzledSelector,originalIMP,originalType);
  class_replaceMethod(originalClass,originalSelector,swizzledIMP,swizzledType);
}

- (void)safe_setObject:(id)anObject forKey:(id)aKey {
  if(anObject && aKey) {
    [self safe_setObject:anObject forKey:aKey];
  } else if(aKey) {
    [(NSMutableDictionary *)self removeObjectForKey:aKey];
  }
}
@end

4.在Method Swizzling之后如何恢复
使用了Method Swizzling的各种姿势之后, 是否有考虑如何恢复到交换之前的现场呢?
一种方案就是通过一个开关标识符, 如果需要从逻辑上面恢复到交换之前, 就设置一下这个标识符, 在实现中判定如果设定了该标识符, 逻辑就直接调用原方法的实现, 其它什么事儿也不干, 这是目前大多数代码的实现方法, 当然也是非常安全的方式, 只不过当交换方法过多时, 每一个交换的方法体中都需要增加这样的逻辑, 并且也需要维护大量这些标识符变量, 只是会觉得不够优雅, 所以此处也就不展开详细讨论了.
那下面来讨论一下有没有更好的方案, 以上描述的Method Swizzling各种场景和处理的技巧, 但综合总结之后最核心的其实也只做了两件事情:
class_addMethod添加一个新的方法,可能是把其它类中实现的方法添加到目标类中,也可能是把父类实现的方法添加一份在子类中,可能是添加的实例方法,也可能是添加的类方法,总之就是添加了方法.
交换 IMP交换方法的实现IMP,完成这个步骤除了使用method_exchangeImplementations这个方法外,也可以是调用了method_setImplementation方法来单独修改某个方法的IMP,或者是采用在调用class_addMethod方法中设定了IMP而直接就完成了IMP的交换,总之就是对IMP的交换.
那我们来分别看一下这两件事情是否都还能恢复:
对于class_addMethod,我们首先想到的可能就是有没有对应的remove方法呢,在Objective-C 1.0的时候有class_removeMethods这个方法,不过在2.0的时候就已经被禁用了,也就是苹果并不推荐我们这样做,想想似乎也是挺有道理的,本来runtime的接口看着就挺让人心惊胆战的,又是添加又是删除总觉得会出岔子,所以只能放弃remove的想法,反正方法添加在那儿倒也没什么太大的影响.
针对IMP的交换,在Method Swizzling时做的交换动作,如果需要恢复其实要做的动作还是交换回来罢了,所以是可以做到的,不过需要怎样做呢?对于同一个类,同一个方法,可能会在不同的地方被多次做Method Swizzling,所以要回退某一次的Method Swizzling,我们就需要记录下来这一次交换的时候是哪两个IMP做了交换,恢复的时候再换回来即可.另一个问题是如果已经经过多次交换,我们怎样找到这两个IMP所对应的Mehod呢,还好runtime提供了一个class_copyMethodList方法,可以直接取出Method列表,然后我们就可以逐个遍历找到IMP所对应的Method了,下面是对上一个示例添加恢复之后实现的代码逻辑:

#import 
@interface MySafeDictionary : NSObject
@end

static NSLock *kMySafeLock = nil;
static IMP kMySafeOriginalIMP = NULL;
static IMP kMySafeSwizzledIMP = NULL;

@implementation MySafeDictionary
+ (void)swizzlling {
  static dispatch_once_t onceToken;
  dispatch_once(&onceToken, ^{
    kMySafeLock = [[NSLock alloc] init];
  });
  
  [kMySafeLock lock];
  do{
    if(kMySafeOriginalIMP || kMySafeSwizzledIMP) {
      break;
    }
    Class originalClass = NSClassFromString(@"__NSDictionaryM");
    if(!originalClass) {
      break;
    }
    Class swizzledClass = [selfclass];
    SEL originalSelector = @selector(setObject:forKey:);
    SEL swizzledSelector = @selector(safe_setObject:forKey:);

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

    if(!originalMethod || !swizzledMethod) {
      break;
    }

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

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

    kMySafeOriginalIMP = originalIMP;
    kMySafeSwizzledIMP = swizzledIMP;

    class_replaceMethod(originalClass,swizzledSelector,originalIMP,originalType);
    class_replaceMethod(originalClass,originalSelector,swizzledIMP,swizzledType);
  } while(NO);
  [kMySafeLock unlock];
}

+ (void)restore {
  [kMySafeLock lock];
  do{
    if(!kMySafeOriginalIMP || !kMySafeSwizzledIMP)break;
    Class originalClass = NSClassFromString(@"__NSDictionaryM");

    if(!originalClass) {
      break;
    }
    Method originalMethod = NULL;
    Method swizzledMethod = NULL;
    unsigned int outCount = 0;

    Method *methodList = class_copyMethodList(originalClass, &outCount);
    for(unsigned int idx=0; idx < outCount; idx++) {
      Method aMethod = methodList[idx];
      IMP aIMP = method_getImplementation(aMethod);
      if(aIMP == kMySafeSwizzledIMP) {
        originalMethod = aMethod;
      } else if(aIMP == kMySafeOriginalIMP) {
        swizzledMethod = aMethod;
      }
    }

    // 尽可能使用exchange,因为它是atomic的
    if(originalMethod && swizzledMethod) {
      method_exchangeImplementations(originalMethod, swizzledMethod);
    } else if(originalMethod) {
      method_setImplementation(originalMethod, kMySafeOriginalIMP);
    } else if(swizzledMethod) {
      method_setImplementation(swizzledMethod, kMySafeSwizzledIMP);
    }

    kMySafeOriginalIMP = NULL;
    kMySafeSwizzledIMP = NULL;
  }while(NO);

  [kMySafeLock unlock];
}

- (void)safe_setObject:(id)anObject forKey:(id)aKey {
  if(anObject && aKey) {
    [self safe_setObject:anObject forKey:aKey];
  } else if(aKey) {
    [(NSMutableDictionary *)self removeObjectForKey:aKey];
  }
}
@end

注意这段代码的Method Swizzling和恢复都需要主动调用, 并且相比上面其它的示例, 这段代码还添加如锁机制来加之保护. 这个示例是以不同的类来实现的Method Swizzling和恢复, 如果是Category或者是类方法, 根据之前的示例也需要做相应的调整.

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,386评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,939评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,851评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,953评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,971评论 5 369
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,784评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,126评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,765评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,148评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,744评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,858评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,479评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,080评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,053评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,278评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,245评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,590评论 2 343

推荐阅读更多精彩内容