iOS 分类原理探究

一、分类的定义

struct category_t {
    const char *name;
    classref_t cls;
    struct method_list_t *instanceMethods;
    struct method_list_t *classMethods;
    struct protocol_list_t *protocols;
    struct property_list_t *instanceProperties;
    // Fields below this point are not always present on disk.
    struct property_list_t *_classProperties;

    method_list_t *methodsForMeta(bool isMeta) {
        if (isMeta) return classMethods;
        else return instanceMethods;
    }

    property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
};

如上面代码显示,分类的本质是一个结构体,它可以存储类的实例方法(instanceMethods)、类方法(classMethods)、协议(protocols)、实例属性(instanceProperties)和类属性(_classProperties)。值得注意的是分类中没有存储成员变量的元素。

二、分类的特点和作用(通过和扩展比较来确定)

分类的特点:
  1. 分类在运行时决议
  2. 可以为系统类添加分类
  3. 分类可以添加实例方法、类方法、协议、属性。添加属性时只声明 set、get 方法,没有对应实现,不会添加成员变量
扩展的特点:
  1. 扩展在编译时决议
  2. 不可以为系统类添加扩展
  3. 扩展可以声明属性、方法(一般不声明方法),大多数寄生在宿主类的 .m 文件中,如果添加属性,在编译时通过 @synthesize (xcode 4.5 后可省略)自动为属性实现 get、set 方法,并添加成员变量,声明方法则需要在宿主类中去实现。
分类中的 +load 和 +initialize 方法
  1. +load 调用时机:是 runtime 的加载类,在程序启动装载类信息的时候调用,通过函数地址直接调用,每个 +load 都会调用且仅调用一次。
  2. +load 调用顺序和方式:顺序:父类 +load --> 子类 +load --> 分类 +load;类中的 + load 优先于分类中的。每个分类中的和原类中的 +load 方法都会被调用,不会互相覆盖。
  3. +initialize 调用时机:+initialize 是通过 objc_msgSend 调用,只在每个类只在第一次初始化时候调用一次,如果子类没有实现 +initialize 会去调用父类的 +initialize。分类中的 + initialize 会覆盖原类的实现。
  4. +initialize 调用顺序和方式: 在某个类第一次初始化时,会先强制为初始化过的父类先调用 +initialize,然后本类再调用 +initialize。

例如: B、C 类都继承 A,且三个类都实现了 +initialize,在依次初始化 B、C 的实例对象时,初始化 B 类时会依次调用A、B 的 +initialize,初始化 C 类时只会调用 C 类的 +initialize,因为 A 的 +initialize 已经在初始化 B 的时候调用了。但如果 B 类没有实现 +initialize,那么在初始化 B 类的时候会调用两次 A 的 +initialize。

分类的其他特点:
  1. 分类添加的方法可以“覆盖”原类的方法,同名分类方法谁能生效取决于编译顺序——最后被编译的分类,会优先被生效。
  2. 可以使用关联对象的技术模拟为分类添加成员变量,其本质与原类中的成员变量并不相同,只是实现了类似的功能。
分类的作用
  1. 声明私有方法
  2. 分解体积庞大的类文件
  3. 把 framework 的私有方法公开化

下面我们探究一下分类的这些特点,我会以 特点1、特点2…… 这些来代表上面分类的特点。

三、分类原理探究(重点)

为了探究分类的原理,需要下载Runtime的源码,官方的工程需要经过大量调试才能使用。这里有处理好的objc4-756.2工程,以下都是基于处理好的objc4-756.2工程说明的。

将分类添加到宿主类中的过程

我们运行 objc-debug TARGET,看看系统如何加载分类的。
首先是 runtime 的初始化函数 _objc_init(void) 函数

void _objc_init(void)
{
    static bool initialized = false;
    if (initialized) return;
    initialized = true;
    
    // fixme defer initialization until an objc-using image is found?
    environ_init();
    tls_init();
    static_init();
    lock_init();
    exception_init();

    _dyld_objc_notify_register(&map_images, load_images, unmap_image);
}

我们看 _dyld_objc_notify_register 中的 &map_images,点进去后

void
map_images(unsigned count, const char * const paths[],
           const struct mach_header * const mhdrs[])
{
    mutex_locker_t lock(runtimeLock);
    return map_images_nolock(count, paths, mhdrs);
}

