Objective-C 扩展与关联对象

一、类扩展分析

1.1 category 与 extension

categoryextension开发中经常遇到,他们的区别对比简单总结下:

category(类别、分类)

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

extension(扩展)

  • 可以称为特殊分类(匿名分类)。
  • 可以给类添加成员属性,但是是私有变量。
  • 可以给类添加方法,也是私有方法。

1.2 extension 底层实现分析

对于extension我们一般常用的方式是将extension与类的实现放在一起都放在.m中:

image.png

也可以创建单独的extension

image.png

这个时候就只生成了对应的.h文件。

  • extension要在类的声明之后,实现之前。
@interface HPObject : NSObject

@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *age;

- (void)instanceMethod;
+ (void)classMethod;

@end

@interface HPObject ()

@property (nonatomic, copy) NSString *ext_name;
@property (nonatomic, copy) NSString *ext_age;

- (void)ext_instanceMethod;
+ (void)ext_classMethod;

@end

@implementation HPObject

- (void)instanceMethod {
    NSLog(@"%s",__func__);
}

+ (void)classMethod {
    NSLog(@"%s",__func__);
}

- (void)ext_instanceMethod {
    NSLog(@"%s",__func__);
}

+ (void)ext_classMethod {
    NSLog(@"%s",__func__);
}

@end

将上面的代码转成.cpp查看对应的实现:

image.png

可以看到extension的内容与类的内容合并到了一起。并且没有搜到与category_t类似的extension_t

realizeClassWithoutSwift中条件断点源码验证:

image.png

可以看到ro中已经有了extension的方法了(包括setter & getter)。

  • 分类会影响到类的编译和加载。
  • 类扩展不会影响类的编译和加载。
  • 类扩展可以写多个,最后也都是合并进主类了,不影响。(写在同一个文件或者不同文件都可以)
  • 类扩展必须与主类在一起,最好定义在主类.m中,单独定义还是要导入主类.m中。(单独写意义不大)

如果extension是单独的文件声明的,需要将.h文件导入主类的.m中(分类不行),否则不会将extension中定义的属性和成员变量加入类中(方法能加入是因为方法本身是实现在主类中的)。所以单独声明定义extension没有任何意义,只是能这样做而已。

二、关联对象

分类中添加属性后会报警告:

Property 'cat_name' requires method 'cat_name' to be defined - use @dynamic or provide a method implementation in this category

image.png

本质上是因为分类中用@property定义变量,只会生成变量的geter & setter方法的声明,不能生成方法的实现和带下划线的成员变量。本质上是因为没有成员变量,getter & setter无法进行存值与取值。

这个时候可以通过关联对象给类添加属性:

- (void)setCat_name:(NSString *)cat_name {
    //对象、标识符、value、策略
    return objc_setAssociatedObject(self, "cat_name", cat_name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

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

2.1 关联对象存储值

那么关联对象是怎么实现的呢?

2.1.1 objc_setAssociatedObject

objc_setAssociatedObject的源码实现:

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

内部直接调用了_object_set_associative_reference

818版本的实现与779版本不同,779版本中 objc_setAssociatedObject 的实现调用的是 SetAssocHook.get()

2.1.2 _object_set_associative_reference

源码解读:

//关联对象 存储 object -> cat_name -> value - policy
void
_object_set_associative_reference(id object, const void *key, id value, uintptr_t policy)
{
    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
    DisguisedPtr disguised{(objc_object *)object};
    //包装 {policy, value} 为 ObjcAssociation
    ObjcAssociation association{policy, value};

    //根据 policy 对 value 进行操作
    association.acquireValue();

    bool isFirstAssociation = false;
    {
        //manager 不是单例,调用构造函数创建 manager,内部进行了加锁和解锁。
        AssociationsManager manager;
        //AssociationsHashMap 是单例,通过 AssociationsManager 获取 AssociationsHashMap。它是在`map_images`的时候初始化。
        AssociationsHashMap &associations(manager.get());

        if (value) {//有值
            //创建/插入bucket
            auto refs_result = associations.try_emplace(disguised, ObjectAssociationMap{});
            if (refs_result.second) {//第一次,也就是插入 bucket 的时候 second 为 true
                /* it's the first association we make */
                isFirstAssociation = true;
            }
            /* establish or replace the association */
            //这个时候 association 还没有存
            auto &refs = refs_result.first->second;
            //相当于第二层
            //这时候的key就是成员变量的key,将 association 插入桶中。有值的情况下没有插入,没有值的情况下才插入。
            auto result = refs.try_emplace(key, std::move(association));
            if (!result.second) {//second为false 证明 LookupBucketFor 找到了。
                // result.first->second 为 association。将旧的值替换为新的。association 变为旧值。
                association.swap(result.first->second);
            }
        } else {//没有值,进行清空处理。
            //找到对象的ref
            auto refs_it = associations.find(disguised);
            if (refs_it != associations.end()) {
                auto &refs = refs_it->second;
                //找到对应key的 association
                auto it = refs.find(key);
                if (it != refs.end()) {
                    //交换值,也就是内存中存储的it修改为nil,association修改为之前的值。
                    association.swap(it->second);
                    //擦除内存中it数据。
                    refs.erase(it);
                    if (refs.size() == 0) {
                        //如果对象的ref没有关联对象了,则整个对象擦除。
                        associations.erase(refs_it);

                    }
                }
            }
        }
    }
    //只在第一次标记对象是否有关联对象
    if (isFirstAssociation)
        object->setHasAssociatedObjects();

    //释放旧值。
    association.releaseHeldValue();
}
  • 对象,值以及类是否禁用关联对象的逻辑判断。
  • 将对象包装成DisguisedPtr统一类型。
  • 将策略和值包装为ObjcAssociation类型并且根据策略对值进行操作。
inline void acquireValue() {
    if (_value) {
        switch (_policy & 0xFF) {
        case OBJC_ASSOCIATION_SETTER_RETAIN:
            _value = objc_retain(_value);
            break;
        case OBJC_ASSOCIATION_SETTER_COPY:
            _value = ((id(*)(id, SEL))objc_msgSend)(_value, @selector(copy));
            break;
        }
    }
}
  • AssociationsManager manager调用构造函数创建 manager,内部进行了加锁和解锁。本身不是单例。

  • AssociationsHashMap通过manager获取,本质上可以认为是一个单例。是在map_images的时候进行的初始化。

  • value有值的情况:

    • try_emplace创建插入bucket。在这一次将object对应的map插入单例map中。
    • 根据refs_result.second判断是否是第一次插入。如果原先已经存在则secondfalse,否则为trueisFirstAssociation进行第一次标记。
    • refs_result.first->second获取到的相当于是object对应的map,这个时候map中还没有存储构造的association,也就是value-policy
    • 再次try_emplacekeyassociation传入就相当于要存储关联对象到map了(内层)。
      • 值存在的时候不会存储进去与上面一样通过second返回结果。为false证明之前有值。会走到association.swap逻辑。
    • association.swap是为了将旧值交换出来,存储新值进去在后续做释放旧值的存在。
      可以通过赋值两次进行验证:
  • value没有值的情况:

    • associations中找到object对应的map存为refs_it
    • map不为结束标记则取refs_it->second,根据keyassociation存入it
    • 不为结束标记则对it进行交换,也就是map中替换为 {policy,nil},将原先的值存入association。然后擦除map中的值。
    • 如果该对象已经没有关联对象了,则擦出该对象的关联对象map。擦除相当于标记位墓碑(erase)。
  • 根据isFirstAssociation,标记对象是否有关联对象。

  • 通过association释放旧值。

2.1.2.1 disguised 与 association 验证

image.png

2.1.2.2 refs_result 结构

refs_result结构:

image.png

格式化后结构如下:

类型:
std::pair<
    objc::DenseMapIterator<
        DisguisedPtr, 
        objc::DenseMap<
            const void *, 
            objc::ObjcAssociation, 
            objc::DenseMapValueInfo, 
            objc::DenseMapInfo, 
            objc::detail::DenseMapPair<
                const void *, 
                objc::ObjcAssociation
            > 
        >, 
        objc::DenseMapValueInfo<
            objc::DenseMap<
                const void *, 
                objc::ObjcAssociation, 
                objc::DenseMapValueInfo, 
                objc::DenseMapInfo, 
                objc::detail::DenseMapPair<
                    const void *, 
                    objc::ObjcAssociation
                > 
            > 
        >, 
        objc::DenseMapInfo<
            DisguisedPtr 
        >, 
        objc::detail::DenseMapPair<
            DisguisedPtr, 
            objc::DenseMap<
                const void *,
                objc::ObjcAssociation, 
                objc::DenseMapValueInfo, 
                objc::DenseMapInfo, 
                objc::detail::DenseMapPair<
                    const void *, 
                    objc::ObjcAssociation
                > 
            > 
        >, 
        false
    >, 
    bool
>

值:
{
  first = {
    Ptr = 0x0000000101016c70
    End = 0x0000000101016cb0
  }
  second = true
}

first对应objc::DenseMapIteratorsecond对应最后一个bool值也就是true

2.1.3 AssociationsManager

class AssociationsManager {
    using Storage = ExplicitInitDenseMap, ObjectAssociationMap>;
    //静态变量,声明在 AssociationsManager 中是一个全局变量,只能 AssociationsManager 调用。 相当于单例。
    static Storage _mapStorage;

public:
    //析构和构造函数,内部进行了加锁和解锁。
    AssociationsManager()   { AssociationsManagerLock.lock(); }
    ~AssociationsManager()  { AssociationsManagerLock.unlock(); }

    //设置和获取 AssociationsHashMap,由于 _mapStorage 是静态的,所以 AssociationsHashMap 也就相当于单例
    AssociationsHashMap &get() {
        return _mapStorage.get();
    }
    //类方法,在 _objc_associations_init 调用。也就是`map_images`的时候。
    static void init() {
        _mapStorage.init();
    }
};
  • _mapStorage是一个静态变量,声明在 AssociationsManager 中是一个全局变量,只能 AssociationsManager 调用。 相当于单例。
  • AssociationsManager在构造和析构函数中进行加解锁操作。
  • AssociationsHashMap通过AssociationsManagerget方法获取。
  • AssociationsHashMapAssociationsManager的类方法init中初始化。

2.1.3.1 AssociationsManager 模仿验证:

struct HPObjectS {
    HPObjectS() {
        printf("Creat HPObjectS\n");
    }
    ~HPObjectS() {
        printf("release HPObjectS\n");
    }
};

调用:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        HPObjectS s;
    }
    return 0;
}

