底层探索--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&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方法)
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,026评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,655评论 1 296
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,726评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,204评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,558评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,731评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,944评论 2 314
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,698评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,438评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,633评论 2 247
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,125评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,444评论 3 255
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,137评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,103评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,888评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,772评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,669评论 2 271

推荐阅读更多精彩内容