再看 map_images_nolock

void 
map_images_nolock(unsigned mhCount, const char * const mhPaths[],
                  const struct mach_header * const mhdrs[])
{
   ..........
    前面还有一堆代码,我们先不关心
   ..........
    if (hCount > 0) {
        _read_images(hList, hCount, totalClasses, unoptimizedTotalClasses);
    }

    firstTime = NO;
}

我们进入 _read_images

void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses)
{
   ..........
    前面还有一堆代码,我们先不关心
   ..........
    // Discover categories. 
    for (EACH_HEADER) {
        category_t **catlist = 
            _getObjc2CategoryList(hi, &count);
        bool hasClassProperties = hi->info()->hasCategoryClassProperties();

        for (i = 0; i < count; i++) {
            category_t *cat = catlist[I];
            Class cls = remapClass(cat->cls);

            if (!cls) {
                // Category's target class is missing (probably weak-linked).
                // Disavow any knowledge of this category.
                catlist[i] = nil;
                if (PrintConnecting) {
                    _objc_inform("CLASS: IGNORING category \?\?\?(%s) %p with "
                                 "missing weak-linked target class", 
                                 cat->name, cat);
                }
                continue;
            }

            // Process this category. 
            // First, register the category with its target class. 
            // Then, rebuild the class's method lists (etc) if 
            // the class is realized. 
            bool classExists = NO;
            if (cat->instanceMethods ||  cat->protocols  
                ||  cat->instanceProperties) 
            {
                addUnattachedCategoryForClass(cat, cls, hi);
                if (cls->isRealized()) {
                    remethodizeClass(cls);
                    classExists = YES;
                }
                if (PrintConnecting) {
                    _objc_inform("CLASS: found category -%s(%s) %s", 
                                 cls->nameForLogging(), cat->name, 
                                 classExists ? "on existing class" : "");
                }
            }

            if (cat->classMethods  ||  cat->protocols  
                ||  (hasClassProperties && cat->_classProperties)) 
            {
                addUnattachedCategoryForClass(cat, cls->ISA(), hi);
                if (cls->ISA()->isRealized()) {
                    remethodizeClass(cls->ISA());
                }
                if (PrintConnecting) {
                    _objc_inform("CLASS: found category +%s(%s)", 
                                 cls->nameForLogging(), cat->name);
                }
            }
        }
    }
   ..........
    后面还有一堆代码,我们先不关心
   ..........
}

我们可以看到 // Discover categories. 的注释,告诉我们这里是关于处理分类的。

最外层的 for 循环判断条件 EACH_HEADER,实际上是#define EACH_HEADER \ hIndex = 0; \ hIndex < hCount && (hi = hList[hIndex]); \ hIndex++ 的宏定义,就是遍历 hList 取出 hi。

hi 中包含各种资源信息,其中就包括分类的信息。
category_t **catlist = _getObjc2CategoryList(hi, &count); 这句代码就是获取 hi 中的分类的信息,返回值是一个 category_t 的数组,category_t 结构体我们在一开始就有了了解

循环里面的逻辑就是:拿到 category_t 类型的 cat,if (cat->instanceMethods || cat->protocols || cat->instanceProperties)if (cat->classMethods || cat->protocols || (hasClassProperties && cat->_classProperties))是对类和其元类是否需要处理分类的判断。处理的具体逻辑都在 remethodizeClass 函数中。

这里我们注意一下 addUnattachedCategoryForClass 这个函数,从名字上的意思就是“为类添加未命中的分类”,这里的 cat 就是一个未命中的分类,在之后的 remethodizeClass 函数中就是去处理未命中的分类的。

image.png

我在这里打个断点,运行后第一次进入会出现上图信息,会处理 NSObject 及其元类的分类,具体处理逻辑请看 remethodizeClass 函数。

static void remethodizeClass(Class cls)
{
    category_list *cats;
    bool isMeta;

    runtimeLock.assertLocked();

    isMeta = cls->isMetaClass();

    // Re-methodizing: check for more categories
    if ((cats = unattachedCategoriesForClass(cls, false/*not realizing*/))) {
        if (PrintConnecting) {
            _objc_inform("CLASS: attaching categories to class '%s' %s", 
                         cls->nameForLogging(), isMeta ? "(meta)" : "");
        }
        
        attachCategories(cls, cats, true /*flush caches*/);        
        free(cats);
    }
}

