iOS底层学习——类扩展和分类(类别)

1,575 阅读10分钟

1.类扩展

扩展是分类的一种特殊形式。

1.使用特点

  • 可以说成是特殊的分类,也称做匿名分类
  • 可以给类添加成员属性,但是都是私有的变量
  • 可以给类添加方法,也是私有的方法

2.使用方式

见下面代码:

image.png

如果将类扩展放在类实现之后,则编译不过,见下图:

image.png

类扩展中的内容是私有的,也就是说外界是不可知的。我们一般会将需要暴露给外界的方法、属性等放在类声明中;将私有的,无需外界可知的方法、属性等放在类扩展中。

3.底层实现原理

通过clang可以将m文件编译成cpp文件,这样我们可以了解更多的关于类扩展的底层实现原理。引入一个案例,见下图:

image.png

clang编译之后,继续探索类扩展!打开编译生成的.cpp文件,分别分析属性和方法。

  1. 属性。搜索类扩展中的成员变量ext_age,在类的实现中,发现类扩展的相关属性和成员变量,和类声明中的属性和成员变量一起,在编译阶段已经放在了类实现中。见下图:

    image.png

  2. 方法。在.cpp文件中,类扩展中的属性自动生成了getset方法,同时,除了类方法以外,其他的对象方法也在编译阶段放在了类中。见下图:

    image.png

通过上面的cpp文件分析,我们可以得出结论,类扩展的相关内容在编译阶段即已存储到类中。

4.源码跟踪验证

跟踪源码验证一下,在类的加载过程中,系统会从编译生成的MachO文件中,获取将类对应的数据,并将其装转为class_ro_t,并赋值给创建的class_rw_t。如果获取的ro数据中包括了类扩展的信息,那么我们就可以确定类扩展在编译阶段即放到了类中。

将类设置为非懒加载,运行程序,见下图:

image.png

2.分类

对一个已经封装好的类(可以是系统类、第三方类),添加一些方法属性等,而又不想去改动类中现有的内容,可以通过增加一个分类来实现。

将一个大型的臃肿的类,拆分成多个不同的分类,在不同的分类中实现类别声明的方法,这样可以将一个类的实现写到多个.m文件中,方便管理和协同开发。

1.使用特点

  • 专门用来给类添加新的方法
  • 不能给类添加成员属性,添加了成员变量,也无法取到
  • 注意:其实可以通过runtime给分类添加属性
  • 分类中用 @propetry 定义变量,只会生成变量的gettersetter方法的声明,不能生成方法实现和带下划线的成员变量

2.使用方式

见下面代码:

    @interface LGPerson (LG)

    @end

    @implementation LGPerson (LG)

    @end
  • 文件名通常为:主类名+分类名
  • 调用方法时,只需要向主类引用发送消息即可

3.功能验证

CategoryObjective-C中常用的语法特性,通过它可以很方便的为已有的类来添加函数。但是Category不允许为已有的类添加新的属性或者成员变量。创建一个LGPerson类的分类,在分类中添加一个属性,见下面代码:

    @interface LGPerson (LGN)
    @property (nonatomic, copy) NSString * cate_name;
    @end

调用cate_nameset方法,编译通过,但是运行时会崩溃报错,说明分类只是声明了属性的getset方法,并没有生成方法实现和带下划线的成员变量。见下图:

image.png

4.关联对象

常见的办法是通过runtime.hobjc_getAssociatedObjectobjc_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都不会报错,见下图:

image.png

3.关联对象实现原理

  • 在查看关联对象的实现原理前,先思考一下,如果我们自己来完成这样的一个功能该如何处理?

    维护了一个公共的数据存储区域,比如一个hashMap,针对每个类设置不同的数据存储空间,因为一个类可能会设置多个属性,那么这个存储空间是一个二维的hashMap

下面进入源码探索其实现原理。我们从设置关联对象开始探索,见下面代码:

image.png

objc_setAssociatedObject调用了_object_set_associative_reference方法,这样隔绝了api,下层api在发生变化的情况下,也不会影响应用层的使用。查看_object_set_associative_reference实现,红框区域使我们的研究重点,见下图:

image.png

_object_set_associative_reference方法三个重要的参数:

  • object,要设置的对象,这里是指LGPerson对象
  • key,设置属性的key
  • value, 属性对应的数值value

1.object封装处理

首先调用DisguisedPtr的构造函数,将object,封装一个DisguisedPtr数据类型,实现代码:

    DisguisedPtr<objc_object> disguised{(objc_object *)object};

DisguisedPtr类,见下图:

image.png

2.value封装处理

value进行封装,下面代码:

    ObjcAssociation association{policy, value};

value封装成一个ObjcAssociation对象,见下图:

image.png

至此底层的数据结构是这样的呢?

  • object -> DisguisedPtr
  • value -> ObjcAssociation

image.png

3.AssociationsManager和AssociationsHashMap

