iOS面试题-Category

1,077 阅读5分钟

Category

  • Category的信息需要在程序运行时,通过Runtime,动态加载到对应类中
  • 把所有Category的方法、属性、协议数据,合并到一个大数组中,后面参与编译的Category数据,会在数组的前面
  • 将合并后的分类数据(方法、属性、协议),插入到类原来数据的前面
  • Category的结构
struct category_t {
    const char *name;
    classref_t cls;
    WrappedPtr<method_list_t, PtrauthStrip> instanceMethods;
    WrappedPtr<method_list_t, PtrauthStrip> classMethods;
    struct protocol_list_t *protocols;
    struct property_list_t *instanceProperties;
    // Fields below this point are not always present on disk.
    struct property_list_t *_classProperties;

    // ...
};

Category的加载处理过程

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

+load方法

  • +load方法会在runtime加载,分类时调用
  • 每个,分类+load,在程序运行过程中只调用一次
  • 调用顺序
    • 先调用类的+load
      • 按照编译先后顺序调用(先编译,先调用)
      • 调用子类的+load之前会先调用父类的+load
    • 再调用分类的+load
      • 按照编译先后顺序调用(先编译,先调用)
  • objc4源码解读过程 image.png

+initialize方法

  • +initialize方法会在第一次接收消息时调用
  • 调用顺序
    • 先调用父类的+initialize,再调用子类的+initialize
    • (先初始化父类,再初始化子类,每个类只会初始化1次)
  • +initialize+load的很大区别是,+initialize是通过objc_msgSend进行调用的,所以有以下特点
    • 如果子类没有实现+initialize,会调用父类的+initialize(所以父类的+initialize可能会被调用多次)
    • 如果分类实现了+initialize,就覆盖类本身的+initialize调用
  • objc4源码解读过程 image.png

+load vs +initialize 异同点和使用场景

  • 区别点 | 区别 | +load | +initialize | |:--:|:--:|:--:| | 调用时机 | 在runtime加载类、分类时调用,在main函数之前调用(只会调用一次) | 在类第一次接收到消息时调用,惰性调用,每个类只会initialize一次(父类的initialize方法可能会被调用多次) | | 调用方式 | 根据方法地址调用 | objc_msgSend | | 调用顺序 | 1.父类 -> 类 -> 子类 -> 分类 2.按照编译先后顺序调用(先编译,先调用) | 1.分类 -> 父类 -> 类 -> 子类 2.如果子类没有实现+initialize,会调用父类的+initialize(所以父类的+initialize可能会被调用多次(只初始化化一次)) | | 方法是否会覆盖调用 | 不会 | 会 | | 调用次数 | 一次 | 可能多次(父类实现里被调用多次) |

  • 相同点

    • 1.loadinitialize会被自动调用,不能手动调用它们。
    • 2.子类实现了loadinitialize的话,会隐式调用父类的loadinitialize方法。
    • 3.loadinitialize方法内部使用了锁,因此它们是线程安全的。
  • 使用场景

    • +load一般是用来交换方法Method Swizzle,由于它是线程安全的,而且一定会调用且只会调用一次,通常在使用UrlRouter的时候注册类的时候也在+load方法中注册。
    • +initialize方法主要用来对一些不方便在编译期初始化的对象进行赋值,或者说对一些静态常量进行初始化操作。
  • 注意点

    • +initialize方法会被自动继承,所以,+initialize的出错率要比+load更大一些。
    • 如何避免
      • 判断类
      + (void)initialize{
          if (self == [MyClass class]) {
                ....
          }
      }
      
      • dispatch_once,一次性代码
      + (void)initialize {
          static dispatch_once_t onceToken;
          dispatch_once(&onceToken, ^{
                  ...
          });
      }
      
  • +load在main函数之前执行,如果+load函数里面任务过重,会影响应用的启动速度

Category关联对象

  • 默认情况下,因为分类底层结构的限制,不能添加成员变量到分类中。但可以通过关联对象来间接实现

关联对象提供了以下API

  • 添加关联对象
void 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)
  • 实际使用
  - (void)setName:(NSString *)name {
      objc_setAssociatedObject(self, @selector(name), name, OBJC_ASSOCIATION_COPY_NONATOMIC);
  }

  - (NSString *)name {
      // 隐式参数 _cmd == @selector(name)
      return objc_getAssociatedObject(self, _cmd);
  }

key的常见用法

static void *MyKey = &MyKey;
objc_setAssociatedObject(obj, MyKey, value, OBJC_ASSOCIATION_RETAIN_NONATOMIC)
objc_getAssociatedObject(obj, MyKey)

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))

objc_AssociationPolicy

objc_AssociationPolicy对应的修饰符
OBJC_ASSOCIATION_ASSIGNassign
OBJC_ASSOCIATION_RETAIN_NONATOMICstrong, nonatomic
OBJC_ASSOCIATION_COPY_NONATOMICcopy, nonatomic
OBJC_ASSOCIATION_RETAINstrong, atomic
OBJC_ASSOCIATION_COPYcopy, atomic

关联对象的原理

  • 实现关联对象技术的核心对象有

    • AssociationsManager
    • AssociationsHashMap
    • ObjectAssociationMap
    • ObjcAssociation
  • objc4源码解读:objc-references.mm
    image.png

  • 设置关联的关键函数

    • 关联对象并不是存储在被关联对象本身内存中
    • 关联对象存储在全局的统一的一个AssociationsManager
    • 设置关联对象为nil,就相当于是移除关联对象
    void objc_setAssociatedObject(id object, const void * key,
                                   id value, 
                                  objc_AssociationPolicy policy)
    

    image.png

面试题

    1. Category的使用场合是什么?
    1. Category的实现原理
    • Category编译之后的底层结构是struct category_t,里面存储着分类的对象方法、类方法、属性、协议信息
    • 在程序运行的时候,runtime会将Category的数据,合并到类信息中(类对象、元类对象中)
    1. Category和Class Extension的区别是什么?
    • Class Extension在编译的时候,它的数据就已经包含在类信息中
    • Category是在运行时,才会将数据合并到类信息中
    1. Category中有load方法吗?load方法是什么时候调用的?load 方法能继承吗?
    • 有load方法
    • load方法在runtime加载类、分类的时候调用
    • load方法可以继承,但是一般情况下不会主动去调用load方法,都是让系统自动调用
    1. Category能否添加成员变量?如果可以,如何给Category添加成员变量?
    • 不能直接Category添加成员变量
    • 可以间接实现,通过Runtime关联对象的方式
    1. 怎么保证在+load方法里交换方法总是在别人交换完之后执行的(交换方法的实现总是以我们交换的为准)