底层探索--Category 、类扩展的本质

Category

  • Category的本质:就是 _category_t结构体类型,以下就是它的定义:
    struct category_t {
        const char *name; //名字
        classref_t cls;
        struct method_list_t *instanceMethods; //对象方法列表结构体
        struct method_list_t *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;  //类属性列表结构体
    
        method_list_t *methodsForMeta(bool isMeta) {
            if (isMeta) return classMethods;
            else return instanceMethods;
        }
    
        property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
        
        protocol_list_t *protocolsForMeta(bool isMeta) {
            if (isMeta) return nullptr;
            else return protocols;
        }
    };
    
    特别注意:_category_t 结构体 中不包含 _ivar_list_t(经Clang编译证实,类的申明中是有『const struct _ivar_list_t *ivars;』) 类型,也就是不包含『成员变量结构体』。这就是为什么类别不能添加成员变量的根本原因。
  • 加载时机:是在运行时阶段动态(dyld 的动态链接器)加载的。

    • dyld 的动态链接器:用来加载所有的库和可执行文件。
    • 1、通过Runtime加载某个类的所有Category数据
    • 2、把所有Category的方法、属性、协议数据,合并到一个大数组中后面参与编译的Category数据,会在数组的前面
    • 3、通过memmove把原有类的移到最后,然后通过memcpy将合并后的分类数据(方法、属性、协议)放到初始位置。-->故类别的优先级高于原有类的方法属性协议等。
  • 添加属性: Category中虽然可以添加属性,但是不会生成对应的成员变量,也不能生成gettersetter方法。

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

+ load分析

  • 源码分析:
    //循环获取类
    static void schedule_class_load(Class cls)
    {
        if (!cls) return;
        ASSERT(cls->isRealized());  // _read_images should realize
    
        if (cls->data()->flags & RW_LOADED) return;
    
        // Ensure superclass-first ordering
        schedule_class_load(cls->superclass); //重点:递归调用此类的superclass,在递归回溯阶段,会将最顶层的superclass添加到数组的最前面,依次往下,直到自身-此类
    
        add_class_to_loadable_list(cls);
        cls->setInfo(RW_LOADED); 
    }
    
    void call_load_methods(void) { //代码片段
        //循环遍历类及其分类
        do {
            // 1. Repeatedly call class +loads until there aren't any more
            while (loadable_classes_used > 0) { //先循环遍历完所有类及其父类
                call_class_loads();  //执行后会直接把 loadable_classes_used = 0;
            }
    
            // 2. Call category +loads ONCE
            more_categories = call_category_loads(); //在遍历所有分类 
    
            // 3. Run more +loads if there are classes OR more untried categories
        } while (loadable_classes_used > 0  ||  more_categories);
    }
    
    //故能得出以下的结论
  • 整体结论:
    • 本类的+ load调用顺序先于分类的+ load
    • + load方法除非主动调用,否则只会调用一次。
    • 调用时机:+load方法在runtime加载类、分类的时候调用。
    • 如果子类没有实现+ load,则不会调用其父类的。
  • 先调用类的+ load
    • 按照编译先后顺序调用(先编译,先调用)
    • 调用子类的+ load之前会先调用父类的+ load
  • 再调用分类的+ load
    • 调用完主类,再调用分类,按照编译顺序,依次调用;
    • 注意:子类和父类的分类+ load调用顺序是按编译顺序决定的,所有使用时注意可能是:父类 -> 子类 -> 父类类别 -> 子类类别,也可能是 父类 -> 子类 -> 子类类别 -> 父类类别

+ initialize分析

  • 源码分析:
    //初始化部分源码
    void initializeNonMetaClass(Class cls)
    {
        ASSERT(!cls->isMetaClass());
    
        Class supercls;
        bool reallyInitialize = NO;
    
        // Make sure super is done initializing BEFORE beginning to initialize cls.
        // See note about deadlock above.
        supercls = cls->superclass; //获取类的superclass
        if (supercls  &&  !supercls->isInitialized()) { //如果存在superclass 且没有初始化,则递归调用-初始化函数,回溯阶段就会先调用父类的initialize,在调用子类的initialize
            initializeNonMetaClass(supercls); //递归方法本身
        }
        
        //调用
        callInitialize(cls);
     }
    
    //调用initialize的方法实现(objc_msgSend发送消息)
    void callInitialize(Class cls)
    {
        ((void(*)(Class, SEL))objc_msgSend)(cls, @selector(initialize));
        asm("");
    }
  • 整体结论:
    • + initialize方法会在类第一次接受到消息时调用;
    • 调用顺序:
      • 先调用父类的+ initialize,在调用子类的+ initialize
      • 如果已经初始化则不会再调用,每个类只会初始化一次。
    • 调用次数:
      • 如果子类没有实现+ initialize,会调用父类的+ initialize(所以父类的+ initialize可能会被调用多次)

