6

OC中load和initialize的区别

 3 years ago
source link: https://www.jianshu.com/p/d728e8c20d7c
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
0.1382020.01.22 10:25:09字数 2,603阅读 175

OC文件在编译后,类相关的数据结构会保留在目标文件中,在运行时得到解析和使用。在应用程序运行起来的时候,类的信息会有加载和初始化过程,这个过程就涉及到了类的两个类方法:loadinitialize。下面我们就来介绍一下这2个方法的区别。(首先要说明一下,这2个方法是系统调用的,开发者一般不会主动去调用者两个方法,这么做也没有什么意义,所以后面的讲解都是针对系统调用,不考虑主动调用的情况)。

1. load方法

1.1 调用时机

当我们启动程序时,参与了编译的类、分类都会被加载进内存,load方法就是在这个类被加载的时候调用的(前提是这个类有实现load方法),这个过程与这个类是否被使用是无关的,也就是说如果有一个类(MyClass)即使在整个程序中都没有用到,甚至没有任何一个文件去引入MyClass的头文件,MyClass的的load的方法一样会被调用。等所有的类、分类都加载进内存后才会调用程序的main函数,所以所有类的load方法都是在main函数之前被调用的。而且每个类、分类的load方法只会被调用一次。

1.2 调用顺序

一个程序中如果所有的类、分类都实现了load方法,那么所有的load方法都会被调用。它们的执行顺序遵循以下规则:

  • 先执行所有类的load方法,再执行所有分类的load方法。
  • 执行类的load方法时,是按照参与编译的顺序,先编译的类先执行,但是如果某个类是继承自另一个类,那么会先执行父类的load方法个再执行自己的load方法。
  • 执行分类的load方法时,是按照分类参与编译的顺序,先编译的分类先执行。

关于编译顺序,我们可以在项目的Build Phases --> Compile Sources查看,最上面的就最先编译,我们可以拖动文件来调整编译顺序。下面举个例子来看下load方法的执行顺序。首先说明一下几个类的关系:Person类有aaabbb两个分类,men类继承自Person类,men也有2个分类cccdddBook类和前面这些类没有任何关系。

image
  • 编译顺序从上到下,上面先编译,下面后编译。由于先执行类的load再执行分类的load,最先参与编译的类是men,而men继承自Person,所以最先执行Personload(虽然Person是后参与编译的,但是它是父类,所以会先执行),然后再执行menload。接着参与编译的是Book类,所以紧接着就是执行Bookload。再接着参与编译的类就是Person,由于它的load方法已经执行过了,此时就不会执行了。
  • 所有的类的load方法都执行完后开始执行分类的load,分类参与编译的顺序是men+ccc-->Person+aaa-->men+ddd-->Person+bbb,所以分类的load方法个也是按照这个顺序执行。

1.3 执行方式

我们知道,当分类中存在和本类中同名的方法时,调用这个方法最终执行的是分类中的方法。那上面就很奇怪了,PersonPerson的分类中都有load方法,按理说调用load方法时最终只会调用其中一个分类的load方法,可结果Person本类和它的2个分类都调用了load方法。这是因为load方法和普通方法调用的方式不一样。普通方法调用是通过消息发送机制实现的,会先去类或元类的方法列表中查找,如果找到了方法就执行,如果没有找到就去父类的方法列表里面找,只要找到就会终止查找,所以只会执行一次。而load方法调用时,每个类都是根据load方法的地址直接调用,而不会走objc_msgSend函数的方法查找流程,也就是说一个类有实现load方法就执行,没有就不执行(没有的话也不会去父类里面查找)。想要了解更加详细的底层实现流程,可以去看objc4源码,https://opensource.apple.com/tarballs/objc4/这里提供一下相关函数调用流程以便进行源码阅读: 首先从objc-os.mm文件的_objc_init函数开始-->load_images-->prepare_load_methods-->schedule_class_load-->add_class_to_loadable_list-->add_category_to_loadable_list-->call_load_methods-->call_class_loads-->call_category_loads-->(*load_method)(cls, SEL_load)

1.4 实现load方法时要注意什么

我们通常在load方法中进行方法交换(Method Swizzle),除此之外,除非真的有必要,我们尽量不要在load方法中写代码,尤其不要在load方法中使用其它的类,因为这个时候其它的类可能还没有被加载进内存,随意使用可能会出问题。如果确实要在load方法写一些代码,那也要尽量精简代码,不要做一些耗时或者等待锁的操作,因为整个程序在执行load方法时都会阻塞,从而导致程序启动时间过长甚至无法启动。

2. initialize方法

2.1 调用时机

initialize方法是在类或它的子类收到第一条消息时被调用的,这里的消息就是指实例方法或类方法的调用,所以所有类的initialize调用是在执行main函数之后调用的。而且一个类只会调用一次initialize方法。如果一个类在程序运行过程中一直没有被使用过,那这个类的initialize方法也就不会被调用,这一点和load方法是不一样的。

