您现在的位置是:首页 >其他 >【iOS-分类,拓展和关联对象底层探究】网站首页其他

【iOS-分类,拓展和关联对象底层探究】

神奇阿道和小司 2023-06-15 00:00:03
简介【iOS-分类,拓展和关联对象底层探究】

前言

寒假分享会问题解决二

请添加图片描述
早在大一的OC的学习过程就知道了分类和拓展的区别和联系,分类不能添加成员变量,而拓展可以添加成员变量。分类是在运行时期实现的,而拓展只是编译器的时候就实现了。对于分类我们可以通过关联对象来为我们需要的分类添加成员变量及其实现。

分类和拓展的使用创建就不过多叙述,这里讲解之前的问题?结论是如何得出的?

1 分类的基本介绍

1.1 分类Category

分类是一种在现有类中添加方法的方式。使用分类,可以将一个类的功能分为多个逻辑部分,使得代码更加清晰、易于维护。

  • 可以为任意一个类添加方法,包括系统自带的类。
    • 创建PersonC和它的分类,在分类实现方法,在本类的对象调用
      请添加图片描述
      分类也遵循公开和私有方法,分类.h
@interface PersonC (p_Category)
- (void)p_category;
- (void)initPerson;

@end

分类m

#import "PersonC+p_Category.h"

@implementation PersonC (p_Category)
- (void)p_category {
    NSLog(@"这是PersonC的Category添加的方法");
}

在ViewComtroller调用

#import "ViewController.h"
#import "PersonC.h"
#import "PersonC+p_Category.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    PersonC *personC = [[PersonC alloc] init];
    [personC p_category];

请添加图片描述

  • 可以为类添加实例方法和类方法。
  • 分类中的方法可以访问原类的所有成员变量和方法。
    分类可以初始化原类成员变量,在原类里有PersonC变量,在分类方法初始化

本类h

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface PersonC : NSObject
@property (nonatomic, strong) id personC;
- (void)coverPerson_C;
@end

NS_ASSUME_NONNULL_END

分类m

- (void)initPerson {
    self.personC = @"Category";
    NSLog(@"在Category设置原类的变量");
    NSLog(@"%@", self.personC);
}

请添加图片描述

  • 分类中的方法可以覆盖原类中同名的方法。
    分类方法的覆盖不需要在分类的h文件声明,你一旦重写了方法编译器就会自己调用新的覆盖方法
    请添加图片描述
- (void)coverPerson_C {
    NSLog(@"未被覆盖");
}

- (void)coverPerson_C {
    NSLog(@"这是被覆盖的方法");
}

请添加图片描述

  • 分类不能添加属性/成员变量,有属性列表, 所以分类可以声明属性 没有成员变量列表,不能声明成员变量,但是分类只会生成该属性对应的get和set的声明,没有去实现该方法
    请添加图片描述

请添加图片描述

1.2 分类结构体决定了什么?

在objc834可编译源码找到了分类的结构体

查询知道了这些结构体的成员变量或者方法代表了什么意思

struct category_t {
    const char *name; // 类别的名称
    classref_t cls; // 指向类别所属的类的指针
    WrappedPtr<method_list_t, method_list_t::Ptrauth> instanceMethods; // 类别中实例方法列表的指针,类型为WrappedPtr<method_list_t, method_list_t::Ptrauth>
    WrappedPtr<method_list_t, method_list_t::Ptrauth> classMethods; // 类别中类方法列表的指针,类型为WrappedPtr<method_list_t, method_list_t::Ptrauth>。
    struct protocol_list_t *protocols; // 类别中协议列表的指针,类型为struct protocol_list_t *。
    struct property_list_t *instanceProperties; // 类别中实例属性列表的指针,类型为struct property_list_t *。
    // Fields below this point are not always present on disk.
    struct property_list_t *_classProperties;  //类别中类属性列表的指针,类型为struct property_list_t *。这个字段只存在于某些特定的情况下。


// 根据传入的参数isMeta决定返回实例方法列表还是类方法列表。
    method_list_t *methodsForMeta(bool isMeta) {
        if (isMeta) return classMethods;
        else return instanceMethods;
    }
// 根据传入的参数isMeta和hi,返回实例属性列表或类属性列表。hi表示当前二进制文件的头信息,用于判断是否需要解密。
    property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
    
// 根据传入的参数isMeta,返回协议列表。
    protocol_list_t *protocolsForMeta(bool isMeta) {
        if (isMeta) return nullptr;
        else return protocols;
    }
};

