iOS底层原理17:类扩展和关联对象

681 阅读5分钟

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

在之前的文章中我们已经分析过的加载原理,今天我们来研究一下类扩展关联对象

类扩展

扩展和分类的区别

1、 分类(类别)

  • 专门用来给类添加新的方法;
  • 不能给类添加成员属性,添加了成员属性,也无法获取到;
  • 可以通过runtime给分类添加属性;
  • 分类中用@property定义变量,只会生成变量的getter,setter方法的声明,不能生成方法实现和带下划线的成员变量。 2、 扩展
  • 可以说成是特殊的分类,也称作匿名分类;
  • 可以给类添加成员属性,但是是私有变量;
  • 可以给类添加方法,也是私有方法;

扩展一定要写在类的声明之后,实现之前;

在日常开发中,我们经常会使用类扩展,比如下边就是一个Person类的类扩展:

类扩展分析

我们在main文件中定义一个Student类如下:

接下来,生成main文件的cpp文件:

可以看到,我们在扩展中定义的方法属性和在类中声明的方法和属性是一样存储的;

那么类扩展是否会影响到主类中数据的加载呢?

我们新建一个类Person的类扩展文件,并声明对象方法类方法,然后在Person.m文件中实现两个方法:

注意,一定要引入Person的扩展类;

运行项目:

类扩展的数据,会随着主类一起加载进来;

关联对象

我们在Person的分类中添加两个属性:

这个时候,我们发现,在分类的.m中惠有警告信息:

这是因为,在分类中默认是不能添加属性的,如果需要添加属性,就需要用到关联对象进行处理,手动添加settergetter方法:

那么,这么做的原理是什么?

关联对象底层分析

objc_setAssociatedObject

我们先来分析一下objc_setAssociatedObject,点击查看时间:

这是一个中间层来隔绝业务,防止下层Api在不断变化的时候,我们在上层可以保持调用不变;

我们此处使用的是818版本的源码,779版本的源码此处是通过调用SetAssocHook.get()来实现的

_object_set_associative_reference

点击进入_object_set_associative_reference方法的实现:

这个方法是一个重点:

关联对象是用来存储值的,在分类中没有setter方法用来存储,所以就通过关联对象来存储;

  • object:对象,也就是被关联着
  • key:标识符
  • value:值
  • policy:关联策略;
  • DisguisedPtr<objc_object> disguised{(objc_object *)object}:将被关联对象进行包装,封装成ptr,统一数据结构;
  • ObjcAssociation association{policy, value}:将policyvalue包装成ObjcAssociation,然后与key进行键值匹配;
  • association.acquireValue():根据policy对新值value进行操作持有
  • AssociationsManager manager:是一个构造函数,在作用域开始进行加锁和结束进行解锁,需要注意的是它并不是单例
  • AssociationsHashMap &associations(manager.get()):是个单例,是一张全局唯一的表;
  • associations.try_emplace(disguised, ObjectAssociationMap{}):创建插入bucket
    • result.second判断是否是第一次插入,如果已经存在bucketsecondfalse,否则为true,并用isFirstAssociation进行标记;
    • refs.try_emplace(key, std::move(association)):由于第一次没有找到bucket时,插入的是个空的bucket,所以需要再次try_emplacekeyassociation传入,把关联对象存储到表中
  • association.releaseHeldValue();:对旧值的释放

其存储结构关系如下:

AssociationsHashMap

我们在上文中知道,它是一张全局的表,是个单例;那么它的数据结构是什么样子的呢?

refs_result

其数据结构类型相当复杂,是HashMap底层的存储结构,我们不用去关心;主需要注意其数据部分就可以了;

try_emplace

我们来看一下try_emplace方法的实现:

  • Key:是disguised,也就是警告封装的Person对象;

然后调用LookupBucketFor:

这里有两个LookupBucketFor,两个方法的参数不一样,一个带const,一个没有;两个方法通过指针传递的方式在内部进行调用;将FoundBucket回传,内部的修改了,外部的FoundBucket也会同步变化;

  • const BucketT *BucketsPtr = getBuckets();:获取buckets的地址
  • const unsigned NumBuckets = getNumBuckets();:获取buckets的数量
  • 如果找到了bucket,那么返回bucket,return true
  • 如果找到了空的bucket,return false
  • 没有找到的时候,重新计算下标,然后继续hash

InsertIntoBucket

当没有找到bucket的时候,插入空的bucket

value最终也在此处进行添加; bucketfirstKeysecondValue

我们发现,这个插入流程依然是达到四分之三之后,2倍扩容;之后经过操作,得到bucket:

关联对象总结

设值流程

  • 1.创建一个AssociationsManager管理类;
  • 2.获取唯一的全局静态哈希Map
  • 3.判断要插入的关联值是否存在:
    • 3.1 存在,则执行4
    • 3.2 不存在,则进行关联对象插入空流程
  • 4.创建一个空的ObjectAssociationMap去取查询的键值对;
  • 5.如果发现没有这个key,就插入一个空的BucketT,并返回;
  • 6.标记对象存在关联对象;
  • 7.用当前 修饰策略 组成一个ObjcAssociation替换原来BucketT中的空
  • 8.标记一下ObjectAssociationMap的第一次为false

关联对象插入空流程

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

取值流程

  • 1.创建一个AssociationsManager管理类;
  • 2.获取唯一的全局静态哈希Map
  • 3.根据DisguisedPtr找到AssociationsHashMap中的iterator迭代查询器;
  • 4.如果这个迭代查询器不是最后一个,获取ObjectAssociationMap(含有策略和value);
  • 5.找到ObjectAssociationMap的迭代查询器,获取一个经过属性修饰符修饰的value
  • 6.返回value