iOS之Category

Category的底层结构

编译后的分类信息都保存在category_t结构体中(定义在objc-runtime-new.h中):

Category的加载处理过程

Category中的方法是在程序运行时通过runtime动态地将分类中的方法合并到类对象、元类对象中。

  1. 通过Runtime加载某个类的所有Category数据(方法、属性、协议);
  2. 把所有Category的方法、属性、协议数据,合并到一个大数组中,参与编译的Category数据,会放在数组的前面
  3. 将合并后的分类数据(方法、属性、协议),插入到类原来数据的前面

Category的实现原理

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

Category和Extension的区别

​ 备注:OC的Extension通常是放在.m中:可以声明私有的成员变量、属性、方法等

  • 决议时间不同:

    • Class Extension在编译的时候,它的数据就已经包含在类信息中;
    • Category是在运行时才将数据合并到类信息中
  • 目标和能力不同:

    • Extension:主要用于扩展类的私有成员、属性、方法
    • Category:
      • 主要用来拆分功能模块;
      • 可以增加instanceMethodsclassMethodsprotocols
      • Category中的properties只会增加该属性对应的setter、getter的方法声明,不会增加对应的成员和方法实现。

Category和load()方法

  • Category中可以有load方法

  • load方法调用

    • +load方法会在runtime加载类、分类时调用
    • 每个类、分类的+load方法在程序运行中只会被调用一次
  • load方法调用顺序:

    1. 先调用类的load方法
      1. 按照编译顺序先后调用(先编译的先调用)
      2. 先调用父类的load方法,再调用子类的load方法
    2. 再调用分类的load方法:按照编译顺序先后调用
  • load方法的独特性

    • 类和分类中的+load方法是直接根据+load函数的指针地址直接调用的;类的其他类方法是使用消息发送机制调用的;
    • load方法可以继承,但是一般不会主动调用+load方法,而是让系统自动调用。
    • 手动调用+load方法时跟系统调用机制不同,手动调用不是通过函数指针地址来直接调用load方法,而是通过objc_msgSendisa来调用。

loadinitialize

  • +initialize方法会在类第一次接收到消息时调用

    这句话可以理解为如果多次调用[ZZPerson alloc]ZZPerson+initialize方法只会被调用一次。

  • 调用顺序:

    1. 先调用父类的initialize,然后调用子类的initialize(先初始化父类,再初始化子类,每个类只会初始化一次);

    2. 如果子类没有实现+initialize,会调用父类的+initialize

      当父类有多个子类,这些子类都没有实现+initialize方法时,最终会通过isa指针和superclass指针找到父类的+initialize方法,所以父类的+initialize可能会被调用多次,多次被调用不代表父类被初始化多次

    3. 如果分类实现了+initialize,会覆盖类本身的+initialize调用。

  • +initialize+load方法的区别:

    1. 调用方式不同: +initialize是通过objc_msgSend进行调用的;+load是通过函数指针直接调用的;
    2. 调用时机不同:+load是在runtime加载类、分类的时候调用,并且只会调用一次;+initialize是在类第一次接收到消息时调用,当前类没有实现+initialize方法时,父类的+initialize可能被调用多次。

Category添加成员的问题

  • Category为什么不能直接添加成员变量?

    已注册类的实例对象大小和内存布局都已经确定,如果添加实例变量就会破坏类的内部布局,这对编译型语言来说是灾难性的。比如:一个类只有注册过才能用来创建对象,假设一个已经注册过的类创建了对象A,然后我们又给这个类增加了一个实例变量,并用这个类又创建了对象B,那么A和B的存储结构都不一样,那么A和B还能算是同一类对象吗?所以从逻辑上讲,也不能允许添加实例变量。

  • Category为什么能添加方法呢?

    因为方法列表存放在类对象中,为类对象增加一个方法,所有该类的实例对象都会拥有这个方法,因此实例对象之间没有造成差异,还是同一个类型。

  • Category添加属性的本质

    在Category中添加属性后,相当于只是声明了属性变量的settergetter,没有实现settergetter,也没有属性对应的成员变量。

  • 添加成员变量的方案:

    • 使用全局的字典变量:使用字典是为了一个类有多个实例对象时防止混淆不同对象的成员变量值。
    • 使用关联对象

    使用全局字典示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    // 全局的字典
    #define ZZKey [NSString stringWithFormat:@"%p", self]

    @implementation ZZPerson (Test)

    NSMutableDictionary *names_;
    NSMutableDictionary *weights_;
    + (void)load
    {
    weights_ = [NSMutableDictionary dictionary];
    names_ = [NSMutableDictionary dictionary];
    }

    - (void)setName:(NSString *)name
    {
    // NSString *key = [NSString stringWithFormat:@"%p", self];
    names_[ZZKey] = name;
    }

    - (NSString *)name
    {
    // NSString *key = [NSString stringWithFormat:@"%p", self];
    return names_[ZZKey];
    }

    - (void)setWeight:(int)weight
    {
    // NSString *key = [NSString stringWithFormat:@"%p", self];
    weights_[ZZKey] = @(weight);
    }

    - (int)weight
    {
    // NSString *key = [NSString stringWithFormat:@"%p", self];
    return [weights_[ZZKey] intValue];
    }

Category和关联对象

Category中通常使用关联对象来添加属性/成员。

  1. 关联对象API:

    • 添加关联对象 void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociatedPolicy policy)
    • 获取关联对象 id objc_getAssociatedObject(id object, const void *key)
    • 移除所有关联对象 void objc_removeAssociatedObject(id object)
  2. 关联策略和对应的修饰符:

    objc_AssociatedPolicy 对应的修饰符
    OBJC_ASSOCIATION_ASSIGN assign
    OBJC_ASSOCIATION_RETAIN_NONATOMIC strong, nonatomic
    OBJC_ASSOCIATION_COPY_NONATOMIC copy, nonatomic
    OBJC_ASSOCIATION_RETAIN strong, atomic
    OBJC_ASSOCIATION_COPY copy, atomic
  3. 关联对象时key的常见用法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // 静态指针
    static void *MyKey = &MyKey;
    objc_setAssoiciatedObject(obj, MyKey, value, OBJC_ASSOCIATION_RETAIN_NONATOMIC)
    objc_getAssociatedObject(obj, MyKey)
    // 静态变量的地址值
    static char MyKey;
    objc_setAssoiciatedObject(obj, &MyKey, value, OBJC_ASSOCIATION_RETAIN_NONATOMIC)
    objc_getAssociatedObject(obj, &MyKey)

    // 使用属性名作为key
    objc_setAssoiciatedObject(obj, @"property", value, OBJC_ASSOCIATION_RETAIN_NONATOMIC)
    objc_getAssociatedObject(obj, @"property")

    // 使用get方法的@selector作为key
    objc_setAssoiciatedObject(obj, @selector(getter), value, OBJC_ASSOCIATION_RETAIN_NONATOMIC)
    objc_getAssociatedObject(obj, @selector(getter))
  4. 关联对象实现原理

    • 关联对象并不是存储在被关联对象自身的内存中

    • 关联对象存储在全局的一个AssociatesManager中,是一个哈希表结构