美文网首页iOS
iOS底层探索之类的加载(四):类的关联对象Associated

iOS底层探索之类的加载(四):类的关联对象Associated

作者: 俊而不逊 | 来源:发表于2021-07-28 11:38 被阅读0次

1. 回顾

在前面的几篇博客中,主要讲了类的加载、包括分类的加载底层探索,本次就类的扩展和关联对象进行分析。

iOS底层探索之类的加载(三): attachCategories分析

核心主题

2. 扩展

2.1 什么是分类和扩展

首先我们来看看什么是分类和扩展

category: 类别/分类

  • 专门用来给类添加新的方法
  • 不能给类添加成员属性,添加了成员变量,也无法取到
  • 注意:其实可以通过runtime给分类添加属性
  • 分类中用@property定义变量,只会生成变量的 getter,setter方法的声明,不能生成方法实现和带下划线的成员变量

extension:类扩展

  • 可以说成是特殊的分类,也称作匿名分类
  • 可以给类添加成员属性,但是是私有变量
  • 可以给类添加方法,也是私有方法

分类我们已经很熟悉了,这里就不必过多赘述了,下面介绍下扩展 extension

2.2 扩展

类扩展,我们平时用的是非常多的,如下

匿名扩展

what ? 什么,这就是扩展吗?天天用居然不知道!
是的,这就是扩展,平时用的是非常之多,但是很多人都不知道。

震惊

注意:类扩展要放在声明之后,实现之前,否则会报错。

给扩展加点属性、方法,下面我们看看底层C++是什么样子的。

cpp代码结构

从底层C++代码可以发现,类的扩展属性会添加到成员变量列表,方法也会放在方法列表里面。

思考:那么扩展是否也会像分类一样,影响主类的加载呢?

  • 建立一个单独的扩展文件
    扩展
  • LGPerson.m 导入#import "LGPerson+Ext.h"头文件,实现扩展里面的方法
- (void)ext_sayHello {
    NSLog(@"%s",__func__);
}
+ (void)ext_classMehod{
    NSLog(@"%s",__func__);
}
  • objc 源码里面,把断点断在realizeClassWithoutSwift
  • lldb打印ro方法列表
(lldb) p ro.baseMethods()
(method_list_t *) $0 = 0x0000000100004190
  Fix-it applied, fixed expression was: 
    ro->baseMethods()
