第十八节—类扩展与关联对象

本文为L_Ares个人写作,以任何形式转载请表明原文出处。

类的扩展作为一种对类的功能和属性进行扩充的方法,经常和分类经常一起被提及。关于分类,已经在前面的一节有过一个总结,清楚了分类的数据加载是分两种情况的。

  • 懒加载的分类 : 懒加载的分类的数据是在编译期完成的,所以数据都在objc_classbitsclass_ro_t结构里面。加载是在主类进行实现的时候开始贴入主类的rwe
  • 非懒加载的分类 : 要在运行时确定数据。通过load_images将数据加载进来,然后贴入主类的rwe

那么关于和分类进行对比的扩展,比如有人会说分类和扩展的差别是扩展添加的都是私有的属性、方法、变量,分类添加属性会报错等等,这些都是本节的重点,一个是探索类的扩展,一个是探索关联对象。

准备 :

objc4-781源码请自行配置或下载。

一、类扩展

类扩展英文名是Extention,顾名思义就是对类进行扩展,添加一些属性,变量,方法等,又被称为匿名分类。

那么先在objc4-781源码中创建一个类JDPerson,然后创建它的扩展。

1. 添加扩展的方式

扩展可以有两种方式。一种是直接在.m文件中进行扩展,另外一种是创建扩展的文件。

  • .m文件中进行扩展 :
图1.1.0.png

图1.1.1.png

然后我们可以在这里添加一些私有的属性或者方法。

  • 创建分类.h文件 :
图1.1.2.png

2. 扩展的探索

在之前的章节中,探索了_objc_init中的map_images,发现在read_images的时候可以找到分类相关的加载源码,有协议的加载源码,但是没有发现有处理扩展的源码。

图1.2.0.png

那么扩展的数据是如何加载进来的呢?

扩展的数据是在编译期就和类的数据一起被编译了。

那么我们有两种方式进行验证。

2.1 通过clang来查看编译的文件

先把创建的扩展.h文件importJDPerson.m中。

然后commond + B进行编译。

打开终端(terminal),进入到objc4-781源码所在文件夹,进入到你的main.m所在的文件夹下,然后输入

clang -rewrite-objc JDPerson.m -o JDPerson.cpp

在当前文件夹下生成编译后的JDPerson.o文件,打开之后找到在扩展里面定义的属性。

图1.2.1.png

可以找打扩展里面增加的属性。

图1.2.2.png

再看method_list

图1.2.3.png

也可以找到定义的方法。这就证明扩展的数据是在编译期就加入到了类里面,直接作为了类的一部分。

2.2 通过ro判定

进入objc4-781源码,找到镜像映射map_images,进入_read_images,或者全搜索_read_images,总之进入_read_images的实现。找到这里。

图1.2.4.png

代码我放在这里。

//自己写的代码。获得cls的ro数据
            const class_ro_t *ro = (const class_ro_t *)cls->data();
            //获取ro里面的名字
            const char *cname = ro->name;
            //用char存储我们自己定义的类的名字
            const char *oname = "JDPerson";
            //如果ro里面的名字和我们定义的类的名字一样且存在于ro里面,打印一下类名
            if (cname && (strcmp(cname, oname) == 0)) {
                //这里挂上断点
                printf("_getObjc2ClassList 类名 : %s - %p\n,",cname,cls);
            }

思路就是 :

如果这个时候可以在JDPerson这个类的ro里面找到在扩展里面添加的方法和属性的gettersetter方法,那么就证明扩展里面的方法和属性在_read_images之前就已经存在了,只是从类的数据里载入,在dyld链接动态库之前就存在,那就证明是在更早的编译期就存在的。

运行之后的结果是 :

图1.2.5.png

如果还觉得这一个方法不能证明,那么可以看看有没有属性的gettersetter方法,那就需要用lldb来查看。

图1.2.6.png

先看ivars里面,ivars里面存储的是类的属性数据,看看类和类扩展的属性是不是都在这里面存在了。