输出:

Creat HPObjectS
release HPObjectS

出了作用域就被释放了。所以AssociationsManager不是单例,内部进行了加解锁操作。

验证c++的构造和析构函数需要修改.m文件为.mm或者修改typec++

image.png

2.1.4 AssociationsHashMap

typedef DenseMap, ObjectAssociationMap> AssociationsHashMap;

AssociationsHashMap &get() {
    return _mapStorage.get();
}

static void init() {
    _mapStorage.init();
}
  • AssociationsHashMap是通过AssociationsManagerget()方法获取的。
  • 初始化时在AssociationsManager的类方法init()中。(这里相当于是总表的初始化)

2.1.4.1 AssociationsHashMap 单例验证

_object_set_associative_reference中添加如下测试代码(也可以多次调用进入进行测试):

//manager 不是单例,调用构造函数创建 manager,内部进行了加锁和解锁。
AssociationsManager manager;
//AssociationsHashMap 是单例,通过 AssociationsManager 获取 AssociationsHashMap。它是在`map_images`的时候初始化。
AssociationsHashMap &associations(manager.get());

//测试代码
printf("manager: %p, associations: %p\n",&manager,&associations);

AssociationsManager manager1;
AssociationsHashMap &associations1(manager1.get());

printf("manager1: %p, associations1: %p\n",&manager1,&associations1);

AssociationsManager manager2;
AssociationsHashMap &associations2(manager2.get());

printf("manager2: %p, associations2: %p\n",&manager2,&associations2);
//end

输出:

manager: 0x7ffeefbff460, associations: 0x10035e188
manager1: 0x7ffeefbff448, associations1: 0x10035e188
manager2: 0x7ffeefbff438, associations2: 0x10035e188
  • AssociationsHashMap多次输出地址一致,为单例对象。
  • AssociationsManager多次输出地址不一致,普通对象,只是内部对静态变量有操作而已。

由于AssociationsManager中有加锁解锁操作,先暂时注释掉加锁解锁操作,否则会造成死锁(锁递归):

image.png

2.1.4.2 AssociationsHashMap 初始化

上面的代码分析可以看到在AssociationsManager的类方法init()中有对_mapStorage的初始化:

static void init() {
    _mapStorage.init();
}

那么初始化是在什么时机调用的呢?既然是单例那么应该只调用一次,直接在init中打断点有如下调用堆栈:

libobjc.A.dylib`objc::AssociationsManager::init() at objc-references.mm:124:21
libobjc.A.dylib`_objc_associations_init at objc-references.mm:137:5
libobjc.A.dylib`arr_init at NSObject.mm:2214:5
libobjc.A.dylib`map_images_nolock(mhCount=261, mhPaths=0x00007ffeefbf4e90, mhdrs=0x00007ffeefbf40f0) at objc-os.mm:544:9
libobjc.A.dylib`map_images(count=261, paths=0x00007ffeefbf4e90, mhdrs=0x00007ffeefbf40f0) at objc-runtime-new.mm:3259:12
  • 是在map_images中初始化的。

调用的入口是map_images_nolock -> arr_init()arr_init的实现:

void arr_init(void) 
{
    //内存页初始化
    AutoreleasePoolPage::init();
    //散列表初始化,引用计数表和弱引用表
    SideTablesMap.init();
    //关联对象map初始化
    _objc_associations_init();
}
  • AutoreleasePoolPage内存页初始化。
  • SideTablesMap散列表初始化,其中包含引用计数表和弱引用表。
  • _objc_associations_init关联对象表初始化,内部调用了AssociationsManager::init()

2.1.5 try_emplace

  template 
  std::pair try_emplace(const KeyT &Key, Ts &&... Args) {
    //与类的cache的bucket_t好像
    BucketT *TheBucket;
    //通过key(key是包装的 DisguisedPtr,也就是要存储到的对象。) 找bucket
    //TheBucket 没有被const 修饰,走的第二个 LookupBucketFor。TheBucket指针传递,值会被带回来。
    //通过 LookupBucketFor TheBucket 要么有值要么为空桶
    if (LookupBucketFor(Key, TheBucket))
      //second value 为false
      return std::make_pair(
               makeIterator(TheBucket, getBucketsEnd(), true),
               false); // Already in map.

    // Otherwise, insert the new element.
    //走到这里证明,bucket为空桶。则将bucket插入桶中。 objc::detail::DenseMapPair
    TheBucket = InsertIntoBucket(TheBucket, Key, std::forward(Args)...);
    //first value, second value 赋值为 true
    return std::make_pair(
             makeIterator(TheBucket, getBucketsEnd(), true),
             true);
  }
  • try_emplacevalue有值的情况下会调用两次,第一次key为包装的对象DisguisedPtr,第二次key为关联对象的key。这里也就说明整个关联对象表是一个双层结构。
  • LookupBucketFor查找key对应的bucket,也就是对象对应的map以及关联对象key对应的value(都是包装后的)。
  • 找到则包装后返回,没有找到就插入bucket。为了返回原始值进行释放,这也就是有旧值的情况下,值不被替换的原因。

2.1.5.1 LookupBucketFor

LookupBucketFor有两个,BucketT参数不同,一个用const修饰了:

image.png

两个,其中进行了内部调用:

  template 
  bool LookupBucketFor(const LookupKeyT &Val, BucketT *&FoundBucket) {
    const BucketT *ConstFoundBucket;
    //调用上面的LookupBucketFor,result 为 true 证明 Val 对应的bucket已经存在。否则bucket不存在,返回了一个空桶。
    bool Result = const_cast(this)
      ->LookupBucketFor(Val, ConstFoundBucket);
    //赋值bucket给传进来的参数。
    FoundBucket = const_cast(ConstFoundBucket);
    return Result;
  }
  • 这里调用了LookupBucketFor,两者参数不一样。返回值为result,找没有找到bucket,也就对应map以及value
  • FoundBucket是指针传值,内部修改后外部也就修改了。
  template
  //BucketT 被 const 修饰了
  bool LookupBucketFor(const LookupKeyT &Val,
                       const BucketT *&FoundBucket) const {
    //获取buckets地址
    const BucketT *BucketsPtr = getBuckets();
    //获取buckets数量
    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!");
    //key & (容量 - 1 ),类似于cache的mask,也就是找到index
    unsigned BucketNo = getHashValue(Val) & (NumBuckets-1);
    unsigned ProbeAmt = 1;
    //与cache插入类似
    while (true) {
      const BucketT *ThisBucket = BucketsPtr + BucketNo;
      // Found Val's bucket?  If so, return it.
      //找到了bucket,也就是bucket已经存在了。LLVM_LIKELY 就是 fastpath
      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.
      // 找到了空桶
      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++;
      //再hash
      BucketNo &= (NumBuckets-1);
    }
  }
  • 这块逻辑与方法缓存很像,首先通过容量 -1(类似mask)计算BucketNo,也就是找到index
  • 根据index查找bucketbucket有值返回true,没有值返回false
  • 没有找到的情况下重新计算下标,再hash

2.1.5.2 InsertIntoBucket

  template 
  BucketT *InsertIntoBucket(BucketT *TheBucket, KeyArg &&Key,
                            ValueArgs &&... Values) {
    TheBucket = InsertIntoBucketImpl(Key, Key, TheBucket);

    TheBucket->getFirst() = std::forward(Key);
    ::new (&TheBucket->getSecond()) ValueT(std::forward(Values)...);
    return TheBucket;
  }
  • 先进行扩容/找到bucket
  • 设置bucket对应的值。

InsertIntoBucketImpl

template 
  BucketT *InsertIntoBucketImpl(const KeyT &Key, const LookupKeyT &Lookup,
                                BucketT *TheBucket) {
    unsigned NewNumEntries = getNumEntries() + 1;
    unsigned NumBuckets = getNumBuckets();
    // 3/4 扩容
    if (LLVM_UNLIKELY(NewNumEntries * 4 >= NumBuckets * 3)) {
      //2倍扩容
      this->grow(NumBuckets * 2);
      LookupBucketFor(Lookup, TheBucket);
      NumBuckets = getNumBuckets();
    } else if (LLVM_UNLIKELY(NumBuckets-(NewNumEntries+getNumTombstones()) <=
                             NumBuckets/8)) {
      this->grow(NumBuckets);
      LookupBucketFor(Lookup, TheBucket);
    }
 ……

    return TheBucket;
  }

内部进行了查找扩容,返回对应的bucket

  • 这里也遵循 负载因子(3/4 以及2倍扩容。
  • (总容量 - 已有元素)* 8 <= 总容量相当于7/8判断。

2.1.6 setHasAssociatedObjects

标记对象是否有关联对象

inline void
objc_object::setHasAssociatedObjects()
{
    //Tagged Pointer 直接返回
    if (isTaggedPointer()) return;
    //纯指针 && 有默认的 release,retain等方法,非future类,非元类
    if (slowpath(!hasNonpointerIsa() && ISA()->hasCustomRR()) && !ISA()->isFuture() && !ISA()->isMetaClass()) {
        //获取_noteAssociatedObjects 方法
        void(*setAssoc)(id, SEL) = (void(*)(id, SEL)) object_getMethodImplementation((id)this, @selector(_noteAssociatedObjects));
        //不为消息转发,也就是找到了方法。
        if ((IMP)setAssoc != _objc_msgForward) {
            //调用 _noteAssociatedObjects
            (*setAssoc)((id)this, @selector(_noteAssociatedObjects));
        }
    }
    //设置新的isa
    isa_t newisa, oldisa = LoadExclusive(&isa.bits);
    do {
        newisa = oldisa;
        //纯指针/已经有关联对象标记
        if (!newisa.nonpointer  ||  newisa.has_assoc) {
            ClearExclusive(&isa.bits);
            return;
        }
        //isa关联对象标记
        newisa.has_assoc = true;
    } while (slowpath(!StoreExclusive(&isa.bits, &oldisa.bits, newisa.bits)));
}
  • isa纯指针走_noteAssociatedObjects逻辑,系统会判断是否实现了_noteAssociatedObjects方法。
  • isa非纯指针直接设置has_assoc标记。
  • _noteAssociatedObjects系统并没有实现,应该是提供给我们实现的。但是以_开头应该是私有方法。一般情况下我们是用不到的。

在判断是否有关联对象的方法中:

inline bool
objc_object::hasAssociatedObjects()
{
    if (isTaggedPointer()) return true;
    if (isa.nonpointer) return isa.has_assoc;
    return true;
}

可以看到默认的情况下返回true

⚠️设值的时候是一个对象,那么可以设置类对象,元类对象,只不过释放就要自己操作了。否则就只能类销毁的时候释放了。也就是伴随着应用程序的整个生命周期了。

2.2 关联对象读取值

上面分析了关联对象的存储值,有存储就有对应的取值,那么应该也是按照两层AssociationsHashMap获取的。objc_getAssociatedObject内部直接调用了_object_get_associative_reference

2.2.1 _object_get_associative_reference

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

    {
        AssociationsManager manager;
        //整体单例map
        AssociationsHashMap &associations(manager.get());
        //找到对象对应的map 参数是 DisguisedPtr
        AssociationsHashMap::iterator i = associations.find((objc_object *)object);
        if (i != associations.end()) {
            ObjectAssociationMap &refs = i->second;
            //找到key对应的 association
            ObjectAssociationMap::iterator j = refs.find(key);
            if (j != refs.end()) {
                association = j->second;
                association.retainReturnedValue();
            }
        }
    }
    //返回 association的 _value 也就是我们存储的 value。
    return association.autoreleaseReturnedValue();
}
  • 获取整体单例associations (AssociationsHashMap)
  • 包装objectDisguisedPtrassociations获取对象对应的i(AssociationsHashMap)
  • 通过key从内层i(ObjectAssociationMap)获取ObjcAssociation
  • ObjcAssociation获取value返回。(ObjcAssociation{_policy,_value}

这样就与存储的时候两层结构对应上了,整个结构图下:


关联对象hashmap结构

2.3 关联对象的释放

官方提供了objc_removeAssociatedObjects供我们释放关联对象。

//移除关联对象
void objc_removeAssociatedObjects(id object) 
{
    if (object && object->hasAssociatedObjects()) {
        _object_remove_assocations(object, /*deallocating*/false);
    }
}

内部实现是调用_object_remove_assocations

2.3.1 _object_remove_assocations

void
_object_remove_assocations(id object, bool deallocating)
{
    ObjectAssociationMap refs{};

    {
        AssociationsManager manager;
        AssociationsHashMap &associations(manager.get());
        AssociationsHashMap::iterator i = associations.find((objc_object *)object);
        if (i != associations.end()) {
            //将值全部存入 refs 临时空间中
            refs.swap(i->second);
            
            // If we are not deallocating, then SYSTEM_OBJECT associations are preserved.
            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)
                //没有重新插入的话则擦除associations
                associations.erase(i);
        }
    }

    // Associations to be released after the normal ones.
    SmallVector 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)
                //dealloc的时候系统关联对象,先放入laterRefs 稍后释放,否则不处理。
                laterRefs.append(&i.second);
        } else {
            //释放非系统的关联对象
            i.second.releaseHeldValue();
        }
    }
    for (auto *later: laterRefs) {
        //dealloc 的情况下释放系统的关联对象
        later->releaseHeldValue();
    }
}
  • 根据第二个参数deallocating判断是不是dealloc的时候调用的。
  • 将对象对应的ObjectAssociationMap存入refs临时空间,本来的空间i置空。
  • deallocating情况下(也就是自己调用),将系统的关联对象(通过policy & OBJC_ASSOCIATION_SYSTEM_OBJECT判断)重新插入i,并标记是否重新插入didReInsert
  • 没有重新插入的情况下擦除object对应的ObjectAssociationMap
  • 创建laterRefs记录稍后要释放的ObjcAssociation
  • 循环refs非系统的关联对象直接释放,系统的关联对象判断是否deallocatingdeallocating的情况下加入laterRefs
  • 循环释放laterRefs,也就是系统的关联对象(deallocating的情况下才有值)。

关联对象流程

  • 设值流程:

    • 1.创建一个 AssociationsManager 管理类。
    • 2.获取唯一的全局静态哈希AssociationsHashMap
    • 3.判断是否插入的关联值是否存在:
      • 3.1存在走插入流程
        • 3.1.1 创建一个空的 ObjectAssociationMap 去取查询的键值对。
        • 3.1.2 如果发现没有这个 key 就插入一个 空的 BucketT进去并返回。
        • 3.1.3 标记对象存在关联对象(isFirstAssociation)。
        • 3.1.4 用当前策略和值组成一个 ObjcAssociation 替换原来 BucketT 中的空。(有旧值需要交换释放旧值)
      • 3.2不存在就走关联对象插入空流程 (插入空置,相当于清除)
        • 3.2.1 根据 DisguisedPtr 找到 ObjectAssociationMap 中的iterator 迭代查询器。
        • 3.2.2 根据key找到ObjcAssociation
        • 3.2.3 交换关联对象,擦除关联对象。
        • 3.2.4 如果object对应的ObjectAssociationMap没有值了,则擦除。
    • 4.根据是否第一个关联对象设置是否存在关联对象。
    • 5.释放旧值
  • 取值流程:

    • 1.创建一个AssociationsManager 管理类。
    • 2.获取唯一的全局静态哈希AssociationsHashMap
    • 3.根据 DisguisedPtr 找到 AssociationsHashMap 中的 iterator 迭代查询器。
    • 4.如果这个迭代查询器不是最后一个,获取ObjectAssociationMap (这里有策略和value)。
    • 5.根据key找到ObjectAssociationMap中对应的ObjcAssociation
    • 6.返回ObjcAssociation中的_value
  • 释放流程:

    • 1.创建一个 AssociationsManager 管理类。
    • 2.获取唯一的全局静态哈希AssociationsHashMap
    • 3.根据 DisguisedPtr 找到 AssociationsHashMap 中的 iterator 迭代查询器。
    • 3.根据 DisguisedPtr 找到 AssociationsHashMap 中的 iterator 迭代查询器。
    • 4.如果这个迭代查询器不是最后一个元素,获取 ObjectAssociationMap
    • 5.循环遍历ObjectAssociationMap在非deallocing的情况下将系统关联对象重新插入 。
    • 6.遍历循环如果是系统关联对象,deallocating的情况下将系统的加入临时数组,释放非系统的关联对象。
    • 7.如果6中的临时数组有数据,遍历释放。(也就是deallocating的情况下释放系统的关联对象)。

总结: 关联对象的存储结构其实就是两层哈希map , 存取的时候进行两层处理(类似二维数组),删除的时候由于存在非dealloc的情况以及系统添加的关联对象,以及擦除值的操作,也需要两层操作。

三、dealloc

既然关联对象是和对象绑定在一起的,那么在对象释放的时候关联对象肯定也要释放,在平常的开发中我们一般不回去主动调用释放关联对象的API objc_removeAssociatedObjects,根据上面的分析在dealloc中肯定调用了_object_remove_assocations
那么在dealloc中都做了什么呢?

- (void)dealloc {
    _objc_rootDealloc(self);
}

_objc_rootDealloc(id obj)
{
    ASSERT(obj);

    obj->rootDealloc();
}

dealloc直接调用了_objc_rootDealloc-> obj->rootDealloc,核心逻辑在rootDealloc中。

3.1 rootDealloc

inline void
objc_object::rootDealloc()
{
    //Tagged Pointer
    if (isTaggedPointer()) return;  // fixme necessary?
    //isa非纯指针
    //弱引用
    //关联对象
    //c++析构函数
    //引用计数表
    //有这五种情况则不能直接释放,否则可以直接free。
    if (fastpath(isa.nonpointer                     &&
                 !isa.weakly_referenced             &&
                 !isa.has_assoc                     &&
#if ISA_HAS_CXX_DTOR_BIT
                 !isa.has_cxx_dtor                  &&
#else
                 !isa.getClass(false)->hasCxxDtor() &&
#endif
                 !isa.has_sidetable_rc))
    {
        assert(!sidetable_present());
        free(this);
    } 
    else {
        //释放对象
        object_dispose((id)this);
    }
}
  • isa非纯指针,有弱引用,有关联对象,有c++析构函数,有引用计数表的情况下不能直接释放对象走object_dispose逻辑,否则直接释放。

3.2 object_dispose

id 
object_dispose(id obj)
{
    if (!obj) return nil;
    //毁坏对象
    objc_destructInstance(obj);    
    free(obj);

    return nil;
}

内部进行了对象的毁坏后释放对象,核心逻辑就在objc_destructInstance中了。

3.2.1 objc_destructInstance

void *objc_destructInstance(id obj) 
{
    if (obj) {
        // Read all of the flags at once for performance.
        bool cxx = obj->hasCxxDtor();
        bool assoc = obj->hasAssociatedObjects();

        // This order is important.
        //调用C++析构函数
        if (cxx) object_cxxDestruct(obj);
        //移除关联对象
        if (assoc) _object_remove_assocations(obj, /*deallocating*/true);
        //清除其它逻辑
        obj->clearDeallocating();
    }

    return obj;
}
  • 调用c++析构函数。
  • 移除关联对象。
  • clearDeallocating清空弱引用表以及引用计数表。

3.2.2 clearDeallocating

inline void 
objc_object::clearDeallocating()
{
    if (slowpath(!isa.nonpointer)) {//纯指针
        // Slow path for raw pointer isa.
        //清空散列表 两个不调用通一个方法的原因是 纯指针需要 SIDE_TABLE_WEAKLY_REFERENCED 判断是否有弱引用,nonpointer 可以通过isa字段判断
        sidetable_clearDeallocating();
    }
    else if (slowpath(isa.weakly_referenced  ||  isa.has_sidetable_rc)) {//非纯指针 有弱引用或者引用计数表
        // Slow path for non-pointer isa with weak refs and/or side table data.
        clearDeallocating_slow();
    }

    assert(!sidetable_present());
}

根据是否纯指针分为两个逻辑,纯指针调用sidetable_clearDeallocating清空散列表,非纯指针调用clearDeallocating_slow

3.2.3 sidetable_clearDeallocating & clearDeallocating_slow

sidetable_clearDeallocating

void 
objc_object::sidetable_clearDeallocating()
{
    SideTable& table = SideTables()[this];

    // clear any weak table items
    // clear extra retain count and deallocating bit
    // (fixme warn or abort if extra retain count == 0 ?)
    table.lock();
    //在散列表中找到自身
    RefcountMap::iterator it = table.refcnts.find(this);
    if (it != table.refcnts.end()) {
        //通过it判断是否有弱引用
        if (it->second & SIDE_TABLE_WEAKLY_REFERENCED) {
            //清空自身弱引用
            weak_clear_no_lock(&table.weak_table, (id)this);
        }
        //擦除引用计数表
        table.refcnts.erase(it);
    }
    table.unlock();
}
  • 在散列表中找到自己。
  • 清空自己的弱引用。根据SIDE_TABLE_WEAKLY_REFERENCED判断是否有弱引用。
  • 擦除自己的引用计数表。

clearDeallocating_slow

NEVER_INLINE void
objc_object::clearDeallocating_slow()
{
    ASSERT(isa.nonpointer  &&  (isa.weakly_referenced || isa.has_sidetable_rc));

    //散列表
    SideTable& table = SideTables()[this];
    table.lock();
    if (isa.weakly_referenced) {
        //清除弱引用表中自己
        weak_clear_no_lock(&table.weak_table, (id)this);
    }
    if (isa.has_sidetable_rc) {
        //擦除引用计数表中自己
        table.refcnts.erase(this);
    }
    table.unlock();    
}
  • 获取散列表。
  • 通过isa.weakly_referenced判断是否有弱引用,有弱引用则清除弱引用。
  • 判断是否有弱引用计数表(isa.has_sidetable_rc),有则擦除弱引用表中自己的计数。

weak_clear_no_lock

void 
weak_clear_no_lock(weak_table_t *weak_table, id referent_id) 
{
    //要清除的对象
    objc_object *referent = (objc_object *)referent_id;
    //
    weak_entry_t *entry = weak_entry_for_referent(weak_table, referent);
 ……

    // zero out references weak对象引用表
    weak_referrer_t *referrers;
    size_t count;
    
    if (entry->out_of_line()) {
        referrers = entry->referrers;
        count = TABLE_SIZE(entry);
    } 
    else {
        referrers = entry->inline_referrers;
        count = WEAK_INLINE_COUNT;
    }
    
    for (size_t i = 0; i < count; ++i) {
        objc_object **referrer = referrers[i];
        if (referrer) {
            if (*referrer == referent) {
                //weak引用置为nil
                *referrer = nil;
            }
            else if (*referrer) {
   ……
                objc_weak_error();
            }
        }
    }
    //移除弱引用表中的entry
    weak_entry_remove(weak_table, entry);
}
  • 在若引用计数表中找到自己的weak_entry_t
  • 根据out_of_line获取指向对象的弱引用指针referrersinline_referrers
  • 循环弱引用指针数组将弱引用指针置为nil
  • 将弱引用weak_entry_t从弱引用表中删除。

dealloc调用流程:

  • 不能直接释放(弱引用、关联对象、c++析构函数、引用计数表)
    • 1.调用c++析构函数。
    • 2.移除关联对象。
    • 3.清空弱引用表(弱引用指针数组全部指针置为nil)。
    • 4.擦除引用计数表中自己。
    • 5.free
  • 能直接释放,free

已知的一个常识是在dealloc中不需要调用[super dealloc],但是在源码分析中并没有看到调用父类的dealloc方法,在llvm中发下如下代码:

- (void)dealloc {
 [_myproperty release];
 [super dealloc];
}

可以看到应该在编译阶段llvm自动添加了super dealloc的调用。反汇编后也能看到在编译阶段就已经编译进去了:

image.png

四、案例分析

关联对象怎么添加weak属性?
在设置关联对象的时候,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. */
};

在这个枚举中并没有发现weak,只发现OBJC_ASSOCIATION_ASSIGN的注释中有weak描述,但是搜索OBJC_ASSOCIATION_ASSIGN却没有调用的地方,那也就说明它不会主动置为nil
既然这样只能另辟蹊径,存储的时候只能是强引用,但是释放后要置为nil。一种主流的实现方案是使用blockblock持有弱引用。

示例代码如下:

@property (nonatomic, weak) id weak_obj;

- (void)setWeak_obj:(id)weak_obj {
    id __weak weakObject = weak_obj;
    //存储 weak 类型的 obj
    id (^block)(void) = ^{ return weakObject; };
    return objc_setAssociatedObject(self, "weak_obj", block, OBJC_ASSOCIATION_COPY);
}

- (id)weak_obj {
    //获取block
    id (^block)(void) = objc_getAssociatedObject(self, "weak_obj");
    //执行block,取到block返回的obj
    id weakObject = (block ? block() : nil);
    return weakObject;
}

实际上关联对象仍然是强引用,但是block内部持有了变量的弱引用。这样就相当于加了个中间层。在weak_obj释放后,block就获取不到weak_obj了。

关联对象在什么情况下造成内存泄漏?
关联对象可以理解就是持有了一个对象,如果是retain等方式的持有,而该对象也持有了本类,那就导致了循环引用。一般block造成循环引用的情况比较多。

五、关联对象流程图

关联对象-取值、设值、释放、初始化流程

你可能感兴趣的:(Objective-C 扩展与关联对象)