Runtime之方法交换

方法交换可以说是我们在开发中比较常用的方法,尤其是在一些需要埋点或者统计的位置,我们可以通过Hook系统的某些方法,通过在这些方法中添加自己代码的方式实现在不入侵业务的情况下实现功能。

首先我们来看下方法交换的实现,下面这段代码使我们在网上随便找了一份方法交换的代码实现:

+ (BOOL)swizzleMethod:(Class)class orgSel:(SEL)origSel swizzSel:(SEL)altSel {
    Method origMethod = class_getInstanceMethod(class, origSel);
    Method altMethod = class_getInstanceMethod(class, altSel);
    if (!origMethod || !altMethod) {
        return NO;
    }
    BOOL didAddMethod = class_addMethod(class,origSel,
                                        method_getImplementation(altMethod),
                                        method_getTypeEncoding(altMethod));
    
    if (didAddMethod) {
        class_replaceMethod(class,altSel,
                            method_getImplementation(origMethod),
                            method_getTypeEncoding(origMethod));
    } else {
        method_exchangeImplementations(origMethod, altMethod);
    }
    
    return YES;
}

这个方法是交换class类中origSel方法和swizzSel方法。我们先来分析这个方法的实现步骤:

  • swizzleMethod方法的三个参数分别为class交换方法的类 origSel 原方法 altSel要替换的方法
  • class_getInstanceMethod通过runtime的这个方法获取方法Method结构体
  • 给class添加origSel方法 方法的实现是altSel的实现
  • 如果第三步添加成功 则替换classaltSel方法实现为origSel
  • 如果第三步添加失败 则交换origMethodaltMethod方法的实现

下面我们先针对上面的实现提出几个自己的问题,然后带着这些问题去研究为什么需要这么实现,这么实现是否有其他的问题。

问题一:class_getInstanceMethod到底是从哪里获取方法实现?如果是父类方法可以获取到吗?
问题二:交换方法之前为何要调用class_addMethod方法先去添加方法?
问题三:为何根据添加方法class_addMethod的返回值去判断下一步的操作?
问题四:method_exchangeImplementationsclass_replaceMethod的区别是什么?
问题五:大多数方法都是交换一个类中的两个方法,是否可以交换两个不同类的方法呢?方法名相同可以交换吗?

下面我们带着这些问题来看方法交换的具体实现:

class_getInstanceMethod

我们先来看下方法的实现:

// 获取cls类中对象方法sel的实现
Method class_getInstanceMethod(Class cls, SEL sel)
{
    if (!cls  ||  !sel) return nil;
    // Search method lists, try method resolver, etc.
    // 搜索方法列表 这个方法是有返回值的 但是在这里并没有用到返回值
    lookUpImpOrNil(cls, sel, nil, 
                   NO/*initialize*/, NO/*cache*/, YES/*resolver*/);
    // 递归获取cls的sel方法
    return _class_getMethod(cls, sel);
}

从这个方法的实现中我们看到在正式调用_class_getMethod方法获取实例方法前,还调用了lookUpImpOrNil方法,这个方法又是做什么的呢?关于这个问题,我们在后面消息转发模块会进一步介绍,这里暂时略过。

我们来看下递归查找方法的实现:

getMethod_nolock
// 无锁情况下过去cls类的实例方法sel
static method_t *
getMethod_nolock(Class cls, SEL sel)
{
    method_t *m = nil;

    runtimeLock.assertLocked();
    // 类是否被实例化 每个被实例化的类实际上是有一个标志位
    assert(cls->isRealized());
    // 这里通过while循环的方式不断的查找当前类->父类->...的方法查找这个方法 直到找到方法或者是找到cls->superclass=nil
    while (cls  &&  ((m = getMethodNoSuper_nolock(cls, sel))) == nil) {
        cls = cls->superclass;
    }

    return m;
}

看到这个实现后,我们的第一个问题的答案就显而易见了。class_getInstanceMethod方法在获取实例方法时会递归遍历所有的父类来查找sel对应的方法,并通过getMethodNoSuper_nolock方法获取方法的method_t结构。

那么getMethodNoSuper_nolock是如何在methodlist中查找对应的方法的呢?

