iOS底层探究-----类扩展&关联对象

413 阅读10分钟

前言

这是我参与8月更文挑战的第3天,活动详情查看:8月更文挑战

上两篇文章,对 类的加载 上类的加载 中类的加载 下 对类和分类的加载进行了底层的探索和分析。那么紧接着,就是对类扩展关联对象的探索。

资源准备

类扩展分析

类扩展的平常展示

ACD65E87-1384-4B5A-80CF-6FD3BD9BCA72.png

  • 内扩展一定是写在声明之后,实现之前的,就如上图的红框里面,这就是内扩展,算得上天天与我们打交道了。它也叫没有名字的分类。

内扩展的C++底层展示

既然知道了内扩展的样式,那么就能在obj源码里面,设置 LGStudent 类,如下图:

961DF16C-361C-47EE-880E-25F761081336.png

然后就可以打开终端,进入到 main.m 的文件,再执行 clang -rewrite-objc main.m -o main.cpp命令,得到对应的 c++ 文件--- main.cpp

打开main.cpp文件,如下图:

3FD4130F-4C7F-410A-B0E9-184D02CCE8F5.png

C++ 底层代码看,类扩展分类还是有差别的,他并没有生成类似category_t那种数据结构。

再看看方法类表:

DF913EFC-536D-4E0F-A0DA-490A24121C99.png

从这里就能分析出,类扩展的成员变量_ext_name、本类的_name、类扩展的方法,还有本类的方法都是放在了同一个列表内。

那么内扩展的方法是在什么时候加载的了?继续进行探索。

内扩展的加载

接下来,我们就创建LGPerson类和他的类扩展:

LGPerson类:

.h BF331560-D06F-4F2F-A3CE-CD42FC1E359E.png .m 01B34CDD-8829-404B-B36B-8E2250CA23E5.png

LGPerson类扩展:

A116F5B0-A032-4ECC-A602-DBDDE64644B8.png

我们知道,在非懒加载类时,会调用realizeClassWithoutSwift函数进行实例化,那么我们就在这个函数里面,进行断点跟踪,运行程序:

8CFAAFDB-3269-49DB-8187-DE67BB6AAC29.png

再通过 lldb 调试,打印里面信息:

1DF11903-60F5-4370-B1F8-9AD8EAC4D026.png

我们可以看到,像方法ext_saySomething和属性ext_name都已经可以打印得到,说明类扩展是伴随这个类的内容一起加载进来了,已经合成为类的一部分了。

内扩展小结

那么结合前几篇文章的分析,可以知道 分类内扩展 的区别了:

1. category:分类

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

2. extension:类扩展

  • 可以说成是特殊的分类,也称作匿名分类
  • 可以给类添加成员变量,但是是私有变量
  • 可以给类添加方法,也是私有方法
  • 在编译期,和主类中的内容一同加载

关联对象的初探

我们知道,在分类中设置的属性,是不会自动生成本类的成员变量的,但是这个问题,却可以通过关联对象来解决。现在,我们创建一个LGPerson+LGA 分类,详情请看下面代码:

---- .h文件

#import "LGPerson.h"
@interface LGPerson (LGA)

@property (nonatomic, copy) NSString *cate_name;

@end

---- .m文件

#import "LGPerson+LGA.h"
#import <objc/runtime.h>

@implementation LGPerson (LGA)

+ (void)load{}

- (void)setCate_name:(NSString *)cate_name{

    /**
     1: 对象
     2: 标识符
     3: value
     4: 策略
     */

    objc_setAssociatedObject(self, "cate_name", cate_name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

- (NSString *)cate_name{

    return  objc_getAssociatedObject(self, "cate_name");
}

那么为什么要这样做了?我们直接进入 objc_setAssociatedObject 函数里面,去查看他的实现:

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

    _object_set_associative_reference(object, key, value, policy);
}

然而,objc_setAssociatedObject 函数还只是一个中间层,其重点,就是 _object_set_associative_reference 函数。

我们根据这个函数的传值,再结合我们所讲的关联对象这个主题,来分析:

  • 关联对象-->存储的是: object(关联到谁) - key(如:cate_name) -> value(值) - policy(关联策略)

那么,我们进入这个函数里面去查看其实现:

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 这种数据结构,因为object传进来可能是各种形式的
    DisguisedPtr<objc_object> disguised{(objc_object *)object};

    //✅ 将 policy、value 包装成  ObjcAssociation 这种数据结构,以面向对象的形式存储
    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();
}

关联对象底层分析

object 封装处理

_object_set_associative_reference 函数里面,首先调用DisguisedPtr的构造函数,将object,封装一个DisguisedPtr数据类型,实现代码:

AB6ADA9A-260C-4690-88F1-E981B7214366.png

value 封装处理

policyvalue 包装成  ObjcAssociation 这种数据结构,以面向对象的形式存储:

8A4FCEDE-8704-4829-B71D-54428E1E7C8B.png

AssociationsManager 分析 (重点)

进入AssociationsManager函数里面:

388C54F7-5456-417C-9F7E-1A23AF445BFA.png