AssociationsManager作为一个关联对象处理流程的管理器,用于处理全局的一个hashMap,该hashMap就是AssociationsHashMapAssociationsHashMap中存储了全部的对象的关联数据。

  1. AssociationsManager提供了一个构造函数和一个析构函数,见下图:

    image.png

    • 构造函数,主要用来在创建对象,并完成对象属性的一些初始化等操作。
    • 析构函数,主要作用就是释放资源,避免内存泄漏。析构函数构造函数相反,当对象结束其生命周期时,系统自动执行析构函数。析构函数往往用来做清理善后的工作。

    下面模拟一个构造函数和析构函数的使用方式,见下图:

    image.png

    在程序执行完成后,selfC的声明周期结束,系统会自动执行其析构函数。

  2. AssociationsManager是否为单例呢?

    为验证这个问题,我们可以修改一下源码,在此基础上多创建几个AssociationsManager,输出AssociationsManager的地址,如果地址一样,说明是单例,否则不是单例。见下图:

    image.png

    很遗憾,报错,没有能够运行成功,原因是什么呢?查看AssociationsManager构造函数,在使用过程中上了锁,在析构函数没有调用释放锁的情况下,重复调用加锁,导致奔溃。去掉构造函数中的锁,继续运行程序,见下图:

    image.png

    通过这个案例可以验证AssociationsManager并不是单例的。

  3. AssociationsHashMap单例验证

    同样上面的案例,我们输出AssociationsHashMap的地址,发现三个都是一样的,所以AssociationsHashMap是单例的,见下图:

    image.png

    我们从源码中也可以验证这一点,AssociationsHashMap是通过一个静态的Storage数据返回的,而静态变量的地址是固定的。同时该静态变量之所以声明在AssociationsHashMap中,是为了设置一个调用方式,只能通过AssociationsHashMap类去调用。见下图:

    image.png

4.数据流程分析

完成相关数据的初始化后,进行数据插入、删除操作,如果value不为空,则进行数据的插入操作,否则抹除关联的数据。见下图:

image.png

下面跟踪流程,探究器实现原理!

  1. 关联对象设置流程

    首先根据DisguisedPtr(object)从单例的AssociationsHashMap数据结构中获取了refs_result,那么该数据结构是怎样的呢?见下图:

    image.png

    分析一下该数据结构:首先$3中有两个数据firstsecond,其中second为一个bool型,那么first是什么呢?是个pair,也就是一个对值,包括PtrEnd

    下面这段代码,这里做了什么呢?

         auto refs_result = associations.try_emplace(disguised, ObjectAssociationMap{});
    

    传入了两个参数,其中disguised也就是我们要关联的对象,进入该方法,查看其源码实现,见下图:

    image.png

    try_emplace方法中首先会调用LookupBucketFor方法,从方法名称可以理解为去寻找该bucket是否存在。进入LookupBucketFor的方法实现,见下图:

    image.png

    在该方法里调用了一个和其同名的方法,但是参数是不一样的,这个方法两个参数都是const型,进入LookupBucketFor的方法实现。同时需要注意的是,这里传入了一个BucketT地址,即到AssociationsHashMap中去查找对象所对应的BucketT,在进行查找过程中进行地址赋值,见下图:

    image.png

    在该方法中,进行object对象对应的bucket的查找,如果存储桶包含键和值,则返回true;否则返设置一个空桶并返回false

    回到try_emplace方法,完成LookupBucketFor流程后,TheBucket已完成了赋值,即使没有找到,也会被赋值一个空桶的地址,并进行相关桶子初始化操作;如果找到,则返回一个pair和一个bool值。见下图:

    image.png

    InsertIntoBucket方法实现见下图:

    image.png

    通过上面的流程,try_emplacesecond参数的值已经被设置为true。所以初次进入,就会进行isFirstAssociation的设置,见下图:

    image.png

    至此完成了object对应的桶子的创建,也就是bueckt的创建,但是此时valuekey还没有设置到对应的桶子中。下面进行association的插入,见下图:

    image.png

    当再次进入该方法是,由于第一个参数此时是key,而不是一个object相关内容,所以进行LookupBucketFor调用,查找bucket时,一定返回false。所以一定会调动InsertIntoBucket方法进行association的插入。见下面代码:

    image.png

    设置总结:

    1. 创建⼀个AssociationsManager管理类
    2. 获取唯⼀的全局静态哈希Map 
    3. 判断是否插⼊的关联值是否存在: 存在⾛第4步;不存在就⾛: 关联对象插⼊空流程
    4. 创建⼀个空的ObjectAssociationMap去取查询的键值对
    5. 如果发现没有这个key就插⼊⼀个空的BucketT进去,并返回
    6. 标记对象存在关联对象
    7. ⽤当前修饰策略和值组成了⼀个ObjcAssociation替换原来BucketT中的空
    8. 标记⼀下ObjectAssociationMap的第⼀次为false
  2. 关联对象插入空流程

    如果传入的value值为空,数据清空处理,见下图:

    image.png

    1. 根据DisguisedPtr找到AssociationsHashMap中的iterator迭代查询器
    2. 清理迭代器
    3. 其实如果插⼊空置,相当于清除

5.关联对象总结

通过上面的分析,可以知道关联对象处理过程中的数据结构关系,和我们在进行设计预想的思路是一致!即有一个全局的数据集合HashMap,内部存储了全部对象的关联数据,一个对象都会对应一个subHashMap,而该对象所关联的属性,都以键值对的形式存储在其subHashMap中。

image.png

通过objc_getAssociatedObject获取关联对象值,会更直观,见下图:

image.png

  1. 创建⼀个AssociationsManager管理类
  2. 获取唯⼀的全局静态HashMapAssociationsHashMap
  3. 根据DisguisedPtr找到AssociationsHashMap中的iterator迭代查询器
  4. 如果这个迭代查询器不是最后⼀个,获取:ObjectAssociationMap(这⾥有策略和value)
  5. 找到ObjectAssociationMap的迭代查询器获取⼀个经过属性修饰符修饰的value
  6. 返回_value

总结: 其实就是两层哈希map, 存取的时候两层处理(类似⼆维数组)

6.关联对象销毁流程

在对象被销毁时会调用dealloc方法,同时会对关联对象进行释放。

dealloc->_objc_rootDealloc->rootDeallocrootDealloc实现源码见下图:

image.png

继续跟踪代码:rootDealloc->object_dispose->objc_destructInstance

image.png

最终通过调用_object_remove_assocations将关联对象清除,见下图:

image.png