美文网首页
OC底层探索(十八): 类扩展和关联对象

OC底层探索(十八): 类扩展和关联对象

作者: ShawnAlex | 来源:发表于2022-05-27 23:59 被阅读0次

所用版本:

  • 处理器: Intel Core i9
  • MacOS 12.3.1
  • Xcode 13.3.1
  • objc4-838

先看下分类和类扩展区别

分类category
  • 专门给类添加新的方法
  • 不能给成员属性添加成员变量也无法获取到(可以通过runtime给分类添加属性)
  • 分类中定义@property, 只会生产set, get 方法, 不会生成员变量
扩展extension
  • 可以说特殊分类, 也称作匿名分类
  • 可以给类添加成员变量(私有变量)
  • 可以给类添加方法 (私有方法)

其实我们常用的@interface XXX()就是类扩展, 没想到吧, 用的频率比分类都多 :)。

同时留意下:

  • @interface XXX()类扩展位置写在 声明之后, 实现之前, 不然会报错。
  • @interface XXX()里面属性不能写进+load之中, 不然会报错。
错误写法1 错误写法2

类扩展extension

现在在类扩展中加个属性, 方法

例子

Clang一下看下底层内部实现

// 命令
clang -rewrite-objc main.m -o main.cpp
cpp

可看到, 扩展 方法和属性的set, get 方法就直接添加到 method_list中,作为类的一部分。其实类扩展是在编译时直接添加到本类里面, 与分类不同, 分类会生成一个category_t 结构体会影响主类, 但是类扩展不会。我们可以在realizeClassWithoutSwift中读一下ro验证下:

调用

留意下: 要先有个调用, 不然不会加载. 挺好的, 省着浪费内存空间

验证

可发现类扩展方法已经在ro中, 从而也可验证类扩展直接添加到本类中



当然类扩展也可以单独创建, 看下extension创建

extension 创建

Objctive-C FileFile Type 选择Extension即可

实现

但是只会生成一个.h 不会生成.m, 实现还是要再本类.m中进行

总结:

  • 类扩展不会影响类的编译和加载。(会作为类的一部分,和类一起编译, 但是分类会影响 !!!)
  • 类扩展可以写多个,最后也都是合并进主类了
  • 类扩展必主类搭配使用,实现要在本类中实现

关联对象

错误例子 错误例子

之前写分类也说过, 分类是不能添加属性的(只声明 get & setter无法存取), 但是可以通过关联对象方法添加属性。关键方法:

  • objc_setAssociatedObject
  • objc_getAssociatedObject

先看下效果:


例子 例子 例子

可看到能给属性正常赋值, 看一下底层

// 第一个参数: 对象
// 第二个参数: 标识符
// 第三个参数: value
// 第四个参数: 策略

void
objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
{
    _object_set_associative_reference(object, key, value, policy);
}

其中:
第一个参数::要关联的对象,即给谁添加关联属性
第二个参数:标识符,方便下次查找
第三个参数:value
第四个参数:属性的策略,nonatomic, atomic, retain, copy 等

属性策略
/**********************************************************************
* Associative Reference Support
**********************************************************************/

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

其实以前版本与现在版本底层变化很大, 但是 objc_setAssociatedObject, objc_getAssociatedObject名字不会变, 即API稳定

_object_set_associative_reference

看下_object_set_associative_reference内部实现, 看下关联对象做了哪些事情

