iOS Method Swizzle 源码分析

iOS Method Swizzle 代码

+ (void)swizzleOriginalSEL:(SEL)originalSEL withSwizzlingSEL:(SEL)swizzlingSEL {
    
    1.Method originalMethod = class_getInstanceMethod(self, originalSEL);
    2.Method swizzlingMethod = class_getInstanceMethod(self, swizzlingSEL);
    
    3.Boolean isAddMethod = class_addMethod(self, originalSEL,
                                          method_getImplementation(swizzlingMethod),
                                          method_getTypeEncoding(swizzlingMethod));
    if (isAddMethod) {
        4.class_replaceMethod(self, swizzlingSEL,
                            method_getImplementation(originalMethod),
                            method_getTypeEncoding(originalMethod));
    } else {
       5. method_exchangeImplementations(originalMethod, swizzlingMethod);
    }
}

平常我们用的方法都是method_exchangeImplementations,其实这种用法是错误的,我们正确的替换方法应该上面的代码。我直接说明上面的代码与直接调用method_exchangeImplementations的区别:为了防止我们所替换子类的实例方法是继承与父类,而且我们并没有在子类中重写该方法。如果直接调用method_exchangeImplementations方法那么我们会直接替换了父类中的方法实现,导致继承此父类的所有子类对应的改实例方法的实现被转换,如果大家已经明白了这个道理就可以绕行了(😆)。我也会介绍一下这几个方法的源码,以及一些runtime基本的知识。

class_replaceMethod 和 class_addMethod 源码分析

class_addMethod源码

BOOL 
class_addMethod(Class cls, SEL name, IMP imp, const char *types)
{
    // 如果cls为nil,直接返回NO,添加方法失败
    if (!cls) return NO;
    // 加锁,我们只需要明白这是加锁为了防止多线程访避免数据同步问题
    rwlock_writer_t lock(runtimeLock);
    // 实现函数,看最后一个函数我们传递的是NO,这个参数要注意。因为我们下面可以看到,我们分析class_replaceMehod源码中也会看到我们调用其实也是addMehtod方法不过最后一个参数是YES
    return ! addMethod(cls, name, imp, types ?: "", NO);
}

class_replaceMethod源码

// 代码注释请参考class_addMethod
IMP 
class_replaceMethod(Class cls, SEL name, IMP imp, const char *types)
{
    if (!cls) return nil;

    rwlock_writer_t lock(runtimeLock);
    return addMethod(cls, name, imp, types ?: "", YES);
}

下面分析addMethod源码

static IMP 
addMethod(Class cls, SEL name, IMP imp, const char *types, bool replace)
{
    IMP result = nil;
    // 加锁
    runtimeLock.assertWriting();
    // 一些断言函数,第一个是保证types必须有值,第二个保证cls是已经实现的状态
    assert(types);
    assert(cls->isRealized());

    method_t *m;
    // getMethodNoSuper_nolock 这个方法非常的关键,他的含义是只在本类中查找是否含有方法,而不再父类中查找。
    if ((m = getMethodNoSuper_nolock(cls, name))) {
        // 可以从本类中查找方法
        if (!replace) {
            // replace 我们在class_addMethod方法中传递的是NO,所以我们这个方法会走这一步,或者其他。如果我们查找的方法结构体中函数指针是有值,那么返回YES,但是class_addMethod会对result取反,所以我们知道如果我们将要添加的method,在cls中存在,并且已经有了函数指针那么我们添加方法失败。
            result = m->imp;
        } else {
            //  repalce 我们在class_replaceMethod中传递的是YES,class_replaceMethod可能会走这一步。
            result = _method_setImplementation(cls, m, imp);
        }
    } else {
        // 没有在本类中查找到对应的方法,则我们在class中添加method,而method的结构体系:class->method_array_t(methods)->method_list_t->method
        // method 的体系会在后面讲解
        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;

        prepareMethodLists(cls, &newlist, 1, NO, NO);
        cls->data()->methods.attachLists(&newlist, 1);
        flushCaches(cls);
        // 返回result是NO,那么class_addMethod取反,那么返回YES
        result = nil;
    }

    return result;
}

现在我们分析_getMethodNoSuper_nolock()方法,其实这个方法是我们在class中查找method的根本方法,我们在class_getInstanceMethod中也是调用了这方法。

static method_t *
getMethodNoSuper_nolock(Class cls, SEL sel)
{
    runtimeLock.assertLocked();

    assert(cls->isRealized());
    // fixme nil cls? 
    // fixme nil sel?
    // for循环cls(本类)属性mehtods,methods中包含每一个method_list_t,之后再method_list_t中根据sel来查找method
    for (auto mlists = cls->data()->methods.beginLists(), 
              end = cls->data()->methods.endLists(); 
         mlists != end;
         ++mlists)
    {
        method_t *m = search_method_list(*mlists, sel);
        if (m) return m;
    }

    return nil;
}

分析search_method_list方法

static method_t *search_method_list(const method_list_t *mlist, SEL sel)
{
    int methodListIsFixedUp = mlist->isFixedUp();
    int methodListHasExpectedSize = mlist->entsize() == sizeof(method_t);
    
    if (__builtin_expect(methodListIsFixedUp && methodListHasExpectedSize, 1)) {
        // 在已经进行过排序的method列表中查找
        return findMethodInSortedMethodList(sel, mlist);
    } else {
        // 这个是我们关注的。我们根据sel作为key,来查找method
        for (auto& meth : *mlist) {
            if (meth.name == sel) return &meth;
        }
    }

#if DEBUG
    // sanity-check negative results
    if (mlist->isFixedUp()) {
        for (auto& meth : *mlist) {
            if (meth.name == sel) {
                _objc_fatal("linear search worked when binary search did not");
            }
        }
    }
#endif

    return nil;
}

根据上面的代码我们可以大致了解到,如果没有查找到对应的method,我们会给class中的methods属性中添加一个method_list_t,而method_list_t中储存着method。那么我们可以得知method的储存结构是数组中包含着数组,子数组中包含着Method,这样设计的目的是区分开Method来自哪里,是来自自己本身、父类、分类、扩展、runtime添加。当我们大致了解了method结构我们在来看看一些addMethod方法中else这一部分的代码

// 创建一个method_list_t *newlist结构体,并且分配内存,和循环标识
method_list_t *newlist;
newlist = (method_list_t *)calloc(sizeof(*newlist), 1);
newlist->entsizeAndFlags = 
            (uint32_t)sizeof(method_t) | fixed_up_method_list;
//对newlist进行复制操作,
newlist->count = 1;
//first,这个属性的类型是Method,所以这里相当于给Method赋值,name就是SEL
newlist->first.name = name;
// 方法type进行赋值
newlist->first.types = strdupIfMutable(types);
// 函数指针进行赋值
newlist->first.imp = imp;
// 对method_list_t进行准备操作
prepareMethodLists(cls, &newlist, 1, NO, NO);
// 将生成好的method_list_t放在methods(method_array_t)中,也就是重新生成一个method_list_t放在class中
cls->data()->methods.attachLists(&newlist, 1);
flushCaches(cls);
// 返回result是NO,那么class_addMethod取反,那么返回YES
result = nil;

class_getInstanceMethod源码

Method class_getInstanceMethod(Class cls, SEL sel)
{
    if (!cls  ||  !sel) return nil;

    // This deliberately avoids +initialize because it historically did so.

    // This implementation is a bit weird because it's the only place that 
    // wants a Method instead of an IMP.

#warning fixme build and search caches
        
    // Search method lists, try method resolver, etc.
    lookUpImpOrNil(cls, sel, nil, 
                   NO/*initialize*/, NO/*cache*/, YES/*resolver*/);

#warning fixme build and search caches
    //这一部分是我们关注的
    return _class_getMethod(cls, sel);
}

static Method _class_getMethod(Class cls, SEL sel)
{
    rwlock_reader_t lock(runtimeLock);
    // 关注这一部分
    return getMethod_nolock(cls, sel);
}

static method_t *
getMethod_nolock(Class cls, SEL sel)
{
    method_t *m = nil;

    runtimeLock.assertLocked();

    // fixme nil cls?
    // fixme nil sel?

    assert(cls->isRealized());
    // 其实在查找method中,我们依然依靠了getMethodNoSuper_nolock方法,只不过是在一个我们会根据superclass来进行循环
    while (cls  &&  ((m = getMethodNoSuper_nolock(cls, sel))) == nil) {
        cls = cls->superclass;
    }

    return m;
}

代码步骤分析

  • 1 & 2获取method结构体,class_getInstanceMethod获取的method可能是在本身的class中获取,或者是在superclass中获取
  • 3 首先查找original mehod是否可以在本类中查找到,如果查到不到,则在本类中添加一个method,而且method的实现方法是 swizzle method的实现方法
  • 4 如果添加方法成功,首先说明了本类中没有original method,我们在第一步中获取的original method 是在父类中的,所以我们自己在本类中创建了一个new original method, 现在new original method 与第一步获取的original method 没有任何的关系。那么现在 new original MethodIMPswizzle IMP,那么我现在调用class_replaceMethod可以把可能从父类中拿到的original method中的实现方法赋值给swizzle method,这样就完成了方法替换
  • 5 如果方法添加没有成功,说明本类中含有这个method,我们直接调用method_exchangeImplementations进行方法IMP替换

Load 方法中调用 method swizzle

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [self swizzleOriginalSEL:@selector(sendEvent:) withSwizzlingSEL:@selector(swizzle_sendEvent:)];
    });
}

为什么要在load方法中增加dispathc_once,因为load方法中本来就会调用一次,我在一篇简书评论中看见一位人这样评论,防止人为调用load,即使这种场景非常少,我感觉这这个解答相当给力,作为程序猿我们应该有意识来方法来进行人为防范,不要想着load真的只会调用一次。如果对load和initialize 不是很了解,可以看看我的另外一篇文(就当增加一些人气了,不然的话太尴尬了。)

引用

objc4-723

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