分类不能添加实例变量,这是由于分类的实现机制所决定
分类在编译期间并不会将实例变量的定义合并到类的定义中,因此无法在运行时为类的实例添加实例变量

1.3 关联对象的引入

关联对象是Objective-C提供的一种机制,可以在运行时为对象添加任意的键值对,因此可以用关联对象来模拟实例变量的效果
分类只能用关联对象添加属性,结构体category_t中的instanceProperties字段表示类别中的实例属性列表,而没有类别中的实例变量列表。

2.1 拓展 Extension

拓展是一种在编译期间为类添加实例变量和属性的方式。使用拓展,可以在不继承原有类的前提下,给类添加一些额外的状态和属性。拓展的特点包括:

  • 只能为自己编写的类添加实例变量和属性,无法为系统自带的类添加。
  • 只能为类添加实例变量和属性,无法添加方法。
  • 扩展是在编译阶段与该类同时编译的,是类的一部分。扩展中声明的方法只能在该类的@implementation中实现。所以这也就意味着我们无法对系统的类使用扩展。
  • 可以为待扩展的类添加额外的 属性 变量 和方法声明
    • 注意私有属性写在类扩展
    • 扩展可以添加属性和成员变量
    • 扩展是本身没有自己的实现的,它和本类共享一个实现

3 关联对象

**关联对象是在运行时为对象动态添加键值对的一种机制。使用 objc_setAssociatedObject 函数可以为对象设置关联对象,即将指定的键值对添加到对象的关联对象表中。**该函数将关联对象与对象相关联,使得在后续的程序执行中,可以通过指定的键获取对象的关联对象值,从而实现一些额外的功能。
需要注意的是,**由于关联对象是在运行时动态添加的,因此会对对象的内存占用和性能产生一定的影响。**因此,应该尽可能地避免滥用关联对象,只在必要的时候使用。同时,在使用关联对象时,需要注意内存管理和线程安全问题,以避免程序出现意外行为。

3.1 关联对象的使用场景

  • 给分类添加属性:在Objective-C中,分类不能添加实例变量,因此也无法添加属性。但可以使用关联对象在运行时为分类对象添加属性,以实现类似属性的功能。(重点)
  • 给系统类添加属性:有时需要给系统类添加一些额外的属性,但这些类是不允许修改的,比如UIView、UIViewController等。这时可以使用关联对象来给这些类添加属性。
  • 为某些对象添加状态信息:有时需要为某些对象添加一些状态信息,但又不想将这些信息放在对象本身中,因为这会导致对象变得臃肿。这时可以使用关联对象来为对象添加状态信息,以避免污染对象本身的属性列表。
  • 拓展第三方库的功能:有时需要给第三方库中的类添加一些额外的功能或属性,但又不能修改这些类的源代码。这时可以使用关联对象来拓展这些类的功能,以实现自己的需求。

3.2 关联对象API

关联对象的实现,主要分为两部分:

  • 通过objc_setAssociatedObject设值流程
  • 通过objc_getAssociatedObject取值流程
  • 通过objc_removeAssociatedObjects移除关联对象

在objc843的可编译源码查找任意一个API 发现他们离的很近。
请添加图片描述

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

typedef void (*objc_hook_setAssociatedObject)(id _Nonnull object, const void * _Nonnull key,
                                              id _Nullable value, objc_AssociationPolicy policy);

void
objc_setHook_setAssociatedObject(objc_hook_setAssociatedObject _Nonnull newValue,
                                 objc_hook_setAssociatedObject _Nullable * _Nonnull outOldValue) {
  // See objc_object::setHasAssociatedObjects() for a replacement
}

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