remethodizeClass 函数里面使用了 unattachedCategoriesForClass 函数,和上面的addUnattachedCategoryForClass 对应,上面是添加,这里是获取。获取到“未命中的分类”后调用了 attachCategories函数

static void 
attachCategories(Class cls, category_list *cats, bool flush_caches)
{
    if (!cats) return;
    if (PrintReplacedMethods) printReplacements(cls, cats);

    bool isMeta = cls->isMetaClass();

    // fixme rearrange to remove these intermediate allocations
    method_list_t **mlists = (method_list_t **)
        malloc(cats->count * sizeof(*mlists));
    property_list_t **proplists = (property_list_t **)
        malloc(cats->count * sizeof(*proplists));
    protocol_list_t **protolists = (protocol_list_t **)
        malloc(cats->count * sizeof(*protolists));

    // Count backwards through cats to get newest categories first
    int mcount = 0;
    int propcount = 0;
    int protocount = 0;
    int i = cats->count;
    bool fromBundle = NO;
    while (i--) {
        auto& entry = cats->list[I];

        method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
        if (mlist) {
            mlists[mcount++] = mlist;
            fromBundle |= entry.hi->isBundle();
        }

        property_list_t *proplist = 
            entry.cat->propertiesForMeta(isMeta, entry.hi);
        if (proplist) {
            proplists[propcount++] = proplist;
        }

        protocol_list_t *protolist = entry.cat->protocols;
        if (protolist) {
            protolists[protocount++] = protolist;
        }
    }

    auto rw = cls->data();

    prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
    rw->methods.attachLists(mlists, mcount);
    free(mlists);
    if (flush_caches  &&  mcount > 0) flushCaches(cls);

    rw->properties.attachLists(proplists, propcount);
    free(proplists);

    rw->protocols.attachLists(protolists, protocount);
    free(protolists);
}

这里面的 cats 是上个函数传过来的“未命中的分类”,显然“未命中的分类”可能不止一个,故而这是一个数组

下面定义的 mlists 、proplists、protolists是类的方法列表、属性列表和协议列表。它们都是一个二维数组,对应每一个“未命中分类”的方法列表、属性列表和协议列表,以 mlists 举例,其数据类型是这样的 [[method_t, method_t], [method_t, method_t, method_t], [method_t, method_t] ...]。因为类方法和实例方法会处理两次,所有这里如果是普通类,存储的是实例方法、属性,元类的话会存储类方法和属性。

while (i--) {...} 循环意味着倒序变量,也就意味着最先访问的是最后编译的分类。这也解释了 特点 8 的原理。这个寻做的事情就是把每个“未命中分类”中的方法、属性、协议倒序放入mlists 、proplists、protolists中。

后面的代码就是将 mlists 、proplists、protolists 附加到 rw 对应元素的里面。auto rw = cls->data(); rw 是类的可读写数据,意味着系统最终将分类里面的元素全部添加到宿主类中了,这也是为什么分类可以为宿主类添加实例方法、类方法、协议、属性的原因。

系统通过 objc_msgSend 函数来调用类的方法,由上面可以知道,系统能够找到分类的方法,顺序是最后添加的先被找到,而分类方法和原类中的方法谁先调用,这个函数中看不出来,所有我们需要再看看 attachLists 函数

    void attachLists(List* const * addedLists, uint32_t addedCount) {
        if (addedCount == 0) return;

        if (hasArray()) {
            // many lists -> many lists
            uint32_t oldCount = array()->count;
            uint32_t newCount = oldCount + addedCount;
            setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
            array()->count = newCount;
            memmove(array()->lists + addedCount, array()->lists, 
                    oldCount * sizeof(array()->lists[0]));
            memcpy(array()->lists, addedLists, 
                   addedCount * sizeof(array()->lists[0]));
        }
        else if (!list  &&  addedCount == 1) {
            // 0 lists -> 1 list
            list = addedLists[0];
        } 
        else {
            // 1 list -> many lists
            List* oldList = list;
            uint32_t oldCount = oldList ? 1 : 0;
            uint32_t newCount = oldCount + addedCount;
            setArray((array_t *)malloc(array_t::byteSize(newCount)));
            array()->count = newCount;
            if (oldList) array()->lists[addedCount] = oldList;
            memcpy(array()->lists, addedLists, 
                   addedCount * sizeof(array()->lists[0]));
        }
    }

