iOS底层系列18 -- 扩展Extension与关联对象

iOS底层系列17 -- 分类的加载机制 探讨了分类category的加载机制,本章来阐述扩展Extension,首先来比较一下两种之间的异同点;

category分类
  • 可以给主类添加方法,分类的方法在运行时会加载进入主类的class_rw_ext中;
  • 可以给主类添加属性,但属性不会生成settergetter方法带下划线的成员变量,必须借助Runtime的关联即属性关联,重写setter,getter方法;
  • 给主类添加的属性,是不会添加到主类中的,而是通过RunTime的关联维护在一个静态全局的HashMap中
  • 不能添加成员变量;

Extension扩展

  • 可以看成是一种特殊的分类,也称作匿名分类;
  • 可以给主类添加方法,但是是私有方法;
  • 可以给主类添加属性,能生成成员变量,但是是私有的成员变量;

Extension的创建

  • Extension的创建通常有两种方式:
    • 第一种:直接在主类的.m文件中书写,写在类的实现之前,如下图所示
Snip20210311_72.png
Snip20210311_73.png

第二种:通过 command+N 新建 -> Objective-C File -> 选择Extension
如下所示:

Snip20210311_78.png

Extension的底层探索

  • 首先针对第一种情况创建的Extension,使用Clang命令将YYPerson.m转成YYPerson.cpp,cd至目标文件夹,终端输入clang -rewrite-objc YYPerson.m -o YYPerson.cpp,得到的文件如下所示:
Snip20210311_75.png
  • 可以看出YYPerson 类扩展的方法,在编译过程中,方法就直接添加到了class_ro_t结构体中,作为类的一部分;

  • 然后针对第二种情况创建的Extension,在objc源码工程中运行,在readClass函数中加入测试代码,并打下断点;

Snip20210311_79.png
  • 断点断住之后,LLDB调试结果如下所示:
Snip20210311_80.png
  • 再次证明类的扩展方法,在编译期时就已经加载到主类的class_ro_t结构体中,与主类合并

  • 总结:

    • 类的扩展 在编译期时 会作为类的一部分,和类一起编译,加载到类的class_ro_t结构体中;
    • 类的扩展只是声明,依赖于当前的主类,没有.m文件,可以理解为一个·h文件;

分类关联对象AssociatedObject

  • 上面说了分类中添加的属性,需要借助关联重写属性的setter,getter方法,才能访问修改属性值,下面来探索关联的底层原理;
  • 准备工作:创建分类,定义属性,使用关联重写属性的setter,getter方法,外界调用;
Snip20210311_81.png
Snip20210311_87.png
setter方法的设值流程
  • 调用objc_setAssociatedObject函数,外界初始化YYPerson实例对象,然后设置second_name属性,来到分类YYPerson+Add_Category中的setter方法;
Snip20210311_83.png
  • 可以看到objc_setAssociatedObject函数有四个参数:

    • 参数1 -- Object:要关联的类对象Object,即给谁添加关联属性;
    • 参数2 -- key:属性对应的标识符字符串,方便下次查找,setter与getter方法中是对应的;
    • 参数3 -- value :属性值;
    • 参数4 -- policy:关联属性的策略,即nonatomic、atomic、assign等;
  • 关联属性到对象在底层中的实现主要涉及到四个核心类分别为:

    • AssociationsManager:关联管理者,提供一个AssociationsHashMap对象;
    • AssociationsHashMap:用来存储关联属性到对象的一个全局的HashMap<Object, ObjcAssociationMap>;
    • ObjcAssociationMap:用来存储<Key, ObjcAssociation>的HashMap;
    • ObjcAssociation: 封装了value与policy的对象
  • 下面提供一张图表示四者之间的关系:

Snip20210312_99.png
  • 进入objc_setAssociatedObject的底层实现如下:
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
{
    SetAssocHook.get()(object, key, value, policy);
}
  • 点击进入get()的方法实现:
Snip20210311_84.png
  • 点击进入SetAssocHook的方法实现:
Snip20210311_85.png
  • 返回值是ChainedHookFunction类型;

  • 上面两者结合调用可以理解成SetAssocHook.get()等价于_base_objc_setAssociatedObject

  • _base_objc_setAssociatedObject函数内部打下断点,确实能执行到这里;

Snip20210311_86.png
  • 紧接着进入_object_set_associative_reference函数,实现如下:
void
_object_set_associative_reference(id object, const 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;

    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));

    DisguisedPtr<objc_object> disguised{(objc_object *)object};
    
    ObjcAssociation association{policy, value};

    // retain the new value (if any) outside the lock.
    association.acquireValue();

    bool isFirstAssociation = false;
    {
        AssociationsManager manager;
        AssociationsHashMap &associations(manager.get());

        if (value) {
            //在全局AssociationsHashMap中 获取object对应的ObjectAssociationMap
            auto refs_result = associations.try_emplace(disguised, ObjectAssociationMap{});
            if (refs_result.second) {//yes
                /* it's the first association we make */
                //object对象 第一次创建 associationMap
                isFirstAssociation = true;
            }

            //获取到ObjcAssociationMap
            auto &refs = refs_result.first->second;
            //在ObjcAssociationMap中 获取key对应的ObjectAssociation
            //key对应的ObjectAssociation 不存在 直接插入
            //key对应的ObjectAssociation 存在  直接覆盖
            auto result = refs.try_emplace(key, std::move(association));
            if (!result.second) {//no 已经存在
                association.swap(result.first->second);
            }
        } else {
            //在AssociationsHashMap中 获取 object对应的ObjectAssociationMap
            auto refs_it = associations.find(disguised);
            if (refs_it != associations.end()) {
                auto &refs = refs_it->second;
                //在ObjcAssociationMap中 获取key对应的ObjectAssociation
                auto it = refs.find(key);
                if (it != refs.end()) {
                    association.swap(it->second);
                    //将ObjectAssociation从ObjectAssociationMap中抹除
                    refs.erase(it);
                    //若ObjectAssociationMap的键值对为0,则从AssociationsHashMap中 抹除
                    if (refs.size() == 0) {
                        associations.erase(refs_it);
                    }
                }
            }
        }
    }

    // Call setHasAssociatedObjects outside the lock, since this
    // will call the object's _noteAssociatedObjects method if it
    // has one, and this may trigger +initialize which might do
    // arbitrary stuff, including setting more associated objects.
    if (isFirstAssociation)
        object->setHasAssociatedObjects();

    // release the old value (outside of the lock).
    association.releaseHeldValue();
}

  • 源码分析如下:
  • 1.首先DisguisedPtr<objc_object> disguised{(objc_object *)object}将目标对象(即YYPerson)封装成一个数组结构类型,类型为DisguisedPtr
  • 2.ObjcAssociation association{policy, value}将value值与policy策略封装到ObjcAssociation类中;
  • 3.AssociationsManager manager创建manager对象;
  • 4.AssociationsHashMap &associations(manager.get())获取associations对象是一个哈希表结构且是全局唯一的,AssociationsHashMap里面存储的是<DisguisedPtr<objc_object>,ObjectAssociationMap>键值对,ObjectAssociationMap也是哈希表结构存储的是<const void *,ObjcAssociation>键值对;
  • LLDB调试结果如下:
Snip20210311_89.png
  • 5.判断value值是否存在,分别执行不同的逻辑:
  • 5.1.当value值存在时,执行associations.try_emplace(disguised, ObjectAssociationMap{}),其含义是:拿目标对象(YYPerson)也就是所谓的Key,在全局HashMap即AssociationsHashMap中进行遍历,最后返回一个refs_result结果,LLDB打印如下:
Snip20210311_90.png
  • try_emplace函数是HashMap的一个函数方法,进入其内部实现:
Snip20210311_93.png
  • 可以看出其返回值refs_result是一个键值对即std::pair<iterator, bool>
  • 根据key(YYPerson)在AssociationsHashMap中找到对应的value,若value存在,则返回一个包含有YYPerson的ObjectAssociationMap的pair键值对;
  • 根据key(YYPerson)在AssociationsHashMap中查找对应的value,若value不存在,则返回一个包含空的ObjectAssociationMap{}的pair键值对;
  • 在AssociationsHashMap中根据Key值查找的逻辑:是调用LookupBucketFor函数,有两个同名方法,其中第二个方法属于重载函数,区别于第一个的是第二个参数没有const修饰,通过调试可知,外部的调用是调用的第二个重载函数,而第二个LookupBucketFor方法,内部的实现是调用第一个LookupBucketFor方法;
Snip20210311_94.png
  • LookupBucketFor函数实现如下:
Snip20210311_96.png
  • 然后执行if (refs_result.second)即判断当前类(YYPerson)是否是第一次进行关联,因为在try_emplace函数中返回的结构体std::pair<iterator, bool>,如果是第一次进行关联bool = true,如果不是第一次进行关联bool = false,refs_result.second本质就是获取std::pair<iterator, bool>结构体中bool成员的值;
  • 接着执行auto &refs = refs_result.first->second,这里获取的是ObjcAssociationMap,然后调用refs.try_emplace(key, std::move(association)),根据Key值(即标识属性的字符串)往ObjcAssociationMap中插入association对象,这里的association就是封装了[policy,value]的ObjcAssociation对象,最后if (!result.second)即判断是否已经插入过了,如果是,则替换之前插入的association对象;
  • 5.2.当value值不存在时,执行以下代码:
image.png
  • 详细逻辑见注释;
getter方法的取值流程
  • 断点依次进入以下函数:
Snip20210312_105.png
Snip20210312_106.png
  • 最后来到_object_get_associative_reference函数,实现如下:
id _object_get_associative_reference(id object, const void *key)
{
    //创建空的关联对象
    ObjcAssociation association{};
    {
        //创建一个AssociationsManager管理类
        AssociationsManager manager;
        //获取全局唯一的静态哈希map
        AssociationsHashMap &associations(manager.get());
        //找到迭代器,即获取buckets
        AssociationsHashMap::iterator i = associations.find((objc_object *)object);
        //如果这个迭代查询器不是最后一个 获取
        if (i != associations.end()) {
            //找到ObjectAssociationMap的迭代查询器获取一个经过属性修饰符修饰的value
            ObjectAssociationMap &refs = i->second;
            //根据key查找ObjectAssociationMap,即获取bucket
            ObjectAssociationMap::iterator j = refs.find(key);
            if (j != refs.end()) {
                //获取ObjcAssociation
                association = j->second;
                association.retainReturnedValue();
            }
        }
    }
    //返回value
    return association.autoreleaseReturnedValue();
}

LLDB调试结果:

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

推荐阅读更多精彩内容