iOS 类扩展以及关联对象

782 阅读16分钟

前言

  在上一篇文章iOS 分类的加载流程分析中我们已经探讨了分类的加载流程,而今天我们将对iOS扩展类以及关联对象进行探讨。

学习重点

类扩展的加载

分类与类扩展的区别

关联对象实现原理

1. 类扩展(extension)

1.1 类扩展

  扩展类在我们的开发过程中其实经常使用的,下图红框部分就是一个ViewController的类扩展。

Pasted Graphic 9.png

  类扩展实际上是一个特殊的分类,也称作匿名分类,创建的类扩展只有.h文件,没有.m文件,如下图所示:

image.png

image.png

  如果不通过创建文件的方式,类扩展的代码只能写在类声明与类实现之间,如下图红框所示:

image.png

1.2 分类与类扩展的区别

  它与分类的区别如下:

  1. category:类别,分类。
  • 专门给类添加新的方法。
  • 不能给类添加成员属性,即使添加了成员变量,也无法获取。
  • 可以通过runtime给分类添加属性。
  • 分类中用@property定义变量,只会生成变量的getter,setter方法的声明,不能生成方法实现和带下划线的成员变量。
  1. extension:类扩展
  • 可以说成是特殊的分类,也称作匿名分类。
  • 可以给类添加成员属性,但是是私有变量。
  • 可以给类添加方法,也是私有方法。

1.3 类扩展的加载

  首先在main.m文件中编写如下代码:

@interface Animal : NSObject

@property (nonatomic, copy) NSString *name;

@property (nonatomic, assign) int age;

- (void)eat;

+ (void)class_method;

@end

@interface Animal ()

@property (nonatomic, copy) NSString *ext_type;

- (void)ext_sleep;

+ (void)ext_class_method;

@end

@implementation Animal

+ (void)load {
    NSLog(@"%s", __func__);
}

- (void)eat {
    NSLog(@"%s\n", __func__);
}

- (void)ext_sleep {
    NSLog(@"%s\n", __func__);
}

+ (void)class_method {
    NSLog(@"%s\n", __func__);
}

+ (void)ext_class_method {
    NSLog(@"%s\n", __func__);
}

@end

int main(int argc, const char * argv[]) {
    Animal *animal = [[Animal alloc] init];
    
    animal.name = @"小狗";
    animal.ext_type = @"柯基";
    
    [animal ext_sleep];
    
    return 0;
}

  使用clang命令将main.m文件编译成C++文件,查看其源码,发现其实类扩展的属性与在主类中定义的属性没什么区别,如下所示:

image.png

  而在类扩展中定义的方法也与主类中定义的方法没什么区别,如下图所示:

image.png

那么我们再来看看类扩展数据是如何加载的,编译运行程序,通过map_images函数间接调用了realizeClassWithoutSwift函数,如下图所示:

image.png

  通过命令打印输出Animal类中所有的方法,如下图所示:

image.png

  通过打印结果,我们发现其实类扩展中的方法以及属性并不是像加载分类数据那样加载的,而是与主类中定义实现的方法以及属性的加载方式一样。

2. 关联对象

2.1 关联对象及其基本使用

  在我们日常开发的过程中,有时我们需要给系统或者某个库中的某个类添加属性,但是我们又无法改变源码,因此我们只能创建此类的一个分类,因为我们在分类中定义的属性实际上只能用作计算属性,但是如果想在这个类的实例对象中存取属性,可以使用runtimeAPI关联对象,这种方式并不是在类的属性列表中添加新的属性,而是通过两次哈希map的形式存取关联对象的值。

  其使用方式如下所示,假设我们在Person的分类CA中想要关联一个Animal对象,可以编写如下的代码:

//Person.h 文件中代码
@interface Person : NSObject

@property (nonatomic, copy) NSString *name;

@end

//Person.m 文件中代码
#import "Person.h"

@implementation Person

@end

//Animal.h 文件中代码
@interface Animal : NSObject

@property (nonatomic, copy) NSString *name;

@property (nonatomic, assign) int age;

- (void)eat;

@end

//Animal.m 文件中代码
#import "Animal.h"

@implementation Animal

- (void)eat {
    NSLog(@"%@吃了, %s\n", self.name, __func__);
}

@end


//Person+CA.h 文件中代码
@interface Person (CA) <NSObject>

@property (nonatomic, strong) Animal *ani;

@end

//Person+CA.m 文件中代码
#import "Person+CA.h"
#import <objc/runtime.h>

static NSString *kAnimal = @"Animal";

@implementation Person (CA)

- (Animal *)ani {
    
    return objc_getAssociatedObject(self, kAnimal);
}

- (void)setAni:(Animal *)dog {
    objc_setAssociatedObject(self, kAnimal, dog, OBJC_ASSOCIATION_RETAIN);
}

