「iOS」————分类与扩展
iOS学习
- 关联对象,分类,扩展
- 扩展:编译期
- 分类:运行期
- 二者总结:
- 分类(category)
- 拓展
- 两者区别
- 关联对象
- objc_setAssociatedObject
- 哈希表(AssociationsHashMap)
- objc_getAssociatedObject
- objc_removeAssociatedObjects
- 关联对象的释放时机
- 小问题
- 关联对象被存储在什么地方,是不是存放在被关联对象本身的内存中?
- 关联对象的生命周期是怎样的,什么时候被释放,什么时候被移除?
- 如何给关联对象设置weak属性
关联对象,分类,扩展
分类(运行期)
-
分类就是为已经存在的类,在运行时添加新的方法的一种机制。
-
分类可以添加协议,类方法,实例方法和属性,不能添加实例变量。
-
添加的属性就不会生成成员变量,也不会生成
setter/getter
方法,需要手动添加setter/getter
方法。
扩展(编译期)
-
扩展和分类很像,扩展只有声明部分,扩展中的定义的方法需要在类的实现部分去实现。
-
可以定义协议,类方法,实例方法,属性和实例变量,在编译期将其中的定义的数据加到该类的数据类表中。如果不实现其中的方法,就会报错。
-
由于系统的类的是实现部分不对用户开放,所以不能给系统的类添加扩展。
总结:
- 分类中没有成员列表,因此不能添加成员变量。但是有属性列表,可以添加属性的声明,但是不会合成set与get方法,如果要使用分类中的属性,需要使用关联对象。
- 分类在运行的时候被整合到类中,扩展在编译的时候被整合到类中,因此分类中的方法不实现不会报错,扩展就会报错。
- 扩展用于声明私有属性与方法。
- 分类中的方法和类中的方法重名,分类中的方法会代替类中的方法。
扩展:编译期
类的扩展 在编译器 会作为类的一部分,和类一起编译进来
类的扩展
只是声明
,依赖于当前的主类
,没有.m文件,可以理解为一个·h文件
可以添加属性和成员变量
分类:运行期
分类编译后的结构体如上,有类指针,实例方法表,类方法表,协议表,属性列表,但是没有成员变量表!
因此:
- 在类中不能定义成员变量!
- 可以声明一个属性,但是不会实现getter和setter方法,这两个方法需要自己实现。有这两个方法的声明
- 我们可以通过关联对象来实现属性。
二者总结:
分类(category)
-
专门用来给类添加新的方法
-
不能给类添加成员属性
,添加了成员属性,也无法取到 -
注意:其实
可以通过runtime 给分类添加属性
,即属性关联,重写setter、getter方法 -
分类中用
@property
定义变量,只会生成
变量的setter、getter
方法的声明
,不能生成方法实现 和 带下划线的成员变量
拓展
-
可以说成是
特殊的分类
,也可称作匿名分类
-
可以
给类添加成员属性
,但是是私有变量
-
可以
给类添加方法
,也是私有方法
-
拓展只可以在本类中使用
两者区别
- 分类原则上只能增加方法,但是也可以通过关联属性增加属性
- 拓展可以增加方法和成员变量,都是私有的,实现部分在类中。
- 扩展只能在自身类中使用,而不是子类或者其他地方。
- 扩展是在编译阶段添加到类中,而分类是在运行时添加到类中
关联对象
我们可以通过runtime给分类添加属性,就是使用关联对象进行操作:
@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
方法有几个参数:
object
: 要添加属性的对象。key
: 用于标识属性的键,通常使用一个NSString
对象。value
: 要添加的属性值。policy
: 关联策略,用于指定属性的生命周期和访问权限。对应的是修饰符,即nonatomic、atomic、assign等
下面这个图展示处理所有对象关联对象的一个属性类型
给key设置一般来说有三种方法:
-
针对每个属性,定义一个全局的key名,然后取其地址,由于这时唯一的,因此要加上static,只在文件内部有效:
static const void *NameKey = &NameKey; static const void *WeightKey = &WeightKey;
-
针对每个属性,因为类中的属性名是唯一的,直接拿属性名作为key名。
#define NameKey = @"name"; #define WeightKey = @"weight";
-
使用@selector作为key
@selector(name)//直接用属性名对应的get方法的selector,有提示不容易写错。并且get方法隐藏参数cmd 可以直接用,看上去就会更加简洁
objc_setAssociatedObject
接着我们看一下这个方法的源码:
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)
{//isa有一位信息为禁止关联对象,如果设置了,直接报错if (!object && !value) return;// 判断runtime版本是否支持关联对象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.// 根据传入的缓存策略,创建一个新的value对象association.acquireValue();bool isFirstAssociation = false;{//调用构造函数,构造函数内加锁操作AssociationsManager manager; // 创建一个管理对象管理单例,类AssociationsManager管理一个锁/哈希表单例对。分配一个实例将获得锁// 并不是全场唯一,构造函数中加锁只是为了避免重复创建,在这里是可以初始化多个AssociationsManager变量的//获取全局的HasMap// 全场唯一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 */// 这是我们建立的第一个关联//说明是第一次设置关联对象,把是否关联对象设置为YESisFirstAssociation = true;}// 建立或替换关联/* establish or replace the association */// 获取ObjectAssociationMap中存储值的地址auto &refs = refs_result.first->second;// 移除之前的关联,根据key// 将需要存储的值存放在关联表中存储值的地址中// 同时会根据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 {// 这里相当于传入的nil,移除之前的关联// 到AssociationsHashMap找到ObjectAssociationMap,将传入key对应的值变为空。// 查找disguised 对应的ObjectAssociationMapauto refs_it = associations.find(disguised);// 如果找到对应的 ObjectAssociationMap 对象关联表if (refs_it != associations.end()) {// 获取 refs_it->second 里面存放了association类型数据auto &refs = refs_it->second;// 根据key查询对应的associationauto it = refs.find(key);if (it != refs.end()) {// 如果找到,更新旧的association里面的值association.swap(it->second);refs.erase(it);if (refs.size() == 0) {// 如果该对象关联表中所有的关联属性数据被清空,那么该对象关联表会被释放associations.erase(refs_it);}}}}}// 在锁外面调用setHasAssociatedObjects,因为如果对象有一个,这个//将调用对象的noteAssociatedObjects方法,这可能会触发initialize,这可能会做任意的事情,包括设置更多的关联对象。if (isFirstAssociation)object->setHasAssociatedObjects();// release the old value (outside of the lock).// 释放旧的值(在锁外部)association.releaseHeldValue();
}
步骤:
-
先处理值,如果为nil没有必要处理,检查对象所属的类的是否禁止关联属性(例如NSWindow等系统类),若禁止则出发崩溃
-
接着封装数据类型,便于底层处理(将
object
封装成DisguisedPtr<objc_object>
类型,将policy
和value
封装成ObjcAssociation
类型) -
接着使用
association.acquireValue()
保留新值,确保新值不会被释放 -
获取全局关联管理器(非单例),再获得对应的一个hashMap,associations (单例)是全局唯一的 AssociationsHashMap,存储所有对象的关联数据。
-
在
AssociationsHashMap
中查找object
对应的关联表ObjectAssociationMap
- 如果没有找到
ObjectAssociationMap
,则创建一个新的ObjectAssociationMap
,并将其插入到AssociationsHashMap
中。 - 如果找到
ObjectAssociationMap
,则在ObjectAssociationMap
接着查找key
对应的关联信息- 如果找到,则使用新值替换旧值。
- 如果没有找到,则创建一个新的关联信息,并将新值存储在
ObjectAssociationMap
中。
- 如果没有找到
-
若为首次关联,调用 setHasAssociatedObjects() 设置对象的 has_assoc 标志位(在对象释放时触发关联对象的清理)
-
如果
value
为nil
,则表示要移除关联。系统会查找ObjectAssociationMap
中key
对应的关联信息,并将其移除。
我们继续走进try_emplace的源码
template <typename... Ts>
std::pair<iterator, bool> try_emplace(const KeyT &Key, Ts &&... Args) {// 声明一个桶指针,用于存储查找结果BucketT *TheBucket;// 尝试查找Key对应的桶// 如果找到了(返回true),说明Key已存在if (LookupBucketFor(Key, TheBucket))return std::make_pair(makeIterator(TheBucket, getBucketsEnd(), true),false); // 已在映射中,返回现有元素的迭代器和false表示未插入新元素// 如果没找到,则插入新元素// InsertIntoBucket会在合适的桶中创建新元素,参数为桶、键和构造值的参数// std::forward<Ts>(Args)... 完美转发构造参数TheBucket = InsertIntoBucket(TheBucket, Key, std::forward<Ts>(Args)...);// 返回新插入元素的迭代器和true表示成功插入了新元素return std::make_pair(makeIterator(TheBucket, getBucketsEnd(), true),true);
}
try_emplace方法的主要逻辑是:
- 首先尝试查找键是否已存在
- 如果键已存在,返回现有元素的迭代器和false
- 如果键不存在,就在适当位置插入新元素,并返回新元素的迭代器和true
哈希表(AssociationsHashMap)
从上面的流程我们可以看出关联对象的一个内存管理全部归我们的一个AssociationsHashMap管理,而不是由当前这个类来管理,也不是它的分类管理
因此我们来了解一下AssociationHashMap
的结构
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
修饰的静态全局变量中取出来的
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
的一个关系
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); }};
objc_getAssociatedObject
id
_object_get_associative_reference(id object, const void *key)
{ObjcAssociation association{};//创建空的关联对象{AssociationsManager manager;//创建一个AssociationsManager管理类AssociationsHashMap &associations(manager.get());//获取全局唯一的静态哈希mapAssociationsHashMap::iterator i = associations.find((objc_object *)object);//找到迭代器,即获取bucketsif (i != associations.end()) {//如果这个迭代查询器不是最后一个 获取ObjectAssociationMap &refs = i->second; //找到ObjectAssociationMap的迭代查询器获取一个经过属性修饰符修饰的valueObjectAssociationMap::iterator j = refs.find(key);//根据key查找ObjectAssociationMap,即获取bucketif (j != refs.end()) {association = j->second;//获取ObjcAssociationassociation.retainReturnedValue();}}}return association.autoreleaseReturnedValue();//返回value
}
-
首先创建AssociationsManager对象,接着通过它来获取全局的
AssociationsHashMap
-
在
AssociationsHashMap
中查找object
对应的ObjectAssociationMap
-
如果找到
ObjectAssociationMap
,则在ObjectAssociationMap
中接着查找key对应的关联信息并赋值给value -
最后返回value
objc_removeAssociatedObjects
// 与设置/获取关联引用不同,此函数对性能敏感,因为原始isa对象(如OS对象)不能跟踪它们是否有关联对象。
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.//如果我们没有回收,那么SYSTEM_OBJECT关联会被保留。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.//如果我们不是在释放,那么RELEASE_LATER关联不会被释放if (deallocating)laterRefs.append(&i.second);} else {i.second.releaseHeldValue();}}for (auto *later: laterRefs) {later->releaseHeldValue();}
}
-
首先使用
AssociationsManager
获取全局的AssociationsHashMap
,并查找object
对应的关联表ObjectAssociationMap
。 -
如果找到关联表,则将其复制到
refs
变量中,以便在锁外进行操作。 -
如果对象正在被释放,则所有关联对象都会被移除,但如果对象只是被修改,则系统关联对象会被保留
-
如果没有保留任何关联对象,则从
AssociationsHashMap
中移除object
对应的关联表 -
遍历
refs
中的所有关联对象,并根据关联对象的策略进行释放。 -
如果关联对象的策略包含
OBJC_ASSOCIATION_SYSTEM_OBJECT
,则将其添加到laterRefs
列表中,以便在所有其他关联对象释放后进行释放。 -
如果关联对象的策略不包含
OBJC_ASSOCIATION_SYSTEM_OBJECT
,则立即释放关联对象。 -
最后遍历
laterRefs
中的所有关联对象,并释放它们
我们来看一张思维导图
以上涉及到多个map和一个manager,他们的特点如下:
- 可以有无限多个Manger,但是我们的
AssociationsHashMap
只有一个,都是通过manger来获取的 - 第一个map:
AssociationsHashMap
对应的是每一个类都作为一个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.
我们可以把多个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
函数,这个函数会执行下面几个方法:
- C++函数释放 :
objc_cxxDestruct
- 移除关联属性:
_object_remove_assocations
- 将弱引用自动设置nil:
weak_clear_no_lock(&table.weak_table, (id)this);
- 引用计数处理:
table.refcnts.erase(this)
- 销毁对象:
free(obj)
。
所以调用dealloc函数时,会自动移除这里的关联对象的属性。不需要手动释放关联对象。
总结
引用月月的图:
小问题
关联对象被存储在什么地方,是不是存放在被关联对象本身的内存中?
关联对象存放在名为ObjectAssociationMap的哈希表中,存放关联对象的哈希表又被存放在名为AssociationsHashMap的哈希表中,通过AssociationsManager来管理。也就是说所有对象的关联对象都存在一个全局map里面。而map的的key是这个对象的指针地址,而这个map的value又是另外一个全局map,里面保存了关联对象的key。
关联对象的生命周期是怎样的,什么时候被释放,什么时候被移除?
关联对象的释放时机与移除时机并不总是一致。关联对象的生命周期取决于关联策略和目标对象的生存期。弱引用策略的关联对象会随着目标对象的释放而被释放,而强引用策略和复制引用策略的关联对象会继续存在,直到它们的引用计数降为 0。
如何给关联对象设置weak属性
> “Objective-C 的关联对象没有直接的 weak 选项。我们可以自定义一个只持有 weak 属性的容器对象,把它 strong 关联到宿主对象上。这样目标对象释放时,weak 属性会自动变 nil,从而实现 weak 关联。”
关联对象默认只有assign/strong/copy,没有weak,可以通过“中间桥接对象”来实现 weak 关联。用一个中间的弱引用容器(桥接对象),它持有 weak 属性,桥接对象再被 strong 关联到宿主对象上。
代码演示:
先定义一个弱引用容器
// WeakObjectContainer.h
@interface WeakObjectContainer : NSObject
@property (nonatomic, weak) id weakObject;
- (instancetype)initWithWeakObject:(id)object;
@end// WeakObjectContainer.m
@implementation WeakObjectContainer
- (instancetype)initWithWeakObject:(id)object {self = [super init];if (self) {_weakObject = object;}return self;
}
@end
使用关联对象实现weak关联
#import <objc/runtime.h>
#import "WeakObjectContainer.h"static const void *kWeakAssociatedKey = &kWeakAssociatedKey;// 设置 weak 关联对象
- (void)setWeakAssociatedObject:(id)object {WeakObjectContainer *container = [[WeakObjectContainer alloc] initWithWeakObject:object];objc_setAssociatedObject(self, kWeakAssociatedKey, container, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}// 获取 weak 关联对象
- (id)getWeakAssociatedObject {WeakObjectContainer *container = objc_getAssociatedObject(self, kWeakAssociatedKey);return container.weakObject;
}
使用
// 假设 self 是宿主对象,obj 是要 weak 关联的对象
[self setWeakAssociatedObject:obj];// 取出 weak 关联对象
id weakObj = [self getWeakAssociatedObject];