图1.2.7.png

再看method_list_t是不是也有扩展里面的方法,还有属性的gettersetter方法。

图1.2.8.png

的确是都实现了。可以仔细看一下图,或者自己做一下。

3. 结论

  • 类扩展的方法、属性等数据,在编译期,会作为类本身的一部分,和类一起编译。
  • 你可以把类扩展理解为一种声明,因为类扩展的数据加载是依赖类的加载,没有.m实现文件,只有.h声明文件。

这也就理解为什么分类“不能”添加属性,而类扩展是可以添加属性的,因为类扩展在编译期就已经将数据放入ro,分类却要在运行时才可以贴数据进去,而ro没有操作能力,无法直接添加分类给主类的属性,分类的属性自然不能贴进ro

二、关联对象

分类和扩展经常拿来被比较,这是大家熟知的,一般的情况下,都会认为分类是不能添加属性的,因为它处在运行时的情况下是无法对ro进行写入操作的,这是不被允许的,但是运行时的特点就是不确定性,关联对象就是对于分类不能添加属性而出现的不确定性。

关于关联对象,在objc4-781中会有两个runtimeapi体现。

  • objc_setAssociatedObject : 设置关联对象
  • objc_getAssociatedObject : 获取关联对象

先创建一个分类,然后再对它们两个进行认识和探索。

创建JDPerson的分类JDPerson+Category,随意在其中添加一个属性name

图2.0.0.png

如图,创建完成cate_name这个属性之后,按照之前探索的分类的数据加载,它会在rweprop_list_t中出现一个cate_name变量,但是不会生成相应的setget方法。

所以需要在JDPerson+JD.m中利用关联属性自己实现cate_namesettergetter。如图 :

图2.0.1.png

1. objc_setAssociatedObject

先从设置关联属性入手。

看一下它的源码。

图2.1.0.png

先说一下苹果为什么这么喜欢这种接口模式。个人感觉最重要的一点是防止hook,也就是不想让开发者调用私有的api。这样会方便设计者在底层进行修改的时候不会影响上层调用者的代码,因为对外接口不会发生改变,改变的只有底层实现。

那么继续进入。先看这个掉用的函数get()

图2.1.1.png

这里就不多说了,进入之后上面有注释的。可以自行翻译。

进入重点的SetAssocHook
图2.1.2.png
_object_set_associative_reference

这里会有真正的代码实现。对于设置关联属性,重点一定是关联属性是怎么存储的。

先看一下源码总体分为4个大块。

图2.1.3.png

先看核心的存储区域。

  • 1 : AssociationsManager manager : 关联对象的管理类。
    这个管理类不是唯一的,在外部是可以创建多个的,但是里面的实现有加锁的操作,原因是为了防止多线程操作造成重复创建。

  • 2 : AssociationsHashMap &associations(manager.get()) :
    这里的AssociationsHashMap是全局唯一的关联关系哈希表。这里看清楚是manager.get(),是管理者调用的get(),所以进去看管理者。

    图2.1.4.png

  • 3 : 判断我们传入的关联值是否为nil,也就是我们有没有给key传了一个valuenil。毕竟这是哈希表。
    这里要搞清楚,传入cate_name不是说把这几个英文字母传进来了,cate_name是我们对它的赋值,cate_name代表的是一个地址,地址里面存着我们给cate_name这个属性赋的值。

