前言
“这是我参与8月更文挑战的第3天,活动详情查看:8月更文挑战”
上两篇文章,对 类的加载 上、类的加载 中 和 类的加载 下 对类和分类的加载进行了底层的探索和分析。那么紧接着,就是对类扩展和关联对象的探索。
资源准备
objc源码下载:多个版本的objc源码- 小板凳、冰🍺
类扩展分析
类扩展的平常展示
- 内扩展一定是写在声明之后,实现之前的,就如上图的红框里面,这就是内扩展,算得上天天与我们打交道了。它也叫没有名字的分类。
内扩展的C++底层展示
既然知道了内扩展的样式,那么就能在obj源码里面,设置 LGStudent 类,如下图:
然后就可以打开终端,进入到 main.m 的文件,再执行 clang -rewrite-objc main.m -o main.cpp命令,得到对应的 c++ 文件--- main.cpp。
打开main.cpp文件,如下图:
从 C++ 底层代码看,类扩展和分类还是有差别的,他并没有生成类似category_t那种数据结构。
再看看方法类表:
从这里就能分析出,类扩展的成员变量_ext_name、本类的_name、类扩展的方法,还有本类的方法都是放在了同一个列表内。
那么内扩展的方法是在什么时候加载的了?继续进行探索。
内扩展的加载
接下来,我们就创建LGPerson类和他的类扩展:
LGPerson类:
.h
.m
LGPerson类扩展:
我们知道,在非懒加载类时,会调用realizeClassWithoutSwift函数进行实例化,那么我们就在这个函数里面,进行断点跟踪,运行程序:
再通过 lldb 调试,打印里面信息:
我们可以看到,像方法ext_saySomething和属性ext_name都已经可以打印得到,说明类扩展是伴随这个类的内容一起加载进来了,已经合成为类的一部分了。
内扩展小结
那么结合前几篇文章的分析,可以知道 分类 和 内扩展 的区别了:
1. category:分类
- 专门用来给类添加
新的方法。 - 不能给类添加成员属性,添加了成员变量,也无法取到。
- 注意:其实可以通过
runtime给分类添加属性。 - 分类中用
@property定义变量,只会生成变量的getter、setter方法的声明,不能生成方法实现和带下划线的成员变量。
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数据类型,实现代码:
value 封装处理
将 policy、value 包装成 ObjcAssociation 这种数据结构,以面向对象的形式存储:
AssociationsManager 分析 (重点)
进入AssociationsManager函数里面:
那么我们也模仿AssociationsManager函数,在 main 里面创建类似的构造函数和析构函数:
struct LGObjc {
LGObjc() { printf("LG 构造函数调用 \n");}
~LGObjc() { printf("LG 析构函数调用 \n"); }
};
接下来断点打印调试:
这说明了,AssociationsManager函数并不是一个单利,而是通过构造函数加锁、析构函数解锁,来达到线程安全。
但是为了让这里在执行之后,其作用域消失,就被释放掉,而是能够持续进行作用,就用到了单利 AssociationsHashMap 函数。就如下面这张关系图:
为什么是这样子了?我们接着分析:
static Storage _mapStorage;-- 全局静态变量,而是在AssociationsManager里面调起来,并不是作用域;AssociationsHashMap这张表是唯一的,是一个单利。通过_mapStorage.get()获取。
数据跟踪
-
根据打印结果,可以看到
disguised、association、associations的数据结构,其中_value = KC,上文中,我们在main里面给person.cate_name = @"KC",证明了目前的调试是正确的。 -
打印
refs_result的信息,refs_result.second这块代码用到了second的值,是true,那么就能进入if判断里面。
关联对象的设值流程
获取 refs_result 是通过 associations.try_emplace 进行传值的,其中 associations 是 AssociationsHashMap 类型,他调用的 try_emplace 是传入了 disguised(是object的封装),和新建的ObjectAssociationMap,将这个两个参数,作为键值对进行关联存到associations中。
那么我们进入到try_emplace里面,看其实现。
try_emplace的分析
- 先是
BucketT *TheBucket,创建一个空的BucketT。 - 然后调用
LookupBucketFor(Key, TheBucket)),其中key是传入的disguised,而新建的TheBucket。
那么再跟着往下走,进入到 LookupBucketFor 里面。
LookupBucketFor 分析
我们进来之后,尽然有两个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_emplace,TheBucket现在是存储在AssociationsHashMap中,但是是空的,只是占位置了,如下图:
所以要为它插入内容。调用InsertIntoBucket,传入的参数有TheBucket、key(也就是disguised(是object的封装)),还有Args(也就是ObjectAssociationMap{})。
那么接下来,就进入InsertIntoBucket里面。
InsertIntoBucket分析
- 对
TheBucket进行绑定、扩容等操作; - 将
Key也就是disguised赋值到TheBucket的first; - 将
ObjectAssociationMap{}赋值到TheBucket的second - 返回
TheBucket
我们还是需要 TheBucket 是如何进行设置的,所以,还是要进入 InsertIntoBucketImpl函数里面。
InsertIntoBucketImpl分析
进行扩容、绑定,最后返回 TheBucket 出去。
返回到 try_emplace 里面,数据跟踪验证下:
对比 插入前 和 插入后 ,插入后就有了值 0x0000000101405d60。而makeIterator(TheBucket, getBucketsEnd(), true)第三个返回的参数,默认为 true 。
接着我们再返回到 _object_set_associative_reference 函数里面:
在有 value 的情况下,会继续往下执行,打印到的 refs -> $4还是没有数据。
当再次进入 try_emplace 该方法是,此时第一个参数传的是key(就是 “cate_name”),而不是disguised,那么在try_emplace函数里面,所进行LookupBucketFor的调用,查找bucket时,一定返回false。所以一定会调动InsertIntoBucket方法进行association的插入。见下面代码:
-
- 创建⼀个
AssociationsManager管理类
- 创建⼀个
-
- 获取唯⼀的全局静态哈希
Map
- 获取唯⼀的全局静态哈希
-
- 判断是否插⼊的关联值是否存在:
- 存在⾛第
4步; - 不存在就⾛: 关联对象插⼊空流程
-
- 创建⼀个空的
ObjectAssociationMap去取查询的键值对
- 创建⼀个空的
-
- 如果发现没有这个
key就插⼊⼀个空的BucketT进去,并返回
- 如果发现没有这个
-
- 标记对象存在关联对象
-
- ⽤当前修饰策略和值组成了⼀个
ObjcAssociation替换原来BucketT中的空
- ⽤当前修饰策略和值组成了⼀个
-
- 标记⼀下
ObjectAssociationMap的第⼀次为false
- 标记⼀下
关联对象插入空流程
插入空,也就是 value 为空,那么进入的是 else 的环节:
- 根据
DisguisedPtr找到AssociationsHashMap中的iterator迭代查询器 - 清理迭代器
- 其实如果插⼊空置,相当于清除
关联对象的取值流程
-
1:创建⼀个AssociationsManager管理类; -
2:获取唯⼀的全局静态哈希Map; -
3:根据DisguisedPtr找到AssociationsHashMap中的iterator迭代查询器; -
4:如果这个迭代查询器不是最后⼀个获取:ObjectAssociationMap(这⾥有策略和value); -
5:找到ObjectAssociationMap的迭代查询器获取⼀个经过属性修饰符修饰的value; -
6:返回_value; -
总结:其实就是两层哈希
map,存取的时候两层处理(类似⼆位数组)
关联对象销毁流程
我们已经知道了关联对象的创建详情,那么他是怎么释放的了?
我们知道在对象的生命周期,当其被销毁时,会调用dealloc方法,同时会对关联对象进行释放。
我们能找到 objc_removeAssociatedObjects 的实现,但是其调用,就没有找到好的线索,那么我们从 dealloc 入手。
可以根据这个流程:dealloc->_objc_rootDealloc->rootDealloc,从dealloc到中间层,最后到rootDealloc实现源码见下图:
对象在销毁的时候,会判断是否存在析构函数、关联对象、弱引用等等。
那么接着继续跟踪:rootDealloc->object_dispose->objc_destructInstance,经过流程,到达objc_destructInstance函数:
从这里,知道这个函数是处理析构函数 和 关联对象的。最后调用 _object_remove_assocations 函数,在这个函数中:
- 最后通过
associations.erase(i);删除i位置的内存。