我们注意看 hasArray() 为 YES 的分支,因为这个我们可以知道分类中的元素究竟是添加在数组的前面,还是后面,这里有两个核心函数 memmove 和 memcpy,所有我们看看它俩究竟干了什么。

// memmove :内存移动。
/*  __dst : 移动内存的目的地
*   __src : 被移动的内存首地址
*   __len : 被移动的内存长度
*   将__src的内存移动__len块内存到__dst中
*/
void    *memmove(void *__dst, const void *__src, size_t __len);

__dst : 移动内存的目的地:对应的是 array()->lists + addedCount 很明显目的地是 array 的首地址加上被添加数组的大小,也就是要把数组前面的地方给留出来,这里我们就差不多知道新元素应该是要添加到数组前面了。

为了了解全貌,我们继续看 __src : 被移动的内存首地址:对应 array()->lists 就是数组的首地址。__len : 被移动的内存长度:对应 addedCount * sizeof(array()->lists[0])就是数组中每个元素的长度。所有 memmove 就是将原来数组的元素向后移动 addedCount 个位置。那么很显然 memcpy 就是将预留出来的地方填满数据。

// memcpy :内存拷贝。
/*  __dst : 拷贝内存的拷贝目的地
*   __src : 被拷贝的内存首地址
*   __n : 被移动的内存长度
*   将__src的内存拷贝__n块内存到__dst中
*/
void    *memcpy(void *__dst, const void *__src, size_t __n);

至此我们将 特点 8 完全讲述清楚了。我们看到分类是在运行后才加到宿主类中的,这也解释了 特点 1,也正是因为 特点 1,我们才可以为系统类添加分类(特点 2),通过分类结构体 category_t 已经刚才的流程说明了 特点 3。那么还有 特点 4、5、6、7、9 没有被解释,我们接下来继续探索。

+ load 和 + initialize

我们回到 _objc_init(void) 函数,最后一行的第二个参数 是 load_images。我们点进去看看

void
load_images(const char *path __unused, const struct mach_header *mh)
{
    // Return without taking locks if there are no +load methods here.
    if (!hasLoadMethods((const headerType *)mh)) return;

    recursive_mutex_locker_t lock(loadMethodLock);

    // Discover load methods
    {
        mutex_locker_t lock2(runtimeLock);
        prepare_load_methods((const headerType *)mh);
    }

    // Call +load methods (without runtimeLock - re-entrant)
    call_load_methods();
}

里面调用了 call_load_methods()。

void call_load_methods(void)
{
    static bool loading = NO;
    bool more_categories;

    loadMethodLock.assertLocked();

    // Re-entrant calls do nothing; the outermost call will finish the job.
    if (loading) return;
    loading = YES;

    void *pool = objc_autoreleasePoolPush();

    do {
        // 1. Repeatedly call class +loads until there aren't any more
        while (loadable_classes_used > 0) {
            call_class_loads();
        }

        // 2. Call category +loads ONCE
        more_categories = call_category_loads();

        // 3. Run more +loads if there are classes OR more untried categories
    } while (loadable_classes_used > 0  ||  more_categories);

    objc_autoreleasePoolPop(pool);

    loading = NO;
}

请看 do while 循环,代码中给了明确的三步的注释。

  1. 循环的调用类中的 +load 方法,直到所有类中的 +load 方法都调用一遍为止。
  2. 调用分类中的 +load 方法一次
  3. 判断是否有类中的 +load 没有调用,或者是否有分类中的 +load 方法没有调用。

这基本解释的 特性 5先调用类中的 +load 方法,后调用分类中的 +load 方法 的小点。

接下来我们看一下 call_class_loads(void) 函数:

static void call_class_loads(void)
{
    int I;
    
    // Detach current loadable list.
    struct loadable_class *classes = loadable_classes;
    int used = loadable_classes_used;
    loadable_classes = nil;
    loadable_classes_allocated = 0;
    loadable_classes_used = 0;
    
    // Call all +loads for the detached list.
    for (i = 0; i < used; i++) {
        Class cls = classes[i].cls;
        load_method_t load_method = (load_method_t)classes[i].method;
        if (!cls) continue; 

        if (PrintLoading) {
            _objc_inform("LOAD: +[%s load]\n", cls->nameForLogging());
        }
        (*load_method)(cls, SEL_load);
    }
    
    // Destroy the detached list.
    if (classes) free(classes);
}