图2.1.5.png
  • 如果传入的不是cate_name属性的值,而是nil,我们走else,这里面下面再单独说。

  • 如果传入的不是nil。走if

  • 4 : 这里就是if的流程。

    图2.1.6.png

    • (1). 根据传进来的object(也就是我创建的JDPerson对象)去到系统表里面找,有没有它这张表。
    • (2).下面是try_emplace的源码,主要就是一个找JDPerson对象这张表的思路——有就直接返回这张表,没有就创建以后再返回,第二个参数就是说我都找到manager管理的总表(关联对象总表)的底部了,当bool值理解吧,因为用lldb调试可以看到,现在先不说,下面会直接上完整的探索流程图。
      • 如果是true,那就是找到底部都没找到,证明没有JDPerson对象这张表。
      • 如果是false,就说明我没找到底部,就找到了JDPerson对象这张表。
        图2.1.7.png
    • (3). 如果没有JDPerson的对象这张表,那么就把JDPerson对象设置成有关联了,然后会被存入manager管理的hashmap,也就是那张关联对象的总表。下图是源码。
      图2.1.8.png
    • (4). 现在无论如何都会有JDPerson这个bucket存储在关联对象总表里面了,所以开始对比key = name,也就是我自己定义的键有没有对应的关联属性值。
      • 如果有,就用新值替换掉旧值。
      • 如果没有就插入新值。


        图2.1.9.png
  • 5 : 这里是else的流程。
    如果传入的valuenil,也就是说对nameJDPersonbucket中置空。

    图2.1.10.png

1.1 举例

  • 流程 :main.m中引入JDPerson分类的头文件。然后设置cate_name的值,挂上断点。并且在分类中设置关联对象的set里面也给objc_setAssociatedObject挂上断点。然后给_object_set_associative_reference挂上断点,看看是不是这么进来的。
图2.1.11.png
图2.1.12.png
图2.1.13.png

运行之后可以发现的确是这样进入的,也就是说我们找到的源码是没有问题的。

  • 变量 :然后看一下disguisedassociationmanagerassociations都是什么。把断点挂在if (value)这里。lldb看一下它们。
图2.1.14.png
  • 存在value,所以走if里面 :看一下refs_result是什么。
图2.1.15.png
  • 看一下try_emplace是怎么找JDPerson然后返回表的 :进入try_emplace,调用了LookupBucketFor,进入它发现有两个。
    下面的那个LookupBucketFor是一个重载,所以它没有const修饰。

    图2.1.16.png

  • 看一下我们调用的是哪个 :段点分别在上面和下面的LookupBucketFor都挂上。重新执行项目,会发现try_emplace是调用了下面的LookupBucketFor里面。但是下面的LookupBucketFor还是调用了上面的LookupBucketFor的实现。存疑的可以走一下断点就会发现跳到上面的LookupBucketFor

图2.1.17.png
  • 看一下上面的LookupBucketFor的实现 :

    图2.1.18.png

  • 看一下try_emplace中的插入新值 :断点挂在try_emplace中的InsertIntoBucket上。因为找不到,所以会插入新的值。给JDPerson一张新的表用来存储关联属性。

    图2.1.19.png

  • try_emplace探索完成,回到外面,看if (refs_result.second) :因为没有找到JDPerson这张表,所以会进入if,然后给JDPerson设置关联属性。setHasAssociatedObjects()来完成设置。

    图2.1.20.png

  • 重新再拿总表 :refs现在就有值了。

    图2.1.21.png

  • 把关联对象的key也就是name和关联对象的值value还有关联策略policy放入JDPerson的关联属性表 :key就是hashkeyhash中的value{policy, value}

图片.png

2. objc_getAssociatedObject

看完设置关联对象就知道了,设置关联对象是一个存储的过程,那么get也就很明显是一个读取的过程。

进入源码 :


图2.2.0.png

接着进 :


图2.2.1.png

图里有注释,我直接说步骤 :

  • 1 : 创建AssociationsManager管理类的对象。
  • 2 : 获取全局唯一的关联对象哈希Map : AssociationsHashMap
  • 3 : 用总表的迭代器递归查找被操作的类在不在关联对象总表里面。
  • 4 : 找到之后,获取对象的关联属性表ObjectAssociationMap,也就是key : {policy ,value}
  • 5 : 然后根据key找出对应{policy ,value}
  • 6 : 取出value并且返回。

获取关联对象的关联属性的值的例子我就不写了。可以自己尝试。下面直接总结。

3. 总结关联对象

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