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

881 阅读9分钟

前言

上一篇 iOS 底层原理:类的加载原理下(分类),对分类加载进行了分析,今天将进行类扩展关联对象的探索。

准备工作

一、类扩展分析

什么是类扩展

image.png

  • 这就是类扩展,我们平时用的是非常多的,它也叫没有名字的分类。

底层 C++ 分析类扩展

main.m中添加如下代码:

@interface SSLPerson : NSObject
@property (nonatomic, copy) NSString *name;
- (void)say1;
@end

@interface SSLPerson ()
@property (nonatomic, copy) NSString *ext_name;
- (void)ext_say1;
@end

@implementation SSLPerson
+ (void)load {}
- (void)say1
{
    NSLog(@"SSLPerson-----say1");
}
- (void)ext_say1
{
    NSLog(@"SSLPerson-----ext_say1");
}
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {}   
    return 0;
}
  • 类扩展要放在声明之后,实现之前,否则会报错。

打开terminalclang -rewrite-objc main.m -o main.cpp,得到main.cpp

...
struct SSLPerson_IMPL {
    struct NSObject_IMPL NSObject_IVARS;
    NSString *_name;
    NSString *_ext_name;
};

static struct /*_method_list_t*/ {
unsigned int entsize;  // sizeof(struct _objc_method)
unsigned int method_count;
struct _objc_method method_list[10];
} _OBJC_$_INSTANCE_METHODS_SSLPerson __attribute__ ((used, section ("__DATA,__objc_const"))) = {
    sizeof(_objc_method),
    10,
    {{(struct objc_selector *)"say1", "v16@0:8", (void *)_I_SSLPerson_say1},
    {(struct objc_selector *)"ext_say1", "v16@0:8", (void *)_I_SSLPerson_ext_say1},
    {(struct objc_selector *)"name", "@16@0:8", (void *)_I_SSLPerson_name},
    {(struct objc_selector *)"setName:", "v24@0:8@16", (void *)_I_SSLPerson_setName_},
    {(struct objc_selector *)"ext_name", "@16@0:8", (void *)_I_SSLPerson_ext_name},
    {(struct objc_selector *)"setExt_name:", "v24@0:8@16", (void *)_I_SSLPerson_setExt_name_},
};
...
  • 可以看到,类扩展并没有像分类一样,生成类似category_t那种数据结构。
  • 类扩展的成员变量_ext_name和主类的_name是放在一起的,类扩展的方法和主类的方法也是被放在了同一个列表内,那么这些方法是在什么时机加入进去的呢,接下来继续分析。

类扩展 加载分析

为方便调试,重新创建下类:

image.png

非懒加载类时会调用realizeClassWithoutSwift函数,我们在其中添加断点进行测试,运行程序进入断点:

image.png

lldb打印:

image.png

  • 可以看到,ext_say1setNameext_name这个时候已经存在了,说明类扩展是伴随这类的内容一起加载进来了,可以说是属于类的一部分。

类扩展 VS 分类

1. category:分类

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

2. extension:类扩展

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

二、关联对象 引入

我们知道分类中的属性是不会自动生成成员变量的,这个问题可以通过关联对象来解决。

创建SSLPerson+SSL1分类:

@interface SSLPerson (SSL1)
@property (nonatomic, copy) NSString *cate_name;
@end

#import <objc/runtime.h>
@implementation SSLPerson (SSL1)

- (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");
}
@end

点击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进行详细分析。

三、关联对象底层分析 初探

_object_set_associative_reference

进入_object_set_associative_reference

void
_object_set_associative_reference(id object, const void *key, id value, uintptr_t policy)
{   // object 是关联对象,key 是一个标识 key,value 是值,policy 是存储策略。

    // 将 object 统一包装成 DisguisedPtr 这种数据结构
    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();
    // 关键代码
    {
        AssociationsManager manager;
        ...
    }
}

template <typename T>
class DisguisedPtr {
    uintptr_t value;
    static uintptr_t disguise(T* ptr) {
        return -(uintptr_t)ptr;
    }
    static T* undisguise(uintptr_t val) {
        return (T*)-val;
    }
}

class ObjcAssociation {
    uintptr_t _policy;
    id _value;
    ...
}
  • DisguisedPtr<objc_object>object进行了包装, ObjcAssociationpolicyvalue进行了包装。
  • 函数中关于AssociationsManager网上有很多文章说它是单例,那它到底是不是单例呢,下面进行分析。

AssociationsManager 分析

1. 构造函数、析构函数 知识补充

创建SSLObjc结构体,实现它的构造函数析构函数

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

接下来断点打印调试:

image.png

  • 这时候SSLObjc objc;刚刚被执行,我们发现构造函数自己就被调用了。

断点向下走一步:

image.png

  • 这时候作用域结束,objc销毁,析构函数也被自动调用。

2. AssociationsManager 源码分析

查看AssociationsManager源码:

class AssociationsManager {
    using Storage = ExplicitInitDenseMap<DisguisedPtr<objc_object>, ObjectAssociationMap>;
    static Storage _mapStorage;
    
public:
    AssociationsManager()   { AssociationsManagerLock.lock(); }
    ~AssociationsManager()  { AssociationsManagerLock.unlock(); }
    
    AssociationsHashMap &get() {
        return _mapStorage.get();
    }
    static void init() {
        _mapStorage.init();
    }
};
  • 可以看到AssociationsManager并不是一个单例,而是通过构造函数加锁、析构函数解锁,来达到线程安全。
  • AssociationsManager只是用来调用AssociationsHashMap的而已,AssociationsHashMap是一个单例,因为它通过_mapStorage.get()获取,_mapStorage是一个全局静态变量,放在任何地方都是唯一的。

关联对象 数据结构图

关联对象相关的数据结构:

image.png

image.png

源码断点调试

main函数中添加代码:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        SSLPerson *person = [SSLPerson alloc];
        person.cate_name = @"SSL";
    }   
    return 0;
}