里面有个 loadable_class 类型的数组 classes 就是实现了 +load 方法的类,有 cls 和 method 两个元素,method 就是 +load 方法的实现

struct loadable_class {
    Class cls;  // may be nil
    IMP method;
};

我们看到 for 循环中使用了 method 直接调用,并没有判断父类,和分类,所有 +load 直接通过函数地址调用,子类不会覆盖父类,分类不会覆盖宿主类。至此 特点 4 基本解释清楚,同时也部分解释了 特性 5

为了更准确了解 +load 的调用顺序,我们在一个新的工程中添加以下类和分类:

@interface SuperObject : NSObject

@end

@interface SubObject : SuperObject

@end

@interface SuperObject (Category1)

@end

@interface SuperObject (Category2)

@end

@interface SubObject (Category1)

@end

@interface SubObject (Category2)

@end

对应的实现

@implementation SuperObject

+ (void)load {
    NSLog(@"%@", NSStringFromClass(self.class));
}

@end

@implementation SubObject

+ (void)load {
    NSLog(@"%@", NSStringFromClass(self.class));
}

@end

@implementation SuperObject (Category1)

+ (void)load {
    NSLog(@"%@ Category1", NSStringFromClass(self.class));
}

@end

@implementation SuperObject (Category2)

+ (void)load {
    NSLog(@"%@ Category2", NSStringFromClass(self.class));
}

@end

@implementation SubObject (Category1)

+ (void)load {
    NSLog(@"%@ Category1", NSStringFromClass(self.class));
}

@end

@implementation SubObject (Category2)

+ (void)load {
    NSLog(@"%@ Category2", NSStringFromClass(self.class));
}

@end

我们定义好了之后不需要任何调用直接运行工程,得到打印

SuperObject
SubObject
SuperObject Category1
SuperObject Category2
SubObject Category1
SubObject Category2

至此 特点 5 完全清楚了。

下面看关于 + initialize 的相关代码,我首先找到了 initializeNonMetaClass 函数,关于这个类外界如何调用它我们先不去考虑

/***********************************************************************
* class_initialize.  Send the '+initialize' message on demand to any
* uninitialized class. Force initialization of superclasses first.
**********************************************************************/
void initializeNonMetaClass(Class cls)
{
    assert(!cls->isMetaClass());

    Class supercls;
    bool reallyInitialize = NO;

    // Make sure super is done initializing BEFORE beginning to initialize cls.
    // See note about deadlock above.
    supercls = cls->superclass;
    if (supercls  &&  !supercls->isInitialized()) {
        initializeNonMetaClass(supercls);
    }
    
    // Try to atomically set CLS_INITIALIZING.
    {
        monitor_locker_t lock(classInitLock);
        if (!cls->isInitialized() && !cls->isInitializing()) {
            cls->setInitializing();
            reallyInitialize = YES;
        }
    }
    
    if (reallyInitialize) {
        // We successfully set the CLS_INITIALIZING bit. Initialize the class.
        
        // Record that we're initializing this class so we can message it.
        _setThisThreadIsInitializingClass(cls);

        if (MultithreadedForkChild) {
            // LOL JK we don't really call +initialize methods after fork().
            performForkChildInitialize(cls, supercls);
            return;
        }
        
        // Send the +initialize message.
        // Note that +initialize is sent to the superclass (again) if 
        // this class doesn't implement +initialize. 2157218
        if (PrintInitializing) {
            _objc_inform("INITIALIZE: thread %p: calling +[%s initialize]",
                         pthread_self(), cls->nameForLogging());
        }

        // Exceptions: A +initialize call that throws an exception 
        // is deemed to be a complete and successful +initialize.
        //
        // Only __OBJC2__ adds these handlers. !__OBJC2__ has a
        // bootstrapping problem of this versus CF's call to
        // objc_exception_set_functions().
#if __OBJC2__
        @try
#endif
        {
            callInitialize(cls);

            if (PrintInitializing) {
                _objc_inform("INITIALIZE: thread %p: finished +[%s initialize]",
                             pthread_self(), cls->nameForLogging());
            }
        }
#if __OBJC2__
        @catch (...) {
            if (PrintInitializing) {
                _objc_inform("INITIALIZE: thread %p: +[%s initialize] "
                             "threw an exception",
                             pthread_self(), cls->nameForLogging());
            }
            @throw;
        }
        @finally
#endif
        {
            // Done initializing.
            lockAndFinishInitializing(cls, supercls);
        }
        return;
    }
    
    else if (cls->isInitializing()) {
        // We couldn't set INITIALIZING because INITIALIZING was already set.
        // If this thread set it earlier, continue normally.
        // If some other thread set it, block until initialize is done.
        // It's ok if INITIALIZING changes to INITIALIZED while we're here, 
        //   because we safely check for INITIALIZED inside the lock 
        //   before blocking.
        if (_thisThreadIsInitializingClass(cls)) {
            return;
        } else if (!MultithreadedForkChild) {
            waitForInitializeToComplete(cls);
            return;
        } else {
            // We're on the child side of fork(), facing a class that
            // was initializing by some other thread when fork() was called.
            _setThisThreadIsInitializingClass(cls);
            performForkChildInitialize(cls, supercls);
        }
    }
    
    else if (cls->isInitialized()) {
        // Set CLS_INITIALIZING failed because someone else already 
        //   initialized the class. Continue normally.
        // NOTE this check must come AFTER the ISINITIALIZING case.
        // Otherwise: Another thread is initializing this class. ISINITIALIZED 
        //   is false. Skip this clause. Then the other thread finishes 
        //   initialization and sets INITIALIZING=no and INITIALIZED=yes. 
        //   Skip the ISINITIALIZING clause. Die horribly.
        return;
    }
    
    else {
        // We shouldn't be here. 
        _objc_fatal("thread-safe class init in objc runtime is buggy!");
    }
}

