【iOS】 分类 拓展 关联对象
文章目录
- [【iOS】 分类 拓展 关联对象](#【iOS】 分类 拓展 关联对象)
前言
之前讲过有关于类对象的内容,这里学习一下有关于类的分类拓展和关联对象的内容:
拓展
这里我们看一下下面这段代码转义程我们的cpp文件:
objc
@interface CJLPerson ()
@property (nonatomic, copy) NSString* name;
- (void)saygogogogo;
@end

这里我们可以看到它被直接存储到了成员变量表中.
方法也是这样直接被添加到metholist
中

类的扩展 在编译器 会作为类的一部分,和类一起编译进来
类的扩展
只是声明
,依赖于当前的主类
,没有.m文件,可以理解为一个·h文件
分类
我们看一下分类的编译后的结构体

发现这个分类的结构体中有类指针,有实例方法表,类方法表,协议表,属性列表,但是没有类有的成员变量表:
这里就说明了
-
我们不可以在类中定义成员变量.(因为没有成员变量表)
-
可以声明一个属性,但是只会生成这些属性的getter和setter方法的声明,并不会自动实现这些方法。也就是说,如果你在分类中添加了一个属性,你还需要自己去实现这个属性的getter和setter方法。
-
既然没有成员变量表,怎么实现我们的一个属性呢,通过关联对象来实现.
分类与拓展的区别
分类
-
专门用来给类添加新的方法
-
不能给类添加成员属性
,添加了成员属性,也无法取到 -
注意:其实
可以通过runtime 给分类添加属性
,即属性关联,重写setter、getter方法 -
分类中用
@property
定义变量,只会生成
变量的setter、getter
方法的声明
,不能生成方法实现 和 带下划线的成员变量
拓展
- 可以说成是
特殊的分类
,也可称作匿名分类
- 可以
给类添加成员属性
,但是是私有变量
- 可以
给类添加方法
,也是私有方法
- 拓展只可以在本类中使用
关联对象
前面讲过可以通过runtime来给分类添加属性,现在我们就来了解一下有关于这里的关联对象的一个内容.
objc
@interface CJLPerson (Test)
@property (nonatomic, copy) NSString* name;
@end
- (void)setName:(NSString *)name {
objc_setAssociatedObject(self, @"name", name, OBJC_ASSOCIATION_COPY_NONATOMIC);
return;
}
我们先讲一下有关于这个方法的几个参数objc_setAssociatedObject
- 参数一:要关联的对象,即给谁添加关联属性
- 参数二:标识符,方便下次查找
- 参数三:value
- 参数四:属性的
策略
,即nonatomic、atomic、assign等
下面这个图展示处理所有对象关联对象的一个属性类型

这里我们来详细了解一下有关于这部分的知识:
objc
void
objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
{
_object_set_associative_reference(object, key, 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; // 先处理值,如果为nil没有必要处理
if (object->getIsa()->forbidsAssociatedObjects()) //检查对象所属的类是否禁止关联属性(例如 NSWindow 等系统类),若禁止则触发崩溃
_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; //获取全局关联管理器,这个并不是单利,可以创建多次
AssociationsHashMap &associations(manager.get()); //获得对应的一个hashmap,associations 是全局唯一的 AssociationsHashMap,存储所有对象的关联数据
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; //得到一个空的桶子,找到引用对象类型,即第一个元素的second值
auto result = refs.try_emplace(key, std::move(association)); //查找当前的key是否有association关联对象
if (!result.second) { //如果结果不存在
association.swap(result.first->second);
}
} else { //当value为nil的时候,将key从hash表中移出
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(); //若为首次关联,调用 setHasAssociatedObjects() 设置对象的 has_assoc 标志位(在对象释放时触发关联对象的清理)
// release the old value (outside of the lock).
association.releaseHeldValue(); //根据策略释放旧值的引用计数(例如 OBJC_ASSOCIATION_RETAIN 会调用 release)
}
步骤
- 先处理值,如果为nil没有必要处理.检查对象所属的类是否禁止关联属性(例如 NSWindow 等系统类),若禁止则触发崩溃
- 获取全局关联管理器,获得对应的一个hashmap,associations 是全局唯一的 AssociationsHashMap,存储所有对象的关联数据
- 处理键值对,如果插入和更新各自处理,如果值为nil就直接移出hash表的数据
- 若为首次关联,调用 setHasAssociatedObjects() 设置对象的 has_assoc 标志位(在对象释放时触发关联对象的清理)
- 根据策略释放旧值的引用计数
哈希表(AssociationsHashMap)
从上面的流程我们可以看出关联对象的一个内存管理全部归我们的一个AssociationsHashMap管理,而不是由当前这个类来管理,也不是它的分类管理
我从上面的流程可以看出他的核心内容是一个AssociationHashMap
我们现在主要了解一个这个hashMap的一个结构:
objc
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();
}
};
这里我们可以看到,我们的AssociationHashMap
是从这个由static
修饰的静态全局变量中取出来的
objc
class AssociationsHashMap : public unordered_map<disguised_ptr_t, ObjectAssociationMap *, DisguisedPointerHash, DisguisedPointerEqual, AssociationsHashMapAllocator> {
public:
void *operator new(size_t n) { return ::malloc(n); }
void operator delete(void *ptr) { ::free(ptr); }
};
从上面的结构体可以看出AssociationsHashMap
内部维护了一个 ObjectAssociationMap
哈希表:
这里的ObjectAssociationMap
内部中关联了ObjcAssociation
和key
的一个关系
objc
class ObjectAssociationMap : public std::map<void *, ObjcAssociation, ObjectPointerLess, ObjectAssociationMapAllocator> {
public:
void *operator new(size_t n) { return ::malloc(n); }
void operator delete(void *ptr) { ::free(ptr); }
};