void
_object_set_associative_reference(id object, const void *key, id value, uintptr_t policy)
{
    // 如果object &&  value 直接return 话说你不调不就行了么 :)
    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
    //  因为传入的可能是: 类名1, 类名2, 类名3 ... 转成一个统一类型方便后面处理
    //  disguised 方法是对ptr 一个处理
    DisguisedPtr<objc_object> disguised{(objc_object *)object};
 
    // 面向对象的处理
    // 包装 {policy, value} 为 ObjcAssociation
    ObjcAssociation association{policy, value};

    bool isFirstAssociation = false;
    {
        // 定义一个构造函数manager, 非单例
        AssociationsManager manager;
        // AssociationsHashMap 是单例,通过 AssociationsManager 获取 AssociationsHashMap。它是在`map_images`的时候初始化。
        AssociationsHashMap &associations(manager.get());

        // 传入的value
        if (value) {
            // 如果 value 存在
            // 将之前 包装disguised 插入 TheBucket,  
            // TheBucket = { first : second } =  { void * : ObjectAssociation }
            auto refs_result = associations.try_emplace(disguised, ObjectAssociationMap{});
            // 如果second存在即value存在
            if (refs_result.second) {
                /* it's the first association we make */
                // refs_result.second = true代表第一次  即首次关联, 
                isFirstAssociation = true;
            }

            /* establish or replace the association */
            // 建立或取代 关联 留意下 这个时候associations还没有插入值
            auto &refs = refs_result.first->second;
            // 第二次 try_emplace 插入
            // key 与 association关联  
            // 这时候的key就是成员变量的key,将 association 插入桶中。有值的情况下没有插入,没有值的情况下才插入。
            auto result = refs.try_emplace(key, std::move(association));
            if (!result.second) {
                // 第二次, 值会发生变化做新旧交换
                association.swap(result.first->second);
            }
        } else {
            // 如果 value 不存在
            // 值不存在或者空这种直接清除 refs.erase(it);
            auto refs_it = associations.find(disguised);
            if (refs_it != associations.end()) {
                // 找到对应的associations
                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);
                    }
                }
            }
        }
    }
    // 第一次时候标记对象是否有关联对象
    if (isFirstAssociation)
        object->setHasAssociatedObjects();

    // 释放
    association.releaseHeldValue();
}

关联对象设计图

关联对象是运行时创建用于存储值 value, 看下关联对象的设计图

关联对象

关联对象流程分析

  • AssociationsManager : 是一个构造函数, 内部进行了加锁和解锁(不是单例)。 构造函数中加锁只是为了避免重复创建,在这里是可以初始化多个AssociationsManager变量

    AssociationsManager
    最下面的 加个static, 相当于类方法调用, 令mapstorage做初始化
  • AssociationsHashMap : 获取唯一的全局静态HashMap, 通过manager获取,是单例。是在map_images的时候进行的初始化。我们也可以读下他的结构

AssociationsHashMap
  • value存在:

    • try_emplace创建空的TheBucket( { first : second } = { void * : ObjectAssociation } )
    • 用当前 policy 和 value 组成了一个 association 替换空的TheBucket
    • 首次关联标记。
    • refs_result.first->second获取object对应的map,此时map中还没有存储构造的association( policy - value )。
    • 第二次try_emplace将key与association传入TheBucket, 即传入map
    • 如果value不存在, association.swap将新旧值交换出来,存储新值进去, 后面会释放旧值。例如:
  • value不存在:

    • associations 根据对象包装disguised 找到编号: ref。
    • 根据key, 找到ref数据it, 并清除
    • 如果该对象已经没有关联对象了,则清除关联对象map
  • try_emplace :
    value有情况下 try_emplace 会走2次

    try_emplace会走2次
  • 第一次参数传入: DisguisedPtr<objc_object> disguised{(objc_object *)object} 闭包

  • 第二次参数传入: key, objc_setAssociatedObject传入的第二个参数自定义的key

try_emplace 关系图
  • InsertIntoBucket
    插桶操作看一下内部
    InsertIntoBucket
    InsertIntoBucketImpl