我特意将这个函数的注释给写了处理,我们看一下最上面两句注释,它的意思就是**为所有 uninitialized 的类发送 “+initialize” 消息。并且在此之前强制 initialization 父类 **

函数实现中第 4~7 句是一个递归操作,在父类 uninitialized 情况下递归的强制父类 initialization。

这里面有一个核心函数 callInitialize,我们看一看它的实现

void callInitialize(Class cls)
{
    ((void(*)(Class, SEL))objc_msgSend)(cls, SEL_initialize);
    asm("");
}

它使用了 objc_msgSend 函数,我们知道这个函数是 OC 调用类的方法所使用的基本函数。故而 callInitialize 做的事情和一个类的普通类方法调用没什么不同,这也就是说,普通类方法子类覆盖父类调用,子类没有实现查找父类,以及分类覆盖宿主类的情况,在 callInitialize 中的情况是一样的。那么 + initialize 唯一与其他类方法不同的地方在于在调用之前回去先调用 uninitialized 的父类。至此 特点 6特点 7 就解释完毕了。下面只剩下 特点 9 没有被解决了。

关联对象
///获取某个对象的关联属性
id objc_getAssociatedObject(id object, const void *key)
///给某个对象添加关联属性
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
///移除对象所有的关联属性
void objc_removeAssociatedObjects(id object)

以上三个函数时关于关联对象的三个函数。我们在分类中声明一个属性时,系统不会为我们生产 get set 方法

以上是分类中添加一个 object 属性报的警告。我们可以在分类的实现中添加 @dynamic 来消除警告,但是这个关键字只是告诉编译器“属性的 setter 与 getter 方法由用户自己实现,你不用管了”,这不过是自欺欺人,你运行时用到该属性时就会崩溃,所有无论如何都要手动去实现 get、set 方法。

但是分类中没有成员变量,使用_属性名会报错,这样我们在类中重写 set、get 方法的方式在这里就不适用了。好在系统为我们提供了 关联对象 技术解决了这一问题,就是上面的三个函数(主要是前两个)的使用!

我们分析一下 objc_setAssociatedObject 它就是简单的调用下面的这个函数