这时候笔者给出一张别人的思维导图来说明这几个表的一个关系:

接下来笔者主要讲一下这里几个map的一个联系与不同:
-
可以有无限多个Manger,但是我们的
AssociationsHashMap
只有一个,都是通过manger来获取的 -
第一个map对应的是每一个类都作为一个key拥有这不同的一个
ObjectAssiciationMap
,每个类维护属于自己的那一个关联对象,就好比person
类维护person
类的,teacher
维护teacher
类的,. -
第二个
ObjectAssiciationMap
的key的类型为const void
再加上我们对于objc_setAssociatedObject(<id _Nonnull object>, <const void * _Nonnull key>, <id _Nullable value>, <objc_AssociationPolicy policy>)
这个就相当于我们前面设置的那个字符串.value的类型是ObjcAssociation
-
最后的value由两个成员变量,一个是内存管理的一个策略,一个是一个value.
这里就是这里我们发现,我们的key是很重要的,我们不可以把多个value关联到同一个key中,所以我们对于key有以下几个用法:
- 采用getter方法关联:
- objc_setAssociatedObject(obj, @selector(getter), value, OBJC_ASSOCIATION_RETAIN_NONATOMIC)
- 采用属性名
- objc_setAssociatedObject(self, @"name", name, OBJC_ASSOCIATION_COPY_NONATOMIC);
- 使用指针名
- static void *MyKey = &MyKey; objc_setAssociatedObject(obj, MyKey, value, OBJC_ASSOCIATION_RETAIN_NONATOMIC)
static void *MyKey = &MyKey;
创建一个静态的void*
指针变量MyKey
,并将其地址赋给自身。这种写法通过静态变量的内存地址唯一性保证键值的全局唯一性
- 使用static字符作为key
- static char MyKey; objc_setAssociatedObject(obj, &MyKey, value, OBJC_ASSOCIATION_RETAIN_NONATOMIC)'
大致工作流程
set
- 调用 objc_setAssociatedObject。
- AssociationsManager 查找或创建与目标对象相关的 ObjectAssociationMap。
- 在 ObjectAssociationMap 中查找或创建对应的 ObjcAssociation。
- 将关联值和存储策略设置到 ObjcAssociation 中
get
- 调用 objc_getAssociatedObject。
- AssociationsManager 查找或创建与目标对象相关的 ObjectAssociationMap。
- 在 ObjectAssociationMap 中查找或创建对应的 ObjcAssociation。
- 返回从ObjcAssociation返回的value
remove
- 调用 objc_removeAssociatedObjects 或 objc_setAssociatedObject 设置为 nil。
- AssociationsManager 查找与目标对象相关的 ObjectAssociationMap。
- 从 ObjectAssociationMap 中移除对应的 ObjcAssociation。
- 如果 ObjectAssociationMap 为空,可能会移除整个映射以释放资源。
关联对象的释放时机
对象销毁的时候会调用一个dealloc
函数,这个函数会执行下面几个方法:
- 1、C++函数释放 :
objc_cxxDestruct
- 2、移除关联属性:
_object_remove_assocations
- 3、将弱引用自动设置nil:
weak_clear_no_lock(&table.weak_table, (id)this);
- 4、引用计数处理:
table.refcnts.erase(this)
- 5、销毁对象:
free(obj)
。
所以这里会自动移除这里的关联对象的属性.
所以我们不需要手动释放这里的关联对象.
总结
这里我们就大致明白了我们关联对象的一个set
过程,get
过程其实也是类似在这个二层hash表中进行一个检索.这里笔者就不多讲述细节了.直接用一张图来总结一下:
