底层原理-16-类拓展&关联对象

555 阅读9分钟

上一篇分析了分类的底层加载流程,那么关于类拓展以及如何关联对象的,接下来将一一分析。

1.类的拓展

  • 分类category:为主类提供分类,在oc中生成.h和.m文件。可以添加方法,实现他供主类调用。也可以添加属性,但是不会生成setter,getter方法,需要手动生成才能调用。
  • 拓展extension:特殊的分类,也叫匿名分类。可以添加属性和方法,但是是私有的。 创建方式: image.png

1.1 拓展extension的cpp文件分析

在日常开发中,我们创建一个ViewController的时候会自动创建它的拓展

image.png 我们在这边添加的属性和方法这在ViewController.m中访问,外界无法访问,所以它是私有的。
那么拓展的底层是什么呢?在main中自定义一个类以及它的拓展。

image.png 使用clang -rewrite-objc main.mm -o main.cpp 生成cpp文件查看下。搜索下ext_name

image.png 属性生成带下划线的变量,并生成setter和getter方法。 image.png 同时在编译的时候把拓展的方法添加到主类的方法列表

1.2 拓展extension的底层源码分析

我们依然在实现类的地方打上断点

image.png 在实现类的时候类拓展的方法也在方法列表中,说明在编译的时候拓展和主类一起编译进来,拓展依附于主类。

2. 分类关联对象分析

之前我们知道在分类中添加属性不会自动生成setter和getter方法

image.png 所以要用运行时手动实现他们,才能储值和取值。

image.png

2.1 关联对象setter方法分析

objc_setAssociatedObject方法

void
//外部调用
objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)

{

    _object_set_associative_reference(object, key, value, policy);//内部实现

}

这个是一种接口模式,外部不会变化,底层更新不会影响外部的调用。参数1:对象,将要赋值的对象;参数2:标识符,方便下次查找;参数3:;参数4:策略,属性的描述比如copy,strong等。
objc_AssociationPolicy如下:

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. */

};

继续看下object_set_associative_reference的代码

void

_object_set_associative_reference(id object, const void *key, id value, uintptr_t policy)

{   //传参:对象,要存值的对象;标识符,方便下次值;要存的值;策略,属性的策略,属性的修饰nomatoic

    //这段代码在nil被传递给object和key时工作。一些代码可能依赖它来避免崩溃。明确地检查和处理它

    // 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};//封装一下object,类型是DisguisedPtr

    ObjcAssociation association{policy, value};//包装一下策略,和值。

    // retain the new value (if any) outside the lock.

    association.acquireValue();//根据策略类型进行处理

    bool isFirstAssociation = false;

    {

        AssociationsManager manager;//初始化manager,不是全场唯一

        AssociationsHashMap &associations(manager.get());//AssociationsHashMap唯一

        if (value) {

            auto refs_result = associations.try_emplace(disguised, ObjectAssociationMap{});//根据

            //返回一个类

            if (refs_result.second) {//refs_result的second是否为true

                /* it's the first association we make */

                isFirstAssociation = true;//对象object标记第一次关联

            }

            /* establish or replace the association *///建立或者替换关联

            auto &refs = refs_result.first->second;//取出关联类的储存关联键值信息。
            /*
             refs是👇对象第一次关联的时候

            second = {

              Buckets = nil

              NumEntries = 0

              NumTombstones = 0

              NumBuckets = 0

            }第二次

             Buckets = 0x0000000101039190

             NumEntries = 1

             NumTombstones = 0

             NumBuckets = 4

             */
            auto result = refs.try_emplace(key, std::move(association));////查找当前的key是否有association关联对象

            if (!result.second) {//如果为false,说明key存储了值

                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)

        //第一次关联 在锁外面调用setHasAssociatedObjects,因为这个将调用对象的_noteAssociatedObjects方法有一个,这个可以触发+initialize任意东西,包括设置更多的关联对象。

        object->setHasAssociatedObjects();

    // release the old value (outside of the lock).

    association.releaseHeldValue();//旧的值release。

}
  1. 判断是否有值并且存储值的对象是否存在?都不存在退出。
  2. 包装一下object成DisguisedPtr数据结构,包装一下策略和值成类型是ObjcAssociation
  3. 初始化AssociationsManager(不是唯一的)。通过manage获取全场唯一AssociationsHashMap(通过map表哈希获取对应的对象表)
  4. 判断当前是否有值?没有就走移除关联操作。
  5. 有值的话,通过try_emplace去对象关联哈希表查找关联的对象,没有关联的对象创建一个,赋值空的数据结构。
  6. 通过key取对象association类型是ObjcAssociation(我们一开始封装的值和策略)
  7. 判断是否存储过值,有的话,把传进来包装过后的值进行替换原来的值。没有的话,创建新的bucket并把包装的数据塞进去,之后 setHasAssociatedObjects标记。
AssociationsManager & AssociationsHashMap分析
typedef DenseMap<const void *, ObjcAssociation> ObjectAssociationMap;
typedef DenseMap<DisguisedPtr<objc_object>, ObjectAssociationMap> 

AssociationsHashMap;
// class AssociationsManager manages a lock / hash table singleton pair.

//AssociationsManager类管理一个锁/哈希表单例对。

// 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();//关联hash表 关联get,_mapStorage是静态变量修饰的,AssociationsHashMap是唯一的

    }

    static void init() {

        _mapStorage.init();//初始化

    }

};

