Category的本质<二>load,initialize方法

Category的本质<一>
Category的本质<三>关联对象
面试题1:Category中有load方法吗?load方法是什么时候调用?
面试题2:load,initialize的区别是什么?它们在Category中的调用顺序以及出现继承时它们之间的调用过程是怎么样的?
那么这篇文章主要就是回答这两个问题。

load方法

load方法什么时候调用?
load方法是在runtime加载类和分类的时候调用。
我们创建了一个Person类和它的两个分类,然后重写了各自的load方法:

//Person
+ (void)load{
    
    NSLog(@"Person + load");
}

//Person+Test1
+ (void)load{
    
    NSLog(@"Person (Test1) + load");
}

//Person+Test2
+ (void)load{
    
    NSLog(@"Person (Test2) + load");
}

然后我们什么也不做,运行代码,看到打印结果:

2018-07-24 20:45:08.369170+0800 interview - Category[14157:409819] Person + load
2018-07-24 20:45:08.371806+0800 interview - Category[14157:409819] Person (Test1) + load
2018-07-24 20:45:08.373190+0800 interview - Category[14157:409819] Person (Test2) + load

通过打印结果我们可以看到Person及其分类的load方法都被调用了,这就证实了load方法是由runtime加载类和分类的时候调用的。
然后我们再给Person类及其子类创建一个+ (void)test方法并实现它:

//Person
+ (void)test{
    
    NSLog(@"Person + test");
}

//Person+Test1
+ (void)test{
    
    NSLog(@"Person (Test1) + test");
}

//Person+Test2
+ (void)test{
    
    NSLog(@"Person (Test2) + test");
}

然后用Person类对象去调用test方法:

[Person test];

得到打印结果:

2018-07-24 21:07:32.886316+0800 interview - Category[14670:428685] Person + load
2018-07-24 21:07:32.887195+0800 interview - Category[14670:428685] Person (Test1) + load
2018-07-24 21:07:32.887461+0800 interview - Category[14670:428685] Person (Test2) + load
2018-07-24 21:07:33.050735+0800 interview - Category[14670:428685] Person (Test2) + test

通过打印结果我们可以看到,Person (Test2)的test方法被调用了,这个很好理解因为我们在Category的本质<一>中说的很清楚了,如果分类和类同时实现了一个方法,那么分类中的方法和类中的方法都会保存下来存入内存中,并且分类的方法在前,类的方法在后,这样在调用的时候就会首先找到分类的方法,给人的感觉就是好像类的方法被覆盖了。
那么问题来了,同样是类方法,同样是分类中实现了类的方法,为什么load方法不像test方法一样,调用分类的实现,而是类和每个分类中的load方法都被调用了呢?load方法到底有什么不同呢?
要想弄清楚其中的原理,我们还是要从runtime的源码入手:

  • 1.找到objc-os.mm这个文件,然后找到这个文件的void _objc_init(void)这个方法,runtime的初始化都是在这个方法里面完成。
  • 2.这个方法的最后一行调用了函数_dyld_objc_notify_register(&map_images, load_images, unmap_image);,我们点进load_images,这是加载模块的意思。
    8855F735-18CF-41EA-B389-DED9B00AB168.png
  • 3.
    19E2523B-9492-4EA6-8510-2323D855B001.png
  • 4
    BC355F3A-CE9D-4C72-9BDE-C804D6B8A0D9.png
  • 5我们点进call_class_loads();这个方法查看对类的load方法的调用过程:
    F892FCFD-7A59-4038-B5A0-BF892B13FD7E.png
  • 6.然后我们再点进call_category_loads()查看对分类的load方法的调用过程:
    AFACEDCA-1420-43D6-BB9A-0583A91B73A5.png

    那么这样我们就搞清楚了为什么load方法不是像test方法一样,执行分类的实现

因为load方法的调用并不是objc_msgSend机制,它是直接找到类的load方法的地址,然后调用类的load方法,然后再找到分类的load方法的地址,再去调用它。

而test方法是通过消息机制去调用的。首先找到类对象,由于test方法是类方法,存储在元类对象中,所以通过类对象的isa指针找到元类对象,然后在元类对象中寻找test方法,由于分类也实现了test方法,所以分类的test方法是在类的test方法的前面,首先找到了分类的test方法,然后去调用它。

有继承关系时load方法的调用顺序

通过上面的分析我们确定了load方法的一个调用规则:先调用所有类的load方法,然后再调用所有分类的load方法。

下面我们再创建一个Student类继承自Person类,并且为Student类创建两个子类Student (Test1), Student (Test2),并且覆写load方法:

//Student
+ (void)load{
    
    NSLog(@"Student + load");
}

//Student (Test1)
+ (void)load{
    
    NSLog(@"Student (Test1) + load");
}

//Student (Test2)
+ (void)load{
    
    NSLog(@"Student (Test2) + load");
}

然后我们运行一下程序,看打印结果:

2018-07-25 15:45:58.605156+0800 interview - Category[13869:359239] Person + load
2018-07-25 15:45:58.605684+0800 interview - Category[13869:359239] Student + load
2018-07-25 15:45:58.606420+0800 interview - Category[13869:359239] Student (Test2) + load
2018-07-25 15:45:58.606870+0800 interview - Category[13869:359239] Person (Test1) + load
2018-07-25 15:45:58.607293+0800 interview - Category[13869:359239] Student (Test1) + load
2018-07-25 15:45:58.607514+0800 interview - Category[13869:359239] Person (Test2) + load
2018-07-25 15:45:58.812025+0800 interview - Category[13869:359239] Person (Test2) + test

通过打印结果我们可以很清楚的看见,Person类和Student类的load方法先被调用,然后调用分类的load方法。再运行多次,都是Person类和Student类的load方法先被调用,然后分类的方法才被调用。并且总是Person类的load在Student类的load方法前面被调用,这会不会和编译顺序有关呢?我们改变一下编译顺序看看:

TARGETS -> Build Phases -> Complle Sources中文件的放置顺序就是文件的编译顺序。

381E32DF-9B1D-4752-AFFB-D3925E70579D.png

目前是Person类在Student类的前面编译,现在我们把Student类放到Person类的前面编译:


4A6F780D-FC13-48B4-9C7C-88ADB832D617.png

然后我们再运行一下程序,查看打印结果:

2018-07-25 15:56:07.270034+0800 interview - Category[14070:367686] Person + load
2018-07-25 15:56:07.270619+0800 interview - Category[14070:367686] Student + load
2018-07-25 15:56:07.271107+0800 interview - Category[14070:367686] Student (Test2) + load
2018-07-25 15:56:07.271494+0800 interview - Category[14070:367686] Person (Test1) + load
2018-07-25 15:56:07.271762+0800 interview - Category[14070:367686] Student (Test1) + load
2018-07-25 15:56:07.272118+0800 interview - Category[14070:367686] Person (Test2) + load
2018-07-25 15:56:07.433068+0800 interview - Category[14070:367686] Person (Test2) + test

我们发现还是Person类的load方法在Student类前面被调用,所以好像和编译顺序无关呀。那么我们就需要思考一下是不是由于Student和Person之间的继承关系导致的呢?
为了搞清楚这个问题,我们只能从runtime的源码入手。

  • 1.objc-os.mm中void _objc_init(void)这个入口方法,点进load_images.
  • 2.在void load_images(const char *path __unused, const struct mach_header *mh)这个方法中,最后有个call_load_methods();方法,点击进去。
  • 3.在void call_load_methods(void)这个方法中,找到call_class_loads();这个方法,上面已经讲到,这是调用类的load方法。点进去。
  • 4


    E6DFD1D9-2810-40D5-86E6-290482445E52.png
  • 5.为了搞清楚这里的classes数组的来历,我们回退到void load_images(const char *path __unused, const struct mach_header *mh)这个方法,这个方法中有一个prepare_load_methods((const headerType *)mh);这个方法,根据方法名可能和我们的问题有关。因此我们点进这个方法查看一下
  • 6.
    5167447D-DDDA-4398-ADFF-9DE78DDEBB3D.png
  • 7.点进
schedule_class_load(remapClass(classlist[i]));

这个方法:
571870F3-8F9C-4C24-82A6-3305ECE9B3ED.png

通过这个方法我们就可以很清晰的看到,当要把一个类加入最终的这个classes数组的时候,会先去上溯这个类的父类,先把父类加入这个数组。
由于在classes数组中父类永远在子类的前面,所以在加载类的load方法时一定是先加载父类的load方法,再加载子类的load方法。

类的load方法调用顺序搞清楚了我们再来看一下分类的load方法调用顺序

我们还是看一下void prepare_load_methods(const headerType *mhdr)这个函数

66AC18CB-9401-4FC4-8F5E-3FAD776954ED.png

通过这个分析我们就能知道,分类的load方法加载顺序很简单,就是谁先编译的,谁的load方法就被先加载。