可看出内部实际是进行了查找,返回对应的 TheBucket。

  • LLVM_UNLIKELY(NewNumEntries * 4 >= NumBuckets * 3): 满足 负载因子 ( 3 / 4 ) 进行 2倍扩容。

  • LLVM_UNLIKELY(NumBuckets-(NewNumEntries+getNumTombstones(): ( 总容量 - 已有元素) <= 总容量 / 8, 相当于 7 / 8 判断。

  • setHasAssociatedObjects 标记关联对象

    setHasAssociatedObjects
  • releaseHeldValue 释放旧对象

    releaseHeldValue

关联对象实测流程

关联对象赋值
test_property 是我们设置的关联对象 关联对象打印
打印一下associationvalue, 对应我们传的值(传入参数) result
打印result, 初始second为true, first里面是其数据, 接下来走try_emplace进行插值 try_emplace
try_emplace 中由于第一次 BucketT 没有值, 所以走下面插入新值 读result

之后走第一次标记关联对象setHasAssociatedObjects以及释放旧value方法releaseHeldValue

释放对象 setHasAssociatedObjects

关联对象释放

void objc_removeAssociatedObjects(id object) 
{
    if (object && object->hasAssociatedObjects()) {
        _object_remove_assocations(object, /*deallocating*/false);
    }
}

内部调用_object_remove_assocations, 看一下

// Unlike setting/getting an associated reference,
// this function is performance sensitive because of
// raw isa objects (such as OS Objects) that can't track
// whether they have associated objects.

// 与设置/获取关联引用不同
// 此函数对性能敏感,因为原始isa对象(如OS对象)无法跟踪它们是否具有关联对象。
void
_object_remove_assocations(id object, bool deallocating)
{
    // 创建 空ObjectAssociationMap refs
    ObjectAssociationMap refs{};

    {
        // 创建 AssociationsManager
        AssociationsManager manager;
        // 创建 AssociationsHashMap
        AssociationsHashMap &associations(manager.get());
        // associations 中找到 iterator 迭代查询器
        AssociationsHashMap::iterator i = associations.find((objc_object *)object);
        
        // 迭代查询器 不等于 associations最后一个
        if (i != associations.end()) {
            
            // 将对象对应的 ObjectAssociationMap 存入refs临时空间
            refs.swap(i->second);

            // If we are not deallocating, then SYSTEM_OBJECT associations are preserved.
            // 如果我们没有取消分配,那么将保留 SYSTEM_OBJECT 关联对象。
            bool didReInsert = false;
            // 对象非释放的情况下
            if (!deallocating) {
                for (auto &ref: refs) {
                    if (ref.second.policy() & OBJC_ASSOCIATION_SYSTEM_OBJECT) {
                        // 重新将系统的关联对象插入
                        i->second.insert(ref);
                        didReInsert = true;
                    }
                }
            }
            
            // 对象释放的情况下
            if (!didReInsert)
                // 清理迭代器i
                associations.erase(i);
        }
    }

    // Associations to be released after the normal ones.
    // 将在正常关联之后释放关联。
    SmallVector<ObjcAssociation *, 4> laterRefs;

    // release everything (outside of the lock).
    // 释放所有
    for (auto &i: refs) {
        if (i.second.policy() & OBJC_ASSOCIATION_SYSTEM_OBJECT) {
            // If we are not deallocating, then RELEASE_LATER associations don't get released.
            if (deallocating)
                laterRefs.append(&i.second);
        } else {
            // 释放非系统的关联对象
            i.second.releaseHeldValue();
        }
    }
    
    // 循环释放 laterRefs
    for (auto *later: laterRefs) {
        later->releaseHeldValue();
    }
}

关联对象总结

设值流程

  • 创建一个AssociationsManager 管理类
  • 获取唯一全局静态哈希Map
  • 判断是否存在关联对象值
    • 存在 :
      • 创建一个空的 ObjectAssociationMap 去取查询的键值对
      • 如果发现没有这个 key 就先插入一个 空的 BucketT
      • 标记对象存在关联对象
      • 用当前 策略 policy值 value 组成了一个 ObjcAssociation 替换之前空的BucketT
      • 标记 ObjectAssociationMap 为 第二次
    • 不存在 :
      • 根据DisguisedPtr 找到 AssociationsHashMap 中的 iterator 迭代查询器
      • 清理迭代器

取值流程

  • 创建一个 AssociationsManager 管理类
  • 获取唯一的全局静态哈希Map
  • 根据 DisguisedPtr 找到 AssociationsHashMap 中的 iterator 迭代查询器
  • 如果这个 迭代查询器 != associations.end(), 即不是最后一个, 那么获取 : ObjectAssociationMap (这里有策略 policy值 value)
  • ObjectAssociationMap的迭代查询器获取一个经过属性修饰符修饰的value
  • 返回value

相关文章

网友评论

      本文标题:OC底层探索(十八): 类扩展和关联对象

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