Category快快现真身

哈哈,有点蒙

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

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

推荐阅读更多精彩内容