属性绑定/关联对象

  • 运行时为一个已存在的类绑定成员变量,(使外部使用达到本身属性的效果)
  • 关联对象不是存在被关联的对象本身内存中;而是存储在全局的统一的一个AssociationsManager中(详情见下图)
    • AssociationsManager:全局管理维护关联属性
    • AssociationsHashMap
    • AssociationsMap
    • ObjectAssociation
  • 设置关联对象为nil,就相当于是移除关联对象
Category 、类扩展的本质之属性绑定.png

涉及方法

//注意以下三个方法的的key必须保持一致。
//赋值方法(类似于setter)
objc_setAssociatedObject(<#id  _Nonnull object#>, <#const void * _Nonnull key#>, <#id  _Nullable value#>, <#objc_AssociationPolicy policy#>);
//获值方法(类似于getter)
objc_getAssociatedObject(<#id  _Nonnull object#>, <#const void * _Nonnull key#>)
//移除方法(一般由系统自行调用,不会主动调用)
objc_removeAssociatedObjects(<#id  _Nonnull object#>)
  • objc_setAssociatedObject的参数policy说明:

    objc_AssociationPolicy 对应的修饰符
    OBJC_ASSOCIATION_ASSIGN assign
    OBJC_ASSOCIATION_RETAIN_NONATOMIC nonatomic&strong
    OBJC_ASSOCIATION_COPY_NONATOMIC nonatomic©
    OBJC_ASSOCIATION_RETAIN atomic&retain
    OBJC_ASSOCIATION_COPY atomic©

使用方式

  1. @select(getter) (最推荐)

     //例如:@selector(isOpenBlank), 而getter方法中,可用 _cmd 代替(因为实际会把隐藏把此参数传进来)
     objc_setAssociatedObject(self, @selector(isOpenBlank), @(isOpenBlank), OBJC_ASSOCIATION_ASSIGN);
     objc_getAssociatedObject(self, _cmd);
    
  2. 静态key--占用字节较少 (推荐)

     static const char isOpenBlank_key_sm;
     objc_setAssociatedObject(self, &isOpenBlank_key_sm, @(isOpenBlank), OBJC_ASSOCIATION_ASSIGN);
     objc_getAssociatedObject(self, &isOpenBlank_key_sm);
    
  3. 静态key-存自身地址(不推荐)

     //使用较麻烦,
     static const void *kname = &kname;
     objc_setAssociatedObject(self, &kname, @(isOpenBlank), OBJC_ASSOCIATION_ASSIGN);
     objc_getAssociatedObject(self, &kname);
    
  4. 直接字面量(不太推荐)

     //改的时候不太方便,需要改两处,当然可以用宏定义(稍显麻烦且没必要)
     objc_setAssociatedObject(self, @"name_k", @(isOpenBlank), OBJC_ASSOCIATION_ASSIGN);
     objc_getAssociatedObject(self, @"name_k");
    

static:表示,作用于仅限于当前的文件,既extern const void * kname;无法进行访问。

面试题

1、 Category和类扩展的区别

  1. Category扩展的(属性、方法、协议)等是在运行时动态的插入到对应类中
    类扩展在编译的时候,他的数据就已经包含在类信息中。
  2. Category无法扩展成员变量,类扩展可以。
  3. Category能实现方法,但类扩展只能申明。

2、 + load的子类、父类及其分类为什么都能在编译时调用?

根据底层源码(如下的源码)分析, + load方法不是通过消息机制调用的,而是通过函数指针找到其内存中的IMP来直接调用。

    struct loadable_class { //结构体:Load方法特用
        Class cls;  // may be nil
        IMP method;
    };
    Class cls = classes[i].cls;
    load_method_t load_method = (load_method_t)classes[i].method; //获取方法实现
    if (!cls) continue;  //如果为空则结束本次循环
    if (PrintLoading) {
        _objc_inform("LOAD: +[%s load]\n", cls->nameForLogging());
    }
    (*load_method)(cls, @selector(load)); //通过函数指针直接调用

3、 + load+ initialize的区别:

  1. 调用时机:+ load在运行时加载类和分类时调用,+ initialize在类第一次发送消息是调用
  2. 调用方式:+ load直接通过函数指针调用其实现,+ initialize则遵守消息机制objc_msgSend进行调用
  3. 类别中实现:+ load的子类、父类及其分类都会调用,而+ initialize则会覆盖本类的实现。
  4. 调用顺序:+ load先类(父类->子类)再分类,+ initialize也是先父类再子类,但如果分类中实现则会调用分类的。
1.调用方式:
    1> load是根据函数地址直接调用
    2> initialize是通过objc_msgSend调用

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

3.load、initialize的调用顺序:
   1.load
    1> 先调用类的load
    a) 先编译的类,优先调用load
    b) 调用子类的load之前,会先调用父类的load
    
    2> 再调用分类的load
    a) 先编译的分类,优先调用load
    
   2.initialize
    1> 先初始化父类
    2> 再初始化子类(可能最终调用的是父类的initialize方法)

你可能感兴趣的:(底层探索--Category 、类扩展的本质)