美文网首页
Category快快现真身

Category快快现真身

作者: MrOreo | 来源:发表于2018-06-28 19:15 被阅读0次
    哈哈,有点蒙

    文中咖啡图片及第一个图片来源百度图片,如涉及到侵权,请联系我删除图片

    原创文章,转载请注明:转自:Try_Try_Try

    更新

    时间:2018.07.06
    内容:添加几张结论图,使得结论更加的直观


    背(吐)景(槽)

    最近被问到了一个category问题,把我问的晕头转向,当时就很(想)佩(打)服(人),无疑是灰秃秃滚回去跪着搓板,面壁思过。

    把这一块知识恶狠狠的补了一桶,奶奶的,了解完之后,发现so easy,被自己蠢哭了。


    引用

    网上也有相关的文章写的很好。我完全读下来的就是美团的那篇深入理解Objective-C:Category

    感觉写的很好。我写这篇文章时,对着读了好多遍。

    所以这次彻底对这篇文章分析一下(其实读很多遍,是因为写的很精简,内部的实现细节需要自己对照源码进行一一查看。这样才能把美团的这篇短短的文章读成一个体系,然后再进行精简,知识就变成我的了---想太多了可能)。


    文章内容结构

    • o 代码结构
    • 1 分类的结构
      • 1.1 题外话
      • 1.2 撕破你这层面纱(让你再给我矫情)
      • 1.3 .cpp文件
      • 1.4 添加了category的消息发送流程总结
    • 2 +load 方法的原理
      • 2.1 题外话
      • 2.2 一探究竟
    • 3 initialize还有谁(有点捏花惹草)
      • 3.1 添加initialize方法进行测试
      • 3.2 猜测
      • 3.3 源码分析-走-起-来
      • 3.4 正经点
      • 3.5 话外
    • 4 联合起来才会更强(关联对象)
      • 4.1 查看分类的结构
        • 4.1.1 查看类的结构
        • 4.1.2 分类结构
      • 4.2 关联对象搞起来
        • 4.2.1 扒关联对象的源码
        • 4.2.2 内部具体实现细节分析

    0. 代码结构

    0.1 代码结构

    Dog 继承自Animate类,Dog中也有父类play方法,其中cat1和cat2中的方法一样,都有play方法,只是打印的内容不一致;

    Animate.h

    #import <Foundation/Foundation.h>
    
    @interface Animate : NSObject
    
    - (void)play;
    
    @end
    

    Animate.m

    #import "Animate.h"
    
    @implementation Animate
    
    - (void)play
    {
        NSLog(@"Animate--%@", NSStringFromSelector(_cmd));
    }
    
    @end
    

    Animate+cat1.h

    #import "Animate.h"
    
    @interface Animate (cat1)
    
    - (void)play;
    
    @end
    

    Animate+cat1.m

    #import "Animate+cat1.h"
    
    @implementation Animate (cat1)
    
    - (void)play
    {
        NSLog(@"Animate (cat1)--%@", NSStringFromSelector(_cmd));
    }
    
    @end
    
    
    

    main.m

    #import <Foundation/Foundation.h>
    #import "Dog.h"
    #import "Dog+cat1.h"
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
           
            Dog *dog = [[Dog alloc] init];
            [dog play];
            
        }
        return 0;
    }
    

    此后代码的分析,也都是基于上述的结构,进行测试。

    • compile 顺序1:


      图0.2 代码编译顺序1

    图0.2对应的执行结果:

    Dog (cat2)--play
    
    • compile 顺序2:


      图0.3 代码编译顺序2

    图0.3对应的执行结果:

    Dog (cat1)--play
    

    ARE YOU READY?接下来是正文:


    1. 分类的结构

    1.1 题外话

    之前看别人的代码时候,一直出现 clang -rewrite-objc filename.m,这里是clang -rewrite-objc Animate+cat1.m命令,然后就出现了神奇的.cpp文件,但是自己在terminal敲了一下,报了一堆错(尼玛,就失去了对.cpp的兴趣了)。

    后来发现:电脑上安装了多个不同版本的Xcode,又更改了名称,所以就无法找到。当更换成Xcode的真名时,重新在代码所处文件clang一下,神奇的.cpp出来了😁(哥终于要研究一番感(懵)人(逼)的c++代码了)。

    生成的文件名称为Animate+cat1.cpp,层级结构如下:

    图1.1 编译之后文件位置

    1.2 撕破你这层面纱(让你再给我矫情)

    我了个曹操,竟然96763行左右,要吓死了(要是工资能达到96k该多好啊,我又做梦了)。这要从哪行开始看啊,拖着滚动条看了一圈,发现前面都是声明和定义,到最后才是真身(这庇护够强啊!你以为你是孙悟空啊,躲在这花果山的最深处)。

    查了一下,发现之所以生成的文件这么大,可能的原因是不同的arm架构都有。

    1.3 .cpp文件

    以下就是编译后的部分关键代码:

     struct _category_t {
        const char *name;
        struct _class_t *cls;
        const struct _method_list_t *instance_methods;
        const struct _method_list_t *class_methods;
        const struct _protocol_list_t *protocols;
        const struct _prop_list_t *properties;
    };
    
    static struct /*_method_list_t*/ {
        unsigned int entsize;  // sizeof(struct _objc_method)
        unsigned int method_count;
        struct _objc_method method_list[1];
    } _OBJC_$_CATEGORY_INSTANCE_METHODS_Animate_$_cat1 __attribute__ ((used, section ("__DATA,__objc_const"))) = {
        sizeof(_objc_method),
        1,
        {{(struct objc_selector *)"play", "v16@0:8", (void *)_I_Animate_cat1_play}}
    };
    
    static struct _category_t _OBJC_$_CATEGORY_Animate_$_cat1 __attribute__ ((used, section ("__DATA,__objc_const"))) = 
    {
        "Animate",
        0, // &OBJC_CLASS_$_Animate,
        (const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_Animate_$_cat1,
        0,
        0,
        0,
    };
    
    • 通过查看 libobjc.order文件,有之后加载的顺序。objc_init->map_images->map_images_nolock->_read_images。

    以下是_read_images中,读取分类的部分代码(处理后的):

    // 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);
    
                // 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;
                    }
                }
            }
        }
    
    • 为了找到本质,还需要继续沿着方法向下走:
      _read_images->addUnattachedCategoryForClass->remethodizeClass ->attachCategories->attachLists

    attachCategories():将分类compile的顺序进行逆序重组到数组中,这里决定了最后编译的分类可能最先执行。

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

    attachLists():将class中的方法和分类中的方法进行移动的操作,使得类中的方法放到数组的尾部,分类放到数组的头部;

    {
                // 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]));
            }
    

    至此,类按照编译顺序逆序的过程结束,然后开始继续消息发送制。(关于消息发送的具体流程,网上很多资料,可以自己分析一波,源码好像是汇编)

    1.4 添加了category的消息发送流程总结

    通过上述分析:这些添加分类神马的,都是在编译阶段,编译器帮我们完成的。因此至于最终如何执行,就得按照运行时正常的消息发送流程,添加上分类后即:

    1. 类(array[cmplN,cmplN_1,cmplN_2.....,cls]->method)->
      父类(array[cmplN,cmplN_1,cmplN_2.....,cls]->method)->
      ->
      ......
      ->
      msgForward->......
    2. 先执行当前类对应的所有分类中最后编译的那个分类方法直到结束,否则一直按照数组顺序向后找。父类也类似。
    2018.7.6 更新 1.4.1 消息发送顺序

    好了,休息一下。

    喝杯咖啡,缓缓

    2. +load 方法的原理

    2.1 题外话

    如果在上述的6个类中都添加load方法,那么实现的逻辑又是怎样的?

    在6个类中都添加如下代码:

    + (void)load
    {
        NSLog(@"%@--%@", 类名/分类名, NSStringFromSelector(_cmd));
    }
    
    运行结果: 图2.1.1 编译顺序 图2.1.2 图2.1.1的运行结果
    图2.1.3 编译顺序 图2.1.4 图2.1.3的运行结果

    2.2 一探究竟

    图2.2.1 load加载层次

    正如图2.2.1显示,关键代码如红色所示,prepare_load_methods加载完才会对call_load_methods进行调用。

    如下是prepare_load_methods()部分代码:

    void prepare_load_methods(const headerType *mhdr)
    {
        size_t count, i;
    
        runtimeLock.assertWriting();
    
        classref_t *classlist = 
            _getObjc2NonlazyClassList(mhdr, &count);
        for (i = 0; i < count; i++) {
            schedule_class_load(remapClass(classlist[i]));
        }
    
        category_t **categorylist = _getObjc2NonlazyCategoryList(mhdr, &count);
        for (i = 0; i < count; i++) {
            category_t *cat = categorylist[i];
            Class cls = remapClass(cat->cls);
            if (!cls) continue;  // category for ignored weak-linked class
            realizeClass(cls);
            assert(cls->ISA()->isRealized());
            add_category_to_loadable_list(cat);
        }
    }
    

    从上述代码中可以并不能看出最终加载的顺序,但是能够看到class loads的顺序。

    其中schedule_class_load是个递归的调用,代码如下:

    static void schedule_class_load(Class cls)
    {
        if (!cls) return;
        assert(cls->isRealized());  // _read_images should realize
    
        if (cls->data()->flags & RW_LOADED) return;
    
        // Ensure superclass-first ordering
        schedule_class_load(cls->superclass);
    
        add_class_to_loadable_list(cls);
        cls->setInfo(RW_LOADED); 
    }
    

    schedule_class_load代码分析:

    1. 递归调用相当于数据结构中的的操作结构。栈底存放的可以看做是schedule_class_load(cls),每次将cls->supercls作为schedule_class_load的参数,然后将其入栈,继续判断cls是否为nil,如果为nil,则出栈,进行处理add_class_to_loadable_list, 然后继续出栈,直到栈空为止。

    2. 从源码可以看出来:关于类的load方法调用,存储顺序是先父类,再子类load

    上述代码中的add_class_to_loadable_list的部分源码如下:

    void add_class_to_loadable_list(Class cls)
    {
        IMP method;
    
        loadMethodLock.assertLocked();
    
        method = cls->getLoadMethod();
        if (!method) return;  // Don't bother if cls has no +load method
         ...  
         ...
         ...
        loadable_classes[loadable_classes_used].cls = cls;
        loadable_classes[loadable_classes_used].method = method;
        loadable_classes_used++;
    }
    

    从上述代码中,有一点是注意的,if (!method) return;如果该类中没有实现load方法,则直接返回,进行出栈的其他操作。

    上述schedule_class_load结束之后,开始add_category_to_loadable_list。该方法的加载就是按照编译时的顺序进行存储。

    当上述操作完成后,load的预加载也结束了;接下来就是真正的call_load_methods的调用。

    call_load_methods才能决定真正的调用流程有没有在这一步分生变化,如下所示(精简):

    void call_load_methods(void)
    {
        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);
    
    }
    

    从上述的call_load_methods方法可以看出,关键的代码是外层的do-while语句。

    明显的可以看出来,是先call_class_loads,然后call_category_loads。因此,可以确定:cls -> category为大致的顺序

    通过查看这两个函数的源码,内部是很常规的for循环,从头到尾。这里至少说是没有颠倒顺序的操作。

    还发现了另一种情况。就是:load方法是主动执行的,就算什么消息都不手动发送,当程序运行起来的时候,它也会执行。毕竟如果按照消息发送机制的逻辑,得我调你,你才执行啊。它是运行时,系统进行调用的。

    综上所述,可以得出结论:

    1. load方法的调用顺序为: 类->分类;
    2. 类中load调用顺序为:父类->类;
    3. 分类load调用顺序为:按照编译的顺序;
    4. 类(call[supercls,curcls])->分类(call[cmpl0.....cmplN])。

    综上,就是load的完整流程。

    2018.7.6 更新 2.2.2 load调用顺序

    好了,休息一下。


    来来来,一起休息一下

    3. initialize还有谁(有点捏花惹草)

    3.1 添加initialize方法进行测试

    在前边6个类中分别添加如下的测试代码

    + (void)initialize
    {
        NSLog(@"%@--%@", 类名/分类名, NSStringFromSelector(_cmd));
    }
    
    运行结果: 图3.1.1 编译顺序 图3.1.2 图3.1.1的运行结果 图3.1.3 编译顺序 图3.1.4 图3.1.3的运行结果

    3.2 猜测

    从结果可以预估:

    1. initialize先执行父类,再执行类。

    2. 而且都只是执行了分类,且执行的分类的顺序是按照编译的逆序进行的,且只执行了一次。

    3. 从1的分析可以看出来加载load的影子。如果按照先加载父类方法这个尿性的话,是不是内部也通过一个递归实现的。那么它和load的递归有区别吗?(妈的,别再yy了。滚去看苹果粑粑的源码😆)

    4. 从2的分析可以看出,是消息发送的机制。 如果真真的如猜测的一样,关于3的消息发送机制,其实还有个小陷阱。即如果当前类没有实现initialize方法,那么按照消息机制的尿性,是不是要找他爹给摆平(毕竟官场气息太重,这社会没爹也是不行啊。没想到代码中早已告诉了我这个道理。[蠢哭])。

    5. 综上来说,initialize确实有点骚。这里摸一下,那里摸一下。所以一会得对照源码进行分析一波(希望脸不要太疼)。

    3.3 源码分析-走-起-来

    皮一下
    写到这个标题,突然想到了我大渤哥(黄渤)在18年春晚的那首跳起来。写到这,我脑袋里毅然神浮了这个魔曲。

    我屁股就坐在办公的凳子上,一边敲着代码,一边左右摆动的甩了起来。突然,被地上的几个轮子亲了一下,疼的脚想踹人。

    毕竟我再晃两下,我司的工学办公椅,它那仅留的两个轮子终将被我通通抛弃😰(看来越发展,这美曰其名的物件,坏在了质量做工啊。是道德的沦丧,还是人性的缺失?欢迎收看今晚的xxxx)。

    3.4 正经点

    当我运行initialize方法时,发现与load方法不一样。load方法是在运行时,主动调用的。而initialize,如果将main.m的代码段注释后,是不会执行。

    这说明从调用的机制可以看出来两者加载的方式是不一样的。当还原.m后,类第一次发送消息时,又开始调用了。从这可以看出,是进行了_objc_msgSend()调用,和刚才的猜想相照应。

    通过查找源码中的libobjc.order文本可以看出来具体的执行顺序。对于我等屌丝来说,他可是个万能的宝(宝?我信了你的袜)。


    图3.4.1 initialize 调用的流程
    1. 从图图3.4.1中发现绿色部分代码,也使用了迭代操作。(哈哈,此刻也应征了它确实偷偷摸了一下load这家伙)。

    _class_lookupMethodAndLoadCache3方法中的关键代码如下:

    IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
    {
        return lookUpImpOrForward(cls, sel, obj, 
                                  YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
    }
    

    lookUpImpOrForward方法中的关键代码如下:

    if (initialize  &&  !cls->isInitialized()) {
            runtimeLock.unlockRead();
            _class_initialize (_class_getNonMetaClass(cls, inst));
            runtimeLock.read();
            // 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
        }
    

    _class_initialize() 的关键代码如下:

    void _class_initialize(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()) {
            _class_initialize(supercls);
        }
        
        // Try to atomically set CLS_INITIALIZING.
        {
            monitor_locker_t lock(classInitLock);
            if (!cls->isInitialized() && !cls->isInitializing()) {
                cls->setInitializing();
                reallyInitialize = YES;
            }
        }
        callInitialize(cls);
    }
    

    该方法主要是为了让父类先执行,直到没有父类,或者父类初始化完成if (supercls && !supercls->isInitialized()),然后执行callInitialize(cls)。

    而callInitialize(cls)代码如下所示:

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

    哇塞(赛哇),真面目出来了,最后一步就是objc_msgSend,哈哈,看你再躲。因此接下来走消息发送的流程(分类initialize-> else -> 类)。这样所有问题到这里就又结束了。

    综上得出以下结论:

    1. 父类(分类 else 类)->类(分类 else 类)`;
    2. 其中()的内容 array[cmplN,cmplN_1,cmplN_2.....,cls]->method。
    3. 即:supercls(array[cmplN,cmplN_1,cmplN_2.....,cls]->method)-curcls(array[cmplN,cmplN_1,cmplN_2.....,cls]->method)
    2018.7.6 更新 3.4.2 initialize调用顺序

    3.5 话外

    等等......

    还有一个问题,就是刚才在代码中看到的一幕。

    • 1.在上述lookUpImpOrForward贴出的源码中的注释,引起了我的兴趣(我相信也引起了你的兴趣。如果没有引起,再去看一遍😆):如果当前发的消息不是[[Dog alloc] init],而换成[Dog initialize]
      结果如下所示:
      图3.5.1 主动调用initialize

    对于图3.5.1所示,多出了一次,无疑是狗狗主动发送initialize引起的。

    其实源码中的注释也解释的非常清楚了(我从上边搬到了下边)。

    if (initialize  &&  !cls->isInitialized()) {
            runtimeLock.unlockRead();
            _class_initialize (_class_getNonMetaClass(cls, inst));
            runtimeLock.read();
            // 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
        }
    

    得出如下结论:initialize方法的调用是在该类在第一次使用时,调用的。而后该类再次使用时,是不会调用的(好像如有所思)。


    等等......(艹,我的横杠分隔符都打上了,你才说还有)

    还有一个问题(别墨迹,快说...)?

    如果当前的子类、子分类中都没有实现initialize方法,只有父类、父分类中实现了initialize方法,那么运行结果如下图所示:

    图3.5.2 只有父类、父分类实现initialize

    至于出现这种情况的原因,也是很容易分析的。

    其实就是源码中一个很小的细节。这也是和load方法的区别。在递归调用方法时有一个条件判断;

    在load中递归调用到最顶层时,开始执行add_class_to_loadable_list方法,其中有一个代码片段是如下情况:

        method = cls->getLoadMethod();
        if (!method) return;  // Don't bother if cls has no +load method
    

    在load中,如果父类中没有load方法,就直接返回,出栈子类,进行下次的操作。而看一下initialize递归处相应的源码:

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

    从initialize源码中可以看到,并没有出现load中的没有该方法时跳出的情况,而是直接继续畅通无阻的执行(因为我是消息发送机制啊)。从而一路到达objc_msgSend。

    这样即使子分类、子类都没有,此时也可以继续寻找父类.

    😜,这次应该不用等了,没了。

    以上。

    好了,休息一下。


    果汁咖啡也挺好喝的

    4 联合起来才会更强(关联对象)

    在Animate+cat1.h中添加如下的测试代码:

    #import "Animate.h"
    
    @interface Animate (cat1)
    
    /** name */
    @property (nonatomic, copy) NSString *name;
    
    - (void)play;
    
    @end
    

    Animate+cat1.m

    
    #import "Animate+cat1.h"
    #import <objc/runtime.h>
    
    @implementation Animate (cat1)
    
    - (void)setName:(NSString *)name
    {
        objc_setAssociatedObject(self, "name", name, OBJC_ASSOCIATION_COPY_NONATOMIC);
    }
    
    - (NSString *)name
    {
        return (NSString *)objc_getAssociatedObject(self, "name");
    }
    @end
    

    上述的第二个参数"name"也可以换成@selector(name),因为第二个参数的类型是:const void * _Nonnull key, 是void *类型,所以在C中,可以看做执行函数的指针类型,因此可以直接换成OC中的SEL类型,且这样写会有提示。

    在main.m中进行简单的测试,赋值,取值操作,就可以看出来使用起来和属性差不多。

    4.1 查看分类的结构

     struct _category_t {
        const char *name;
        struct _class_t *cls;
        const struct _method_list_t *instance_methods;
        const struct _method_list_t *class_methods;
        const struct _protocol_list_t *protocols;
        const struct _prop_list_t *properties;
    };
    

    上述category结构中包含了:名称、所属类别、对象方法列表、类方法列表、协议列表、属性列表。

    于是可以给分类中添加方法,协议和属性。但是好像没有实例变量列表,那是不是说明分类中不可以添加实例变量呢?

    可以从以下两方面入手:

    4.1.1 查看类的结构

    如果在Animate基类中,添加一个属性和一个实例变量。代码如下:

    Animate.h

    #import <Foundation/Foundation.h>
    
    @interface Animate : NSObject
    
    /** name */
    @property (nonatomic, copy) NSString *name;
    
    - (void)play;
    
    @end
    

    Animate.m

    #import "Animate.h"
    
    @interface Animate()
    {
        NSString *_height;
        
    }
    @end
    
    @implementation Animate
    
    + (void)load
    {
        NSLog(@"Animate--%@", NSStringFromSelector(_cmd));
    }
    
    + (void)initialize
    {
        NSLog(@"Animate--%@", NSStringFromSelector(_cmd));
    }
    
    - (void)play
    {
        NSLog(@"Animate--%@", NSStringFromSelector(_cmd));
    }
    
    @end
    

    从代码中,可以看到添加了一个属性name和一个实例变量_height,其中还包含有2个类方法,1个实例方法。

    由属性name的特性可知,会自动生成实例变量:_name,-setName:和-name方法的声明及其实现。

    因此:包含的内容应该是:1个属性,2个实例变量,2个类方法,3个对象方法的实现

    通过将Animate.m文件进行clang,可以查看生成的c++关键源码如下(精简后):

    struct _class_t OBJC_CLASS_$_Animate  = {
        0, // &OBJC_METACLASS_$_Animate,
        0, // &OBJC_CLASS_$_NSObject,
        0, // (void *)&_objc_empty_cache,
        0, // unused, was (void *)&_objc_empty_vtable,
        &_OBJC_CLASS_RO_$_Animate,
    };
    
    struct _class_t {
        struct _class_t *isa;
        struct _class_t *superclass;
        void *cache;
        void *vtable;
        struct _class_ro_t *ro;
    };
    
    struct _class_ro_t {
        unsigned int flags;
        unsigned int instanceStart;
        unsigned int instanceSize;
        unsigned int reserved;
        const unsigned char *ivarLayout;
        const char *name;
        const struct _method_list_t *baseMethods;
        const struct _objc_protocol_list *baseProtocols;
        const struct _ivar_list_t *ivars;
        const unsigned char *weakIvarLayout;
        const struct _prop_list_t *properties;
    };
    

    从源码中可以看出,结构体的声明和赋值操作;我们关心的内容在结构体中_class_ro_t中:方法列表、协议列表、属性列表、实例变量列表、属性列表;然后看一下对应的各个列表的源码:

    • 属性列表:
    static struct /*_prop_list_t*/ {
        unsigned int entsize;  // sizeof(struct _prop_t)
        unsigned int count_of_properties;
        struct _prop_t prop_list[1];
    } _OBJC_$_PROP_LIST_Animate  = {
        sizeof(_prop_t),
        1,
        {{"name","T@\"NSString\",C,N,V_name"}}
    };
    
    struct _prop_t {
        const char *name;
        const char *attributes;
    };
    

    看到存储了一个结构体的属性数组中存放着唯一的name属性。

    • 实例变量列表:
    static struct /*_ivar_list_t*/ {
        unsigned int entsize;  // sizeof(struct _prop_t)
        unsigned int count;
        struct _ivar_t ivar_list[2];
    } _OBJC_$_INSTANCE_VARIABLES_Animate  = {
        sizeof(_ivar_t),
        2,
        {{(unsigned long int *)&OBJC_IVAR_$_Animate$_height, "_height", "@\"NSString\"", 3, 8},
         {(unsigned long int *)&OBJC_IVAR_$_Animate$_name, "_name", "@\"NSString\"", 3, 8}}
    };
    
    struct _ivar_t {
        unsigned long int *offset;  // pointer to ivar offset location
        const char *name;
        const char *type;
        unsigned int alignment;
        unsigned int  size;
    };
    

    看到存储了2个结构体的数组包含了原有的_height和属性生成的_name。

    • 对象方法列表:
    static struct /*_method_list_t*/ {
        unsigned int entsize;  // sizeof(struct _objc_method)
        unsigned int method_count;
        struct _objc_method method_list[3];
    } _OBJC_$_INSTANCE_METHODS_Animate __attribute__ ((used, section ("__DATA,__objc_const"))) = {
        sizeof(_objc_method),
        3,
        {{(struct objc_selector *)"play", "v16@0:8", (void *)_I_Animate_play},
        {(struct objc_selector *)"name", "@16@0:8", (void *)_I_Animate_name},
        {(struct objc_selector *)"setName:", "v24@0:8@16", (void *)_I_Animate_setName_}}
    };
    
    struct _objc_method {
        struct objc_selector * _cmd;
        const char *method_type;
        void  *_imp;
    };
    

    存储了3个结构体的数组,是原有的play方法和属性生成的setName和name方法。

    也可以看到其他的类方法列表存储在metacls中等。通过查看类的结构,就能对之前的猜测进行有力的验证。

    接下来就据此,对比的看一下,如果在分类中添加属性列表和实例变量,又如何呢?

    4.1.2 分类结构
    • 布置内容

    首先在Animate+cat1.h中添加属性name。当我尝试添加实例变量时,发现没法添加,一写就报错💔;代码如下:
    Animate+cat1.h

    #import "Animate.h"
    
    @interface Animate (cat1)
    
    /** name */
    @property (nonatomic, copy) NSString *name;
    
    - (void)play;
    
    @end
    
    • 通过clang查看Animate+cat1.m的源码
      我们只关心属性列表、方法列表、方法实现
    static struct /*_prop_list_t*/ {
        unsigned int entsize;  // sizeof(struct _prop_t)
        unsigned int count_of_properties;
        struct _prop_t prop_list[1];
    } _OBJC_$_PROP_LIST_Animate_$_cat1  = {
        sizeof(_prop_t),
        1,
        {{"name","T@\"NSString\",C,N"}}
    };
    

    wtf,瞅了一圈,愣是没有看到实例变量列表结构,不死心又去查看了一波实例方法列表如下:

    static struct /*_method_list_t*/ {
        unsigned int entsize;  // sizeof(struct _objc_method)
        unsigned int method_count;
        struct _objc_method method_list[1];
    } _OBJC_$_CATEGORY_INSTANCE_METHODS_Animate_$_cat1  = {
        sizeof(_objc_method),
        1,
        {{(struct objc_selector *)"play", "v16@0:8", (void *)_I_Animate_cat1_play}}
    };
    

    好吧,啥都没有,只有之前写的play方法。服了,其实这也可以理解的。

    4.1 查看分类的结构开头,就直接说明了_category_t结构包含的内容。里边确实是没有ivar_lists。这样一来,即时结构中给了一个属性列表,用处也不是很大啊。

    没法使用其中的set和get方法进行操作,不能够存储内容。直接输入_name或者self->_name也是行不通的。因此为了能够存储数据,苹果粑粑又跳出来了。

    4.2 关联对象搞起来

    苹果粑粑托梦

    粑粑:小子,我给你属性列表了,你只需要重写相应的set和get方法。

    :好像是啊😓。那......,那我该怎么实现呢?

    我又没办法定义一个实例变量,莫非再让我定义一个属性,这样有没有set,get方法,这样又开始循环了(子子孙孙,无穷匮也!)。
    但我又没办法声明一个实例变量。在分类中声明一个实例变量,想想就别扭啊。

    如果在.m中声明一个实例变量,一般都是extention,()中也没有名称。在分类中,()中又是有内容的。如果这样写,又报了一对错误(啊啊啊啊,我疯了)。

    粑粑:傻儿子,继续想。拿出你C语言中长久不用的大招。

    : 啥?奥(dingdong)我知道了,你让我用全局变量吗,这样也行。但是以后我每新加了一个属性,都要重新定义一个全局变量。这样粑粑会不会打死我,抢你太多的饭了(内存)。

    那我定义一个字典就好了啊(等待粑粑夸我)。

    粑粑:不要抢老子的饭。

    : 大哭(心想:屎粑粑,你那么有钱,已经从你的开发者中通过内购剥削了3分,还这样对我......)。

    粑粑:好吧,不逗你了,其实我已经给你提供了一个关联对象,方便你管理分类中的属性。至于在哪,你小子自己去找吧。毕竟粑粑有些东西是不能够给你说太清楚的,否则都要来我这里更改东西了。

    : 恩,谢谢粑粑(😒,没有我找不到的东西)。

    4.2.1 扒关联对象的源码

    通过搜索associated关键字可以找到,步骤:objc_setAssociatedObject()->_object_set_associative_reference(), 如下:

    /**********************************************************************
    * Associative Reference Support
    **********************************************************************/
    
    id objc_getAssociatedObject(id object, const void *key) {
        return _object_get_associative_reference(object, (void *)key);
    }
    
    void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy) {
        _object_set_associative_reference(object, (void *)key, value, policy);
    }
    
    void objc_removeAssociatedObjects(id object) 
    {
        if (object && object->hasAssociatedObjects()) {
            _object_remove_assocations(object);
        }
    }
    

    跳进_object_get_associative_reference,可以看到里边出现了四个类:AssociationsManager、AssociationsHashMap、ObjectAssociationMap、ObjcAssociation。查看它们的结构如下:

    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;
        }
    };
    
    class AssociationsHashMap : public unordered_map<disguised_ptr_t, ObjectAssociationMap *, DisguisedPointerHash, DisguisedPointerEqual, AssociationsHashMapAllocator> {
        public:
            void *operator new(size_t n) { return ::malloc(n); }
            void operator delete(void *ptr) { ::free(ptr); }
        };
    
    class ObjectAssociationMap : public std::map<void *, ObjcAssociation, ObjectPointerLess, ObjectAssociationMapAllocator> {
        public:
            void *operator new(size_t n) { return ::malloc(n); }
            void operator delete(void *ptr) { ::free(ptr); }
        };
    
    class ObjcAssociation {
            uintptr_t _policy;
            id _value;
        public:
            ObjcAssociation(uintptr_t policy, id value) : _policy(policy), _value(value) {}
            ObjcAssociation() : _policy(0), _value(nil) {}
    
            uintptr_t policy() const { return _policy; }
            id value() const { return _value; }
            
            bool hasValue() { return _value != nil; }
        };
    

    这段代码怎么理解?

    可以看到里边有两个map,类似于OC中的字典,后边加了类似泛型的东西。

    再仔细一看,这不就是一个二维数组吗?

    奶奶的,代码写这么多,为啥不加上一个注释说:都看好了,这一堆代码像极了一个二维数组。总结之后,结构如下图所示:

    图4.2.1.1 4个类大致结构关系

    可以看出:

    • AssociationsManager中有一个对象AssociationsHashMap指针,它是二维数组的地址,相当于二位数组的名称。该值也是AssociationsManager的地址。他管理着内存中所有的关联对象。

    • 纵坐标为当前的分类对象object,object下标对应的整行为ObjectAssociationMap。而该内部就是该分类对象下所有的关联对象。可能有name的关联对象,age,size等等。

    • 横坐标为当前的具体key。而如果能够查找到,该单元格就是ObjcAssociation。它是value与policy通过运算的出的值。
      以上就是基本的结构。

    • 如果继续扩展,可以将它看到两个表的组合。而object是最外层表的主键,而内层可以看成内部表的主键。这样也能说得通。

    • 也可以将他看成一张表,只不过是双主键罢了(object,key)。

    综上,通过上边的图,很容易看清楚其结构。至于其添加值,获取值,销毁对象的过程,通过上表也可以很容易的分析。

    4.2.2 内部具体实现细节分析

    以objc_setAssociatedObject()为例进行分析:

    void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) {
        // retain the new value (if any) outside the lock.
        ObjcAssociation old_association(0, nil);
    
     // 其中acquireValue()函数是为了对value根据相应的内存策略进行处理
        id new_value = value ? acquireValue(value, policy) : nil;
        {
            AssociationsManager manager;
            AssociationsHashMap &associations(manager.associations());
        // DISGUISE() ,是将object转换为另一种类型:disguised_ptr_t
            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);
    }
    

    前边的代码加上了注释。从if开始继续分析:

    • 如果new_value有值,则要存进去;否则相当于清空表中对应的数据;

    • if分析:如果AssociationsHashMap列表中有disguised_object这条记录。取出该条记录对应的ObjectAssociationMap指针,再根据key取得ObjcAssociation对象。然后将新的 ObjcAssociation(policy, new_value)填充到该位置即可。如果没有根据找到该key对应的值,则直接手动添加即可。

    • else分析:相当于拿到清空原先关联对象的值(或者成为初始化)。

    相应的objc_getAssociatedObject、objc_removeAssociatedObjects也是如此,可以自己查看相应源码进行分析。
    好了,休息一下。


    这个小熊虎的可爱
    • 本文相关的demo已放置github.
    • 在阅读中,如果发现文章有不合理的地方,欢迎提出疑问至邮箱:B12050217@163.com.
    • 原创文章,转载请注明:转自:Try_Try_Try
    • OK, game over.

    相关文章

      网友评论

          本文标题:Category快快现真身

          本文链接:https://www.haomeiwen.com/subject/rmlgyftx.html