getMethodNoSuper_nolock
/ 在类的方法列表中查找对应的方法实现
static method_t *
getMethodNoSuper_nolock(Class cls, SEL sel)
{
    runtimeLock.assertLocked();

    assert(cls->isRealized());

    // 方法列表的遍历
    for (auto mlists = cls->data()->methods.beginLists(),
              end = cls->data()->methods.endLists(); 
         mlists != end;
         ++mlists)
    {
        // 在mlists中查找sel
        method_t *m = search_method_list(*mlists, sel);
        if (m) return m;
    }

    return nil;
}

我们先忽略for循环里的逻辑,我们先看下search_method_list这个方法:

search_method_list

这个方法的实现 主要实现是findMethodInSortedMethodList方法实现,我们直接看下这个方法

// 在list中查找方法为key的
static method_t *findMethodInSortedMethodList(SEL key, const method_list_t *list)
{
    assert(list);

    const method_t * const first = &list->first;
    const method_t *base = first;
    // 标志位
    const method_t *probe;
    // 要查找的方法的名称
    uintptr_t keyValue = (uintptr_t)key;
    // 方法列表中方法的个数
    uint32_t count;
    // 遍历方法列表 每次遍历count >>= 1 count每次遍历/2
    for (count = list->count; count != 0; count >>= 1) {
        // base为列表中第一个 count>>1 = count /2 即表示列表中间的位置index
        probe = base + (count >> 1);
        // 中间位置的方法的方法名
        uintptr_t probeValue = (uintptr_t)probe->name;
        // 方法名对比 如果刚好中间位置的方法的方法名等于要查找的方法名
        if (keyValue == probeValue) {
            // `probe` is a match.
            // Rewind looking for the *first* occurrence of this value.
            // This is required for correct category overrides.
            // 即使查找到了对应的方法 也要向前查找 找到在方法列表中靠前的方法实现
            // 方法列表中是可能存在同名方法的 比如分类重写了方法实现 那么肯定会找到分类的实现返回
            while (probe > first && keyValue == (uintptr_t)probe[-1].name) {
                probe--;
            }
            // 返回方法列表中对应方法实现
            return (method_t *)probe;
        }
        // 如果要查找的方法大于中间位置的方法
        if (keyValue > probeValue) {
            // 从中间位置作为base 查找 base-count之间的方法 此处为二分查找 这也侧面验证了method_list_t是一个有序列表
            base = probe + 1;
            count--;
        }
    }
    
    return nil;
}

通过方法的注释我们可以清晰的看到这个方法的功能是:在方法列表中查找传入的方法,查找方法为二分查找,比较方法是否相等的条件为判断方法名是否相同。当然这里还存在一个如果方法列表中包含多个同名函数我们会向前查找,找到方法列表中第一个与要查找方法同名的方法。

class_addMethod

顾名思义,这个方法的作用是在类中添加一个方法,我们先来看下方法实现:

class_addMethod(Class cls, SEL name, IMP imp, const char *types)
{
    if (!cls) return NO;

    mutex_locker_t lock(runtimeLock);
    return ! addMethod(cls, name, imp, types ?: "", NO);
}

class_addMethod中主要是调用了addMethod静态方法,该方法的返回值是要添加的方法的实现。

// 像cls类中添加名为name实现为imp编码为types的方法
// replace 表示是否需要替换方法实现
static IMP 
addMethod(Class cls, SEL name, IMP imp, const char *types, bool replace)
{
    IMP result = nil;

    runtimeLock.assertLocked();

    checkIsKnownClass(cls);
    
    assert(types);
    assert(cls->isRealized());//

    method_t *m;
    // 先从cls中获取名为name的方法实现 如果可以找到方法
    if ((m = getMethodNoSuper_nolock(cls, name))) {
        // already exists
        // 类中已有该方法 判断是否需要替换方法实现
        if (!replace) {
            // 不需要替换直接返回该方法的实现
            result = m->imp;
        } else {
            // 需要替换则调用_method_setImplementation方法替换方法实现
            result = _method_setImplementation(cls, m, imp);
        }
    } else {
        // fixme optimize
        // 如果当前类的方法列表中不包含名为name的方法
        // 新创建一个method_list_t结构体 并赋值
        method_list_t *newlist;
        newlist = (method_list_t *)calloc(sizeof(*newlist), 1);
        newlist->entsizeAndFlags = 
            (uint32_t)sizeof(method_t) | fixed_up_method_list;
        newlist->count = 1;
        newlist->first.name = name;
        newlist->first.types = strdupIfMutable(types);
        newlist->first.imp = imp;
        // 将新建的newlist添加到已有的方法列表中
        prepareMethodLists(cls, &newlist, 1, NO, NO);
        cls->data()->methods.attachLists(&newlist, 1);
        // 刷新缓存
        flushCaches(cls);

        result = nil;
    }

    return result;
}