void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) {
    // This code used to work when nil was passed for object and key. Some code
    // probably relies on that to not crash. Check and handle it explicitly.
    // rdar://problem/44094390
    if (!object && !value) return;
    
    assert(object);
    
    if (object->getIsa()->forbidsAssociatedObjects())
        _objc_fatal("objc_setAssociatedObject called on instance (%p) of class %s which does not allow associated objects", object, object_getClassName(object));
    
    // retain the new value (if any) outside the lock.
    ObjcAssociation old_association(0, nil);
    id new_value = value ? acquireValue(value, policy) : nil;
    {
        AssociationsManager manager;
        AssociationsHashMap &associations(manager.associations());
        disguised_ptr_t disguised_object = DISGUISE(object);
        if (new_value) {
            // break any existing association.
            AssociationsHashMap::iterator i = associations.find(disguised_object);
            if (i != associations.end()) {
                // secondary table exists
                ObjectAssociationMap *refs = i->second;
                ObjectAssociationMap::iterator j = refs->find(key);
                if (j != refs->end()) {
                    old_association = j->second;
                    j->second = ObjcAssociation(policy, new_value);
                } else {
                    (*refs)[key] = ObjcAssociation(policy, new_value);
                }
            } else {
                // create the new association (first time).
                ObjectAssociationMap *refs = new ObjectAssociationMap;
                associations[disguised_object] = refs;
                (*refs)[key] = ObjcAssociation(policy, new_value);
                object->setHasAssociatedObjects();
            }
        } else {
            // setting the association to nil breaks the association.
            AssociationsHashMap::iterator i = associations.find(disguised_object);
            if (i !=  associations.end()) {
                ObjectAssociationMap *refs = i->second;
                ObjectAssociationMap::iterator j = refs->find(key);
                if (j != refs->end()) {
                    old_association = j->second;
                    refs->erase(j);
                }
            }
        }
    }
    // release the old value (outside of the lock).
    if (old_association.hasValue()) ReleaseValue()(old_association);
}

我们从 AssociationsManager manager; 这句话开始看先看一看 AssociationsManager 这个 C++ 类。

class AssociationsManager {
    // associative references: object pointer -> PtrPtrHashMap.
    static AssociationsHashMap *_map;
public:
    AssociationsManager()   { AssociationsManagerLock.lock(); }
    ~AssociationsManager()  { AssociationsManagerLock.unlock(); }
    
    AssociationsHashMap &associations() {
        if (_map == NULL)
            _map = new AssociationsHashMap();
        return *_map;
    }
};

它的作用就是管理一个 AssociationsHashMap 类型的数据,没有就创建,有就获取,也就是说通过这个类每次获取的 _map 都是同一个对象。

代码中先是获取了 AssociationsHashMap 类型的 associations 变量,然后做了一个 disguised_ptr_t disguised_object = DISGUISE(object) 操作,其实这个的意思是把 object 的地址按位取反。它的意义稍后再说

我们再看 new_value 不为空的分支,第一句话AssociationsHashMap::iterator i = associations.find(disguised_object);就是获取 associations 中的元素,而且是通过 disguised_object 这个值进行的键值查找。由于 disguised_object 是 object 地址的按位取反,所以只要通过这个变量取反后直接获取地址中的内容,就可以获取到 associations 中的元素了,这样就不需要去变量 associations 元素,大大降低了系统的查找时间。这其实是一种哈希算法,OC 中的NSDictionary的查找也是类似的哈希算法,只不过具体的算法不是简单的通过“按位取反”的方式罢了。

接下来的判断就是看是否取到了 i 元素,如果有,就对原有元素进行操作,没有,就新创建一个元素然后再操作。我们主要看第一个分支,即存在 i 元素的情况。

这里面又获取了一个元素 refs,是 ObjectAssociationMap 类型的,然后获取里面的元素 j,这个 j 是通过函数传递过来的参数 key 找到的,实际上是一种和 OC NSDictionary 类似的哈希查找方式。j 中的 second 就是属性的旧值,我们可以通过 ObjcAssociation 函数更新旧值。

我们再看一下 new_value 为空的分支,这里并不是什么都不做,而是 refs->erase(j); 将这个 key 的条目直接擦除了。

分析了objc_setAssociatedObject代码后我们对关联对象技术有了比较深刻的理解,其他两个函数代码这里就不分析了,为了更好理解关联对象技术,请看下面的关系图

关联对象.jpg

我们解决了 特点 9,关于分类的原理就探究到这里。

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

推荐阅读更多精彩内容