(lldb) p *$0
(method_list_t) $1 = {
  entsize_list_tt<method_t, method_list_t, 4294901763, method_t::pointer_modifier> = (entsizeAndFlags = 24, count = 8)
}
(lldb) p $1.get(0).big()
(method_t::big) $2 = {
  name = "saySomething"
  types = 0x0000000100003e09 "v16@0:8"
  imp = 0x00000001000039f0 (ObjcBuild`-[LGPerson(LGA) saySomething])
}
(lldb) p $1.get(1).big()
(method_t::big) $3 = {
  name = "cateA_instanceMethod1"
  types = 0x0000000100003e09 "v16@0:8"
  imp = 0x0000000100003a20 (ObjcBuild`-[LGPerson(LGA) cateA_instanceMethod1])
}
(lldb) p $1.get(2).big()
(method_t::big) $4 = {
  name = "cateA_instanceMethod2"
  types = 0x0000000100003e09 "v16@0:8"
  imp = 0x0000000100003a50 (ObjcBuild`-[LGPerson(LGA) cateA_instanceMethod2])
}
(lldb) p $1.get(3).big()
(method_t::big) $5 = {
  name = "saySomething"
  types = 0x0000000100003e09 "v16@0:8"
  imp = 0x00000001000038a0 (ObjcBuild`-[LGPerson saySomething])
}
(lldb) p $1.get(4).big()
(method_t::big) $6 = {
  name = "sayHello1"
  types = 0x0000000100003e09 "v16@0:8"
  imp = 0x00000001000038d0 (ObjcBuild`-[LGPerson sayHello1])
}
(lldb) p $1.get(5).big()
(method_t::big) $7 = {
  name = "ext_sayHello"
  types = 0x0000000100003e09 "v16@0:8"
  imp = 0x0000000100003900 (ObjcBuild`-[LGPerson ext_sayHello])
}
(lldb) p $1.get(6).big()
(method_t::big) $8 = {
  name = "name"
  types = 0x0000000100003de3 "@16@0:8"
  imp = 0x0000000100003930 (ObjcBuild`-[LGPerson name])
}
(lldb) 

从调式信息可以看到,在$1.get(5).big()打印了扩展的ext_sayHello方法,这也就证明了,类的扩展信息,也会作为类一部分加载到类里面。

3. 关联对象

在分类里面是不可以直接添加成员变量的,但是我们可以间接的添加,这就涉及到关联对象的知识了。

// 获取关联对象
id
objc_getAssociatedObject(id object, const void *key)
{
    return _object_get_associative_reference(object, key);
}

// 设置关联对象
void
objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
{
    _object_set_associative_reference(object, key, value, policy);
}

// 移除关联对象
void objc_removeAssociatedObjects(id object) 
{
    if (object && object->hasAssociatedObjects()) {
        _object_remove_assocations(object, /*deallocating*/false);
    }
}
  • objc_getAssociatedObject 获取关联对象
  • objc_setAssociatedObject 设置关联对象
  • objc_removeAssociatedObjects 移除关联对象

3.1 设值流程

我们来看看下objc_setAssociatedObject设置关联对象方法的里面调用了_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));

    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) {
            auto refs_result = associations.try_emplace(disguised, ObjectAssociationMap{});
            if (refs_result.second) {
                /* it's the first association we make */
                isFirstAssociation = true;
            }

            /* establish or replace the association */
            auto &refs = refs_result.first->second;
            auto result = refs.try_emplace(key, std::move(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);

                    }
                }
            }
        }
    }

    // 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();
}

四个主要的参数是:

  • objc: 要关联的对象,即给谁添加关联属性
  • key: 标识符,方便下次查找
  • value: 要存的值
  • policy: 关联策略

DisguisedPtr方法是一种包装策略,就像快递一样,打包成一个个包裹,方便存储和查找

class DisguisedPtr {
    uintptr_t value;

    static uintptr_t disguise(T* ptr) {
        return -(uintptr_t)ptr;
    }

    static T* undisguise(uintptr_t val) {
        return (T*)-val;
    }

ptr进行了处理,也就是value的处理,也就是对object包装了一下,包装成统一的数据结构。

  • AssociationsManager
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();
    }
};

AssociationsManager::Storage AssociationsManager::_mapStorage;

} 
  • 很多人第一次看到AssociationsManager肯定以为是单例,但是这并不是一个单例,而是通过构造函数加锁析构函数解锁,以此来达到线程安全。
  • AssociationsManager只是用来调用AssociationsHashMap方法的作用,但是AssociationsHashMap是一个单例,因为它通过_mapStorage.get()获取,_mapStorage是一个全局静态变量,放在任何地方都是唯一的。

新建一个分类,里面写几个属性和方法进行验证

@interface LGPerson (LGA)

@property (nonatomic, copy) NSString *cate_name;
@property (nonatomic, copy) NSString *cate_age;

- (void)saySomething;

- (void)cateA_instanceMethod1;
- (void)cateA_instanceMethod2;

+ (void)cateA_classMethod1;
+ (void)cateA_classMethod2;

@end


- (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");
}


- (void)setCate_age:(NSString *)cate_age{
    objc_setAssociatedObject(self, "cate_age", cate_age, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

- (NSString *)cate_age{
    return objc_getAssociatedObject(self, "cate_age");
}

然后再属性赋值,断点提示

LGPerson * person = [LGPerson alloc];
 person.cate_name  = @"jp";
 person.cate_age   = @"20";
 [person saySomething];
源码查看

可以看到associations的结构和refs_result结构,这样看可能不太清楚,那么再lldb看看

associations和refs_result结构

纳尼?什么鬼啊!refs_result这是个什么玩意啊!

什么鬼啊!

靓仔,不用慌,淡定,请看看下面这个取值

if (refs_result.second) {
                /* it's the first association we make */
  isFirstAssociation = true;
}

虽然refs_result很长,很变态,前面那么长只是类型,相当于NSObjectLGPerson这种,但是真正的内容只是后面几个,而这里只是用到了second,其他的压根就不用关心,这一波是不是就很舒服了,哈哈😁

refs_result是从associations.try_emplace(disguised, ObjectAssociationMap{})来的

 auto refs_result = associations.try_emplace(disguised, ObjectAssociationMap{});
  • *try_emplace
template <typename... Ts>
  std::pair<iterator, bool> try_emplace(const KeyT &Key, Ts &&... Args) {
    BucketT *TheBucket;
    if (LookupBucketFor(Key, TheBucket))
      return std::make_pair(
               makeIterator(TheBucket, getBucketsEnd(), true),
               false); // Already in map.

    // Otherwise, insert the new element.
    TheBucket = InsertIntoBucket(TheBucket, Key, std::forward<Ts>(Args)...);
    return std::make_pair(
             makeIterator(TheBucket, getBucketsEnd(), true),
             true);
  }

这里会创建新的桶子BucketT,这里的keyLGPerson的地址。
首先会进入LookupBucketFor去查找是有已经有了桶子,看到两个实现,因为在try_emplace中是BucketT没有const,所以走的是下面的实现。下面的实现会调用上面的实现。

LookupBucketFor

上面这里是两个同名的方法,其中一个参数不一样,下面的调用上面的方法,传入一个地址,说白了就是一个指针的传递。图中我也标记了。

在这里插入图片描述
  • 获取哈希下标
  • 开始死循环找bucket
  • 匹配处理
  • 没找到就再哈希

当第一次进来的时候,LookupBucketFor(Key, TheBucket)是找不到的,所以走到了下面。

插入bucket

这里就插入一个空的桶子进去,并且还进行了3/4扩容2倍的操作。

插入bucket操作

第一次是个空的桶子里面还没有值,如下:

空桶子没有值

第二次进来插入值了,如下

属性赋值
第二次桶子有值
  • 设值流程小结
  1. 创建⼀个 AssociationsManager 管理类
  2. 获取唯⼀的全局静态哈希Map
  3. 判断是否插⼊的关联值是否存在:
    3.1: 存在⾛第4步
    3.2: 不存在就⾛ : 关联对象插⼊空流程
  4. 创建⼀个空的 ObjectAssociationMap 去取查询的键值对
  5. 如果发现没有这个 key 就插⼊⼀个 空的 BucketT进去 返回
  6. 标记对象存在关联对象
  7. ⽤当前 修饰策略 和 值 组成了⼀个 ObjcAssociation 替换原来BucketT 中的
  8. 标记⼀下 ObjectAssociationMap的第⼀次为 false

3.2 插入空值流程

value没有值的时候会走到if判断的else里面

当value没有值的时

associations表中查找disguised,如果没有找到进行erase清空操作。

关联对象插⼊空流程

  1. 根据 DisguisedPtr 找到 AssociationsHashMap 中的 iterator 迭代查询器
  2. 清理迭代器
  3. 其实如果插⼊空值,就是相当于清除的操作

3.3 取值流程

  • objc_getAssociatedObject
id
objc_getAssociatedObject(id object, const void *key)
{
    return _object_get_associative_reference(object, key);
}

  • _object_get_associative_reference
id
_object_get_associative_reference(id object, const void *key)
{
    ObjcAssociation association{};

    {
        AssociationsManager manager;
        AssociationsHashMap &associations(manager.get());
        AssociationsHashMap::iterator i = associations.find((objc_object *)object);
        if (i != associations.end()) {
            ObjectAssociationMap &refs = i->second;
            ObjectAssociationMap::iterator j = refs.find(key);
            if (j != refs.end()) {
                association = j->second;
                association.retainReturnedValue();
            }
        }
    }

    return association.autoreleaseReturnedValue();
}

  • 查找桶子
  iterator find(const_arg_type_t<KeyT> Val) {
    BucketT *TheBucket;
    if (LookupBucketFor(Val, TheBucket))
      return makeIterator(TheBucket, getBucketsEnd(), true);
    return end();
  }
  const_iterator find(const_arg_type_t<KeyT> Val) const {
    const BucketT *TheBucket;
    if (LookupBucketFor(Val, TheBucket))
      return makeConstIterator(TheBucket, getBucketsEnd(), true);
    return end();
  }
  • autoreleaseReturnedValue 返回_value
inline id autoreleaseReturnedValue() {
        if (slowpath(_value && (_policy & OBJC_ASSOCIATION_GETTER_AUTORELEASE))) {
            return objc_autorelease(_value);
        }
        return _value;
    }
  1. 创建⼀个AssociationsManager 管理类
  2. 获取唯⼀的全局静态哈希Map
  3. 根据 DisguisedPtr 找到 AssociationsHashMap 中的 iterator 迭代查询器
  4. 如果这个迭代查询器不是最后⼀个 获取 : ObjectAssociationMap (这⾥有策略和value)
  5. 找到ObjectAssociationMap的迭代查询器获取⼀个经过属性修饰符修饰的value
  6. 返回_value
  • 总结: 其实就是两层哈希map , 存取的时候两层处理(类似⼆位数组)

4. 总结

  • category不能给类添加成员属性,添加了成员变量,也无法取到
  • 可以通过runtime给分类添加属性
  • objc_setAssociatedObject设置
  • objc_getAssociatedObject取值
  • extension是特殊的分类,也称作匿名分类
  • 为了便于理解,画了如下结构图
关联对象
AssociationsHashMap

更多内容持续更新

🌹 喜欢就点个赞吧👍🌹

🌹 觉得学习到了的,可以来一波,收藏+关注,评论 + 转发,以免你下次找不到我😁🌹

🌹欢迎大家留言交流,批评指正,互相学习😁,提升自我🌹

相关文章

网友评论

    本文标题:iOS底层探索之类的加载(四):类的关联对象Associated

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