@end

//main.m中代码
#import <Foundation/Foundation.h>
#import "Person.h"
#import "Person+CA.h"


int main(int argc, const char * argv[]) {
    Animal *dog = [[Animal alloc] init];
    
    dog.name = @"小狗";
    dog.age = 1;
    
    Person *p = [[Person alloc] init];
    
    [p setAni:dog];
    p.name = @"芜湖";
    [p.ani eat];
    NSLog(@"%@ --- %@ \n", p.ani.name, p.name);
    
    Animal *cat = [[Animal alloc] init];
    
    cat.name = @"小猫";
    cat.age = 1;
    
    [p setAni:cat];
    [p.ani eat];
    NSLog(@"%@ --- %@ \n", p.ani.name, p.name);
    
    return 0;
}

  编译运行程序,打印信息如下所示:

image.png

2.2 关联对象实现原理

  知道如何关联对象之后,我们再来探究一下关联对象的实现原理,首先,我们来看看设置关联对象的原理。

2.2.1 关联对象的设值

  查看objc_setAssociatedObject函数,其源码如下所示:

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

  _object_set_associative_reference函数代码如下所示:

image.png

  在这个函数中,我们先对DisguisedPtr这个C++模板类进行探究,其部分代码如下图所示:

image.png

  在_object_set_associative_reference函数中创建的disguised变量,调用了DisguisedPtr的构造函数,传入的是被关联对象的对象地址,经过disguise函数的伪装(指针值转换为十进制如果为正数,按位取反,末尾加一,指针值转换为十进制如果为负数,末尾值-1,然后按位取反)存储到disguised的成员变量value中。

  然后创建的association变量为ObjcAssociation``C++类类型,初始化调用了其构造函数,将关联对象以及关联策略分别存储到其成员变量_value以及_policy中保存起来,如下图红框所示:

image.png

  然后association调用函数acquireValue持有此关联对象,这个函数中的代码如下所示:

image.png

  在acquireValue这个函数中会根据关联策略的不同,进行不同的存储如果关联策略是OBJC_ASSOCIATION_RETAIN或者OBJC_ASSOCIATION_RETAIN_NONATOMIC类型,就会使用objc_retain函数持有这个对象,然后这个对象的引用计数值就会加一,在objc_retain函数调用前后打印此对象isa的引用计数值,验证如下所示:

image.png

  可以看到这个对象中isa中后8位由1变成了2,也就是其引用计数的值增加了1,其实这也可以叫做浅拷贝,而OBJC_ASSOCIATION_SETTER_COPY则需要此对象所属类实现NSCopying协议中的copyWithZone实例方法。

  但是如果关联值不是类对象,而是基本数据类型或者TaggedPointer,就是直接存储其值。

  紧接着,定义了isFirstAssociation变量用来判断是不是首次关联,然后定义了一个C++类类型为AssociationsManager的变量managerAssociationsManager这个类定义代码如下图所示:

image.png

  其中红框所示变量_mapStorage为StorageExplicitInitDenseMap类的别名)类型的局部静态变量,也就是说_mapStorage只会被初始化一次,并且其生命周期为从初始化一直到应用程序运行结束,因此不管以后再创建多少个AssociationsManager类型变量,_mapStorage仅初始化一次并且一直存在,类似于一个单例,而ExplicitInitDenseMap这个C++类代码如下所示:

image.png

  代码很少,但是主要代码是在其父类ExplicitInit中,代码如下图所示:

image.png

  在ExplicitInit这个模板类中有一个uint8_t(无符号8位整型,1字节大小)类型的数组成员变量_storage,这个数组初始化大小为模板类传入的类型的大小。

  然后通过调用manager变量中的get函数获取到AssociationsHashMap类型的变量associations,而实际上associations是一个哈希表,键值对分别为包装后的被关联对象类型DisguisedPtr以及存储这个被关联对象所关联对象信息的哈希表,如果关联对象的值_value不为空,首先就会调用associationstry_emplace函数尝试设置关联值哈希表中键值为disguised所对应的值为一个空的ObjectAssociationMap,因为被关联对象在还未设置关联对象之前,是不会被加入到关联值哈希表中的,如果是首次加入,还需要为其创建一个空的ObjectAssociationMap变量,实际上ObjectAssociationMap也是一个哈希表,是用来存储其关联对象信息,这两种类型的定义如下图所示:

image.png

  其实这两种类型都是属于DenseMap这个C++模板类,只不过它们对应的模板不同而已,DenseMap模板类中定义了如下图所示几个成员变量。

image.png

try_emplace函数代码如下所示:

image.png

  可以看到在这个函数中会调用LookupBucketFor函数查找这个被关联对象在关联哈希表中是否存在对象关联哈希表,LookupBucketFor函数代码如下:

