1.类扩展
扩展是分类的一种特殊形式。
1.使用特点
- 可以说成是特殊的分类,也称做匿名分类
- 可以给类添加成员属性,但是都是私有的变量
- 可以给类添加方法,也是私有的方法
2.使用方式
见下面代码:
如果将类扩展放在类实现之后,则编译不过,见下图:
类扩展中的内容是私有的,也就是说外界是不可知的。我们一般会将需要暴露给外界的方法、属性等放在类声明中;将私有的,无需外界可知的方法、属性等放在类扩展中。
3.底层实现原理
通过clang
可以将m
文件编译成cpp
文件,这样我们可以了解更多的关于类扩展的底层实现原理。引入一个案例,见下图:
clang
编译之后,继续探索类扩展!打开编译生成的.cpp
文件,分别分析属性和方法。
-
属性。搜索类扩展中的成员变量
ext_age
,在类的实现中,发现类扩展的相关属性和成员变量,和类声明中的属性和成员变量一起,在编译阶段已经放在了类实现中。见下图: -
方法。在
.cpp
文件中,类扩展中的属性自动生成了get
、set
方法,同时,除了类方法以外,其他的对象方法也在编译阶段放在了类中。见下图:
通过上面的cpp
文件分析,我们可以得出结论,类扩展的相关内容在编译阶段即已存储到类中。
4.源码跟踪验证
跟踪源码验证一下,在类的加载过程中,系统会从编译生成的MachO
文件中,获取将类对应的数据,并将其装转为class_ro_t
,并赋值给创建的class_rw_t
。如果获取的ro
数据中包括了类扩展的信息,那么我们就可以确定类扩展在编译阶段即放到了类中。
将类设置为非懒加载,运行程序,见下图:
2.分类
对一个已经封装好的类(可以是系统类、第三方类),添加一些方法属性等,而又不想去改动类中现有的内容,可以通过增加一个分类来实现。
将一个大型的臃肿的类,拆分成多个不同的分类,在不同的分类中实现类别声明的方法,这样可以将一个类的实现写到多个.m
文件中,方便管理和协同开发。
1.使用特点
- 专门用来给类添加新的方法
- 不能给类添加成员属性,添加了成员变量,也无法取到
- 注意:其实可以通过
runtime
给分类添加属性 - 分类中用
@propetry
定义变量,只会生成变量的getter
,setter
方法的声明,不能生成方法实现和带下划线的成员变量
2.使用方式
见下面代码:
@interface LGPerson (LG)
@end
@implementation LGPerson (LG)
@end
- 文件名通常为:主类名+分类名
- 调用方法时,只需要向主类引用发送消息即可
3.功能验证
Category
是Objective-C
中常用的语法特性,通过它可以很方便的为已有的类来添加函数。但是Category
不允许为已有的类添加新的属性或者成员变量。创建一个LGPerson
类的分类,在分类中添加一个属性,见下面代码:
@interface LGPerson (LGN)
@property (nonatomic, copy) NSString * cate_name;
@end
调用cate_name
的set
方法,编译通过,但是运行时会崩溃报错,说明分类只是声明了属性的get
、set
方法,并没有生成方法实现和带下划线的成员变量。见下图:
4.关联对象
常见的办法是通过runtime.h
中objc_getAssociatedObject
、objc_setAssociatedObject
来访问和生成关联对象,通过这种方法来模拟生成属性。见下面代码:
@interface LGPerson (LG)
@property (nonatomic, copy) NSString * cate_name;
@end
static const void *NameKey = &NameKey;
@implementation LGPerson (LG)
- (NSString *)cate_name{
return objc_getAssociatedObject(self, NameKey);
}
- (void)setCate_name:(NSString *)cate_name{
objc_setAssociatedObject(self, NameKey, cate_name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
@end
此种情况下,设置和获取cate_name
都不会报错,见下图:
3.关联对象实现原理
-
在查看关联对象的实现原理前,先思考一下,如果我们自己来完成这样的一个功能该如何处理?
维护了一个公共的数据存储区域,比如一个
hashMap
,针对每个类设置不同的数据存储空间,因为一个类可能会设置多个属性,那么这个存储空间是一个二维的hashMap
。
下面进入源码探索其实现原理。我们从设置关联对象开始探索,见下面代码:
objc_setAssociatedObject
调用了_object_set_associative_reference
方法,这样隔绝了api
,下层api
在发生变化的情况下,也不会影响应用层的使用。查看_object_set_associative_reference
实现,红框区域使我们的研究重点,见下图:
_object_set_associative_reference
方法三个重要的参数:
object
,要设置的对象,这里是指LGPerson对象
key
,设置属性的key
值value
, 属性对应的数值value
1.object封装处理
首先调用DisguisedPtr
的构造函数,将object
,封装一个DisguisedPtr
数据类型,实现代码:
DisguisedPtr<objc_object> disguised{(objc_object *)object};
DisguisedPtr
类,见下图:
2.value封装处理
对value
进行封装,下面代码:
ObjcAssociation association{policy, value};
将value
封装成一个ObjcAssociation
对象,见下图:
至此底层的数据结构是这样的呢?
object
->DisguisedPtr
value
->ObjcAssociation
3.AssociationsManager和AssociationsHashMap
AssociationsManager
作为一个关联对象处理流程的管理器,用于处理全局的一个hashMap
,该hashMap
就是AssociationsHashMap
,AssociationsHashMap
中存储了全部的对象的关联数据。
-
AssociationsManager
提供了一个构造函数和一个析构函数,见下图:- 构造函数,主要用来在创建对象,并完成对象属性的一些初始化等操作。
- 析构函数,主要作用就是释放资源,避免内存泄漏。
析构函数
与构造函数
相反,当对象结束其生命周期时,系统自动执行析构函数。析构函数往往用来做清理善后
的工作。
下面模拟一个构造函数和析构函数的使用方式,见下图:
在程序执行完成后,
selfC
的声明周期结束,系统会自动执行其析构函数。 -
AssociationsManager
是否为单例呢?为验证这个问题,我们可以修改一下源码,在此基础上多创建几个
AssociationsManager
,输出AssociationsManager
的地址,如果地址一样,说明是单例,否则不是单例。见下图:很遗憾,报错,没有能够运行成功,原因是什么呢?查看
AssociationsManager
构造函数,在使用过程中上了锁,在析构函数没有调用释放锁的情况下,重复调用加锁,导致奔溃。去掉构造函数中的锁,继续运行程序,见下图:通过这个案例可以验证
AssociationsManager
并不是单例的。 -
AssociationsHashMap
单例验证同样上面的案例,我们输出
AssociationsHashMap
的地址,发现三个都是一样的,所以AssociationsHashMap
是单例的,见下图:我们从源码中也可以验证这一点,
AssociationsHashMap
是通过一个静态的Storage数据
返回的,而静态变量的地址是固定的。同时该静态变量之所以声明在AssociationsHashMap
中,是为了设置一个调用方式,只能通过AssociationsHashMap
类去调用。见下图:
4.数据流程分析
完成相关数据的初始化后,进行数据插入、删除操作,如果value
不为空,则进行数据的插入操作,否则抹除关联的数据。见下图:
下面跟踪流程,探究器实现原理!
-
关联对象设置流程
首先根据
DisguisedPtr(object)
从单例的AssociationsHashMap
数据结构中获取了refs_result
,那么该数据结构是怎样的呢?见下图:分析一下该数据结构:首先
$3
中有两个数据first
和second
,其中second
为一个bool
型,那么first
是什么呢?是个pair
,也就是一个对值,包括Ptr
和End
。下面这段代码,这里做了什么呢?
auto refs_result = associations.try_emplace(disguised, ObjectAssociationMap{});
传入了两个参数,其中
disguised
也就是我们要关联的对象,进入该方法,查看其源码实现,见下图:在
try_emplace
方法中首先会调用LookupBucketFor
方法,从方法名称可以理解为去寻找该bucket
是否存在。进入LookupBucketFor
的方法实现,见下图:在该方法里调用了一个和其同名的方法,但是参数是不一样的,这个方法两个参数都是
const
型,进入LookupBucketFor
的方法实现。同时需要注意的是,这里传入了一个BucketT
地址,即到AssociationsHashMap
中去查找对象所对应的BucketT
,在进行查找过程中进行地址赋值,见下图:在该方法中,进行
object
对象对应的bucket
的查找,如果存储桶包含键和值,则返回true
;否则返设置一个空桶并返回false
。回到
try_emplace
方法,完成LookupBucketFor
流程后,TheBucket
已完成了赋值,即使没有找到,也会被赋值一个空桶的地址,并进行相关桶子初始化操作;如果找到,则返回一个pair
和一个bool
值。见下图:InsertIntoBucket
方法实现见下图:通过上面的流程,
try_emplace
中second
参数的值已经被设置为true
。所以初次进入,就会进行isFirstAssociation
的设置,见下图:至此完成了
object
对应的桶子的创建,也就是bueckt
的创建,但是此时value
和key
还没有设置到对应的桶子中。下面进行association
的插入,见下图:当再次进入该方法是,由于第一个参数此时是
key
,而不是一个object
相关内容,所以进行LookupBucketFor
调用,查找bucket
时,一定返回false
。所以一定会调动InsertIntoBucket
方法进行association
的插入。见下面代码:设置总结:
- 创建⼀个
AssociationsManager
管理类 - 获取唯⼀的全局静态哈希
Map
- 判断是否插⼊的关联值是否存在: 存在⾛第
4
步;不存在就⾛: 关联对象插⼊空流程 - 创建⼀个空的
ObjectAssociationMap
去取查询的键值对 - 如果发现没有这个
key
就插⼊⼀个空的BucketT
进去,并返回 - 标记对象存在关联对象
- ⽤当前修饰策略和值组成了⼀个
ObjcAssociation
替换原来BucketT
中的空 - 标记⼀下
ObjectAssociationMap
的第⼀次为false
- 创建⼀个
-
关联对象插入空流程
如果传入的
value
值为空,数据清空处理,见下图:- 根据
DisguisedPtr
找到AssociationsHashMap
中的iterator
迭代查询器 - 清理迭代器
- 其实如果插⼊空置,相当于清除
- 根据
5.关联对象总结
通过上面的分析,可以知道关联对象处理过程中的数据结构关系,和我们在进行设计预想的思路是一致!即有一个全局的数据集合HashMap
,内部存储了全部对象的关联数据,一个对象都会对应一个subHashMap
,而该对象所关联的属性,都以键值对的形式存储在其subHashMap
中。
通过objc_getAssociatedObject
获取关联对象值,会更直观,见下图:
- 创建⼀个
AssociationsManager
管理类 - 获取唯⼀的全局静态
HashMap
,AssociationsHashMap
- 根据
DisguisedPtr
找到AssociationsHashMap
中的iterator
迭代查询器 - 如果这个迭代查询器不是最后⼀个,获取:
ObjectAssociationMap
(这⾥有策略和value
) - 找到
ObjectAssociationMap
的迭代查询器获取⼀个经过属性修饰符修饰的value
- 返回
_value
总结: 其实就是两层哈希map
, 存取的时候两层处理(类似⼆维数组)
6.关联对象销毁流程
在对象被销毁时会调用dealloc
方法,同时会对关联对象进行释放。
dealloc
->_objc_rootDealloc
->rootDealloc
,rootDealloc
实现源码见下图:
继续跟踪代码:rootDealloc
->object_dispose
->objc_destructInstance
最终通过调用_object_remove_assocations
将关联对象清除,见下图: