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
    • ObjcAssociationMap:用来存储的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 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 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,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
  • 根据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,如果是第一次进行关联bool = true,如果不是第一次进行关联bool = false,refs_result.second本质就是获取std::pair结构体中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确实是全局唯一的;

你可能感兴趣的:(iOS底层系列18 -- 扩展Extension与关联对象)