2.2 调用方式

initialize方法的调用和普通方法调用一样,也是走的objc_msgSend流程。所以如果一个类和它的分类都实现了initialize方法,那最终调用的会是分类中的方法。如果子类和父类都实现了initialize方法,那么会先调用父类的方法,然后调用子类的方法个(这里注意子类中不需要写[super initialize]来调用父类的方法,通过查看源码得知它是在底层实现过程中主动调用的父类的initialize方法)。下面看一个例子:父类Person实现了initializePersonSub1PersonSub2这两个子类也实现了initializePersonSub3PersonSub4这两个子类没有实现了initialize,按照下面的顺序实例化对象:

PersonSub1 *ps1 = [[PersonSub1 alloc] init];
Person *person = [[Person alloc] init];
PersonSub2 *ps2 = [[PersonSub2 alloc] init];
PersonSub3 *ps3 = [[PersonSub3 alloc] init];
PersonSub4 *ps4 = [[PersonSub4 alloc] init];

// ***************打印结果***************
2020-01-06 15:52:38.429218+0800 CommandLine[68706:7207027] +[Person initialize]
2020-01-06 15:52:38.429250+0800 CommandLine[68706:7207027] +[PersonSub1 initialize]
2020-01-06 15:52:38.429287+0800 CommandLine[68706:7207027] +[PersonSub2 initialize]
2020-01-06 15:52:38.429347+0800 CommandLine[68706:7207027] +[Person initialize]
2020-01-06 15:52:38.429380+0800 CommandLine[68706:7207027] +[Person initialize]

看到这个运行结果,有人就有疑问了:不是说一个类只会调用一次initialize方法吗,为什么这里Personinitialize方法被调用了3次?这里就需要讲解一下底层源码的执行流程了,每个类都有一个标记记录这个类是否调用过initialize,我这里就用一个BOOL类型的isInitialized来表示,然后用selfClass来表示自己的类,用superClass来表示父类,下面我用伪代码来描述一下底层源码执行流程:

// 如果自己没有调用过initialize就执行里面的代码
if(!selfClass.isInitialized){
    if(!superClass.isInitialized){
 // 如果父类没有执行过initialize就给父类发消息(一旦成功执行initialize就将父类的isInitialized置为YES)
 objc_msgSend(superClass,@selector(initialize));
 }
 // 再给自己的类发消息(一旦成功执行initialize就将自己类的isInitialized置为YES)
 objc_msgSend(selfClass,@selector(initialize));
}

接下来我来按照这个流程来解释一下上面运行的结果:

  • 首先PersonSub1被使用,而此时PersonSub1isInitialized为NO,而且父类PersonisInitialized也为NO,所以先给父类发消息执行initialize,执行完后PersonisInitialized变为YES。然后PersonSub1执行自己的initialize,执行完后PersonSub1isInitialized变为YES。所以这一步先打印+[Person initialize],然后打印+[PersonSub1 initialize]
  • 然后是Person实例化,此时PersonisInitialized为YES,所以不会再调用initialize。所以这一步什么都没打印。
  • 接着是PersonSub2实例化,此时PersonSub2isInitialized为NO,父类PersonisInitialized为YES,所以只有PersonSub2会执行initialize,执行完后PersonSub2isInitialized变为YES。所以这一步打印的是+[PersonSub2 initialize]
  • 再接着是PersonSub3实例化,此时PersonSub3isInitialized为NO,父类PersonisInitialized为YES,所以只有PersonSub3会执行initialize,但是由于PersonSub3没有实现initialize,它就会去父类找这个方法的实现,找到后就执行父类Personinitialize(注意这里是PersonSub3执行的Person中的initialize,而不是Person执行的),执行完后PersonSub3isInitialized变为YES。所以这一步打印的是+[Person initialize]。(注意这里打印的是方法信息,表示执行的是Person中的initialize,而不是说是Person调用的initialize)。
  • 最后是PersonSub4实例化,这一步过程和上面一步是一样的,执行完后PersonSub4isInitialized变为YES。这一步打印的是+[Person initialize]

所以最后的结果就是PersonPersonSub1PersonSub2PersonSub3PersonSub4这5个类都执行了一次initialize,虽然从运行结果来看Personinitialize执行了3次,其实后面2次是PersonSub3PersonSub4调用的。

2.3 使用注意事项

虽然使用initialize要比使用load安全(因为在调用initialize时所有类已经被加载进内存了),但我们还是要尽量少用initialize这个方法个,尤其要谨慎在分类中实现initialize方法,因为如果在分类中实现了,本类实现的initialize方法将不会被调用。实际开发中initialize方法一般用于初始化全局变量或静态变量。

转自:https://juejin.im/post/5e130ba05188253a5d560155


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK