iOS 底层原理记录 03 – Category

发布于 2022-08-15  125 次阅读



这是图片



Categor 分类

分类、分类,分门别类。 也就是一个本体的影分身,每个分身做不同的事情。

Category 小结

  1. 所有的分类(影分身😄)的实现方法,其实都是在原来类的类对象的方法列表中。
  2. 所有分类的类方法,都是放在原来类的元类的方法列表中。
  3. 通过 runtime 动态将分类的方法合并到类对象、元类对象中。 所以是在运行的时候做的,并非是合并的时候做的。
  4. 每创建一个分类,其实底层都会创建一个结构体(编译后),这个结构体存储了当前分类的一些信息:方法、类方法、遵守的协议、属性。(不能有成员变量,会报错的)
  5. 由于合并的时候,是先把原来的方法往后移,然后把分类的方法插到前面,所以同样的方法,优先调用分类的方法。而在runtime中,组合分类方法列表的时候,最后面的一个分类方法放在最前面,所以如果多个方法都重复,则优先调用编译时的最后一个。编译的顺序,在 Build Phases 中的 Complie Sources 中设置。
  6. Category 中的属性,底层只会帮忙生成该属性的 seter、geter 方法声明。 并不会生成该属性的实例变量,以及对应的 seter 、geter 方法实现。

Category 的加载处理过程(Runtime)

  1. 通过 Runtime 加载某个类的所有 Category 数据。
  2. 把所有 Category 的方法、属性、协议数据,合并到一个大的数组中,最后参与编译的 Category 会放在数组的前面。
  3. 将合并后的分类数据(方法、属性、协议),插入到类原来数据的前面。(先根据分类的数据量,申请了新的内存空间,旧的方法列表指向这个新的内存空间。 然后旧方法列表根据插入的数据量往后移动,最后把新的方法列表插入到旧方法列表的最前面,最终形成一个新的方法列表)

Extension 类扩展

扩展很像是匿名分类(并不是)。
但是扩展是相当于给原来的类在 .m 文件中扩展了私有的属性、方法、成员变量。
而且扩展是在编译的时候就已经合并到原来的类对应的数据列表中了。


Load 方法

load 方法小结

  1. +(void)load 方法会在 runtime 加载 类、分类 的时候调用。也就是在程序运行的时候,无论程序是否用到这个类,都会调用 load 方法将类或者分类加载进内存中,而且只调用一次
  2. +(void)load 方法是在 runtime 运行时直接通过内存地址,找到类、分类的内存地址,直接调用。而不是到合并后的元类对象中去调用的。先调用原来的类,再调用子类的,然后调用原来类的分类的,最后调用子类分类的load方法
  3. 而自定义的类方法,不是系统的,则本质上是通过 objc_msgSend 来执行的,而这个发送消息的机制,本质上就是通过 isa 指针,找元类对象,然后找到元类对象里的类方法列表,然后执行,找不到就通过 元类对象的 superClass 指针去找父类的,直到找到。

load 方法的调用顺序

  1. 总的来说,先调用类的 load 方法(),再调用分类的 load 方法。

如果不是父子类关系,则先编译的类先调用。
父类先调用,然后是子类

  1. 再调用分类的 load 方法

先编译的分类优先调用

标签 描述
A 原来的类的 load 方法
B 子类的load方法
C 原来类的分类的load方法
D 子类的分类的load方法
graph TD;
    A-->B;
    A-->C;
    B-->D;
    C-->D;

initialize 方法

  • initialize 方法会在第一次接收到消息时调用。且只调用一次。
    调用此方法的机制,就是OC的消息机制。 也是通过 ISA 找到元类对象的方法列表。所以会被分类的相同方法覆盖掉。
    调用顺序: 先调用父类的 initialize ,然后再调用子类的 initialize 方法。如果直接调用子类,那么也会先调用父类的 initialize ,然后再调用子类的 initialize 方法。(其实就是子类主动调用了一下父类的 initialize 方法

关联对象

把一个对象关联到另外一个对象上

API

添加关联对象

id objc_setAssociatedObject(id object, const void * key, id value, objc_AssociationPolicy policy)

获得关联对象

id objc_getAssociatedObject(id object, const void * key)

移除所有的关联对象

void objc_removeAssociatedObjects(id object)

移除单个关联对象

往添加关联对象的时候传 nil 即可,内部会将其移除。

使用方法

  • 第一种
//使用变量地址作为 key
static void *MyKey = &MyKey;
objc_setAssociatedObject(obj, MyKey, value, OBJC_ASSOCIATION_RETAIN_NONATOMIC)
objc_getAssociatedObject(obj, MyKey)
  • 第二种
//使用一个字节的变量地址作为 key
static char MyKey;
objc_setAssociatedObject(obj, &MyKey, value, OBJC_ASSOCIATION_RETAIN_NONATOMIC)
objc_getAssociatedObject(obj, &MyKey)

  • 第三种
//使用属性名作为key
objc_setAssociatedObject(obj, @"property", value, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
objc_getAssociatedObject(obj, @"property");

  • 第四种
//使用get方法的@selecor作为key
objc_setAssociatedObject(obj, @selector(getter), value, OBJC_ASSOCIATION_RETAIN_NONATOMIC)
objc_getAssociatedObject(obj, @selector(getter))

关联对象的实现原理

其实就是 Runtime 自己维护了一个 HashMap (在OC中指的就是字典) ,而且这个是个全局的 HashMap 。
第一层 key 为根据被关联的对象A对象生成的key,value 是另外一种 ObjectAssociationMap 对象。
第二层的 key 为关联对象时传入的 key ,这是个 void * 的指针。 而值为另外一个 新的 ObjectAssociation 对象,这个对象里有最终关联的值value,还有关联策略 policy 。

iOS 底层原理记录 03 - Category

对象移除/销毁后,关联的map也会移除。


知识点小结

  1. 一个对象的全局变量,会被这个对象的多个实例共享,所以并不能保证唯一性。

  2. 全局变量,可以在外部通过 extern 拿到,然后进行修改。如果不想外部获取并修改,那么可以增加 static 修饰符,让这个变量或者常量只在当前文件内部有效。

  3. 字符串常量NSString *str = @”aaa“;与 NSString *str2 = @"aaa"; 以及 @"aaa" 这种是放在常量区,那么无论写多少遍,它们的内存地址都是固定的。

  4. 关联对象的时候,没有弱引用的策略,需要注意。


面试题

Category 的实现原理是什么?

答:
Category 的底层结构是 struct category_t,里面存储着分类对象的对象方法、类方法、属性、协议信息。
在程序运行的时候,runtime 会将 Category 的数据,合并到原来的类信息中(类对象、元类对象中)

Category 和 Extension 的区别是什么?

答:
Class Extension 在编译的时候,它的数据就已经包含在类信息中。你必须拥有类的源码,才能为类添加这些信息。比如系统的 NSString 这种就不行。扩展主要用来隐藏类的一些私有信息。
Category 是在运行时,才会将数据合并到类信息中。

Category 中有 load 方法吗? load 方法是什么时候调用的? load 方法能继承吗?

答:

load 方法是在 runtime 加载类、分类的时候调用(不管这些类、分类是否被调用)
load 方法可以继承,但是一般情况下,不会主动调用 load 方法,都是让系统自动调用。

load、initialize 方法的区别是什么?它们在 category 中的调用顺序是什么样的?以及出现继承时它们之间的调用过程是怎样的?

答:
initialize 和 load 的很大区别是, initialize 是最终通过 objc_msgSend 进行调用,所以有以下特点:

  1. 如果子类没有实现 initialize ,那么就会调用父类的 initialize (所以父类的 initialize 可能会被调用多次,但是不代表父类被初始化了多次 -- 在子类没实现 initialize 的情况下)。
  2. 如果分类实现了 initialize,就会覆盖类本身的 initialize 调用
  3. initialize 是首次类接收到消息的时候执行(这个时候类早就被加载到内存中了),而 load 是在运行时,加载到内存后会立刻且只调用一次。
  4. initialize 本质上是OC的消息机制调用的(也就是通过 isa 指针找到元类对象,然后找到方法列表中去)。 而 load 方法是直接通过该方法的地址指针调用的。所以多个分类的load方法都会独立执行,而多个分类的 initialize 只会执行一个(根据编译熟顺序)。
load、initialize方法的区别什么?
1.调用方式
1> load是根据函数地址直接调用
2> initialize是通过objc_msgSend调用

2.调用时刻
1> load是runtime加载类、分类的时候调用(只会调用1次)
2> initialize是类第一次接收到消息的时候调用,每一个类只会initialize一次(父类的initialize方法可能会被调用多次)

load、initialize的调用顺序?
1.load
1> 先调用类的load
a) 先编译的类,优先调用load
b) 调用子类的load之前,会先调用父类的load

2> 再调用分类的load
a) 先编译的分类,优先调用load

2.initialize
1> 先初始化父类(父类被初始化一次,只是父类的 initialize 会由于消息机制被子类调用了多次 -- 子类没实现 initialize 的情况下)
2> 再初始化子类(可能最终调用的是父类的initialize方法)

Category 能否添加成员变量? 如果可以,如何给 Category 添加成员变量?

答:

不能直接给 Category 添加成员变量,但是可以间接实现 Category 有成员变量的效果。

通过关联对象来实现。


迷雾尽散 破晓而生