addMethod方法主要的实现为:

先从cls中获取名为name的方法实现 如果可以找到方法,判断是否需要替换方法实现 如果需要则替换 如果不需要直接返回,如果找不到新创建一个method_list_t结构体 并赋值,将新建的newlist添加到已有的方法列表中,刷新缓存 并返回nil。

因此这个方法的返回值表示:

如果返回值不为空则表示这个方法本来就存在于方法列表中,如果返回值为nil则表示这个方法不存在与方法列表中。而class_addMethod则是对addMethod方法的返回值进行取反,因此class_addMethod的返回值如果为true则表示方法不存在与方法列表中(然后添加到了方法列表中),返回值为false则表示方法之前就在方法列表中,不需要添加。

看完这部分问题二和问题三的答案也比较明显了:实际上class_addMethod的调用实际上是为了确认

  • 要替换的方法是否已经在方法列表中(方法是否已经实现)
  • 如果不在方法列表中,那么需要先将方法添加到方法列表中

如果方法没有在方法列表中,那么我们要完成方法交换需要调用class_replaceMethod方法,如果方法列表中已经有了该方法那么我们直接调用method_exchangeImplementations来实现方法的交换。

class_replaceMethod

我们先来看下这个方法的实现:

IMP 
class_replaceMethod(Class cls, SEL name, IMP imp, const char *types)
{
    if (!cls) return nil;

    mutex_locker_t lock(runtimeLock);
    // 这个方法的实现实际上也是调用了addMethod方法 注意这里replace参数为yes
    return addMethod(cls, name, imp, types ?: "", YES);
}

第一眼看到这个方法的实现时,我也是一脸懵逼,说好的替换方法呢 为何只是调用了addMethod

首先 我们要务必再次明确调用这个方法的前提是:这个类中并没有要交换的方法。虽然我们在前一步中添加了实现,但是如果我们要实现方法交换肯定是要存在两个方法的,因此带着这个疑问我们再次看下方法交换的代码实现:

BOOL didAddMethod = class_addMethod(class,origSel,
                                        method_getImplementation(altMethod),
                                        method_getTypeEncoding(altMethod));
    
    if (didAddMethod) {
        class_replaceMethod(class,altSel,
                            method_getImplementation(origMethod),
                            method_getTypeEncoding(origMethod));
    }

在调用class_addMethod我们传递的参数是:

方法方法名参数
class_addMethodorigSel 原始方法名新自定义方法的方法实现和方法参数编码
class_replaceMethodaltSel 新定义的方法名原始方法的方法实现和方法编码
method_exchangeImplementations

下面我们在来看下这个方法的实现:

// 方法交换实现 m1 和 m2 分别表示要交换的两个方法
void method_exchangeImplementations(Method m1, Method m2)
{
    if (!m1  ||  !m2) return;

    mutex_locker_t lock(runtimeLock);

    // 方法交换仅仅是交换了两个方法的imp指针
    IMP m1_imp = m1->imp;
    m1->imp = m2->imp;
    m2->imp = m1_imp;


    // RR/AWZ updates are slow because class is unknown
    // Cache updates are slow because class is unknown
    // fixme build list of classes whose Methods are known externally?
    // 更新缓存
    flushCaches(nil);
    // 更新两个方法的自定义 方法  rr 表示retain/release等方法 awz 表示allocwithzone方法
    updateCustomRR_AWZ(nil, m1);
    updateCustomRR_AWZ(nil, m2);
}