下面我们通过打印结果验证一下,这是编译顺序:
C0E77171-09EC-415E-85B0-6FB874EEB8B4.png

按照我们前面的分析,load方法的调用顺序应该是:
Person -> Student -> Person + Test1 -> Student + Test2 -> Student + Test1 -> Person + Test2。
我们看一下打印结果:

2018-07-25 16:48:10.271679+0800 interview - Category[15094:408222] Person + load
2018-07-25 16:48:10.272357+0800 interview - Category[15094:408222] Student + load
2018-07-25 16:48:10.272661+0800 interview - Category[15094:408222] Person (Test1) + load
2018-07-25 16:48:10.272872+0800 interview - Category[15094:408222] Student (Test2) + load
2018-07-25 16:48:10.273103+0800 interview - Category[15094:408222] Student (Test1) + load
2018-07-25 16:48:10.273434+0800 interview - Category[15094:408222] Person (Test2) + load
2018-07-25 16:48:10.441457+0800 interview - Category[15094:408222] Person (Test2) + test

打印结果完美的验证了我们的结论。

总结 load方法调用顺序

1.先调用类的load方法
  • 按照编译先后顺序调用(先编译,先调用)
  • 调用子类的load方法之前会先调用父类的load方法
2.再调用分类的load方法
  • 按照编译先后顺序,先编译,先调用

initialize方法

initialize方法的调用时机
  • initialize在类第一次接收到消息时调用,也就是objc_msgSend()。
  • 先调用父类的+initialize,再调用子类的initialize。
    我们首先给Student类和Person类覆写+initialize方法:
//Person
+ (void)initialize{
    
    NSLog(@"Person + initialize");
}

//Person+Test1
+ (void)initialize{
    
    NSLog(@"Person (Test1) + initialize");
}

//Person+Test2
+ (void)initialize{
    
    NSLog(@"Person (Test2) + initialize");
}

//Student
+ (void)initialize{
    
    NSLog(@"Student + initialize");
}

//Student (Test1)
+ (void)initialize{
    
    NSLog(@"Student (Test1) + initialize");
}

//Student (Test2)
+ (void)initialize{
    
    NSLog(@"Student (Test2) + initialize");
}

我们运行程序,发现什么也没有打印,说明在运行期没有调用+initialize方法。
然后我们给Person类发送消息,也就是调用函数:

[Person alloc];

打印结果:

2018-07-25 17:26:22.462601+0800 interview - Category[15889:437305] Person (Test2) + initialize

可以看到调用了Person类的分类的initialize方法。通过这个打印结果我们能看出initialize方法和load方法的不同,load方法由于是直接获取方法的地址,然后调用方法,所以Person及其分类的load方法都会调用。而initialize方法则更像是通过消息机制,也即是objc_msgend(Person, @selector(initialize))这种来调用的。
然后我多次调用alloc方法:

[Person alloc];
[Person alloc];
[Person alloc];

打印结果:

018-07-25 17:26:22.462601+0800 interview - Category[15889:437305] Person (Test2) + initialize

可见initialize方法只在类第一次收到消息时调用。然后我们再给Student类发送消息:

[Student alloc];

打印结果:

2018-07-25 18:34:14.648279+0800 interview - Category[17187:473502] Person (Test2) + initialize
2018-07-25 18:34:14.648394+0800 interview - Category[17187:473502] Student (Test1) + initialize

我们看到不仅调用了Student类的initialize方法,而且还调用了Student类的父类,Person类的方法,因此我们猜测在调用类的initialize方法之前会先调用父类的initialize方法。
以上仅仅是我们根据打印结果的猜测,还需要通过源码来验证。
[Person alloc]就相当于objc_msgSend([Person class], @selector(alloc)),说明objc_msgSend()内部会去调用initialize方法,判断是第几次接收到消息。

  • 1.我们去runtime源码中搜索class_getClassmethod方法,会在objc-class.mm这个文件中找到这个方法的实现:
    2B3F2677-4F69-40A7-8E12-983A3ECCC2FE.png
  • 2.我们点进class_getInstanceMethod(cls->getMeta(), sel);这个方法:
    C04ACE66-648D-48A7-B6AB-8F687260D4E7.png
  • 3.点进这个方法:
    6D91C93E-4782-4044-8CDE-662AC2D599DE.png
  • 4.继续寻找lookUpImpOrForward这个方法的实现,我截取其中有价值的代码块:
    E221D122-E2BE-42BC-9C71-767B80358116.png
    这个代码也说明了每个类的+initialize方法只会被调用一次。
  • 5.我们点进_class_initialize (_class_getNonMetaClass(cls, inst));寻找真正的实现:
    FDE84505-3F22-4458-971D-64C0F0F20096.png
  • 6.然后我们通过callInitialize(cls);查看具体的调
    77228E87-0910-452F-B04D-6FA295164C07.png

    这样一来+initialize方法的调用过程就很清楚了。
