美文网首页
底层探索--Category 、类扩展的本质

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

作者: 永断阎罗 | 来源:发表于2021-10-13 14:41 被阅读0次

    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&copy
      OBJC_ASSOCIATION_RETAIN atomic&retain
      OBJC_ASSOCIATION_COPY atomic&copy

    使用方式

    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 、类扩展的本质

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