断点进入_object_set_associative_reference

image.png

  • 根据打印结果,可以看到disguisedassociationassociations的数据结构,其中_value = SSL,证明了目前的调试是正确的。

向下走:

image.png

  • 打印refs_result,发现数据结构非常的长,其实上面那么长只是类型,不用太关注。真实数据就下面那几行,还有就是refs_result.second这块代码用到了second

四、关联对象底层分析 设值流程

重新运行,继续断点调试:

image.png

  • 接下来调用associations.try_emplace函数,associationsAssociationsHashMap类型的。
  • 传入disguised它是object的封装,和一个新建的ObjectAssociationMap,将它两作为键值对进行关联存到associations中。

try_emplace

断点进入try_emplace

image.png

  • BucketT *TheBucket,创建一个空的BucketT
  • 调用LookupBucketFor(Key, TheBucket)),传入keydisguised,和TheBucket

LookupBucketFor

进入LookupBucketFor

image.png

  • 有两个LookupBucketFor函数,传入的是没有constBucketT *&FoundBucket参数,所以先调用下面的。
  • 函数内部又调用了上面的LookupBucketForFoundBucket是指针传递,有值是可以被带回去的,所以关键代码在上面的函数。

进入上面的LookupBucketFor函数:

template<typename LookupKeyT>
  bool LookupBucketFor(const LookupKeyT &Val,
                       const BucketT *&FoundBucket) const {
    const BucketT *BucketsPtr = getBuckets();
    const unsigned NumBuckets = getNumBuckets();
    
    // FoundTombstone - Keep track of whether we find a tombstone while probing.
    const BucketT *FoundTombstone = nullptr;
    const KeyT EmptyKey = getEmptyKey();
    const KeyT TombstoneKey = getTombstoneKey();

    // Val 是 disguised(包装的对象), hash 函数得到下标
    // 这是个 hash 函数,计算出下标
    unsigned BucketNo = getHashValue(Val) & (NumBuckets-1);
    unsigned ProbeAmt = 1;
    
    // while 死循环 查找位置
    // 找到,  对FoundBucket进行赋值,返回 true
    // 没找到,再 hash 查找
    while (true) {
    
      const BucketT *ThisBucket = BucketsPtr + BucketNo;
      
      // 找到所在下标位置有值,FoundBucket = 这个地址,函数返回
      if (LLVM_LIKELY(KeyInfoT::isEqual(Val, ThisBucket->getFirst()))) {
        FoundBucket = ThisBucket;
        return true;
      }
      
      // 找到所在下标位置没有值,FoundBucket = 这个地址,函数返回
      if (LLVM_LIKELY(KeyInfoT::isEqual(ThisBucket->getFirst(), EmptyKey))) {
        FoundBucket = FoundTombstone ? FoundTombstone : ThisBucket;
        return false;
      }
      
      BucketNo += ProbeAmt++;
      BucketNo &= (NumBuckets-1);
    }
  }
  • 通过getHashValue(Val) & (NumBuckets-1)哈希函数,计算出BucketNo下标。
  • while循环,根据下标找位置,找到位置后将地址赋给FoundBucket,我们这里是没有赋值过的地址即isEqual(ThisBucket->getFirst(), EmptyKey),所以返回false

返回 try_emplace

函数LookupBucketFor走完后,回到try_emplace

image.png

  • 返回的false,所以走到了下面。TheBucket现在是存储在AssociationsHashMap中有位置了,但还没有东西,所以要为它插入内容。
  • 调用InsertIntoBucket,传入TheBucket,传入key也就是disguised,传入Args也就是ObjectAssociationMap{}

