美文网首页iOS Kit
类扩展 与 关联对象的底层原理探索

类扩展 与 关联对象的底层原理探索

作者: 远方竹叶 | 来源:发表于2021-03-01 15:19 被阅读0次

    类扩展、分类的区别

    1. Category(分类或者类别)

    • 专门给主类添加新的方法

    • 不能给分类添加成员变量(添加了也获取不到)

    • 可以给分类添加属性,但是通过 @property 定义的属性,只会生成变量的 settergetter 方法声明,不会生成方法实现(可以通过 rentime 来重写 settergetter 的实现,即 关联对象)以及带下划线的成员变量

    2. Extension(类扩展)

    • 可以当作是一个特殊的分类,也称作 匿名分类

    • 可以给主类添加方法、属性、成员变量(都是私有的,即自己可见,外界不能调用)

    类扩展的底层原理

    类的扩展有两种创建方式

    • 直接在主类中添加:永远都是在主类的声明之后、实现之前(.m 文件中添加)

    • 通过 commond + N 新建 -> Objective-C File -> Extension

    单独创建好的类扩展文件如下

    类扩展的本质

    有两种方法可以探究:clang源码

    通过 clang 底层编译

    下面我们通过 clang 底层编译,查看类扩展的底层实现

    • 类扩展代码如下
    @interface LGPerson : NSObject
    
    - (void)instanceMethod;
    + (void)classMethod;
    
    @end
    
    @interface LGPerson ()
    
    @property (nonatomic, copy) NSString *lg_name;
    
    - (void)ext_instanceMethod;
    + (void)ext_classMethod;
    
    @end
    
    @implementation LGPerson
    
    - (void)instanceMethod {
        
    }
    
    + (void)classMethod {
        
    }
    
    - (void)ext_instanceMethod {
        
    }
    
    + (void)ext_classMethod {
        
    }
    
    @end
    
    • 通过 clang -rewrite-objc main.m -o main.cpp 命令生成 cpp 文件,打开 cpp 文件,搜索 lg_name
    • 再查看 LGPerson 的类扩展方法,在 编译过程 中,方法就直接添加到了 methodlist 中,作为类的一部分,即 编译时期直接添加到本类里面
    通过 objc 源码探索
    • 创建 LGPerson 类,并创建 LGPerson_Ext.h 主类的类扩展,声明两个方法
    #import "LGPerson.h"
    
    @interface LGPerson ()
    
    @property (nonatomic, copy) NSString *ext_name;
    
    - (void)ext_intanceMethod3;
    - (void)ext_intanceMethod4;
    
    @end
    
    • LGPerson 中实现上面两个方法
    /* ------ LGPerson.h ------*/
    @interface LGPerson : NSObject
    
    - (void)lc_intanceMethod1;
    
    - (void)lc_intanceMethod2;
    
    - (void)lc_intanceMethod3;
    
    - (void)lc_intanceMethod4;
    
    @end
    
    /* ------ LGPerson.m ------*/
    @implementation LGPerson
    
    + (void)load {
        
    }
    
    - (void)lc_intanceMethod1 {
        NSLog(@"%s", __func__);
    }
    
    - (void)lc_intanceMethod2 {
        NSLog(@"%s", __func__);
    }
    
    - (void)lc_intanceMethod3 {
        NSLog(@"%s", __func__);
    }
    
    - (void)lc_intanceMethod4 {
        NSLog(@"%s", __func__);
    }
    
    - (void)ext_intanceMethod {
        NSLog(@"%s", __func__);
    }
    
    - (void)ext_classMethod {
        NSLog(@"%s", __func__);
    }
    
    @end
    
    • 运行 objc 源码,在 readClass 处下个断点,查看此时的 ro 情况

    总结

    • 类扩展在编译时期会作为类的一部分,和主类一起编译进来

    • 类扩展只是声明,它需要依赖当前主类,它没有 .m 文件

    关联对象的底层原理

    主要分为两部分:

    • 通过 objc_setAssociatedObject 设值

    • 通过 objc_getAssociatedObject 取值

    在分类中添加属性 cate_name,通过 runtime 的关联属性方法重写它的 set、get 方法,并在 main 函数中调用如下

    @interface LGPerson : NSObject
    
    @end
    
    @implementation LGPerson
    
    @end
    
    @interface LGPerson (LG)
    
    @property (nonatomic, copy) NSString *cate_name;
    
    @end
    
    @implementation LGPerson (LG)
    
    - (void)setCate_name:(NSString *)cate_name {
        objc_setAssociatedObject(self, "cate_name", cate_name, OBJC_ASSOCIATION_COPY_NONATOMIC);
    }
    
    - (NSString *)cate_name {
        return objc_getAssociatedObject(self, "cate_name");
    }
    
    @end
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            
            LGPerson *p = [[LGPerson alloc] init];
            p.cate_name = @"lg_lc";
            NSLog(@"%@", p.cate_name);
            
        }
        return 0;
    }
    

    关联对象的设值流程

    1. main 函数中 cate_name 赋值处和分类的 setCate_name 方法中打个断点,运行程序
    1. 继续往下运行

    其中 objc_setAssociatedObject 方法有四个参数:

    • 参数1:要关联的对象,即给谁添加关联属性
    • 参数2:标识符,方便下次查找
    • 参数3:设置的value
    • 参数4:属性策略,即 nonatomic、atomic、assign 等,枚举如下
    /**
     * Policies related to associative references.
     * These are options to objc_setAssociatedObject()
     */
    typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
        OBJC_ASSOCIATION_ASSIGN = 0,           /**< Specifies a weak reference to the associated object. */
        OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, /**< Specifies a strong reference to the associated object. 
                                                *   The association is not made atomically. */
        OBJC_ASSOCIATION_COPY_NONATOMIC = 3,   /**< Specifies that the associated object is copied. 
                                                *   The association is not made atomically. */
        OBJC_ASSOCIATION_RETAIN = 01401,       /**< Specifies a strong reference to the associated object.
                                                *   The association is made atomically. */
        OBJC_ASSOCIATION_COPY = 01403          /**< Specifies that the associated object is copied.
                                                *   The association is made atomically. */
    };
    
    1. 进入 objc_setAssociatedObject 的源码实现
    void
    objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
    {
        _object_set_associative_reference(object, key, value, policy);
    }
    
    _object_set_associative_reference

    进入 _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));
        //object封装成一个数组结构类型,类型为DisguisedPtr
        DisguisedPtr<objc_object> disguised{(objc_object *)object};//相当于包装了一下 对象object,便于使用
        // 包装一下 policy - value
        ObjcAssociation association{policy, value};
    
        // retain the new value (if any) outside the lock.
        association.acquireValue();//根据策略类型进行处理
        //局部作用域空间
        {
            //初始化manager变量,相当于自动调用AssociationsManager的析构函数进行初始化
            AssociationsManager manager;//并不是全场唯一,构造函数中加锁只是为了避免重复创建,在这里是可以初始化多个AssociationsManager变量的
        
            AssociationsHashMap &associations(manager.get());//AssociationsHashMap 全场唯一
    
            if (value) {
                auto refs_result = associations.try_emplace(disguised, ObjectAssociationMap{});//返回的结果是一个类对
                if (refs_result.second) {//判断第二个存不存在,即bool值是否为true
                    /* it's the first association we make 第一次建立关联*/
                    object->setHasAssociatedObjects();//nonpointerIsa ,标记位true
                }
    
                /* establish or replace the association 建立或者替换关联*/
                auto &refs = refs_result.first->second; //得到一个空的桶子,找到引用对象类型,即第一个元素的second值
                auto result = refs.try_emplace(key, std::move(association));//查找当前的key是否有association关联对象
                if (!result.second) {//如果结果不存在
                    association.swap(result.first->second);
                }
            } else {//如果传的是空值,则移除关联,相当于移除
                auto refs_it = associations.find(disguised);
                if (refs_it != associations.end()) {
                    auto &refs = refs_it->second;
                    auto it = refs.find(key);
                    if (it != refs.end()) {
                        association.swap(it->second);
                        refs.erase(it);
                        if (refs.size() == 0) {
                            associations.erase(refs_it);
    
                        }
                    }
                }
            }
        }
    
        // release the old value (outside of the lock).
        association.releaseHeldValue();//释放
    }
    

    通过以上源码可知,_object_set_associative_reference 方法主要分为以下几部分:

      1. 创建一个 AssociationsManager 管理类
      1. 获取全局唯一的静态哈希表:AssociationsHashMap
      1. 判断插入的 value 值是否存在
      • 3.1:存在走第四步

      • 3.2:不存在则 移除关联对象

      1. 创建一个空的 ObjectAssociationMap,通过 try_emplace 方法去查询并获取键值对(如果发现没有这个 key 就插入一个 空的 BucketT 进去并返回 true
      1. 如果没有这个 key,就标记为第一次创建
      1. 用当前 policyvalue 组成了一个 ObjcAssociation 替换原来的 BucketT 或者创建新的
      1. 如果是第一次创建的,通过 setHasAssociatedObjects 方法标记对象存在关联对象,即设置 isa 指针的 has_assoc 属性为 true
    源码调试流程
    • 定义 AssociationsManager 变量,从源码可以得知,就是自动调用 AssociationsManager 的构造函数和析构函数
    AssociationsManager()   { AssociationsManagerLock.lock(); }
    ~AssociationsManager()  { AssociationsManagerLock.unlock(); }
    

    加锁并不代表唯一,只是为了避免多线程重复创建,可以在外层定义多个 AssociationsManager 变量的

    • 定义 AssociationsHashMap 类型的哈希 map
    // 定义变量
    AssociationsHashMap &associations(manager.get());
    

    进入 manager.get() 源码,如下

    typedef DenseMap<const void *, ObjcAssociation> ObjectAssociationMap;
    typedef DenseMap<DisguisedPtr<objc_object>, ObjectAssociationMap> AssociationsHashMap;
    
    // class AssociationsManager manages a lock / hash table singleton pair.
    // Allocating an instance acquires the lock
    
    class AssociationsManager {
        using Storage = ExplicitInitDenseMap<DisguisedPtr<objc_object>, ObjectAssociationMap>;
        static Storage _mapStorage; // 静态变量
    
    public:
        AssociationsManager()   { AssociationsManagerLock.lock(); } // 构造函数
        ~AssociationsManager()  { AssociationsManagerLock.unlock(); } // 析构函数
    
        AssociationsHashMap &get() {
            return _mapStorage.get(); // 从上面定义的静态变量中获取,全场唯一
        }
    
        static void init() {
            _mapStorage.init();
        }
    };
    

    从源码中我们可以得知,_mapStorage 是一个静态变量,静态变量又是全局唯一的,AssociationsHashMap 是从静态变量 _mapStorage 中获取的,所以 AssociationsHashMap 是全局唯一的

    • 查看目前的数据结构

    运行项目,走到断点处,打印变量的数据结构

    • disguised : 其中的 value 是来自 object 还原出来的
    • association : 包装的策略类型处理
    • manager : AssociationsManager 管理类
    • associations : 目前的 associations0x0,表示还没有查找到相应的递归查找域中

    继续向下执行,此时传入的 value 有值,所以走 if 流程(如果传入的 value 为空,就会走 移除关联函数 流程,即 else 流程),查看 refs_result 的数据结构,类型很长,可以进行拆解

    // pair 表示有键值对
    (std::pair<
     objc::DenseMapIterator<DisguisedPtr<objc_object>,
     
     objc::DenseMap<const void *, objc::ObjcAssociation, objc::DenseMapValueInfo<objc::ObjcAssociation>, objc::DenseMapInfo<const void *>, objc::detail::DenseMapPair<const void *, objc::ObjcAssociation> >,
     
     objc::DenseMapValueInfo<objc::DenseMap<const void *, objc::ObjcAssociation, objc::DenseMapValueInfo<objc::ObjcAssociation>, objc::DenseMapInfo<const void *>, objc::detail::DenseMapPair<const void *, objc::ObjcAssociation> > >,
     
     objc::DenseMapInfo<DisguisedPtr<objc_object> >,
     
     objc::detail::DenseMapPair<DisguisedPtr<objc_object>, objc::DenseMap<const void *, objc::ObjcAssociation, objc::DenseMapValueInfo<objc::ObjcAssociation>, objc::DenseMapInfo<const void *>, objc::detail::DenseMapPair<const void *, objc::ObjcAssociation> > >,
     
     false>,
     
     bool>)
     
     👇
     
     // 简写
     (std::pair<
     
     objc,
     
     bool>)
    
    try_emplace 源码

    进入 try_emplace 源码实现,如下

    从源码可以看到,有两个返回:

    • 通过查找桶子,如果 map 中存在,则直接返回,并将 make_pair 的第二个参数置为 false

    • 如果没找到,证明是第一次进入,则通过 InsertIntoBucket 插入 map,然后返回,并将 make_pair 的第二个参数置为 true

    ** LookupBucketFor 源码**

    LookupBucketFor 源码有两个同名方法,区别是第一个方法中第二个参数有 const 修饰,通过 try_emplace 源码可知,调用的是第二个方法(重载函数),而第二个方法内部实现是调用第一个 LookupBucketFor 方法

    • 第一个 LookupBucketFor 源码实现如下
    • 第二个 LookupBucketFor 源码实现如下

    继续往下走,运行至 TheBucket = InsertIntoBucket(TheBucket, Key, std::forward<Ts>(Args)...);,查看此时的 TheBucket

    可以看到 TheBucket 的类型与 refs_result 中属性的类型是一致的

    • 继续执行,查看 refs 执行 refs.try_emplace 前后变化

    ** 两次 try_emplace 的区别**

    • 第一次执行 try_emplace 查看 AssociationsHashMap 全局哈希 map 中是否存在该对象的桶子,如果没有,则插入空的桶子

    • 第二次执行 try_emplace 往上面的空桶子中插入 ObjcAssociation (policy, value) 返回 ture

    • result.second 为 NO,证明哈希表中已存在,需要更换最新的

    继续执行,断点在 object->setHasAssociatedObjects();,源码如下

    由此可知,通过 setHasAssociatedObjectsnonpointerIsahas_assoc 标记为 true,到此就将属性与 value 关联上了

    关联对象设值流程图

    从上面分析可以得出,关联对象的哈希 map 结构如下

    • AssociationsHashMap 中有很多的关联对象 map,它的 keyDisguisedPtr<objc_object>,是一个包装的对象(例如 LGPerson、LGTeacher 等),它的 value 是也是一个 map(ObjectAssociationMap)
    typedef DenseMap<DisguisedPtr<objc_object>, ObjectAssociationMap> AssociationsHashMap;
    
    • ObjectAssociationMap 表中有很多的 key-value 键值对,它的 key 类型为 const void *key,这个 key 就是分类里面传过来的;它的 value 也是一个包装好的对象 ObjcAssociation
    typedef DenseMap<const void *, ObjcAssociation> ObjectAssociationMap;
    
    • ObjcAssociation 是用于包装 policyvalue 的一个类

    关联对象的取值流程

    • main.m 添加断点,如下

    继续执行下一步,断点来到重写分类的属性 get 方法,进入 objc_getAssociatedObject 源码的实现

    _object_get_associative_reference

    其源码实现如下:

    id
    _object_get_associative_reference(id object, const void *key)
    {
        ObjcAssociation association{}; // 创建空的关联对象
    
        {
            AssociationsManager manager; // 创建一个 管理类
            AssociationsHashMap &associations(manager.get()); // 获取全局唯一的静态哈希 map
            AssociationsHashMap::iterator i = associations.find((objc_object *)object); // 根据 object 查找 AssociationsHashMap,即获取 buckets
            if (i != associations.end()) { // 如果迭代器不是最后一个
                ObjectAssociationMap &refs = i->second; // 找到 ObjectAssociationMap 的迭代查询器,获取一个经过属性修饰符修饰的 value
                ObjectAssociationMap::iterator j = refs.find(key); //根据 key 查找 ObjectAssociationMap,即获取 bucket
                if (j != refs.end()) {
                    association = j->second; // //获取 ObjcAssociation
                    association.retainReturnedValue(); // retain 处理
                }
            }
        }
    
        return association.autoreleaseReturnedValue(); // 返回 value
    }
    

    通过以上源码,可以将取值流程分为以下几部分:

      1. 创建一个 AssociationsManager 管理类变量
      1. 通过管理类变量获取全局唯一的静态哈希表 AssociationsHashMap
      1. 通过 find 方法根据 object 找到 AssociationsHashMap 中的 iterator,迭代查询器
      1. 如果这个迭代器不是最后一个,获取 ObjectAssociationMap
      1. 通过 find 方法根据 key 找到 ObjectAssociationMap 中的迭代查询器获取一个经过属性修饰符修饰的 value
      1. 返回 value
    调用流程
    • 进入 find 方法,根据关联对象迭代查找 AssociationsHashMap,即 buckets,源码实现如下
    • 打印 迭代器 i, 以及 i->second 查看它们的类型
    • 再次通过 find 方法,在 buckets 中查找与 key 匹配的 bucket,打印 find 方法执行前后 j 的变化

    总结

    综上所述,关联对象的底层调用,主要就是两层哈希 map 的处理,即存取时都是两层处理。流程如下图所示

    相关文章

      网友评论

        本文标题:类扩展 与 关联对象的底层原理探索

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