那么我们也模仿AssociationsManager函数,在 main 里面创建类似的构造函数析构函数

struct LGObjc {
    LGObjc()   { printf("LG 构造函数调用 \n");}
    ~LGObjc()  {  printf("LG 析构函数调用 \n"); }
};

接下来断点打印调试:

4C3AB074-5CE5-45C5-B713-6010D09F075B.png

这说明了,AssociationsManager函数并不是一个单利,而是通过构造函数加锁、析构函数解锁,来达到线程安全。

但是为了让这里在执行之后,其作用域消失,就被释放掉,而是能够持续进行作用,就用到了单利 AssociationsHashMap 函数。就如下面这张关系图:

B58361A0-42FC-4273-A6F5-9254037EF9EA.png BFB7BD69-B803-47E4-B42C-071C898C2269.png

为什么是这样子了?我们接着分析:

00C1060D-E591-4C57-B998-0A7F273487D8.png

  • static Storage _mapStorage; -- 全局静态变量,而是在 AssociationsManager 里面调起来,并不是作用域;
  • AssociationsHashMap 这张表是唯一的,是一个单利。通过_mapStorage.get()获取。

数据跟踪

3C183A11-819E-4CC5-8F9E-785776EBE6C4.png

  • 根据打印结果,可以看到disguisedassociationassociations的数据结构,其中_value = KC,上文中,我们在 main 里面给 person.cate_name  = @"KC",证明了目前的调试是正确的。

  • 打印refs_result的信息,refs_result.second这块代码用到了second的值,是 true,那么就能进入 if 判断里面。

关联对象的设值流程

1953FA88-A26C-49FC-886B-1AC1B6BBFD05.png

获取 refs_result 是通过 associations.try_emplace 进行传值的,其中 associationsAssociationsHashMap 类型,他调用的 try_emplace 是传入了 disguised(是object的封装),和新建的ObjectAssociationMap,将这个两个参数,作为键值对进行关联存到associations中。

那么我们进入到try_emplace里面,看其实现。

try_emplace的分析

B9B69D13-CE59-49A6-8793-C30DC41A2DC8.png

  • 先是BucketT *TheBucket,创建一个空的BucketT
  • 然后调用LookupBucketFor(Key, TheBucket)),其中key是传入的disguised,而新建的TheBucket

那么再跟着往下走,进入到 LookupBucketFor 里面。

LookupBucketFor 分析

B683A07E-4450-40C4-8514-2A41CB857480.png

我们进来之后,尽然有两个LookupBucketFor方法,第一个方法的 BucketT 参数有 const,而第二方法里面却没有,根据传入的参数来看,先是进入到第二个方法,然后再内部调用 LookupBucketFor ,其参数是 const BucketT *ConstFoundBucket,所以再调用第一个 LookupBucketFor

其中,FoundBucket是指针传递,有值是可以被带回去的,所以关键代码在第一个LookupBucketFor里面。

template<typename LookupKeyT>
  bool LookupBucketFor(const LookupKeyT &Val,
                       const BucketT *&FoundBucket) const {
    const BucketT *BucketsPtr = getBuckets();
    const unsigned NumBuckets = getNumBuckets();
    
    if (NumBuckets == 0) {
      FoundBucket = nullptr;
      return false;
    }
    
    // FoundTombstone - Keep track of whether we find a tombstone while probing.

    const BucketT *FoundTombstone = nullptr;
    const KeyT EmptyKey = getEmptyKey();
    const KeyT TombstoneKey = getTombstoneKey();

    assert(!KeyInfoT::isEqual(Val, EmptyKey) &&
           !KeyInfoT::isEqual(Val, TombstoneKey) &&
           "Empty/Tombstone value shouldn't be inserted into map!");

//✅ Val 是 disguised(包装的对象), 通过 hash 函数计算得到下标     
//✅ 这是个 hash 函数,计算出下标
    unsigned BucketNo = getHashValue(Val) & (NumBuckets-1);
    unsigned ProbeAmt = 1;

//✅ while 死循环 下标平移,根据下标查找位置     
//✅ 找到,  对FoundBucket进行赋值,返回 true     
//✅ 没找到,再 hash 查找
    while (true) {
      const BucketT *ThisBucket = BucketsPtr + BucketNo;

      // Found Val's bucket?  If so, return it.
      //✅ 找到所在下标位置有值,FoundBucket = 这个地址,函数返回ThisBucket
      if (LLVM_LIKELY(KeyInfoT::isEqual(Val, ThisBucket->getFirst()))) {
        FoundBucket = ThisBucket;
        return true;
      }
      
      // If we found an empty bucket, the key doesn't exist in the set.
      // Insert it and return the default value.
      //✅ 找到所在下标位置没有值,FoundBucket = 这个地址,函数返回 false
      if (LLVM_LIKELY(KeyInfoT::isEqual(ThisBucket->getFirst(), EmptyKey))) {
        // If we've already seen a tombstone while probing, fill it in instead
        // of the empty bucket we eventually probed to.
        FoundBucket = FoundTombstone ? FoundTombstone : ThisBucket;
        return false;
      }

      // If this is a tombstone, remember it.  If Val ends up not in the map, we
      // prefer to return it than something that would require more probing.
      // Ditto for zero values.
      if (KeyInfoT::isEqual(ThisBucket->getFirst(), TombstoneKey) && !FoundTombstone)
        FoundTombstone = ThisBucket;  // Remember the first tombstone found.

      if (ValueInfoT::isPurgeable(ThisBucket->getSecond())  &&  !FoundTombstone)
        FoundTombstone = ThisBucket;

      // Otherwise, it's a hash collision or a tombstone, continue quadratic
      // probing.
      if (ProbeAmt > NumBuckets) {
        FatalCorruptHashTables(BucketsPtr, NumBuckets);
      }

      BucketNo += ProbeAmt++;
      BucketNo &= (NumBuckets-1);//✅ 再哈希
    }
  }