InsertIntoBucket

进入InsertIntoBucket

template <typename KeyArg, typename... ValueArgs>
  BucketT *InsertIntoBucket(BucketT *TheBucket, KeyArg &&Key,
                            ValueArgs &&... Values) {
    // 进行扩容等操作
    TheBucket = InsertIntoBucketImpl(Key, Key, TheBucket);
    
    // 将Key也就是disguised 赋值到 TheBucket 的 first
    TheBucket->getFirst() = std::forward<KeyArg>( Key 也就是 );
    
    // 将 ObjectAssociationMap{} 赋值到 TheBucket 的 second
    ::new (&TheBucket->getSecond()) ValueT(std::forward<ValueArgs>(Values)...);
    return TheBucket;
  }

进入InsertIntoBucketImpl

template <typename LookupKeyT>
  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);
    }
    
    if (KeyInfoT::isEqual(TheBucket->getFirst(), getEmptyKey())) {
      // Replacing an empty bucket.
      // 增加数量
      incrementNumEntries();
    } else if (KeyInfoT::isEqual(TheBucket->getFirst(), getTombstoneKey())) {
      // Replacing a tombstone.
      // 增加数量
      incrementNumEntries();
      decrementNumTombstones();
    } else {
      // we should be purging a zero. No accounting changes.
      TheBucket->getSecond().~ValueT();
    }
    return TheBucket;
  }
  • 当数量大于等于3/4时,进行2倍扩容,已经增加num数量+1的操作。

const void *、association 设置

回到try_emplace

image.png

  • 可以看到TheBucket已经有值了。
  • makeIterator中的iteratorDenseMapIterator类型。 image.png
  • makeIterator(TheBucket, getBucketsEnd(), true)的最后一个参数为定值true

回到_object_set_associative_reference函数:

image.png

  • 现在已经可以看到const void *, objc::ObjcAssociation这种,属于ObjectAssociationMap的数据结构。
  • 这个时候valuepolicy还没有存,还在association里。

继续调试:

image.png

  • refs_result.first获取到的是bucketbucket.second获取到的是上面创建的ObjectAssociationMap,也就是说refs是上面创建的ObjectAssociationMap
  • 接下来又是调用try_emplace函数,传入key(这里是"cate_name"),和association。将它们以键值对的方式存储到refsbucket中,到此两层哈希存储完成。

关联对象 设值流程总结

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

五、关联对象底层分析 插入空流程

value没有值时,代码走到else

void
_object_set_associative_reference(id object, const void *key, id value, uintptr_t policy)
{   // object 是关联对象,key 是一个标识 key,value 是值,policy 是存储策略。

    // 将 object 统一包装成 DisguisedPtr 这种数据结构
    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();
    // 关键代码
    {
        AssociationsManager manager;
        AssociationsHashMap &associations(manager.get());
        if (value) {
            ...
        } 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);
                    }
                }
            }
        }
    }
}
  • associations表中查找disguised,如果没有找到进行erase清空操作。

六、关联对象 取值流程

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

七、关联对象释放

关联对象是什么时候释放的呢,object的生命周期决定了关联对象的生命周期,我们以object的释放为切入点进行分析。

看下面源码:

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

void objc_removeAssociatedObjects(id object) 
{
    if (object && object->hasAssociatedObjects()) {
        _object_remove_assocations(object, /*deallocating*/false);
    }
}
  • 我们看到有objc_removeAssociatedObjects函数,可以通过反向思维来推导,看什么时候调用的这个函数。
  • 它应该是在对象释放的时候被调用的。

搜索dealloc {,进入dealloc

// Replaced by NSZombies
- (void)dealloc {
    _objc_rootDealloc(self);
}

进入_objc_rootDealloc

void
_objc_rootDealloc(id obj)
{
    ASSERT(obj);
    obj->rootDealloc();
}

进入rootDealloc

inline void
objc_object::rootDealloc()
{
    if (isTaggedPointer()) return// fixme necessary?
    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);
    }
}

进入object_dispose

id 
object_dispose(id obj)
{
    if (!obj) return nil;
    objc_destructInstance(obj);    
    free(obj);
    return nil;
}

进入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.
        if (cxx) object_cxxDestruct(obj);
        if (assoc) _object_remove_assocations(obj, /*deallocating*/true);
        obj->clearDeallocating();
    }
    return obj;
}

进入_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.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.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();
        }
    }
    for (auto *later: laterRefs) {
        later->releaseHeldValue();
    }
}
  • 创建一个AssociationsManager管理类,获取唯一的全局静态哈希Map,定义为associations
  • objectkey,在associations中取出iterator定义为i
  • 经过一些判断后,通过associations.erase(i);删除i