欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

iOS:Runtime之Category、Extension、load、initialize

程序员文章站 2022-05-30 17:31:40
...

大家好,我是OB!

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

一、category

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

先创建一个Person类和

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

从两个角度说:

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

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

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

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

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中的方法还没有添加到类对象,或者元类对象中。只有运行时,动态的添加到类对象或者元类对象;以下摘抄自源码。

可以看出,后编译的categorywhile中先取出来添加到类对象的方法列表中,由于后面还有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的顺序在这里

iOS:Runtime之Category、Extension、load、initialize

二、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(),而是直接调用loadIMP,所以不会出现只调用分类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方法

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