交换方法的实现就更简单了:直接交换Method结构体中的imp指针,imp指针又是指向了方法的实现。因此这里就完成了两个方法的交换。

上面我们就看完了方法交换的整个流程,我们仍有几个问题没想通

问题一:

大多数方法都是交换一个类中的两个方法,是否可以交换两个不同类的方法呢?方法名相同可以交换吗?

我封装了下面这个方法 用来交换两个类的两个方法



+ (BOOL)swizzleMethod:(Class)originClass swizzCls:(Class)swizzClass orgSel:(SEL)origSel swizzSel:(SEL)swizzSel {
    Method origMethod = class_getInstanceMethod(originClass, origSel);
    Method altMethod = class_getInstanceMethod(swizzClass, swizzSel);
    NSLog(@"swizzleMethod origMethod %@",NSStringFromSelector(method_getName(origMethod)));
    NSLog(@"swizzleMethod altMethod %@",NSStringFromSelector(method_getName(altMethod)));

    BOOL didAddMethod = class_addMethod(originClass,origSel,
                                        method_getImplementation(altMethod),
                                        method_getTypeEncoding(altMethod));

    NSLog(@"swizzleMethod didAddMethod %@",@(didAddMethod));
    if (didAddMethod) {
        class_replaceMethod(swizzClass,swizzSel,
                            method_getImplementation(origMethod),
                            method_getTypeEncoding(origMethod));
    } else {
        method_exchangeImplementations(origMethod, altMethod);
    }

    return YES;
}

方法交换:

[self swizzleMethod:[Person class] swizzCls:[Tiger class] orgSel:@selector(speak) swizzSel:@selector(yall)];

这里我们交换了Person类的speak方法和Tiger类的yall方法,然后分别调用这两个方法

Tiger.m
- (void)yall {
    NSLog(@"Tiger -- yall");
}
-------
Person.m
- (void)speak {
    NSLog(@"Person speak");
}

ViewController.m
- (void)testExchangeTwoClsTwoMethod {
    Person *p = [[Person alloc] init];
    [p speak];
    NSLog(@"-----");
    Tiger *t = [[Tiger alloc] init];
    [t yall];
}

打印结果为:

2020-10-25 20:05:24.400998+0800 MethodSwizzlzeDemo[27125:11429192] swizzleMethod didAddMethod 0
2020-10-25 20:05:24.455871+0800 MethodSwizzlzeDemo[27125:11429192] Tiger -- yall
2020-10-25 20:05:24.456007+0800 MethodSwizzlzeDemo[27125:11429192] -----
2020-10-25 20:05:24.456143+0800 MethodSwizzlzeDemo[27125:11429192] Person speak

从调用结果可以看出我们的方法交换是成功的。因此 这可以证明我们是可以交换两个类的两个方法的。

问题二:

我们是否可以交换一个不存在的方法?会有什么效果?

方法交换调用:

        [self swizzleMethod:[Person class] swizzCls:[Tiger class] orgSel:@selector(speak) swizzSel:@selector(speak)];

我们的Tiger类并没有speak方法,我们看下控制台输出

2020-10-25 20:08:34.567600+0800 MethodSwizzlzeDemo[27181:11431963] swizzleMethod origMethod speak
2020-10-25 20:08:34.568215+0800 MethodSwizzlzeDemo[27181:11431963] swizzleMethod altMethod (null)
2020-10-25 20:08:34.568399+0800 MethodSwizzlzeDemo[27181:11431963] swizzleMethod didAddMethod 0
2020-10-25 20:08:34.614912+0800 MethodSwizzlzeDemo[27181:11431963] Person speak
2020-10-25 20:08:34.615029+0800 MethodSwizzlzeDemo[27181:11431963] -----
2020-10-25 20:08:34.615114+0800 MethodSwizzlzeDemo[27181:11431963] Tiger -- yall

由上面控制台输出结果我们看出,方法交换实际并未成功,且在方法交换是打印altMethod结果为null。因此如果去交换一个并不存在的方法是不可能实现的。