void objc_removeAssociatedObjects(id object) 
{
    if (object && object->hasAssociatedObjects()) {
        _object_remove_assocations(object, /*deallocating*/false);
    }
}

3.2.3 objc_setHook

  • 看到了一个陌生的面孔objc_setHook_setAssociatedObject,简单说一下带过。
IMP objc_setHook(Class cls, SEL selector, IMP newImplementation);

  • cls参数是要替换方法的类,selector参数是要替换的方法名,newImplementation参数是新的方法实现。该函数会将原始方法的实现保存起来,并将新的方法实现设置为该方法的实现。
  • objc_setHook 函数只能替换类中的实例方法,而不能替换类方法。如果需要替换类方法,可以使用 class_replaceMethod 函数

3.2.3 objc_setAssociatedObject -设值

在讲取值之前看一下如何使用关联对象的方法实现分类添加属性
在分类的h文件设置一个 my_name属性
请添加图片描述
在m文件里面重写set get方法,使用runtime的方法,记得添加#import <objc/runtime.h>头文件
请添加图片描述

在viewController即可当成正常Person的某个属性调用
请添加图片描述

看看 objc_setAssociatedObject

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

使用 objc_setAssociatedObject 函数可以为对象设置关联对象,即将指定的键值对添加到对象的关联对象表中, 那么const void *key 和 value就是对应的键值对。

  • object: 要关联的对象,即给谁添加关联属性
  • const void *key:标识符,方便下次查找
  • id value:value
  • objc_AssociationPolicy policy:属性的策略,即nonatomic、atomic、assign、copy等

仔细观察,第四个参数的结构体里面出现了 和内存管理相关的关键字。

policyobjc_AssociationPolicy 类型的枚举值,用于指定关联对象的内存管理策略

typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
    OBJC_ASSOCIATION_ASSIGN = 0,           /**< Specifies a weak reference to the associated object. */
    OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, /**< Specifies a strong reference to the associated object. 
                                            *   The association is not made atomically. */
    OBJC_ASSOCIATION_COPY_NONATOMIC = 3,   /**< Specifies that the associated object is copied. 
                                            *   The association is not made atomically. */
    OBJC_ASSOCIATION_RETAIN = 01401,       /**< Specifies a strong reference to the associated object.
                                            *   The association is made atomically. */
    OBJC_ASSOCIATION_COPY = 01403          /**< Specifies that the associated object is copied.
                                            *   The association is made atomically. */
};
  • OBJC_ASSOCIATION_ASSIGN:表示使用弱引用来关联对象。当关联对象所属的对象释放时,关联对象也会被自动释放。
  • OBJC_ASSOCIATION_RETAIN_NONATOMIC:表示使用强引用来关联对象,并且不考虑多线程安全问题。当关联对象所属的对象释放时,关联对象会被自动释放。
  • OBJC_ASSOCIATION_COPY_NONATOMIC:表示使用复制来关联对象,并且不考虑多线程安全问题。当关联对象所属的对象释放时,关联对象会被自动释放。
  • OBJC_ASSOCIATION_RETAIN:表示使用强引用来关联对象,并且考虑多线程安全问题。当关联对象所属的对象释放时,关联对象会被自动释放。
    需要注意多线程安全问题,在不同线程中访问关联对象时,需要采取相应的线程安全措施

3.2.4 objc_setAssociatedObject的底层源码

点进去 objc_setAssociatedObject的实现发现还有一个方法。

请添加图片描述

_object_set_associative_reference(id object, const void *key, id value, uintptr_t policy)

这两个函数的参数看起来很像的样子,但是系统在外部调用objc_setAssociatedObject的原因是_object_set_associative_reference是一个私有的方法,而objc_setAssociatedObject则属于外部的公共接口,这提示我们尽量避免直接调用私有的函数/方法。

在这个函数的第四个参数 使用了uintptr_t

请添加图片描述
uintptr_t 是一种无符号整数类型,它的大小足以存储指针类型变量的值,通常被用来在不同的数据类型之间进行指针类型的转换

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;
//判断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.
    // 根据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,进行两倍扩容)
