重学OC第十四篇:类的扩展&关联对象

本文探讨了Objective-C中类扩展的本质,强调它作为类的一部分,用于隐藏属性和方法。类扩展在编译时期即加入方法列表。对比了分类与类扩展的区别。此外,详细分析了关联对象的使用,包括objc_setAssociatedObject和objc_getAssociatedObject的源码解析,以及在对象释放时自动清理关联对象的过程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、类的扩展

1.1 类扩展的本质

@interface TestB : NSObject

@end

@interface TestB () {
    NSString *_name;
}

@property (nonatomic, assign) int ext_b;
- (void)ext_testB;

@end

@implementation TestB

- (void)ext_testB {
    NSLog(@"TestA method name : %s", __func__);
}

@end

类的扩展只能位于TestB类的@interface与@implementation之间。它可以包括成员变量、属性、方法。通过clang把代码转为c++查看

struct TestB_IMPL {
	struct NSObject_IMPL NSObject_IVARS;
	NSString *_name;
	int _ext_b;
};

static void _I_TestB_ext_testB(TestB * self, SEL _cmd) {
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_fq_p4vwghvd0hnf6v4v0cylt6xc0000gn_T_TestB_42363d_mi_0, __func__);
}

static int _I_TestB_ext_b(TestB * self, SEL _cmd) { return (*(int *)((char *)self + OBJC_IVAR_$_TestB$_ext_b)); }
static void _I_TestB_setExt_b_(TestB * self, SEL _cmd, int ext_b) { (*(int *)((char *)self + OBJC_IVAR_$_TestB$_ext_b)) = ext_b; }

从中可以看出它的属性可以自动实现getter/setter方法,也会生成成员变量,并且直接加入到了TestB_IMPL结构体中,并没有像分类那样是个单独的结构体,显然类的扩展实质上就是类的一部分,只是不能像在类的@interface中声明的属性和方法那样直接被外部调用,相当于对外部隐匿起来了。

//这里ext_b、setExt_b出现了两次,用@property写的属性自动生成就会加入4个,自己手写getter/setter和成员变量就只会加入2个。应该是编译器做的处理,先不管它。
static struct /*_method_list_t*/ {
	unsigned int entsize;  // sizeof(struct _objc_method)
	unsigned int method_count;
	struct _objc_method method_list[5];
} _OBJC_$_INSTANCE_METHODS_TestB __attribute__ ((used, section ("__DATA,__objc_const"))) = {
	sizeof(_objc_method),
	5,
	{{(struct objc_selector *)"ext_testB", "v16@0:8", (void *)_I_TestB_ext_testB},
	{(struct objc_selector *)"ext_b", "i16@0:8", (void *)_I_TestB_ext_b},
	{(struct objc_selector *)"setExt_b:", "v20@0:8i16", (void *)_I_TestB_setExt_b_},
	{(struct objc_selector *)"ext_b", "i16@0:8", (void *)_I_TestB_ext_b},
	{(struct objc_selector *)"setExt_b:", "v20@0:8i16", (void *)_I_TestB_setExt_b_}}
};

从上面代码可以看出类扩展在编译时方法已经加入到方法列表中了,也可以通过查看ro来证明这一点,在readClass方法中打上断点。
在这里插入图片描述

