在前面的文章中,我们分析了类和分类的本质和加载过程,本文主要来分析类扩展
和关联对象
【面试题】类扩展与分类的区别
1、category类别(分类)
专门用来给类添加新的方法
-
不能给类添加成员属性
,添加了成员属性,也无法取到 - 【注意】:其实
可以通过runtime 给分类添加属性
,即属性关联,重写setter、getter方法 - 分类中用
@property
定义的变量,只会生成变量的setter、getter方法的声明
,不能生成方法实现和带下划线的成员变量
2、extension(类扩展)
- 可以说成是
特殊的分类
,也可称作匿名分类
- 可以
给类添加成员属性
,但是是私有变量
- 可以
给类添加方法
,也是私有方法
类扩展底层原理探索
类的扩展有两种创建方式:
- 直接写在
.m
文件中,我们平时开发中最常用的写法
- 通过
command+N
新建 -> 选择Objective-C File
-> 选择Extension
类扩展的本质
通过clang底层编译探索
- 在
main.m
中定义HTPerson
类及其扩展
- 通过
clang -rewrite-objc main.m -o main.cpp
命令生成.cpp文件
,打开main.cpp
文件,搜索ext_name
属性
- 查看成员变量列表
_ivar_list_t
,发现有两个成员变量_name
和_ext_name
- 查看
对象方法
列表
- 查看
类方法
列表
从上面我们可以发现,类扩展中的方法,在编译过程
中,方法就直接添加到类的 methodlist
中,作为类的一部分,即编译时期直接添加到本类里面
通过源码调试探索
- 在
methodizeClass
方法处设置断点,函数调用栈如下:
- 查看
ro
数据
总结
-
类的扩展
在编译时会作为类的一部分,和类一起编译进来 -
类的扩展
只是声明
,依赖于当前的主类
,没有.m文件,可以理解为一个·h
文件
关联对象的底层原理
关联对象的底层原理的实现,主要分为两部分:
- 通过
objc_setAssociatedObject
设值流程 - 通过
objc_getAssociatedObject
取值流程 - 通过
objc_removeAssociatedObjects
移除关联对象
分类中用@property
定义的变量,只会生成变量的setter、getter方法的声明
,不能生成方法实现和带下划线的成员变量
关联对象-设值流程
- 创建
HTPerson (HT)
分类,添加cat_name
和cat_age
属性
- 重写
cat_name
的set
、get
方法,通过runtime
的属性关联方法实现
- 运行程序,断点断在
main.m
中cat_name
赋值处
- 继续往下运行,断在分类的
setCat_name
方法中
其中objc_setAssociatedObject
方法有四个参数,分别表示:
参数1:要关联的
对象
,即给谁添加关联属性参数2:
标识符
,方便下次查找参数3:
value
-
参数4:属性的
策略
,即nonatomic、atomic、assign、copy等,如下所示
进入
objc_setAssociatedObject
源码实现
这种设计模式属于是接口模式
,对外的接口不变,内部的逻辑变化不影响外部的调用, 类似于set
方法的底层源码实现
- 进入
_object_set_associative_reference
方法,源码如下
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 目的是方便底层统一处理
DisguisedPtr<objc_object> disguised{(objc_object *)object};
// 将 policy和value 封装成ObjcAssociation,目的是方便底层统一处理
ObjcAssociation association{policy, value};
// retain the new value (if any) outside the lock.
// 根据policy策略去判断是进去 retain 还是 copy 操作
association.acquireValue();
bool isFirstAssociation = false;//用来判断是否是,第一次关联该对象
{
// 实例化 AssociationsManager 注意这里不是单例
AssociationsManager manager;
// 实例化 全局的关联表 AssociationsHashMap 这里是单例
AssociationsHashMap &associations(manager.get());
if (value) {
// AssociationsHashMap:关联表 ObjectAssociationMap:对象关联表
// 首先根据对象封装的disguised去关联表中查找有没有对象关联表
// 如果有直接返回结果,如果没有则根据`disguised`去创建对象关联表
// 创建ObjectAssociationMap时当(对象的个数+1大于等于3/4,进行两倍扩容)
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 */
// 获取ObjectAssociationMap中存储值的地址
auto &refs = refs_result.first->second;
// 将需要存储的值存放在关联表中存储值的地址中
// 同时会根据key去查找,如果查找到`result.second` = false ,如果找不到就创建`result.second` = true
// 创建association时,当(association的个数+1)超过3/4,就会进行两倍扩容
auto result = refs.try_emplace(key, std::move(association));
if (!result.second) {
// 交换association和查询到的`association`
// 其实可以理解为更新查询到的`association`数据,新值替换旧值
association.swap(result.first->second);
}
} else { // value没有值走else流程
// 查找disguised 对应的ObjectAssociationMap
auto refs_it = associations.find(disguised);
// 如果找到对应的 ObjectAssociationMap 对象关联表
if (refs_it != associations.end()) {
// 获取 refs_it->second 里面存放了association类型数据
auto &refs = refs_it->second;
// 根据key查询对应的association
auto it = refs.find(key);
if (it != refs.end()) {
// 如果找到,更新旧的association里面的值
association.swap(it->second);
// value= nil时释放关联对象表中存的`association`
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.
// 首次关联对象调用setHasAssociatedObjects方法
// 通过setHasAssociatedObjects方法`标记对象存在关联对象`设置`isa指针`的`has_assoc`属性为`true`
if (isFirstAssociation)
object->setHasAssociatedObjects();
// release the old value (outside of the lock).
// 释放旧值因为如果有旧值会被交换到`association`中
// 原来`association`的新值会存放到对象关联表中
association.releaseHeldValue();
}
_object_set_associative_reference
方法主要有下列两步操作:
- 根据
object
在全局关联表(AssociationsHashMap)
中查询ObjectAssociationMap
,如果没有就去开辟内存创建ObjectAssociationMap
,创建的规则就是在3/4
时,进行两倍扩容
,扩容的规则和cache
方法存储的规则是一样的 - 将根据
key
查询到相关的association
(即关联的数据value
和policy
),如果查询到直接更新里面的数据,如果没有则去获取空的asociation
类型然后将值存放进去,扩容的规则和cache
方法存储的规则是一样的
AssociationsManager
AssociationsManager manager
并不是单例,AssociationsHashMap &associations(manager.get());
获取的关联表是全局唯一的
-
AssociationsManager
的源码如下:
class AssociationsManager {
using Storage = ExplicitInitDenseMap<DisguisedPtr<objc_object>, ObjectAssociationMap>;
static Storage _mapStorage;
public:
// 构造函数(在作用域内加锁)
AssociationsManager() { AssociationsManagerLock.lock(); }
// 析构函数(离开作用域,解锁)
~AssociationsManager() { AssociationsManagerLock.unlock(); }
// 获取全局的一张AssociationsHashMap表
AssociationsHashMap &get() {
return _mapStorage.get();
}
static void init() {
_mapStorage.init();
}
};
从源码我们可以发现:static Storage _mapStorage;
,_mapStorage
是全局静态变量,因此获取的AssociationsHashMap关联表
也是全局唯一的一份
AssociationsManager
的构造函数AssociationsManager()
和析构函数~AssociationsManager()
主要是在相应作用域内加锁
,为了防止多线程访问出现混乱
try_emplace方法探究
try_emplace
方法的作用就是去表中查找Key
相应的数据,不存在就创建:
- 通过
LookupBucketFor
方法去表中查找Key
对应的TheBucket
是否有存在,如果存在对TheBucket
进行包装然后返回 - 如果不存在,通过
InsertIntoBucket
方法插入新值,扩容的规则和cache
方法存储的规则是一样的
template <typename... Ts>
std::pair<iterator, bool> try_emplace(const KeyT &Key, Ts &&... Args) {
BucketT *TheBucket;
// 根据key去查找对应的TheBucket
if (LookupBucketFor(Key, TheBucket))
// 通过make_pair生成相应的键值对
return std::make_pair(
makeIterator(TheBucket, getBucketsEnd(), true),
false); // Already in map.表示【表中】已经存在bucket
// Otherwise, insert the new element.
// 如果没有查询到 将数据(键值)插入TheBucket中
TheBucket = InsertIntoBucket(TheBucket, Key, std::forward<Ts>(Args)...);
// 通过make_pair生成相应的键值对
return std::make_pair(
makeIterator(TheBucket, getBucketsEnd(), true),
true); // true表示第一次往哈希关联表中添加bucket
}
LookupBucketFor方法
这个方法就是 根据Key
去表中查找Bucket
,如果已经缓存过,返回true
,否则返回false
////// LookupBucketFor - Lookup the appropriate bucket for Val, returning it in
/// FoundBucket. If the bucket contains the key and a value, this returns
/// true, otherwise it returns a bucket with an empty marker or tombstone and
/// returns false.
template<typename LookupKeyT>
bool LookupBucketFor(const LookupKeyT &Val,
const BucketT *&FoundBucket) const {
// 获取buckets的首地址
const BucketT *BucketsPtr = getBuckets();
// 获取可存储的buckets的总数
const unsigned NumBuckets = getNumBuckets();
if (NumBuckets == 0) {
// 如果NumBuckets = 0 返回 false
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!");
// 计算hash下标
unsigned BucketNo = getHashValue(Val) & (NumBuckets-1);
unsigned ProbeAmt = 1;
while (true) {
// 内存平移:找到hash下标对应的Bucket
const BucketT *ThisBucket = BucketsPtr + BucketNo;
// Found Val's bucket? If so, return it.
if (LLVM_LIKELY(KeyInfoT::isEqual(Val, ThisBucket->getFirst()))) {
// 如果查询到`Bucket`的`key`和`Val`相等 返回当前的Bucket说明查询到了
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.
// 如果bucket为空,说明当前key还不在表中,返回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);
}
// 重新计算hash下标
BucketNo += ProbeAmt++;
BucketNo &= (NumBuckets-1);
}
}
InsertIntoBucket方法
通过断点调试,我们看看InsertIntoBucket
方法做了什么
- 在
InsertIntoBucket
方法调用出设置断点,打印TheBucket
的值为nil
- 执行完
InsertIntoBucket
方法后,继续打印TheBucket
的值
👇我们来看看InsertIntoBucket
方法源码
template <typename KeyArg, typename... ValueArgs>
BucketT *InsertIntoBucket(BucketT *TheBucket, KeyArg &&Key,
ValueArgs &&... Values) {
// 根据Key 找到TheBucket的内存地址
TheBucket = InsertIntoBucketImpl(Key, Key, TheBucket);
// 将 Key 和 Values保存到TheBucket中
TheBucket->getFirst() = std::forward<KeyArg>(Key);
::new (&TheBucket->getSecond()) ValueT(std::forward<ValueArgs>(Values)...);
return TheBucket;
}
InsertIntoBucketImpl方法分析
主要的工作都是在InsertIntoBucketImpl
方法中完成的
- 计算实际占用
buckets
的个数,如果超过负载因子(3/4)
,进行扩容操作this->grow(NumBuckets * 2);
- 找到
TheBucket
的内存地址:LookupBucketFor(Lookup, TheBucket);
- 更新占用的容量个数:
incrementNumEntries();
template <typename LookupKeyT>
BucketT *InsertIntoBucketImpl(const KeyT &Key, const LookupKeyT &Lookup,
BucketT *TheBucket) {
// If the load of the hash table is more than 3/4, or if fewer than 1/8 of
// the buckets are empty (meaning that many are filled with tombstones),
// grow the table.
//
// The later case is tricky. For example, if we had one empty bucket with
// tons of tombstones, failing lookups (e.g. for insertion) would have to
// probe almost the entire table until it found the empty bucket. If the
// table completely filled with tombstones, no lookup would ever succeed,
// causing infinite loops in lookup.
// 计算实际占用buckets的个数,如果超过负载因子(3/4),进行扩容操作
unsigned NewNumEntries = getNumEntries() + 1;
// 获取buckets的总容量
unsigned NumBuckets = getNumBuckets();
if (LLVM_UNLIKELY(NewNumEntries * 4 >= NumBuckets * 3)) {
// 如果哈希表的负载大于等于3/4,进行二倍扩容
this->grow(NumBuckets * 2); // 首次分配 4 的容量
LookupBucketFor(Lookup, TheBucket);
NumBuckets = getNumBuckets();
} else if (LLVM_UNLIKELY(NumBuckets-(NewNumEntries+getNumTombstones()) <=
NumBuckets/8)) {
this->grow(NumBuckets);
LookupBucketFor(Lookup, TheBucket);
}
ASSERT(TheBucket);
// Only update the state after we've grown our bucket space appropriately
// so that when growing buckets we have self-consistent entry count.
// If we are writing over a tombstone or zero value, remember this.
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.
ASSERT(ValueInfoT::isPurgeable(TheBucket->getSecond()));
TheBucket->getSecond().~ValueT();
}
return TheBucket;
}
try_emplace方法调用两次
我们发现 try_emplace
方法调用了两次,这两次有什么区别呢,通过断点来看看
- 第一次
try_emplace
方法调用前,通过p associations
来查看全局的关联表,发现是空的
- 第一次
try_emplace
方法调用后
- 第二次
try_emplace
方法调用前,refs
是对象关联表,此时关联属性
还未存储,所以refs
的值也是空的
- 第二次
try_emplace
方法调用后
- 查看关联属性返回的结果
result
isFirstAssociation首次关联对象
首次关联对象,需要更新对象isa
的标志位has_assoc
,表示是否有关联对象
// 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.
// 首次关联对象调用setHasAssociatedObjects方法
// 通过setHasAssociatedObjects方法`标记对象存在关联对象`设置`isa指针`的`has_assoc`属性为`true`
if (isFirstAssociation)
object->setHasAssociatedObjects();
- 查看
setHasAssociatedObjects
方法
通过setHasAssociatedObjects
方法设置对象存在关联对象,即isa指针
的has_assoc
位域设置为true
最后通过releaseHeldValue
方法释放旧值
关联对象的数据结构
关联对象-取值流程
id
objc_getAssociatedObject(id object, const void *key)
{
return _object_get_associative_reference(object, key);
}
objc_getAssociatedObject
调用了_object_get_associative_reference
。进入_object_get_associative_reference
方法,关联对象取值就是比较简单的了就是查表,源码如下:
id
_object_get_associative_reference(id object, const void *key)
{
// 创建空的关联对象
ObjcAssociation association{};
{
// 实例化 AssociationsManager 注意这里不是单例
AssociationsManager manager;
// 实例化 全局的关联表 AssociationsHashMap 这里是单例
AssociationsHashMap &associations(manager.get());
// iterator是个迭代器,实际上相当于找到object和对应的ObjectAssociationMap(对象关联表)
AssociationsHashMap::iterator i = associations.find((objc_object *)object);
if (i != associations.end()) {
// 获取ObjectAssociationMap(对象关联表)
ObjectAssociationMap &refs = i->second;
// 迭代获取key对应的数据
ObjectAssociationMap::iterator j = refs.find(key);
if (j != refs.end()) {
// 获取 association
association = j->second;
// retain 新值
association.retainReturnedValue();
}
}
}
// release旧值,返回新值
return association.autoreleaseReturnedValue();
}
关联对象-移除流程
关联对象的移除流程分类两种情况:
- 手动调用
objc_removeAssociatedObjects
方法进行移除 - 对象销毁时,系统会自动移除关联对象
objc_removeAssociatedObjects方法
void objc_removeAssociatedObjects(id object)
{
if (object && object->hasAssociatedObjects()) {
_object_remove_assocations(object, /*deallocating*/false);
}
}
-
_object_remove_assocations
源码如下
// Unlike setting/getting an associated reference,
// this function is performance sensitive because of
// raw isa objects (such as OS Objects) that can't track
// whether they have associated objects.
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();
}
}
对象销毁dealloc时,销毁相关的关联对象
调用流程:dealloc
--> _objc_rootDealloc
--> rootDealloc
--> object_dispose
--> objc_destructInstance
--> _object_remove_assocations
总结
总的来说,关联对象
主要就是两层哈希map的处理
,即存取时都是两层处理,类似于二维数组