iOS:Runtime之Category、Extension、load、initialize

    技术2022-07-11  128

    大家好,我是OB!

    今天来聊聊Runtime和四剑客(Category、Extension、load、initialize)爱恨情仇!

    一、category

    1、category中的方法会覆盖主类的方法吗

    先创建一个Person类和

    先看现象 编译时我们可以发现,主类先开始compile,然后才是compile 分类。所以从两个角度说:

    a:宏观(现象)角度:分类会覆盖主类的方法!

    当分类和主类同时实现- (void)walk;方法时,调用结果是调用的分类里面的方法,不管是哪个分类,都优先与主类方法;

    b:微观(源码)角度:分类不会覆盖主类的方法!

    虽然优先调用分类的方法。但是主类的方法依然存在(分类方法和主类的方法都在一个metho_list中)。只不过调用优先级比分类的低,当向Person发送一个walk消息时,由于在分类中找到了该方法的实现,所以不再继续查找方法实现了,也就不会在调用主类里面的方法了,因此现象上是被覆盖。本质是分类方法通过runtime合并到了method_list的前面,分类优先调用,导致主类不会执行。

    2、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; };

    编译时,category中的方法还没有添加到类对象,或者元类对象中。只有运行时,动态的添加到类对象或者元类对象;以下摘抄自源码。

    可以看出,后编译的category在while中先取出来添加到类对象的方法列表中,由于后面还有method_t扩容,而且category的方法插入到主类方法列表前面。所以同样的方法,先响应category的方法,然后停止,自然不能响应主类中的方法,造成了被覆盖的假象。

    method_list_t **mlists = malloc(...) property_list_t **proplists = malloc(...) protocol_list_t **protolists = malloc(...) int i = category_t_s->count; while (i--) { auto& entry = category_t_s->list[i]; method_list_t *mlist = entry.cat->methodsForMeta(isMeta); mlists[mcount++] = mlist; fromBundle |= entry.hi->isBundle(); property_list_t *proplist = entry.cat->propertiesForMeta(isMeta, entry.hi); proplists[propcount++] = proplist; protocol_list_t *protolist = entry.cat->protocols; protolists[protocount++] = protolist; } //array()->lists 原始method list memmove(array()->lists + addedCount, array()->lists, oldCount * sizeof(array()->lists[0])); //addedLists category method list memcpy(array()->lists, addedLists, addedCount * sizeof(array()->lists[0]));

    对了,category的compile的顺序在这里

    所以:对于编译顺序,当父类和本类的load方法调用完成了,然后就是先编译的category的load方法先执行,后编译后执行; 但是对于普通方法,由于i-- ,所以普通category的方法调用顺序是相反的,就是先编译的category方法排在主列表后面,也就是后编译的方法,先执行

    3、category能添加属性吗?

    不能,可以用@property (nonatomic, copy)NSString *name;添加属性,但是它不会生成setName和name方法的实现,也不会生成 成员变量_name,

    为什么不能呢?

    因为对象的内存布局不能被破坏,我们通过objc_setAssociatedObject关联的对象,也不是生成了属性,而是重新开辟了内存,通过key关联到了当前对象。所以当我们objc_setAssociatedObject后,再通过runtime的Ivar * ivars = class_copyIvarList(person.class, &count);遍历属性也会发现,没有动态添加的关联对象。

    二、Extension

    这个不陌生,类的拓展在.m文件中添加,主要是私有化property,方法声明

    类的拓展在编译时就已经将其添加的property,**方法声明添加到主类的类对象或者元类对象**了

    //这就是 Person类的拓展 @interface Person() @end @implementation Person - (void)walk { NSLog(@"我在主类中:%s",__func__); } @end

    三、load

    load方法在运行时,由runtime调用,并且只要类存在在项目中,不管有没有用到,也不管有没有import,都会调用load。

    由于源码利用递归,所以先添加父类,在添加子类到数组里面,最后在while循环去加载load,

    所以总的顺序:先加载某个类的父类再加载子类,最后在加载分类。在循环加载。

    static void schedule_class_load(Class cls) { schedule_class_load(cls->superclass); //将类对象添加到数组, add_class_to_loadable_list(cls); } / do { while (dable_classes_used > 0) { call_class_loads(); } // 2. Call category +loads ONCE more_categories = call_category_loads(); } while (loadable_classes_used > 0 || more_categories);

    注意:runtime load方法调用时(如下代码),并不是利用消息发送函数 objc_msgSend(),而是直接调用load的IMP,所以不会出现只调用分类load方法,也不会覆盖主类的load,都要调用

    load_method_t load_method = (load_method_t)classes[i].method; //地址调用 (*load_method)(cls, SEL_load);

    四、initialize

    当一个类第一次收到消息时,就会调用类的initialize方法

    [Dog alloc];//就会调用Dog的 initialize方法,这时Animal的initialize也会调用

    1、如果父类还没有被调用:

    那么优先调用父类的initialize方法,在调用子类的initialize方法(如果子类有实现initialize)

    2、如果有分类,并且父类还没有被调用:

    那么会优先调用父类,在调用分类中的initialize方法。

    3、如果主类自己也没有重写initialize方法,分类中没有initialize方法:

    那么会调用父类的initialize方法,

    注意:这时可能父类会调用多次initialize方法,第一次调用initialize是父类第一次收到消息时调用。然后子类收到消息,但是子类没有实现initialize方法,根据消息机制,会调用父类的initialize方法,此时又会调用父类的initialize方法

    loadinitialize调用时机runtime 加载类,分类时调用类第一次收到消息时调用(父类可能会多次调用)调用方式通过函数地址直接调用消息机制objc_msgSend()调用调用顺序父类->子类->分类先初始化父类,此时调用一次父类的initialize,然后初始化子类,调用子类的 initialize(如果子类没有实现,那么又会回到父类的initialize)
    Processed: 0.010, SQL: 9