image.png

  第一个LookupBucketFor函数代码如下所示:

image.png

  这个函数中的代码逻辑是从Buckets中查找Val这个Key所对应的Bucket,首先获取到Buckets中的首个Bucket的首地址以及Buckets数量,如果Buckets数量为0,说明AssociationsHashMap为一个空的hash表,当然找不到任何Bucket,就直接返回false,如果不为空,就通过hash函数获取到Val这个Key值在Buckets所对应的存储位置,如果此位置的Bucket存在且Key值为Val,那么就说明之前已经创建过了这个Val所对应的ObjectAssociationMap,那么就获取这个位置的Bucket并返回true,如果此位置的Bucket为空,就说明还未插入Val作为KeyBucket,那么就获取这个位置的Bucket并返回false,获取到这个位置的Bucket是方便存值,如果此位置的Bucket存在但不等于Val对应KeyBucket,那么就产生了hash碰撞,那么就再hash获取,获取下一个Bucket再次进行以上判断,直到产生碰撞的次数大于了Buckets的数量,如果此时都未找到Val所对应的Bucket,那么就说明发生了错误。

  try_emplace调用完LookupBucketFor函数后,如果找到了Val所对应的Bucket,就会直接返回pair<iterator, bool>这种类似于元组的值,其中iterator实际上是一个名为DenseMapIteratorC++模板类,在try_emplace函数调用所返回的iterator类型的值中,Ptr是指向所找到对应ValBucket的指针,End是指向AssociationsHashMapBuckets中最后一个Bucket的指针,然后pari中第二个bool值设置为true是表示找到了,但是如果调用完LookupBucketFor也没有找到Val所对应的Bucket,就会调用InsertIntoBucket函数在Buckets中插入一个新值,也就是初始化Val所对应位置的Bucket,其代码如下所示:

image.png

  而在这个函数中又调用了InsertIntoBucketImpl函数,其实这个函数的代码如下图所示:

image.png

  可以看到其实这个函数首先会判断是否需要对AssociationsHaseMapBuckets进行扩容,如果其存储容量超过了总容量的3/4(装载因子),就会调用grow函数创建一个容量为当前容量的两倍大小的hash表,并且将之前旧表中的Bucket添加到这个新的hash表中,,然后根据传入的Key(此时也就是所包装的被关联对象)调用LookupBucketFor函数重新获取对应位置Bucket,然后Buckets总数量加1,返回这个Bucket,而在InsertIntoBucket函数中获取到这个Bucket之后,就会对这个BucketKey以及value进行赋值,将包装的被关联对象通过函数forward赋值给Key,将新创建的ObjectAssociationMap通过包装赋值给Value

  接着回到_object_set_associative_reference函数中,在调用完try_emplace函数获取到refs_result这个变量后,会根据其第二个值判断被关联对象是否是第一次加入到AssociationsHaseMap中,如果是将之前的isFirstAssociation设置为true,然后获取到被关联对象的ObjectAssociationMap,也就是refs,如果此时是第一次为这个被关联对象添加关联对象,那么refs表肯定是个空表,此时调用refs的函数try_emplace获取传入的key所对应的关联对象信息(也就是ObjcAssociation包装了policy(关联策略)以及value(关联对象)的类型),并传入当前的association作为参数,此时refs调用try_emplace函数的逻辑是与associations调用try_emplace函数的逻辑是一样的,这里就不详细阐述了,但是另外有一点不同的是,如果refs调用try_emplace函数获取到Bucket是之前存在了的,就需要将association中的管理策略以及关联与refskey所对应的bucket中的valueObjcAssociation类型)管理策略以及关联对象分别进行交换。

  然后又会判断isFirstAssociation是否为真,如果为真,就会调用被关联对象的setHasAssociatedObjects函数设置这个被关联对象的isa的值,将has_assoc这个段的数据设置为true,然后调用releaseHeldValue函数释放旧的association中的_value,这就是关联对象的设值流程。

2.2.2 单个关联对象的删除

  如果你想要删除某个被关联对象的某个关联对象,只需要在调用objc_setAssociatedObject函数时将传入的value值设置为nil就可以了,而在_object_set_associative_reference函数中会判断value是否为空,如果value不为空,执行的就是关联对象的设值流程,如果value为空,执行的就是关联对象的删除流程,代码如下图所示:

image.png

  首先会调用associationsfind函数,找到被关联对象所对应的AssociationHashMap表,find函数代码如下图所示:

image.png

  在find函数中也是通过LookupBucketFor函数进行查找到,如果查找到了,就会返回iterator类型的变量,如果查找不到就会返回调用end()函数之后的返回值,回到_object_set_associative_reference函数,紧接着会判断是否能在AssociationHashMap中找到被关联对象所对应的ObjectAssociationMap表,如果找不到就什么都不用做了,如果找到了对应的ObjectAssociationMap表,就会调用find函数在ObjectAssociationMap表中查找对应的keyvalue(也就是ObjcAssociation)是否存在,如果不存在,也什么都不用做,如果存在,就会将ObjectAssociationMap表中key所对应的value中的policy以及value设置为相应策略以及nil值,然后调用erase函数擦除ObjectAssociationMap中这对键值,再判断ObjectAssociationMap表中Buckets是否为空,如果为空,就会调用erase函数擦除AssociationHashMap中被关联对象与ObjectAssociationMap这对键值。

2.2.3 关联对象的取值

  如果想要获取关联对象的值,可以通过调用objc_getAssociatedObject函数来获取,其代码如下所示:

image.png

  在objc_getAssociatedObject函数中是通过_object_get_associative_reference取值的,其代码如下图所示:

image.png

  可以发现关联对象的取值流程与关联对象的删除流程是极其相似的,不同的地方就在于在取值的时候,如果获取到了关联对象的包装类型association,会判断这个关联对象是不是OC对象并且为不为,如果不为空并且是OC对象,如果关联策略是OBJC_ASSOCIATION_RETAIN类型,还会对这个关联对象的引用计数值加一,但是虽然后面调用了autoreleaseReturnedValue这个函数,但是关联对象的引用计数值并没有减1,如下图所示:

image.png

  这是因为在autoreleaseReturnedValue函数的底部函数调用过程中并不是发送了autorelease消息,而是调用了rootAutorelease函数,如下图所示:

image.png

  最后返回这个关联对象值,这就是关联对象取值的整个过程了。

2.2.4 删除所有关联对象

  但目前为止,我们已经知道了单个关联对象的设值、删除、以及取值流程,那么如果一个被关联对象调用dealloc方法释放的时候,其所有的关联对象又是如何处理的呢?其实在查看Objc关联源码的时候我们已经注意到了一个函数,就是objc_removeAssociatedObjects,其代码如下图所示:

image.png

  在这个函数中,首先会判断被关联对象是否存在并且被关联对象是否有关联对象,如果为真,就会调用_object_remove_assocations函数,其代码如下图所示:

image.png

在这个函数中,首先创建一个ObjectAssociationMap类型的临时变量refs,其成员变量初始化为对应的空值,然后会根据manager变量的get()函数获取到AssociationsHashMap这张全局哈希表(存放的是所有被关联对象以及其对应的ObjectAssociationMap表),然后在这张表中查找传入的被关联对象object是否存在对应的ObjectAssociationMap表,如果找到了,就将这个被关联对象objectObjectAssociationMap表中的数据域refs中的数据进行交换,初始化一个bool类型的变量didReInsert(用来判断是不是重新插入),初始值为false,判断传入的参数deallocating(释放分配空间)的值是否为false,如果不为false,那么就是重新插入值,然后会遍历refsObjectAssociationMap表,存放的是设置关联对象时传入的key以及对应的ObjcAssociation(包装了关联策略以及关联对象)变量)中Buckets中每个Bucket,调用Bucket对中secondpolicy()函数获取此时这个Bucket中关联对象的关联策略,&OBJC_ASSOCIATION_SYSTEM_OBJECT这个枚举值,如果值为真,也就是表示是此关联对象为系统对象,就将这个关联对象重新插入到被关联对象在AssociationsHashMap表中所对应的ObjectAssociationMap表中,然后将didReInsert赋值为true,如果遍历完整个ObjcAssociationMap表都没有需要重新插入的系统对象,就调用erase函数将AssociationsHashMap表中此被关联对象对应的关联对象信息抹除,然后再次遍历refs这个表中所有的key以及对应的关联对象信息,调用releaseHeldValue函数将所有非系统对象的关联对象的引用计数值减1,然后定义一个SmallVector类型的laterRefs存储所有系统的关联对象,最后将laterRefs中所有的关联对象引用计数值减1

  但是什么情况下会调用_object_remove_assocations这个函数呢?我们来反向查找一下,全局搜索_object_remove_assocations关键字,发现在如下图所示的函数中被调用:

image.png

  而objc_destructInstance函数又在object_dispose函数中被调用,如下图所示:

image.png

  而object_dispose函数在objc_object::rootDealloc函数中被调用,如下图所示:

image.png

  而objc_object::rootDealloc函数在_objc_rootDealloc函数中被调用,如下图所示:

image.png

  而最后,_objc_rootDealloc函数在dealloc方法中被调用,当对象的引用计数为0的时候系统会对其dealloc方法进行调用,这就是删除对象的所有关联对象的调用流程了。

3. 总结

  经过以上探讨,我们已经清楚的知道了,关于一个对象的关联对象的存储在底层中实际上是一个两层哈希表结构,如下图所示:

image.png