定义AssociationsManager,相当于自动执行它的代码块进行析构初始化,加锁不是唯一的,只是为了确保线程重复创建,初始化只是作用外面的代码块区间。外面可以定义多个AssociationsManager manager。我们仿照这个格式自己写一个

image.png AssociationsHashMap类型的哈希map, 是通过_mapStorage.get()获取的,而_mapStorage是stantic修饰的所以是唯一的
之后对要储存的值进行包装

image.png 发现refs_result的类型很多,我们只要关注它的值就好,也就是由{objc,bool}组成。

try_emplace分析
// Inserts key,value pair into the map if the key isn't already in the map.

  //如果键不在映射中,则将键、值对插入映射。

  // The value is constructed in-place if the key is not in the map, otherwise

  // it is not moved.如果key不在map中,则就地构造该值,否则不会移动它。

  template <typename... Ts>

  std::pair<iterator, bool> try_emplace(const KeyT &Key, Ts &&... Args) {

    BucketT *TheBucket;

    if (LookupBucketFor(Key, TheBucket))//根据key(对象的封装)查找,找到了赋值给TheBucket

      return std::make_pair(

               makeIterator(TheBucket, getBucketsEnd(), true),

               false); // Already in map.//已经在map表中,返回TheBucket,false表示已经存在了

    // Otherwise, insert the new element.

    TheBucket = InsertIntoBucket(TheBucket, Key, std::forward<Ts>(Args)...);//插入新的

    return std::make_pair(

             makeIterator(TheBucket, getBucketsEnd(), true),

             true);//true 表示第一次往哈希map中添加

  }
  • 根据这个key也就是我们之前包装的对象,在哈希map表中查找,有的话直接返回TheBucket,并构造makeIterator最后一个传false,表示已经存在了。
  • 没有找到,就创建一个BucketT,返回这个空的BucketT,最后一个值传true,表示第一次关联这个对象。 进入LookupBucketFor进行查看发现2个这个方法,但是传参不一样。调试外部是调用第二个重载函数,内部是调第一个。

image.png 先看内部的实现LookupBucketFor主要内部通过whild循环,哈希查找。

image.png 外部外部调用LookupBucketFor

image.png 继续下面的流程,通过template我们会在对象的哈希表中查找到这个对象TheBucket,没有的话,就创建一个,并进行标记也就是下面second的值为true

image.png 继续下面的流程,isFirstAssociation =true 标记第一次关联对象。之后我们打印下关联对象的键值

image.png 第一次打印的时候是空的,下次给这个对象关联赋值的时候有值了。

image.png 查找到对象后在相同的方式通过try_emplace哈希查找我们要存储的键值对,没有的话就把之前包装的赋值进去,已经有值的话,包装的替换原有的

  • 简单总结下setter的过程: 通过AssociationsManager获取到全局唯一的关联对象哈希表,通过我们传进来的对象使用try_emplace哈希查找这个对象的哈希表,没找到就创建一个空的对象哈希表。之后根据我们传的key再次用try_emplace在对象哈希表查找对应的键值,没有的话就把包装好的键值塞进去,有值的话替换成新的。二次哈希表查找,如下图:

image.png

2.2 关联对象的getter分析

objc_getAssociatedObject外部调用和上面set类似都是借口模式。看下内部具体代码

id

_object_get_associative_reference(id object, const void *key)

{
    ObjcAssociation association{};
    {

        AssociationsManager manager;//初始化manager

        AssociationsHashMap &associations(manager.get());//通过manager获取关联对象哈希表

        AssociationsHashMap::iterator i = associations.find((objc_object *)object);//查找这个对象buckets

        if (i != associations.end()) {//如果这个迭代查询器不是最后一个 获取

            ObjectAssociationMap &refs = i->second;//找到迭代器的属性键值表

            ObjectAssociationMap::iterator j = refs.find(key);//根据key去查找属性列表找到对应的bucket

            if (j != refs.end()) {

                association = j->second;//找到对应的ObjcAssociation

                association.retainReturnedValue();//策略类型是retain的话 objc_retain(_value)

            }

        }

    }

    return association.autoreleaseReturnedValue();//返回值_value

}
  1. 和setter中一样创建初始化manger,通过manger获取唯一的关联对象哈希map表
  2. 根据传入的object在对象哈希表中根据find查找这个迭代查询器
  3. 如果这个迭代查询器不是最后一个,取出ObjectAssociationMap,就是关联对象。
  4. 在关联对象有Buckets表中通过keyfind这个对应的桶子。
  5. 找到这个桶子赋值给开始定义的association
  6. 通过association取出这个值返回。 具体流程:

image.png 哈希表中NumBuckets:开辟的空间大小几个bucket;Buckets:桶子数组类;NumEntries:实际存储的个数;NumTombstones:销毁的数量。根据对象找到对应的迭代器。

image.png 发现其实关联对象哈希表和具体属性哈希表结构都一样都是BucketsNumEntriesNumTombstonesNumBuckets。这里通过find查找键值对,找到bucket

3.补充

  • 不管对象哈希表还是键值哈希表,在插入的时候都遵守3/4和2倍扩容规律

image.png 内部实现的方法InsertIntoBucketImpl和我们之前cache_t中缓存策略类似

image.png 3/4扩容。我们新增1个属性,一共3个属性。

image.png

  • AssociationsHashMap 唯一性验证 我们在作用域复制一份相同代码

image.png 同时关闭线程锁

image.png 添加断点,打印里面的值说明是一样的

image.png

4. 总结

  • 类的拓展是一个匿名的分类,它的属性和方法是私有的。在oc中,通常在.m系统会帮我们自动实现。在编译的时候就添加到主类的方法列表和属性列表中在data()中,实现类的时候会赋值给类。
  • 分类通常支持属性或者成员变量的settergetter方法,需要添加的话,要手动实现它的settergetter方法。
  • setter方法底层是通过关联对象哈希表对应的object找到这个对象,没有的话创建一个空的桶子。在根据对象的属性哈希表通过传入的key 哈希找到键值对bucket。是否是存在,不存在包装好的association存入到对应的key。存在的话进行替换。也就是2次哈希查找。
  • getter方法底层是通过传入的对象,用关联对象哈希表查找到这个对象,之后在这个对象的属性列表中通过key哈希查找到对应的value,返回。

分类的属性底层-3.jpg