+initialize的调用过程:
  • 1查看本类的initialize方法有没有实现过,如果已经实现过就返回,不再实现。
  • 2.如果本类没有实现过initialize方法,那么就去递归查看该类的父类有没有实现过initialize方法,如果没有实现就去实现,最后实现本类的initialize方法。并且initialize方法是通过objc_msgSend()实现的。
+initialize和+load的一个很大区别是,+initialize是通过objc_msgSend进行调用的,所以有以下特点:
  • 如果子类没有实现+initialize方法,会调用父类的+initialize(所以父类的+initialize方法可能会被调用多次)
  • 如果分类实现了+initialize,会覆盖类本身的+initialize调用。

下面我们把Student类及其分类中的+initialize这个方法的实现去掉,然后增加一个Teacher类继承自Person类。然后我们给Student类和Teacher类都发送alloc消息:

    [Student alloc];
    [Teacher alloc];

这个时候也就是只有Person类及其分类实现了+initialize方法。那么打印结果会是怎样呢?

2018-07-25 21:47:59.899995+0800 interview - Category[20981:582224] Person (Test2) + initialize
2018-07-25 21:47:59.900112+0800 interview - Category[20981:582224] Person (Test2) + initialize
2018-07-25 21:47:59.900240+0800 interview - Category[20981:582224] Person (Test2) + initialize

这里Person类的+initialize方法竟然被调用了三次,这多少有些出乎意外吧。下面我们来分析一下。

    BOOL studentInitialized = NO;
    BOOL personinitialized = NO;
    BOOL teacherInitialized = NO;
    
    [Student alloc];
    //判断Student类是否初始化了,这里Student类还没有被初始化,所以进入条件语句。
    if(!studentInitialized){
        //判断Student类的父类Person类是否初始化了
        if(!personinitialized){
            //这里Person类还没有初始化,就利用objc_msgSend调用initialize方法
            objc_msgSend([Person class], @selector(initialize));
            //变更Person类是否初始化的状态
            personinitialized = YES;
        }
        //利用objc_msgSend调用Student的initialize方法
        objc_msgSend([Student class], @selector(initialize));
        //变更Student是否初始化的状态
        studentInitialized = YES
    }
    
    [Teacher alloc];
    
    //判断Teacher类是否已经初始化了,这里Teacher类还没有初始化,进入条件语句
    if(!teacherInitialized){
        //判断其父类Person类是否初始化了,这里父类已经初始化了,所以不会进入这个条件语句
        if(!personinitialized){
            
            objc_msgSend([Person class], @selector(initialize));
            personinitialized = YES;
        }
        //利用objc_msgSend调用Teacher类的initialize方法
        objc_msgSend([Teacher class], @selector(initialize));
        //变更状态
        teacherInitialized = YES;
    }

上面列出来的是调用initialize的伪代码,下面再详细说明这个过程:

  • 1.Student类收到alloc消息,开始着手准备调用initialize方法。首先判断自己有没有初始化过。
  • 2.判断自己没有初始化过,所以就去找自己的父类Person类,看Person类有没有初始化过,发现Person类也没有初始化过,且Person类也没有父类,多以对Person类使用objc_msgSend([Person class], @selector(initialize))调用Person类的initialize方法。这是第一次调用Person类的initialize方法。
  • 3.父类处理完后,再通过objc_msgSend([Student class], @selector(initialize));调用Student类的initialize方法,但是由于Student类没有实现initialize方法,所以通过其superclass指针找到父类Person类,然后调用了Person类的initialize实现。这是第二次调用Person类的initialize方法。
  • 4.Teacher类收到alloc方法,开始准备调用initialize放啊发。首先判断自己有没有被初始化过。
  • 5.判断自己没有被初始化过后,又开始判断其父类Person类有没有被初始化过,刚刚父类Person类已经被初始化过。
  • 6.于是通过objc_msgSend([Teacher class], @selector(initialize))调用Teacher类的initialize方法。但是由于Teacher类没有实现initialize方法,所以只能通过superclass指针去查找父类有没有实现initialize方法,发现父类Person类实现了initialize方法,于是调用父类的initialize方法。这是第三次调用Person类的initialize方法。

推荐阅读更多精彩内容