try_emplace方法的作用就是去表中查找Key相应的数据,不存在就创建:
            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();
}

在对象的isa结构体里有这样一个指针变量用来判断该对象是否含有关联对象uintptr_t has_assoc 为1则是有
在这里插入图片描述
就是在下面的代码实现的

// 首次关联对象调用setHasAssociatedObjects方法
// 通过setHasAssociatedObjects方法`标记对象存在关联对象`设置`isa指针`的`has_assoc`属性为`true`
if (isFirstAssociation)
    object->setHasAssociatedObjects();

请添加图片描述

过程总结
_object_set_associative_reference方法主要有下列两步操作:

  • 根据object在全局关联表(AssociationsHashMap)中查询ObjectAssociationMap,如果没有就去开辟内存创建ObjectAssociationMap
  • 将根据key查询到相关的association(即关联的数据 value和policy),如果查询到直接更新里面的数据,如果没有则去获取空的asociation类型然后将值存放进去

代码如何理解

  • 创建一个AssociationsManager 管理类,获取唯一全局静态哈希Map
  • 判断是否存在关联对象值:try_emplace方法的作用就是去表中查找Key相应的数据,不存在就创建
    • 存在 :创建一个空的 ObjectAssociationMap 去取查询的键值对,如果发现没有这个 key 就先插入一个 空的 BucketT标记对象存在关联对象,用当前 策略 policy 和 值 value 组成了一个 ObjcAssociation 替换之前空的BucketT
      标记 ObjectAssociationMap 为 第二次
      ( LookupBucketFor这个方法就是 根据Key去表中查找Bucket,如果已经缓存过,返回true,否则返回false)
    • 不存在 :
      根据DisguisedPtr 找到 AssociationsHashMap 中的 iterator 迭代查询器->清理迭代器

关联对象的数据结构可以理解为是两层map的调用(截取别人的图)

关联对象的数据结构

3.2.5 objc_setAssociatedObject取值

取值流程的原型如下请添加图片描述
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();
}
  • 创建一个 AssociationsManager 管理类,获取唯一的全局静态哈希Map
  • 根据 DisguisedPtr 找到 AssociationsHashMap 中的 iterator 迭代查询器,如果这个 迭代查询器 != associations.end(), 即不是最后一个, 那么获取 : ObjectAssociationMap (这里有策略 policy 和 值 value)
    ObjectAssociationMap的迭代查询器获取一个经过属性修饰符修饰的value,release旧值,返回新值。

3.2.6 objc_removeAssociatedObjects 移除关联对象

函数原型如图所示
请添加图片描述

  • 通常情况下,我们不需要手动调用 _object_remove_assocations 方法来移除对象的关联对象。Objective-C 运行时框架会在对象释放时自动移除其所有的关联对象。
  • 当一个对象的引用计数变为 0 时,系统会调用 dealloc 方法释放对象内存空间。在 dealloc 方法中,会调用 _object_remove_assocations 函数来移除对象的所有关联对象。因此,我们通常无需手动调用该函数。
  • 需要注意的是,在使用关联对象时,如果我们设置了对象的关联对象为弱引用(OBJC_ASSOCIATION_ASSIGN 或 OBJC_ASSOCIATION_WEAK),则在关联对象所引用的对象被释放时,关联对象的值会自动被设置为 nil。因此,在这种情况下,我们不需要手动调用 _object_remove_assocations 函数来移除关联对象。

调用流程:dealloc --> _objc_rootDealloc --> rootDealloc --> object_dispose --> objc_destructInstance --> _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.
// 与设置/获取关联引用不同,此函数对性能敏感,因为原始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();
    }
}


总结

  • 关联对象其实就是 ObjcAssociation 对象
  • 关联对象由AssociationsManager管理并在 AssociationsHashMap 存储
  • 对象的指针以及其对应 ObjectAssociationMap 以键值对的形式存储在 AssociationsHashMap 中
  • ObjectAssociationMap 则是用于存储关联对象的数据结构
风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。