但是如果我们换成另外一种写法:

        [self swizzleMethod:[Tiger class] swizzCls:[Person class] orgSel:@selector(speak) swizzSel:@selector(speak)];

我们再看下输出结果:

2020-10-26 22:29:43.179796+0800 MethodSwizzlzeDemo[1853:33972] swizzleMethod origMethod (null)
2020-10-26 22:29:43.180534+0800 MethodSwizzlzeDemo[1853:33972] swizzleMethod altMethod speak
2020-10-26 22:29:43.180695+0800 MethodSwizzlzeDemo[1853:33972] swizzleMethod didAddMethod 1

从日志我们看到,方法添加成功!也就是说 我们可以交换一个不存在的方法,因为在方法交换时我们在利用运行时方法在类里添加了对应方法。那么我们看下是否可以成功调用呢?

- (void)testExchangeTwoClsTwoMethod {
    Person *p = [[Person alloc] init];
    [p speak];
    NSLog(@"-----");
    Tiger *t = [[Tiger alloc] init];
    [t performSelector:@selector(speak)];
}

打印结果:

2020-10-26 22:35:40.918735+0800 MethodSwizzlzeDemo[1957:39109] swizzleMethod origMethod (null)
2020-10-26 22:35:40.919307+0800 MethodSwizzlzeDemo[1957:39109] swizzleMethod altMethod speak
2020-10-26 22:35:40.919464+0800 MethodSwizzlzeDemo[1957:39109] swizzleMethod didAddMethod 1
2020-10-26 22:36:15.806909+0800 MethodSwizzlzeDemo[1957:39109] Person speak
2020-10-26 22:36:15.807017+0800 MethodSwizzlzeDemo[1957:39109] -----
2020-10-26 22:36:15.807117+0800 MethodSwizzlzeDemo[1957:39109] Person speak

从结果我们看出,调用tigerspeak方法时打印出了Person speak,这说明为Tiger动态增加了一个方法方法实现和Person相同,然后在去替换Person类的speak时因为originMethodnull因此替换不成功,因此这个方法相当于给Tiger增加了一个speak方法。

问题三:

仔细看下下面这段代码:

        [self swizzleMethod:[Tiger class] swizzCls:[Person class] orgSel:@selector(walk) swizzSel:@selector(speak)];

Tiger继承自Animal,walkAnimal对象中的一个实例方法:

@interface Animal : NSObject

- (void)walk;

- (void)eat;

@end

@interface Tiger : Animal

- (void)yall;

@end

但是当我断点调试时,却发现

从图中我们看到:altMethodorigMethod这两个方法是可以获取到的,但是didAddMethod的值返回了YES。这说明我们在Tiger的方法列表中并没有找到walk方法。但是使用class_getInstanceMethod方法获取到的方法是有值的,这是怎么回事呢?

当我们仔细查看class_addMethod方法实现时我们发现了一处代码:

   if ((m = getMethodNoSuper_nolock(cls, name))) {
       // 找到该类中对应的方法
   } else {
      // 没有找到对应的方法
   }

我们在对比class_getInstanceMethod方法获取方法的位置实现:

    // 这里通过while循环的方式不断的查找当前类->父类->...的方法查找这个方法 直到找到方法或者是找到cls->superclass=nil
    while (cls  &&  ((m = getMethodNoSuper_nolock(cls, sel))) == nil) {
        cls = cls->superclass;
    }

经过代码的对比我们很容易发现,在class_addMethod方法中我们只是查找了当前类的方法列表,而在class_getInstanceMethod方法中我们会递归查询当前类以及其父类的方法列表。

因此在Tiger类中找不到walk方法是正常的。

类方法的方法交换

跟交换对象方法相同,我们先来看下这个方法:

+ (BOOL)swizzleMethod:(Class)class orgSel:(SEL)origSel swizzSel:(SEL)altSel {
    // 获取方法改为class_getClassMethod方法
    Method origMethod = class_getClassMethod(class, origSel);
    Method altMethod = class_getClassMethod(class, altSel);
    
    
    BOOL didAddMethod = class_addMethod(class,origSel,
                                        method_getImplementation(altMethod),
                                        method_getTypeEncoding(altMethod));
    
    if (didAddMethod) {
      // 方法已经添加
        class_replaceMethod(class,altSel,
                            method_getImplementation(origMethod),
                            method_getTypeEncoding(origMethod));
    } else {
        method_exchangeImplementations(origMethod, altMethod);
    }
    
    return YES;
}