(lldb) p *(ro->baseMethodList)
(method_list_t) $0 = {
  entsize_list_tt<method_t, method_list_t, 3> = {
    entsizeAndFlags = 24
    count = 4
    first = {
      name = "ext_testB"
      types = 0x0000000100003ec0 "v16@0:8"
      imp = 0x0000000100003d30 (KCObjc`-[TestB ext_testB])
    }
  }
}
(lldb) p $0.get(0)
(method_t) $1 = {
  name = "ext_testB"
  types = 0x0000000100003ec0 "v16@0:8"
  imp = 0x0000000100003d30 (KCObjc`-[TestB ext_testB])
}
(lldb) p $0.get(1)
(method_t) $2 = {
  name = "ext_b"
  types = 0x0000000100003ee9 "i16@0:8"
  imp = 0x0000000100003d60 (KCObjc`-[TestB ext_b])
}
(lldb) p $0.get(2)
(method_t) $3 = {
  name = "setExt_b:"
  types = 0x0000000100003ef1 "v20@0:8i16"
  imp = 0x0000000100003d80 (KCObjc`-[TestB setExt_b:])
}
(lldb) p $0.get(3)
(method_t) $4 = {
  name = ".cxx_destruct"
  types = 0x0000000100003ec0 "v16@0:8"
  imp = 0x0000000100003da0 (KCObjc`-[TestB .cxx_destruct])
}

可以看出在类还没有进行实现前ro中的方法列表中就有了扩展中的方法。可以间接说明类扩展是在编译期间就被加入到了类中。

1.2 分类 VS 类扩展

分类和类扩展如果是单独的文件都需要被导入。分类一般用来为类添加方法,类扩展可以用来减少不想对外暴露的属性、方法,但并不是真正的私有。

分类
类扩展
运行时加载编译时加载
有单独的结构category_t,通过运行时附加的方式直接加入主类结构
有.h、.m文件只有.h文件, 导入后与主类共用.m文件
不能给类添加成员变量可以添加成员变量
属性只会生成getter/setter声明,可通过关联对象添加getter/setter实现属性会生成getter/setter方法声明和实现,也会生成成员变量
方法可外部访问因一般直接写在主类的.m文件中,方法对外部是隐匿的。虽也可写在分类中,但没实际意义

二、关联对象

#import <objc/runtime.h>

static const void *gc_kOneStr = &gc_kOneStr;
@implementation TestA (One) 

- (NSString *)oneStr {
    return objc_getAssociatedObject(self, gc_kOneStr);
}

- (void)setOneStr:(NSString *)oneStr {
    objc_setAssociatedObject(self, gc_kOneStr, oneStr, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

分类不能添加成员变量,但可以通过关联对象的方式为类添加属性的实现。

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        TestA *testA = [[TestA alloc] init];
        testA.oneStr = @"one";
        NSLog(@"%@", testA.oneStr);
    }
    return 0;
}

运行后打印出one,说明通过关联对象的方式成功的设置了oneStr的值,也成功取出了设置的值。

2.1 objc_setAssociatedObject源码解析

/*	定义了objc_hook_setAssociatedObject函数指针
	object:关联的源对象
	key:关联的key
	value: 与对象的key相关联的值。传递nil以清除现有的关联。
	policy:关联选择的策略
*/
typedef void (*objc_hook_setAssociatedObject)(id _Nonnull object, const void * _Nonnull key,id _Nullable value, objc_AssociationPolicy policy);

static void _base_objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
{
  _object_set_associative_reference(object, key, value, policy);
}

static ChainedHookFunction<objc_hook_setAssociatedObject> SetAssocHook{_base_objc_setAssociatedObject};

template <typename Fn>
class ChainedHookFunction {
    std::atomic<Fn> hook{nil};

public:
	//构造方法,结合上面可以得知这里的f为_base_objc_setAssociatedObject,而Fn为objc_hook_setAssociatedObject
    ChainedHookFunction(Fn f) : hook{f} { };

    Fn get() {
        return hook.load(std::memory_order_acquire);
    }

    void set(Fn newValue, Fn *oldVariable)
    {
        Fn oldValue = hook.load(std::memory_order_relaxed);
        do {
            *oldVariable = oldValue;
        } while (!hook.compare_exchange_weak(oldValue, newValue,
                                             std::memory_order_release,
                                             std::memory_order_relaxed));
    }
};

void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
{
	//结合上面可把下面代码看作_base_objc_setAssociatedObject(object, key, value, policy);来理解
    SetAssocHook.get()(object, key, value, policy);
}

从上面可以了解到对objc_setAssociatedObject方法的调用其实就是对_base_objc_setAssociatedObject方法的调用,_base_objc_setAssociatedObject中又调用了_object_set_associative_reference方法。

void _object_set_associative_reference(id object, const void *key, id value, uintptr_t policy)
{
    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));

    DisguisedPtr<objc_object> disguised{(objc_object *)object};
    ObjcAssociation association{policy, value};

    // retain the new value (if any) outside the lock.
    //如果value存在,根据policy的值对value值作相应的操作。
    association.acquireValue();

    {//局部作用域
    	//manager在构造时加锁,析构时开锁
        AssociationsManager manager;   
        //DenseMap<DisguisedPtr<objc_object>, DenseMap<const void *, ObjcAssociation>> 就是个嵌套的的DenseMap, manager.get()操作的是一个static的_mapStorage变量,而associations的数据是从manager.get()初始化来的,而且manager.get()返回的是带&的引用,所以associations也可以看做是静态的。
        AssociationsHashMap &associations(manager.get());

        if (value) {
        	//try_emplace在前面分类就已经解释过了,在这disguised作为key查找,如果已经在associations表中,就把查找到的桶作为DenseMapIterator的位置指针进行初始化,然后用pair包装后返回;key没在associations表中就把disguised作为key,ObjectAssociationMap{}作为value存入桶中,然后把该桶作为DenseMapIterator的位置指针进行初始化,然后用pair包装后返回。返回值类型std::pair<DenseMapIterator, bool>
            auto refs_result = associations.try_emplace(disguised, ObjectAssociationMap{});
            if (refs_result.second) {
                //能进来这里,说明disguised为key的桶是新插入进来的,所以根据条件设置isa_t中的has_assoc位为true
                object->setHasAssociatedObjects();
            }

            //找到associations中的disguised对应的ObjectAssociationMap表
            auto &refs = refs_result.first->second;
            //用key在ObjectAssociationMap表中查找,如果表中不存在该key那么就把key和association对应插入到ObjectAssociationMap中
            auto result = refs.try_emplace(key, std::move(association));
            //result.second为false, 说明ObjectAssociationMap表中原来已有该key,不会移动,所以这里进行了swap的操作来交换association的值。
            if (!result.second) {
                association.swap(result.first->second);
            }
        } else {   //value为nil, 取消关联。看懂了上面这里就很简单了。
        	//先从associations表中找到disguised对应的ObjectAssociationMap表,又用pair包装后返回。
            auto refs_it = associations.find(disguised);
            //如果从associations表中找到了disguised对应的ObjectAssociationMap表,就走进去
            if (refs_it != associations.end()) {
            	//从pair中拿到ObjectAssociationMap表
                auto &refs = refs_it->second;
                //从ObjectAssociationMap表中查找key对应的association,然后把它作为DenseMapIterator的位置指针初始化后返回
                auto it = refs.find(key);
                //如果找到了就进去
                if (it != refs.end()) {
                	//这里交换值是为了把要擦除的association记录下来,因为下面还要进行releaseHeldValue
                    association.swap(it->second);
                    //从ObjectAssociationMap表中擦除association以及其他相应的操作
                    refs.erase(it);
                    if (refs.size() == 0) {
                    	//说明没有关联的值了,从associations表中擦除ObjectAssociationMap表
                        associations.erase(refs_it);
                    }
                }
            }
        }
    }

    // release the old value (outside of the lock).
    association.releaseHeldValue();
}


typedef DenseMap<const void *, ObjcAssociation> ObjectAssociationMap;
typedef DenseMap<DisguisedPtr<objc_object>, ObjectAssociationMap> AssociationsHashMap;

// class AssociationsManager manages a lock / hash table singleton pair.
class AssociationsManager {
    using Storage = ExplicitInitDenseMap<DisguisedPtr<objc_object>, ObjectAssociationMap>;
    static Storage _mapStorage;  //静态变量

public:
    AssociationsManager()   { AssociationsManagerLock.lock(); }
    ~AssociationsManager()  { AssociationsManagerLock.unlock(); }
	//注意这里的&,返回的是引用
    AssociationsHashMap &get() {
        return _mapStorage.get();
    }
	//map_images_nolock中第一次调用时,通过arr_init调用此方法进行_mapStorage初始化
    static void init() {
        _mapStorage.init();
    }
};

下面通过断点打印来帮助理解源码中的内容,可以看到设置的值"one"
在这里插入图片描述
先打印了最初的association和associations

(lldb) p association
(objc::ObjcAssociation) $0 = {
  _policy = 3
  _value = 0x0000000100004038 "one"
}
(lldb) p associations
(objc::AssociationsHashMap) $1 = {
  Buckets = 0x0000000000000000
  NumEntries = 0
  NumTombstones = 0
  NumBuckets = 0
}

接着打印了refs_result的一些内容,下面会省略一些类型的参数来方便查看

(lldb) p refs_result
//这里省略了DenseMapIterator的参数内容
(std::pair<DenseMapIterator, bool>) $2 = {
  first = {
    Ptr = 0x0000000102018aa0
    End = 0x0000000102018b00
  }
  second = true
}

(lldb) p *refs_result.first
(objc::DenseMapIterator) $2 = {
  std::__1::pair<DisguisedPtr<objc_object>, objc::DenseMap<const void *, objc::ObjcAssociation, objc::DenseMapValueInfo<objc::ObjcAssociation>, objc::DenseMapInfo<const void *>, objc::detail::DenseMapPair<const void *, objc::ObjcAssociation> > > = {
    first = (value = 18446744069408131216)
    second = {
      Buckets = 0x0000000000000000
      NumEntries = 0
      NumTombstones = 0
      NumBuckets = 0
    }
  }
}

(lldb) p refs_result.first->second
(objc::DenseMap<const void *, objc::ObjcAssociation, objc::DenseMapValueInfo<objc::ObjcAssociation>, objc::DenseMapInfo<const void *>, objc::detail::DenseMapPair<const void *, objc::ObjcAssociation> >) $3 = {
  Buckets = 0x0000000000000000
  NumEntries = 0
  NumTombstones = 0
  NumBuckets = 0
}
(lldb) p result
(std::pair<objc::DenseMapIterator<const void *, objc::ObjcAssociation, objc::DenseMapValueInfo<objc::ObjcAssociation>, objc::DenseMapInfo<const void *>, objc::detail::DenseMapPair<const void *, objc::ObjcAssociation>, false>, bool>) $4 = {
  first = {
    Ptr = 0x000000010110fdb8
    End = 0x000000010110fdd0
  }
  second = true
}
(lldb) p result.first->second
(objc::ObjcAssociation) $5 = {
  _policy = 3
  _value = 0x0000000100004038 "one"
}

最后再打印一下association和associations

(lldb) p association
(objc::ObjcAssociation) $6 = {
  _policy = 0
  _value = nil
}
(lldb) p associations
(objc::AssociationsHashMap) $7 = {
  Buckets = 0x000000010110fcf0
  NumEntries = 1
  NumTombstones = 0
  NumBuckets = 4
}

到这里可以得出下面的一张关系图
在这里插入图片描述

AssociationsHashMap表是以伪装后的objc_object指针为key,ObjectAssociationMap是以const void *类型的指针为key。

2.2 objc_getAssociatedObject源码分析

id objc_getAssociatedObject(id object, const void *key)
{
    return _object_get_associative_reference(object, key);
}

id _object_get_associative_reference(id object, const void *key)
{	
	//先初始化一个用来接收值的association
    ObjcAssociation association{};
    {
        AssociationsManager manager;
        AssociationsHashMap &associations(manager.get());
        //用object作为key从associations表中找到对应的ObjectAssociationMap表
        AssociationsHashMap::iterator i = associations.find((objc_object *)object);
        if (i != associations.end()) {
            ObjectAssociationMap &refs = i->second;
            //用key在ObjectAssociationMap表中搜索对应的ObjcAssociation
            ObjectAssociationMap::iterator j = refs.find(key);
            if (j != refs.end()) {
            	//找到后赋值给association,然后retain
                association = j->second;
                association.retainReturnedValue();
            }
        }
    }

    return association.autoreleaseReturnedValue();
}

2.3 关联对象释放

在对象调用dealloc方法释放时,通过_objc_rootDealloc->rootDealloc->object_dispose->objc_destructInstance

void *objc_destructInstance(id obj) 
{
    if (obj) {
        // Read all of the flags at once for performance.
        bool cxx = obj->hasCxxDtor();
        bool assoc = obj->hasAssociatedObjects();

        // This order is important.
        if (cxx) object_cxxDestruct(obj);
        //如果有关联对象,移除
        if (assoc) _object_remove_assocations(obj);
        obj->clearDeallocating();
    }
    return obj;
}

void _object_remove_assocations(id object)
{
    ObjectAssociationMap refs{};
    {
        AssociationsManager manager;
        AssociationsHashMap &associations(manager.get());
        AssociationsHashMap::iterator i = associations.find((objc_object *)object);
        if (i != associations.end()) {
            refs.swap(i->second);
            associations.erase(i);
        }
    }
    // release everything (outside of the lock).
    for (auto &i: refs) {
        i.second.releaseHeldValue();
    }
}

从中可以看出关联对象不用手动移除,在对象释放时会自动移除。

总结

  • 类扩展是在编译期就已经是类的一部分了。一般用于对外隐藏属性和方法,但并不是真正的私有。
  • 关联对象通过manager维护了以伪装后的objc_object指针为key的AssociationsHashMap表和以const void *类型的指针为key的ObjectAssociationMap表。ObjectAssociation就存在ObjectAssociationMap表中。
  • 关联对象在对象释放时会自动移除。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值