zoukankan      html  css  js  c++  java
  • Method Swizzling 剖析

    一、背景介绍     

    关于Method Swizzling的文章一大堆,讲的非常好的也数不胜数。不过,很多人只是会用,知道一些注意点。深入一点问的话,估计就答得不好。归其原因就是对Method Swizzling 理解的不够透彻。本文些的初衷就是为了让大家更容易理解,仅此而已。如若有错之处,还望指正。

    二、经典代码

       SEL originalSelector = @selector(applicationDidBecomeActive:);
        SEL swizzledSelector = @selector(my_applicationDidBecomeActive:);
        
        Method originalMethod = class_getInstanceMethod([self class], originalSelector);
        Method swizzledMethod = class_getInstanceMethod([self class], swizzledSelector);
        
        BOOL didAddMethod =
        class_addMethod([self class],
                        originalSelector,
                        method_getImplementation(swizzledMethod),
                        method_getTypeEncoding(swizzledMethod));
        
        if (didAddMethod) {
            class_replaceMethod([self class],
                                swizzledSelector,
                                method_getImplementation(originalMethod),
                                method_getTypeEncoding(originalMethod));
        } else {
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    

       上面这段代码是相对较完善的一段,也有同学会写成下面这种:

       SEL originalSelector = @selector(applicationDidBecomeActive:);
        SEL swizzledSelector = @selector(my_applicationDidBecomeActive:);
        
        Method originalMethod = class_getInstanceMethod([self class], originalSelector);
        Method swizzledMethod = class_getInstanceMethod([self class], swizzledSelector);
        
        method_exchangeImplementations(originalMethod, swizzledMethod);
    

         大部分人也是懂原因的:如果Swizzling的 方法是父类里的,而子类里面没有重写,第二种写法就有可能不妥。因为按照第二种写法,会把父类的方法Swizzling,这样会导致所有的子类都会受影响,这个可能不是你想要的。 

    三、解释一下细节 

        大部分人只是知道怎么用,用的时候copy一下,但是要有刨根问底的精神,知其然,知其所以然。

        SEL :类成员方法的指针,但不同于C语言中的函数指针,函数指针直接保存了方法的地址,但SEL只是方法编号。在内存中每个类的方法都存储在类对象中,每个方法都有一个与之对应的SEL类型的数据,根据一个SEL数据就可以找到对应的方法地址,进而调用方法。

    SEL类型的定义:  typedef struct objc_selector *SEL
    

        IMP:Implement缩写,表示指向方法的实现地址,可通过IMP来调用方法。  

        Method:An opaque type that represents a method in a class definition。

    struct objc_method {
        SEL method_name                                          OBJC2_UNAVAILABLE;
        char *method_types                                       OBJC2_UNAVAILABLE;
        IMP method_imp                                           OBJC2_UNAVAILABLE;
    }                                                            OBJC2_UNAVAILABLE;
    

     我们看一下类里面是怎样存放Method的:

    struct objc_class {
        Class isa  OBJC_ISA_AVAILABILITY;
    
    #if !__OBJC2__
        Class super_class                                        OBJC2_UNAVAILABLE;
        const char *name                                         OBJC2_UNAVAILABLE;
        long version                                             OBJC2_UNAVAILABLE;
        long info                                                OBJC2_UNAVAILABLE;
        long instance_size                                       OBJC2_UNAVAILABLE;
        struct objc_ivar_list *ivars                             OBJC2_UNAVAILABLE;
        struct objc_method_list **methodLists                    OBJC2_UNAVAILABLE;
        struct objc_cache *cache                                 OBJC2_UNAVAILABLE;
        struct objc_protocol_list *protocols                     OBJC2_UNAVAILABLE;
    #endif
    
    } OBJC2_UNAVAILABLE;
    
    struct objc_method_list {
        struct objc_method_list *obsolete                        OBJC2_UNAVAILABLE;
    
        int method_count                                         OBJC2_UNAVAILABLE;
    #ifdef __LP64__
        int space                                                OBJC2_UNAVAILABLE;
    #endif
        /* variable length structure */
        struct objc_method method_list[1]                        OBJC2_UNAVAILABLE;
    }                                                            OBJC2_UNAVAILABLE;
    

      

     

     struct objc_method_list **methodLists  这个就是类存放Method的数组。

          可以看到 Class 、SEL、Method等 都是结构体,IMP是函数 的指针地址。

          @selector(applicationDidBecomeActive:) 获取applicationDidBecomeActive:方法的 SEL。

          class_getInstanceMethod([self class], originalSelector); 获取applicationDidBecomeActive:方法的 Method。

          以上都是一些简单概念,接下来我们讲下重点,也就是几个系统API。

    class_getInstanceMethod源码:

    /***********************************************************************
    * class_getInstanceMethod.  Return the instance method for the
    * specified class and selector.
    **********************************************************************/
    Method class_getInstanceMethod(Class cls, SEL sel)
    {
    //先判读 传入的cls 和 sel是否为nil,如果nil 直接return nil了 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. // 先从方法缓存列表里面寻找 ,找到return ,找不到继续 Method meth; meth = _cache_getMethod(cls, sel, _objc_msgForward_impcache); if (meth == (Method)1) { // Cache contains forward:: . Stop searching. return nil; } else if (meth) { return meth; } //根据下面再次从缓存寻找,也知道lookUpImpOrNil的作用就是从 objc_method_list寻找,但并没有直接return,而是找完丢入了方法缓存列表里了 // Search method lists, try method resolver, etc. lookUpImpOrNil(cls, sel, nil, NO/*initialize*/, NO/*cache*/, YES/*resolver*/);

    //再次从方法缓存列表里面寻找 meth = _cache_getMethod(cls, sel, _objc_msgForward_impcache); if (meth == (Method)1) { // Cache contains forward:: . Stop searching. return nil; } else if (meth) { return meth; } return _class_getMethod(cls, sel); }

      我们来看下lookUpImpOrNil 具体干了什么:

    /***********************************************************************
    * lookUpImpOrNil.
    * Like lookUpImpOrForward, but returns nil instead of _objc_msgForward_impcache
    **********************************************************************/
    IMP lookUpImpOrNil(Class cls, SEL sel, id inst, 
                       bool initialize, bool cache, bool resolver)
    {
        IMP imp = lookUpImpOrForward(cls, sel, inst, initialize, cache, resolver);
        if (imp == _objc_msgForward_impcache) return nil;
        else return imp;
    }
    

      里面搞了个传送门lookUpImpOrForward,我们看看lookUpImpOrForward做了什么:

    /***********************************************************************
    * lookUpImpOrForward.
    * The standard IMP lookup. 
    * initialize==NO tries to avoid +initialize (but sometimes fails)
    * cache==NO skips optimistic unlocked lookup (but uses cache elsewhere)
    * Most callers should use initialize==YES and cache==YES.
    * inst is an instance of cls or a subclass thereof, or nil if none is known. 
    *   If cls is an un-initialized metaclass then a non-nil inst is faster.
    * May return _objc_msgForward_impcache. IMPs destined for external use 
    *   must be converted to _objc_msgForward or _objc_msgForward_stret.
    *   If you don't want forwarding at all, use lookUpImpOrNil() instead.
    **********************************************************************/
    IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 
                           bool initialize, bool cache, bool resolver)
    {
        Class curClass;
        IMP methodPC = nil;
        Method meth;
        bool triedResolver = NO;
    
        methodListLock.assertUnlocked();
    
        // Optimistic cache lookup
        if (cache) {
            methodPC = _cache_getImp(cls, sel);
            if (methodPC) return methodPC;    
        }
    
        // Check for freed class
        if (cls == _class_getFreedObjectClass())
            return (IMP) _freedHandler;
    
        // Check for +initialize
        if (initialize  &&  !cls->isInitialized()) {
            _class_initialize (_class_getNonMetaClass(cls, inst));
            // If sel == initialize, _class_initialize will send +initialize and 
            // then the messenger will send +initialize again after this 
            // procedure finishes. Of course, if this is not being called 
            // from the messenger then it won't happen. 2778172
        }
    
        // The lock is held to make method-lookup + cache-fill atomic 
        // with respect to method addition. Otherwise, a category could 
        // be added but ignored indefinitely because the cache was re-filled 
        // with the old value after the cache flush on behalf of the category.
     retry:
        methodListLock.lock();
    
        // Try this class's cache.
    
        methodPC = _cache_getImp(cls, sel);
        if (methodPC) goto done;
    
        // Try this class's method lists.
    
        meth = _class_getMethodNoSuper_nolock(cls, sel);
        if (meth) {
            log_and_fill_cache(cls, cls, meth, sel);
            methodPC = method_getImplementation(meth);
            goto done;
        }
    
        // Try superclass caches and method lists.
    
        curClass = cls;
        while ((curClass = curClass->superclass)) {
            // Superclass cache.
            meth = _cache_getMethod(curClass, sel, _objc_msgForward_impcache);
            if (meth) {
                if (meth != (Method)1) {
                    // Found the method in a superclass. Cache it in this class.
                    log_and_fill_cache(cls, curClass, meth, sel);
                    methodPC = method_getImplementation(meth);
                    goto done;
                }
                else {
                    // Found a forward:: entry in a superclass.
                    // Stop searching, but don't cache yet; call method 
                    // resolver for this class first.
                    break;
                }
            }
    
            // Superclass method list.
            meth = _class_getMethodNoSuper_nolock(curClass, sel);
            if (meth) {
                log_and_fill_cache(cls, curClass, meth, sel);
                methodPC = method_getImplementation(meth);
                goto done;
            }
        }
    
        // No implementation found. Try method resolver once.
    
        if (resolver  &&  !triedResolver) {
            methodListLock.unlock();
            _class_resolveMethod(cls, sel, inst);
            triedResolver = YES;
            goto retry;
        }
    
        // No implementation found, and method resolver didn't help. 
        // Use forwarding.
    
        _cache_addForwardEntry(cls, sel);
        methodPC = _objc_msgForward_impcache;
    
     done:
        methodListLock.unlock();
    
        return methodPC;
    }
    

      代码有点长,我不想一一解释了,简单描述一下:

          1.就是先从缓存列表里面寻找Method,找到就return;

          2.找不到就从类的struct objc_method_list **methodLists里面寻找,找到return;

          3.如果还是找不到,就去父类 缓存找,找不到就去父类 方法列表 找;

          4.如果还是找不到,再去父类的父类的 缓存找。。。  while 这一块相当于一个递归吧,一层一层往上撸。

          5.代码里面有log_and_fill_cache 或是_cache_addForwardEntry  ,他们作用就是将 找到的Method 加到缓存里。

          6.另外我们看了锁 和 强大的 goto,goto是 C 里面强大的 “时光传送门”,菜鸟慎用 :) 大神也有坠机时候。

          感觉有点写不下去了,捂着眼睛继续吧,来~,我们看一下log_and_fill_cache的实现:

    /***********************************************************************
    * log_and_fill_cache
    * Log this method call. If the logger permits it, fill the method cache.
    * cls is the method whose cache should be filled. 
    * implementer is the class that owns the implementation in question.
    **********************************************************************/
    static void
    log_and_fill_cache(Class cls, Class implementer, Method meth, SEL sel)
    {
    #if SUPPORT_MESSAGE_LOGGING
        if (objcMsgLogEnabled) {
            bool cacheIt = logMessageSend(implementer->isMetaClass(), 
                                          cls->nameForLogging(),
                                          implementer->nameForLogging(), 
                                          sel);
            if (!cacheIt) return;
        }
    #endif
        _cache_fill (cls, meth, sel);
    }
    

      我不管了,继续_cache_fill:

    /***********************************************************************
    * _cache_fill.  Add the specified method to the specified class' cache.
    * Returns NO if the cache entry wasn't added: cache was busy, 
    *  class is still being initialized, new entry is a duplicate.
    *
    * Called only from _class_lookupMethodAndLoadCache and
    * class_respondsToMethod and _cache_addForwardEntry.
    *
    * Cache locks: cacheUpdateLock must not be held.
    **********************************************************************/
    bool _cache_fill(Class cls, Method smt, SEL sel)
    {
        uintptr_t newOccupied;
        uintptr_t index;
        cache_entry **buckets;
        cache_entry *entry;
        Cache cache;
    
        cacheUpdateLock.assertUnlocked();
    
        // Never cache before +initialize is done
        if (!cls->isInitialized()) {
            return NO;
        }
    
        // Keep tally of cache additions
        totalCacheFills += 1;
    
        mutex_locker_t lock(cacheUpdateLock);
    
        entry = (cache_entry *)smt;
    
        cache = cls->cache;
    
        // Make sure the entry wasn't added to the cache by some other thread 
        // before we grabbed the cacheUpdateLock.
        // Don't use _cache_getMethod() because _cache_getMethod() doesn't 
        // return forward:: entries.
        if (_cache_getImp(cls, sel)) {
            return NO; // entry is already cached, didn't add new one
        }
    
        // Use the cache as-is if it is less than 3/4 full
        newOccupied = cache->occupied + 1;
        if ((newOccupied * 4) <= (cache->mask + 1) * 3) {
            // Cache is less than 3/4 full.
            cache->occupied = (unsigned int)newOccupied;
        } else {
            // Cache is too full. Expand it.
            cache = _cache_expand (cls);
    
            // Account for the addition
            cache->occupied += 1;
        }
    
        // Scan for the first unused slot and insert there.
        // There is guaranteed to be an empty slot because the 
        // minimum size is 4 and we resized at 3/4 full.
        buckets = (cache_entry **)cache->buckets;
        for (index = CACHE_HASH(sel, cache->mask); 
             buckets[index] != NULL; 
             index = (index+1) & cache->mask)
        {
            // empty
        }
        buckets[index] = entry;
    
        return YES; // successfully added new cache entry
    }
    

      上面就是将Method加到cache里的,细节我就不说了,不过有一点我们还是要说下: Use the cache as-is if it is less than 3/4 full,是的,针对缓存,苹果是有个机制的,当缓存达到3/4时候会释放的,重新来,是不是想起了NSCache?

           _cache_addForwardEntry这个方法:

    /***********************************************************************
    * _cache_addForwardEntry
    * Add a forward:: entry  for the given selector to cls's method cache.
    * Does nothing if the cache addition fails for any reason.
    * Called from class_respondsToMethod and _class_lookupMethodAndLoadCache.
    * Cache locks: cacheUpdateLock must not be held.
    **********************************************************************/
    void _cache_addForwardEntry(Class cls, SEL sel)
    {
        cache_entry *smt;
      
        smt = (cache_entry *)malloc(sizeof(cache_entry));
        smt->name = sel;
        smt->imp = _objc_msgForward_impcache;
        if (! _cache_fill(cls, (Method)smt, sel)) {  // fixme hack
            // Entry not added to cache. Don't leak the method struct.
            free(smt);
        }
    }
    

      一样的,也是调用了_cache_fill,嗯!关于class_getInstanceMethod就聊到这里,打住!

    OBJC_EXPORT IMP method_getImplementation(Method m) 
        OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0);
    

      知道Method是个结构体以及其结构,就能知道这个方法实现其实很简单:

    IMP method_getImplementation(Method m)
    {
        if (!m) return nil;
        return oldmethod(m)->method_imp;
    }
    //就这么简单

      我们看看method_exchangeImplementations的实现:

    void method_exchangeImplementations(Method m1, Method m2)
    {
        if (!m1  ||  !m2) return;
    
        rwlock_writer_t lock(runtimeLock);
        // 这不是 a b容器里的内容交换么,找到了第三个容器c来帮忙,是不是源码也是很简单的,瞄一眼就知道了。
        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);
    
        updateCustomRR_AWZ(nil, m1);
        updateCustomRR_AWZ(nil, m2);
    }
    

      交换两个Method 结构体里面的IMP而已。

    class_addMethod 我们再来看一下这个函数:

    /***********************************************************************
    * class_addMethod
    **********************************************************************/
    BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types)
    {
        IMP old;
        if (!cls) return NO;
    
        old = _class_addMethod(cls, name, imp, types, NO);
        return !old;
    }
    

      

    /***********************************************************************
    * class_addMethod
    **********************************************************************/
    static IMP _class_addMethod(Class cls, SEL name, IMP imp, 
                                const char *types, bool replace)
    {
        old_method *m;
        IMP result = nil;
    
        if (!types) types = "";
    
        mutex_locker_t lock(methodListLock);//加锁
    
    // 先判断该方法是否存在这个类里,如果存在,只改下Method 的IMP就可以 if ((m = _findMethodInClass(cls, name))) { // already exists // fixme atomic result = method_getImplementation((Method)m); if (replace) { method_setImplementation((Method)m, imp); } } else {
    // 如果不存在这个类里,则要动态为这个class 插入一个方法 // fixme could be faster old_method_list *mlist = (old_method_list *)calloc(sizeof(old_method_list), 1); mlist->obsolete = fixed_up_method_list; mlist->method_count = 1; mlist->method_list[0].method_name = name; mlist->method_list[0].method_types = strdup(types); mlist->method_list[0].method_imp = imp; // 向类里插入 方法 _objc_insertMethods(cls, mlist, nil);
    // 清空 类的缓存 if (!(cls->info & CLS_CONSTRUCTING)) { flush_caches(cls, NO); } else { // in-construction class has no subclasses flush_cache(cls); } result = nil; } return result; }

      

    官方wiki:

    https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtInteracting.html#//apple_ref/doc/uid/TP40008048-CH103-SW1

    待续。。。。

  • 相关阅读:
    算法两则
    windows XP 神key
    mysql空间型数据使用python executemany批量插入报错
    关于集合的相似度测量方法
    读取经纬度坐标并存储为字典格式,即key为ID,value为轨迹点
    ubuntu下安装软件时报错解决:Unmet dependencies. Try 'apt-get -f install' with no packages
    ubuntu环境下pycharm编译程序import包出错:ImportError: dynamic module does not define init function (init_caffe)
    linux Ubuntu14.04 make编译文件报错:No rule to make target `/usr/lib/libpython2.7.so', needed by `python/_pywraps2.so'. Stop.
    U盘安装Ubuntu14.04&配置远程win10远程连接
    解决:error LNK1169: 找到一个或多个多重定义的符号
  • 原文地址:https://www.cnblogs.com/qiyer/p/7446431.html
Copyright © 2011-2022 走看看