美文网首页
iOS 开发:Runtime(详解四)Category

iOS 开发:Runtime(详解四)Category

作者: 哈布福禄克 | 来源:发表于2019-11-07 14:19 被阅读0次

1. Category (分类)简介

1.1 什么是 Category(分类)?

  • Category(分类)主要作用是为已经存在的类添加方法。Category 可以做到在既不子类化,也不侵入一个类的源码的情况下,为原有的类添加新的方法,从而实现扩展一个类或者分离一个类的目的。
  • 虽然继承也能为已有类增加新的方法,而且还能直接增加属性,但继承关系增加了不必要的代码复杂度,在运行时,也无法与父类的原始方法进行区分。所以我们可以优先考虑使用自定义 Category(分类)。

通常 Category(分类)有以下几种使用场景:

  • 把类的不同实现方法分开到不同的文件里。
  • 声明私有方法。
  • 模拟多继承。
  • 将 framework 私有方法公开化。

1.2 Category(分类)和 Extension(扩展)

Category(分类)看起来和Extension(扩展)有点相似。Extension(扩展)有时候也被称为 匿名分类。但两者实质上是不同的东西。Extension(扩展)是在编译阶段与该类同时编译的,是类的一部分。而且Extension(扩展)中声明的方法只能在该类的@implementation 中实现,这也就意味着: 你无法对系统的类(例如 NSString 类)使用 Extension(扩展)
而且和 Category(分类)不同的是,Extension(扩展)不但可以声明方法,还可以声明成员变量,这是 Category(分类)所做不到的。

1.3 为什么 Category(分类)不能像 Extension(扩展)一样添加成员变量?

  • 因为 Extension(扩展)是在编译阶段与该类同时编译的,就是类的一部分。既然作为类的一部分,且与类同时编译,那么就可以在编译阶段为类添加成员变量。
  • 而 Category(分类)则不同, Category(分类)的特性是:可以在运行时阶段动态地为已有类添加新行为。 Category(分类)是在运行时期间决定的。而成员变量的内存布局已经在编译阶段确定好了,如果在运行时阶段添加成员变量的话,就会破坏原有类的内存布局,从而造成可怕的后果,所以 Category(分类)无法添加成员变量。

2. Category 的结构体简介

以前的篇章我们知道了:Object(对象) 和 Class(类) 的实质分别是objc_object 结构体objc_class 结构体,这里 Category 也不例外,在objc-runtime-new.h 中,Category(分类)被定义为 category_t 结构体。category_t 结构体 的数据结构如下:

typedef struct category_t *Category;

struct category_t {
    const char *name;                                // 类名
    classref_t cls;                                  // 类,在运行时阶段通过 clasee_name(类名)对应到类对象
    struct method_list_t *instanceMethods;           // Category 中所有添加的对象方法列表
    struct method_list_t *classMethods;              // Category 中所有添加的类方法列表
    struct protocol_list_t *protocols;               // Category 中实现的所有协议列表
    struct property_list_t *instanceProperties;      // Category 中添加的所有属性
};

从 Category(分类)的结构体定义中也可以看出, Category(分类)可以为类添加对象方法、类方法、协议、属性。同时,也能发现 Category(分类)无法添加成员变量。

3. Category 的加载过程

3.1 dyld 加载大致流程

之前我们谈到过 Category(分类)是在运行时阶段动态加载的。而 Runtime(运行时) 加载的过程,离不开一个叫做 dyld 的动态链接器。
在 MacOS 和 iOS 上,动态链接加载器 dyld 用来加载所有的库和可执行文件。而加载Runtime(运行时) 的过程,就是在 dyld 加载的时候发生的。

dyld 的相关代码可在苹果开源网站上进行下载。 链接地址:dyld 苹果开源代码

dyld 加载的流程大致是这样:

1.配置环境变量;
2.加载共享缓存;
3.初始化主 APP;
4.插入动态缓存库;
5.链接主程序;
6.链接插入的动态库;
7.初始化主程序:OC, C++ 全局变量初始化;
8.返回主程序入口函数。

本文中,我们只需要关心的是第 7 步,因为 Runtime(运行时) 是在这一步初始化的。加载 Category(分类)自然也是在这个过程中。

3.2 需要注意一些细节问题:

  • Category(分类)的方法、属性、协议只是添加到原有类上,并没有将原有类的方法、属性、协议进行完全替换。
    举个例子说明就是:假设原有类拥有 MethodA方法,分类也拥有 MethodA 方法,那么加载完分类之后,类的方法列表中会拥有两个 MethodA方法。
  • Category(分类)的方法、属性、协议会被添加到原有类的方法列表、属性列表、协议列表的最前面,而原有类的方法、属性、协议则被移动到了列表后面。
    因为在运行时查找方法的时候是顺着方法列表的顺序依次查找的,所以 Category(分类)的方法会先被搜索到,然后直接执行,而原有类的方法则不被执行。这也是 Category(分类)中的方法会覆盖掉原有类的方法的最直接原因。

4. Category(分类)和 Class(类)的 +load 方法

Category(分类)中的的方法、属性、协议附加到类上的操作,是在 + load方法执行之前进行的。也就是说,在 + load方法执行之前,类中就已经加载了 Category(分类)中的的方法、属性、协议。

而 Category(分类)和 Class(类)的 + load 方法的调用顺序规则如下所示:

  1. 先调用主类,按照编译顺序,顺序地根据继承关系由父类向子类调用;
  2. 调用完主类,再调用分类,按照编译顺序,依次调用;
  3. + load方法除非主动调用,否则只会调用一次。

通过这样的调用规则,我们可以知道:

主类的 + load方法调用一定在分类 + load方法调用之前。但是分类 + load 方法调用顺序并不是按照继承关系调用的,而是依照编译顺序确定的,这也导致了 + load方法的调用顺序并不一定确定。一个顺序可能是:父类 -> 子类 -> 父类类别 -> 子类类别,也可能是 父类 -> 子类 -> 子类类别 -> 父类类别

5. Category 与关联对象

  • 之前我们提到过,在 Category 中虽然可以添加属性,但是不会生成对应的成员变量,也不能生成 getter、setter 方法。因此,在调用 Category 中声明的属性时会报错。
  • 我们可以自己来实现 getter、setter 方法,并借助关联对象(Objective-C Associated Objects)来实现 getter、setter 方法。关联对象能够帮助我们在运行时阶段将任意的属性关联到一个对象上。

具体需要用到以下几个方法:

// 1. 通过 key : value 的形式给对象 object 设置关联属性
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);

// 2. 通过 key 获取关联的属性 object
id objc_getAssociatedObject(id object, const void *key);

// 3. 移除对象所关联的属性
void objc_removeAssociatedObjects(id object);

注意:使用 objc_removeAssociatedObjects可以断开所有的关联。通常情况下不建议使用,因为它会断开所有的关联。如果想要断开关联可以使用 objc_setAssociatedObject,将关联对象传入nil即可。

相关文章

网友评论

      本文标题:iOS 开发:Runtime(详解四)Category

      本文链接:https://www.haomeiwen.com/subject/kjoybctx.html