「OC」源码学习——关联属性再探索
文章目录
前言
在寒假学习小蓝书的时候就已经接触过了关联属性,现在在学习源码的过程之中,在进行复习巩固以及深入学习
引入
对于分类来说,我可以进入源码查看他的实现
// 表示Objective-C分类(Category)的运行时内部结构
struct category_t {
// 分类的名称,例如为NSString添加的分类名为"MyCategory"
const char *name;
// 指向该分类所扩展的原始类的引用(编译时填充为指向类结构的指针)
classref_t cls;
// 实例方法列表(包装指针,包含指针验证逻辑,如Ptrauth用于ARM64e架构的指针签名)
WrappedPtr<method_list_t, method_list_t::Ptrauth> instanceMethods;
// 类方法列表(同样包含指针验证)
WrappedPtr<method_list_t, method_list_t::Ptrauth> classMethods;
// 该分类遵循的协议列表(protocol_list_t结构指针)
struct protocol_list_t *protocols;
// 实例属性列表(property_list_t结构指针,用于声明@property)
struct property_list_t *instanceProperties;
// 类属性列表(注意:此字段在磁盘上的分类结构中可能不存在,运行时加载时动态填充)
struct property_list_t *_classProperties;
// 根据目标是否为元类返回对应方法列表
// isMeta=true时返回类方法列表,否则返回实例方法列表
method_list_t *methodsForMeta(bool isMeta) const {
return isMeta ? classMethods : instanceMethods;
}
// 根据目标元类状态返回属性列表(需结合header_info头信息处理)
property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi) const;
// 返回协议列表(元类不持有协议,因此返回nullptr)
protocol_list_t *protocolsForMeta(bool isMeta) const {
return isMeta ? nullptr : protocols;
}
};
可以看到在category_t
之中,并没有Ivar
的实例变量列表,而且发现分类里即是声明了属性,但并不会给我们生成getter/setter
。那就需要使用动态关联属性的方式自己写一个getter/setter
,具体例子如下:
// MyPerson的分类
@interface MyPerson (Test)
@property (nonatomic, copy) NSString *name;
- (void)cate_instanceMethod;
+ (void)cate_classMethod;
@end
@implementation MyPerson (Test)
- (void)setName:(NSString *)name {
objc_setAssociatedObject(self, MyNameKey, name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSString *)name {
return objc_getAssociatedObject(self, MyNameKey);
}
- (void)cate_instanceMethod {
NSLog(@"%s", __func__);
}
+ (void)cate_classMethod {
NSLog(@"%s", __func__);
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSLog(@"main");
MyPerson *p = [MyPerson alloc];
[p speak];
p.name = @"bb";
NSLog(@"%@", p.name);
}
return 0;
}
以这个例子我们先来了解一下这个关联对象的双层哈希表的结构
1. 第一层哈希表(AssociationsHashMap)
- 键(Key):每个实例的伪装指针
DisguisedPtr<objc_object>
- 通过位运算将对象的内存地址转换为整型,避免直接暴露指针。
- 每个实例的地址唯一,即使同一类创建多个实例,它们的地址也不同。
- 值(Value):指向该实例专属的 第二层哈希表(ObjectAssociationMap) 的指针。
示例: 若 MyPerson
类创建了实例 p1
和 p2
,则第一层哈希表中会有两个键:
p1
的地址 →p1
的关联表p2
的地址 →p2
的关联表
2. 第二层哈希表(ObjectAssociationMap)
- 键(Key):开发者定义的静态键(如
@selector(name)
或全局变量地址)。 - 值(Value):封装关联值和内存策略的
ObjcAssociation
结构体。
示例: 若 p1
和 p2
都通过 objc_setAssociatedObject
设置了 name
属性:
p1
的第二层表中存储key: name → value: "Alice"
p2
的第二层表中存储key: name → value: "Bob"
源码探索
objc_setAssociatedObject
我们先看一下objc_setAssociatedObject
的源码
void
_object_set_associative_reference(id object, const void *key, id value, uintptr_t policy)
{
// 此代码在 object 和 key 传入 nil 时仍能工作。某些代码可能依赖此行为而不崩溃。需显式检查处理。
// 原始问题记录:rdar://problem/44094390
if (!object && !value) return;
// 检查对象所属类是否禁止关联对象(通过 class_ro_t->flags 的 RO_FORBIDS_ASSOCIATED_OBJECTS 标志位)
if (object->getIsa()->forbidsAssociatedObjects())
_objc_fatal("objc_setAssociatedObject 在类 %s 的实例 (%p) 上被调用,但该类禁止关联对象",
object_getClassName(object), object);
// 将对象指针伪装为 DisguisedPtr(防止调试工具直接暴露内存地址)
DisguisedPtr<objc_object> disguised{(objc_object *)object};
// 创建关联对象封装结构,包含内存策略和值
ObjcAssociation association{policy, value};
// 在加锁前执行新值的 retain/copy 操作(根据内存策略)
association.acquireValue();
// 标记是否是首次关联(用于后续触发 has_assoc 标志位更新)
bool isFirstAssociation = false;
{
// 获取全局关联对象管理器(内部包含互斥锁,确保线程安全)
AssociationsManager manager;
// 获取全局关联对象哈希表 AssociationsHashMap 的引用
AssociationsHashMap &associations(manager.get());
if (value) { // 关联新值
// 尝试插入或查找对象对应的二级哈希表(ObjectAssociationMap)
// try_emplace 返回 pair<iterator, bool>,second 表示是否为新插入
auto refs_result = associations.try_emplace(disguised, ObjectAssociationMap{});
if (refs_result.second) { // 新插入条目,说明是首次关联
isFirstAssociation = true;
}
// 获取二级哈希表引用,进行键值操作
auto &refs = refs_result.first->second;
// 尝试插入或替换当前键的关联值
auto result = refs.try_emplace(key, std::move(association));
if (!result.second) { // 键已存在,执行替换
association.swap(result.first->second); // 交换新旧关联值
}
} else { // value 为 nil,表示移除关联对象
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);
}
}
}
}
} // 此处自动释放 AssociationsManager 的锁
// 在锁外设置 has_assoc 标志位(可能触发 +initialize 方法)
// 注意:必须在锁外调用,因为 _noteAssociatedObjects 可能触发其他关联对象操作
if (isFirstAssociation)
object->setHasAssociatedObjects(); // 设置对象的 HAS_ASSOCIATED_OBJECTS 标志位
// 在锁外释放旧值(根据内存策略执行 release 或 autorelease)
association.releaseHeldValue();
}
-
DisguisedPtr<objc_object>:
- 将对象指针伪装为整型,防止调试工具直接暴露内存地址
- 实现方式:对指针值进行位运算混淆(如 ptr ^ DISGUISE_MASK)
-
AssociationsManager:
- 全局单例管理器,通过 C++ RAII 模式管理互斥锁
- 构造函数加锁,析构函数解锁,确保线程安全
-
AssociationsHashMap:
- 第一层哈希表,键为 DisguisedPtr<objc_object>,值为 ObjectAssociationMap
- 使用 C++11 unordered_map 实现,冲突处理为链地址法
-
ObjectAssociationMap:
- 第二层哈希表,键为 const void* (开发者传入的 key),值为 ObjcAssociation
- 存储具体的关联值及其内存策略
- 会存储多个不同的关联对象
-
ObjcAssociation:
- 封装关联值和内存策略的结构体
- 内存策略和@property的用法类似
objc_getAssociatedObject
id
_object_get_associative_reference(id object, const void *key)
{
// 创建空的关联对象封装结构(用于存储返回值)
ObjcAssociation association{};
{
// 获取全局关联对象管理器(RAII锁:构造函数加锁,析构函数解锁)
AssociationsManager manager;
// 获取全局第一层哈希表 AssociationsHashMap 的引用
AssociationsHashMap &associations(manager.get());
// 在第一层哈希表中查找对象的关联表(键为对象指针)
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 = j->second; // 复制关联值和策略
// 根据策略 retain 值(例如 RETAIN/COPY 策略需增加引用计数)
association.retainReturnedValue();
}
}
} // 此处自动释放锁
// 将关联值注册到自动释放池(遵循ARC内存管理规则,调用者无需手动释放)
// 注意:即使策略是 ASSIGN,这里也会执行 autorelease 以保证安全
return association.autoreleaseReturnedValue();
}
-
AssociationsManager:
- 通过 C++ RAII 模式管理互斥锁,构造函数中加锁,析构函数解锁
- 确保在哈希表操作期间的线程安全
-
双层哈希表查找:
- 第一层:对象地址 → ObjectAssociationMap*
- 第二层:开发者key → ObjcAssociation
- 例如:对象0x7f8a3b01 的 “name” 属性需两次哈希查找
-
retainReturnedValue:
- 根据关联策略(如 OBJC_ASSOCIATION_RETAIN)对值执行 retain
- 特殊处理 COPY 策略:如果值支持 NSCopying 协议,执行 copy 操作
-
autoreleaseReturnedValue:
- 将返回值加入当前自动释放池,延迟释放时机
- 即使策略是 ASSIGN 也强制 autorelease,避免野指针风险
移除关联对象
关联属性不需要我们程序员去管理内存
inline void
objc_object::rootDealloc()
{
// TaggedPointer 无需内存回收(特殊标记的小对象,内存直接存储在指针值中)
if (isTaggedPointer()) return; // fixme necessary?
// 快速路径判断:对象满足以下所有条件时可直接释放
if (fastpath(isa.nonpointer && // 使用优化的非指针型isa
!isa.weakly_referenced && // 无弱引用指向该对象
!isa.has_assoc && // 未设置关联对象
#if ISA_HAS_CXX_DTOR_BIT
!isa.has_cxx_dtor && // 无C++析构函数(新版本ISA标志位)
#else
!isa.getClass(false)->hasCxxDtor() && // 旧版本检查类是否有C++析构
#endif
!isa.has_sidetable_rc)) // 未使用sidetable存储引用计数
{
assert(!sidetable_present());
free(this); // 直接释放内存
}
else {
object_dispose((id)this); // 需要复杂处理的场景
}
}
// 对象销毁入口函数
id
object_dispose(id obj)
{
if (!obj) return nil;
objc_destructInstance(obj); // 执行实例销毁逻辑
free(obj); // 释放对象内存
return nil;
}
// 对象实例销毁核心逻辑
void *objc_destructInstance(id obj)
{
if (obj) {
// 一次性读取所有标志位以优化性能
bool cxx = obj->hasCxxDtor(); // 检查是否有C++析构函数
bool assoc = obj->hasAssociatedObjects(); // 检查是否有关联对象
// 处理顺序非常重要(先析构再移除关联)
if (cxx) object_cxxDestruct(obj); // 执行C++析构函数
if (assoc) _object_remove_assocations(obj, /*deallocating*/true); // 移除关联对象
obj->clearDeallocating(); // 清理弱引用和sidetable引用计数
}
return obj;
}