在这个函数里面,进行object对象所对应的bucket的查找,如果存储桶包含键和值,则返回true;否则返设置一个空桶并返回false

如果在LookupBucketFor里面没有找到,那么在接下来就返回到try_emplaceTheBucket现在是存储在AssociationsHashMap中,但是是空的,只是占位置了,如下图:

8127CC5A-8C8C-40E3-AE65-4EE22D81B0B4.png

所以要为它插入内容。调用InsertIntoBucket,传入的参数有TheBucketkey(也就是disguised(是object的封装)),还有Args(也就是ObjectAssociationMap{})。

那么接下来,就进入InsertIntoBucket里面。

InsertIntoBucket分析

BC627C58-09A8-45D6-A6F6-62DBB5201C1A.png

  • TheBucket 进行绑定、扩容等操作;
  • Key 也就是 disguised 赋值到 TheBucketfirst
  • ObjectAssociationMap{} 赋值到 TheBucketsecond
  • 返回 TheBucket

我们还是需要 TheBucket 是如何进行设置的,所以,还是要进入 InsertIntoBucketImpl函数里面。

InsertIntoBucketImpl分析

5DD6B7EF-9152-484D-AACE-DD4B6CCD1B54.png

进行扩容、绑定,最后返回 TheBucket 出去。

返回到 try_emplace 里面,数据跟踪验证下:

86220E48-1595-4D0F-9B05-A1F23EEEA805.png

对比 插入前插入后 ,插入后就有了值 0x0000000101405d60。而makeIterator(TheBucket, getBucketsEnd(), true)第三个返回的参数,默认为 true

接着我们再返回到 _object_set_associative_reference 函数里面:

9301571A-2DB1-4AE1-BA42-0880078135C6.png

在有 value 的情况下,会继续往下执行,打印到的 refs -> $4还是没有数据。

当再次进入 try_emplace 该方法是,此时第一个参数传的是key(就是 “cate_name”),而不是disguised,那么在try_emplace函数里面,所进行LookupBucketFor的调用,查找bucket时,一定返回false。所以一定会调动InsertIntoBucket方法进行association的插入。见下面代码:

19EF6BC4-7397-4707-A2D8-77AE7899D2B6.png

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

关联对象插入空流程

插入空,也就是 value 为空,那么进入的是 else 的环节:

9A78CE53-A086-4412-894A-25AEAF591F32.png

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

关联对象的取值流程

  • 1:创建⼀个 AssociationsManager 管理类;

  • 2:获取唯⼀的全局静态哈希 Map

  • 3:根据 DisguisedPtr 找到 AssociationsHashMap中的iterator 迭代查询器;

  • 4:如果这个迭代查询器不是最后⼀个获取: ObjectAssociationMap (这⾥有策略和 value );

  • 5:找到 ObjectAssociationMap 的迭代查询器获取⼀个经过属性修饰符修饰的 value

  • 6:返回 _value

  • 总结:其实就是两层哈希 map,存取的时候两层处理(类似⼆位数组)

关联对象销毁流程

我们已经知道了关联对象的创建详情,那么他是怎么释放的了?

我们知道在对象的生命周期,当其被销毁时,会调用dealloc方法,同时会对关联对象进行释放。

2C0E89CD-8AE5-4BD8-8334-53FF51028868.png

我们能找到 objc_removeAssociatedObjects 的实现,但是其调用,就没有找到好的线索,那么我们从 dealloc 入手。

可以根据这个流程:dealloc->_objc_rootDealloc->rootDealloc,从dealloc到中间层,最后到rootDealloc实现源码见下图:

8DAD624F-6677-450F-8713-128A2FC3DB23.png

对象在销毁的时候,会判断是否存在析构函数、关联对象、弱引用等等。

那么接着继续跟踪:rootDealloc->object_dispose->objc_destructInstance,经过流程,到达objc_destructInstance函数:

79F44BDB-AFD4-42A0-8F9B-D559DB125E46.png

从这里,知道这个函数是处理析构函数 和 关联对象的。最后调用 _object_remove_assocations 函数,在这个函数中:

C201E3CF-92C9-4886-9C84-3422EF356C77.png

  • 最后通过associations.erase(i);删除i位置的内存。