从上面的代码我们看到,交换类方法和交换对象方法相比我们只是修改了获取方法结构的方法为class_getClassMethod

下面我们看下这个方法的实现:

class_getClassMethod
// 获取指定类的类方法
Method class_getClassMethod(Class cls, SEL sel)
{
    if (!cls  ||  !sel) return nil;

    // 获取类的元类的方法
    return class_getInstanceMethod(cls->getMeta(), sel);
}

其实内部实现很简单,获取类的元类对应的实例方法。

下面我们看下方法调用和最终结果:

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [self swizzleClassMethod:[self class] orgSel:@selector(classMethod1) swizzSel:@selector(classMethod2)];
    });
}

+ (void)classMethod1 {
    NSLog(@"classMethod1");
}

+ (void)classMethod2 {
    NSLog(@"classMethod2");
}

- (void)viewDidLoad {
    [super viewDidLoad];
    [ViewController classMethod1];
    NSLog(@"-------------------------");
    [ViewController classMethod2];
}

我们来看下打印结果:

2020-10-27 22:42:20.314694+0800 MethodSwizzlzeDemo[4950:153595] swizzleClassMethod didAddMethod 1
2020-10-27 22:42:20.360568+0800 MethodSwizzlzeDemo[4950:153595] classMethod1
2020-10-27 22:42:20.360679+0800 MethodSwizzlzeDemo[4950:153595] -------------------------
2020-10-27 22:42:20.360790+0800 MethodSwizzlzeDemo[4950:153595] classMethod2

从结果我们看到,我们的交换方法并不成功。而且我们发现didAddMethod标记为1,也就是说在class中并未找到类方法origSel。很明显问题出在了class_addMethod方法里。上面我们已经分析了这个方法,他的实现实际上是从类中非递归的在方法列表中查找方法。

但是我们都知道类方法实际上是存放在元类中的,这一点在class_getClassMethod的实现中我们也能得到验证

 class_getInstanceMethod(cls->getMeta(), sel);

那么我们这里在方法添加时也应该从元类的方法列表中查找对应的类方法,那么如何获取这个类的元类呢,我们知道每个类对象中都有一个isa指针指向了他的元类。因此我们可以通过下面这个方法获取。

object_getClass
Class object_getClass(id obj)
{
    if (obj) return obj->getIsa();
    else return Nil;
}

利用上面这个方法我们对方法交换函数进行修改

+ (BOOL)swizzleMethod:(Class)class orgSel:(SEL)origSel swizzSel:(SEL)altSel {
    // 获取方法改为class_getClassMethod方法
    Method origMethod = class_getClassMethod(class, origSel);
    Method altMethod = class_getClassMethod(class, altSel);
    
    // 这里改为object_getClass
    BOOL didAddMethod = class_addMethod(object_getClass(class),origSel,
                                        method_getImplementation(altMethod),
                                        method_getTypeEncoding(altMethod));
    
    if (didAddMethod) {
      // 方法已经添加
        class_replaceMethod(class,altSel,
                            method_getImplementation(origMethod),
                            method_getTypeEncoding(origMethod));
    } else {
        method_exchangeImplementations(origMethod, altMethod);
    }
    
    return YES;
}

我们再来看下输出结果:

2020-10-27 22:56:35.223362+0800 MethodSwizzlzeDemo[5144:163474] swizzleClassMethod didAddMethod 0
2020-10-27 22:56:35.267136+0800 MethodSwizzlzeDemo[5144:163474] classMethod2
2020-10-27 22:56:35.267236+0800 MethodSwizzlzeDemo[5144:163474] -------------------------
2020-10-27 22:56:35.267320+0800 MethodSwizzlzeDemo[5144:163474] classMethod1

我们发现这里方法交换是成功的了!这样也就完成了类方法的交换

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值