美文网首页
iOS 类方法load和initialize详解

iOS 类方法load和initialize详解

作者: 恋空K | 来源:发表于2019-08-23 17:51 被阅读0次

    iOS开发中总能看到+load和+initialize的身影,网上对于这两个方法有很多解释,官方也有说明,但有些细节不够清楚,今天我们来详细扒一扒这两个方法.

    load函数调用特点如下:

    当类被引用进项目的时候就会执行load函数(在main函数开始执行之前),与这个类是否被用到无关,每个类的load函数只会自动调用一次.由于load函数是系统自动加载的,因此不需要调用父类的load函数,否则父类的load函数会多次执行。

    1.当父类和子类都实现load函数时,父类的load方法执行顺序要优先于子类

    2.当子类未实现load方法时,不会调用父类load方法

    3.类中的load方法执行顺序要优先于类别(Category)

    4.当有多个类别(Category)都实现了load方法,这几个load方法都会执行,但执行顺序不确定(其执行顺序与类别在Compile Sources中出现的顺序一致)

    5.当然当有多个不同的类的时候,每个类load 执行顺序与其在Compile Sources出现的顺序一致

    下面通过实例来一起验证下:

    我们新建2个类:Person继承NSObject,Student和Teacher均继承Person

    Person : NSObject

    Student : Person

    Teacher : Person

    新建3个Person分类:

    Person (Category)

    Person (Category2)

    Person (Category3)

    在Person,Student,Person (Category),Person (Category2),Person (Category3)的.m文件中实现下面方法(Teacher除外)

    //In Person.m

    +(void)load

    {

        NSLog(@"%s",__FUNCTION__);

    }

    //In Student.m(继承自Person)

    +(void)load

    {

        NSLog(@"%s",__FUNCTION__);

    }

    //In Teacher.m(继承Person)

    不实现load方法

    //In Person+Category.m

    +(void)load

    {

        NSLog(@"%s",__FUNCTION__);

    }

    //In Person+Category2.m

    +(void)load

    {

        NSLog(@"%s",__FUNCTION__);

    }

    //In Person+Category3.m

    +(void)load

    {

        NSLog(@"%s",__FUNCTION__);

    }

    运行结果:

    2017-05-04 11:11:40.612 LoadAndInitializeExample[27745:856244] +[Person load]

    2017-05-04 11:11:40.615 LoadAndInitializeExample[27745:856244] +[Student load]

    2017-05-04 11:11:40.616 LoadAndInitializeExample[27745:856244] +[Person(Category3) load]

    2017-05-04 11:11:40.617 LoadAndInitializeExample[27745:856244] +[Person(Category) load]

    2017-05-04 11:11:40.617 LoadAndInitializeExample[27745:856244] +[Person(Category2) load]

    可以看到执行顺序依次为:

    1.首先执行的是父类Person load方法,再执行的是子类Student load方法,说明父类的load方法执行顺序要优先于子类

    2.子类Teacher中没有实现load方法,没有打印,说明子类没有实现load方法时并不会调用父类load方法

    3.最后执行的是Person 3个Category的load方法,并且没有顺序,说明类别(Category)中的load方法要晚于类,多个类别(Category)都实现了load方法,这几个load方法都会执行,但执行顺序不确定

    4.同时我们也可以看到这几个Category load 执行顺序与其在Compile Sources中出现的顺序一致

    5.当然多个不同的类 其load执行顺序,也与其在Compile Sources(编译源文件)出现的顺序一致

    initialize(初始化)函数调用特点如下:‘

    initialize在类或者其子类的第一个方法被调用前调用。即使类文件被引用进项目,但是没有使用,initialize不会被调用。由于是系统自动调用,也不需要再调用 [super initialize] ,否则父类的initialize会被多次执行。假如这个类放到代码中,而这段代码并没有被执行,这个函数是不会被执行的。

    1.父类的initialize方法会比子类先执行

    2.当子类未实现initialize方法时,会调用父类initialize方法,子类实现initialize方法时,会覆盖父类initialize方法.

    3.当有多个Category都实现了initialize方法,会覆盖类中的方法,只执行一个(会执行Compile Sources 列表中最后一个Category 的initialize方法)

    我们同样通过实例来一起验证下:

    我们添加Person类,在.m中实现+ initialize方法

    //In Person.m

    +(void)initialize

    {

        NSLog(@"%s",__FUNCTION__);

    }

    运行结果:

    无打印...

    啥也没打印

    说明:只是把类文件被引用进项目,没有使用的话,initialize不会被调用

    我们在Teacher(继承Person).m中不实现initialize方法

    //In Person.m

    +(void)initialize

    {

        NSLog(@"%s",__FUNCTION__);

    }

    //In Teacher.m(继承自Person)

    Teacher.m中不实现initialize方法

    我们调用下Teacher的new方法,运行

    [Teacher new];

    运行结果:

    2017-05-04 14:06:45.051 LoadAndInitializeExample[29238:912579] +[Person initialize]

    2017-05-04 14:06:45.051 LoadAndInitializeExample[29238:912579] +[Person initialize]

    运行后发现父类的initialize方法竟然调用了两次:

    可见当子类未实现initialize方法,会调用父类initialize方法.

    但为什么会打印2次呢?

    我的理解:

    子类不实现initialize方法,会把继承父类的initialize方法并调用一遍。在此之前,父类初始化时,会先调用一遍自己initialize方法.所以出现2遍,所以为了防止父类initialize中代码多次执行,我们应该这样写:

    //In Person.m

    +(void)initialize

    {

    if(self == [Person class])

    {

              NSLog(@"%s",__FUNCTION__);

    }

    }

    下面我们在 Teacher.m中实现initialize方法

    //In Person.m

    +(void)initialize

    {

        NSLog(@"%s",__FUNCTION__);

    }

    //In Teacher.m(继承自Person)

    +(void)initialize

    {

         NSLog(@"%s",__FUNCTION__);

    }

    同样调用Teacher 的new方法,运行

    [Teacher new];

    运行结果:

    2017-05-04 14:07:45.051 LoadAndInitializeExample[29238:912579] +[Person initialize]

    2017-05-04 14:07:45.051 LoadAndInitializeExample[29238:912579] +[Teacher initialize]

    可以看到 当子类实现initialize方法后,会覆盖父类initialize方法,这一点和继承思想一样

    我们在Person.m和Person+Category.m中实现initialize方法

    //In Person.m

    +(void)initialize

    {

        NSLog(@"%s",__FUNCTION__);

    }

    //In Person+Category.m

    +(void)initialize

    {

        NSLog(@"%s",__FUNCTION__);

    }

    调用Person 的 new方法,运行

    [Person new];

    运行结果:

    2017-05-05 09:46:51.054 LoadAndInitializeExample[38773:1226306] +[Person(Category) initialize]

    运行后可以看到Person的initialize方法并没有被执行,已经被Person+Category中的initialize取代了

    当有多个Category时会怎样了,我们在Person,Person+Category,Person+Category2,Person+Category3 的.m中都实现initialize方法

    //In Person.m

    +(void)initialize

    {

        NSLog(@"%s",__FUNCTION__);

    }

    //In Person+Category.m

    +(void)initialize

    {

        NSLog(@"%s",__FUNCTION__);

    }

    //In Person+Category2.m

    +(void)initialize

    {

        NSLog(@"%s",__FUNCTION__);

    }

    //In Person+Category3.m

    +(void)initialize

    {

        NSLog(@"%s",__FUNCTION__);

    }

    调用Person new方法,运行

    [Person new];

    运行结果:

    2017-05-05 09:49:38.853 LoadAndInitializeExample[38825:1227819] +[Person(Category2) initialize]

    可以看到,当存在多个Category时,也只执行一个,具体执行哪一个Category中的initialize方法,测试后便可发现,会执行Compile Sources 列表中最后一个Category 的initialize方法

    什么情况下使用:

    ####+load

    由于调用load方法时的环境很不安全,我们应该尽量减少load方法的逻辑。另一个原因是load方法是线程安全的,它内部使用了锁,所以我们应该避免线程阻塞在load方法中

    load很常见的一个使用场景,交换两个方法的实现

    //摘自MJRefresh

    + (void)load

    {

        [self exchangeInstanceMethod1:@selector(reloadData) method2:@selector(mj_reloadData)];

        [self exchangeInstanceMethod1:@selector(reloadRowsAtIndexPaths:withRowAnimation:) method2:@selector(mj_reloadRowsAtIndexPaths:withRowAnimation:)];

        [self exchangeInstanceMethod1:@selector(deleteRowsAtIndexPaths:withRowAnimation:) method2:@selector(mj_deleteRowsAtIndexPaths:withRowAnimation:)];

        [self exchangeInstanceMethod1:@selector(insertRowsAtIndexPaths:withRowAnimation:) method2:@selector(mj_insertRowsAtIndexPaths:withRowAnimation:)];

        [self exchangeInstanceMethod1:@selector(reloadSections:withRowAnimation:) method2:@selector(mj_reloadSections:withRowAnimation:)];

        [self exchangeInstanceMethod1:@selector(deleteSections:withRowAnimation:) method2:@selector(mj_deleteSections:withRowAnimation:)];

        [self exchangeInstanceMethod1:@selector(insertSections:withRowAnimation:) method2:@selector(mj_insertSections:withRowAnimation:)];

    }

    + (void)exchangeInstanceMethod1:(SEL)method1 method2:(SEL)method2

    {

        method_exchangeImplementations(class_getInstanceMethod(self, method1), class_getInstanceMethod(self, method2));

    }

    ####+initialize initialize方法主要用来对一些不方便在编译期初始化的对象进行赋值。比如NSMutableArray这种类型的实例化依赖于runtime的消息发送,所以显然无法在编译器初始化:

    // In Person.m

    // int类型可以在编译期赋值

    static int someNumber = 0;

    static NSMutableArray *someArray;

    + (void)initialize {

        if (self == [Person class]) {

            // 不方便编译期复制的对象在这里赋值

            someArray = [[NSMutableArray alloc] init];

        }

    }

    总结:

    load和initialize的共同点

    1.如果父类和子类都被调用,父类的调用一定在子类之前

    +load方法要点

    当类被引用进项目的时候就会执行load函数(在main函数开始执行之前),与这个类是否被用到无关,每个类的load函数只会自动调用一次.由于load函数是系统自动加载的,因此不需要再调用[super load],否则父类的load函数会多次执行。

    1.当父类和子类都实现load函数时,父类的load方法执行顺序要优先于子类

    2.当一个类未实现load方法时,不会调用父类load方法

    3.类中的load方法执行顺序要优先于类别(Category)

    4.当有多个类别(Category)都实现了load方法,这几个load方法都会执行,但执行顺序不确定(其执行顺序与类别在Compile Sources中出现的顺序一致)

    5.当然当有多个不同的类的时候,每个类load 执行顺序与其在Compile Sources出现的顺序一致

    注意:

    load调用时机比较早,当load调用时,其他类可能还没加载完成,运行环境不安全.

    load方法是线程安全的,它使用了锁,我们应该避免线程阻塞在load方法.

    +initialize方法要点

    initialize在类或者其子类的第一个方法被调用前调用。即使类文件被引用进项目,但是没有使用,initialize不会被调用。由于是系统自动调用,也不需要显式的调用父类的initialize,否则父类的initialize会被多次执行。假如这个类放到代码中,而这段代码并没有被执行,这个函数是不会被执行的。

    1.父类的initialize方法会比子类先执行

    2.当子类不实现initialize方法,会把父类的实现继承过来调用一遍。在此之前,父类的方法会被优先调用一次

    3.当有多个Category都实现了initialize方法,会覆盖类中的方法,只执行一个(会执行Compile Sources 列表中最后一个Category 的initialize方法)

    注意:

    在initialize方法收到调用时,运行环境基本健全。

    initialize内部也使用了锁,所以是线程安全的。但同时要避免阻塞线程,不要再使用锁

    load和initialize的区别:

    +load 方法。注意,这里是(调用分类的 +load 方法也是如此)直接使用函数内存地址的方式 (*load_method)(cls, SEL_load); 对 +load 方法进行调用的,而不是使用发送消息 objc_msgSend 的方式。

    这样的调用方式就使得 +load 方法拥有了一个非常有趣的特性,那就是子类、父类和分类中的 +load 方法的实现是被区别对待的。也就是说如果子类没有实现 +load 方法,那么当它被加载时 runtime 是不会去调用父类的 +load 方法的。同理,当一个类和它的分类都实现了 +load 方法时,两个方法都会被调用。因此,我们常常可以利用这个特性做一些“邪恶”的事情,比如说方法混淆(Method Swizzling)。

    +initialize 方法的本质),runtime 使用了发送消息 objc_msgSend的方式对 +initialize 方法进行调用。也就是说 +initialize 方法的调用与普通方法的调用是一样的,走的都是发送消息的流程。换言之,如果子类没有实现 +initialize 方法,那么继承自父类的实现会被调用;如果一个类的分类实现了 +initialize 方法,那么就会对这个类中的实现造成覆盖。因此,如果一个子类没有实现 +initialize 方法,那么父类的实现是会被执行多次的。有时候,这可能是你想要的;但如果我们想确保自己的 +initialize 方法只执行一次,避免多次执行可能带来的副作用时,我们可以使用下面的代码来实现:

    + (void)initialize {

    if (self == [ClassName self]) {

    // ... do the initialization ...

    }

    }

    总结

    通过阅读 runtime 的源码,我们知道了 +load 和 +initialize 方法实现的细节,明白了它们的调用机制和各自的特点。下面我们绘制一张表格,以更加直观的方式来巩固我们对它们的理解:

                                +load                                            +initialize

    调用时机             被添加到 runtime 时                    收到第一条消息前,可能永远不调用

    调用顺序              父类->子类->分类                       父类->子类

    调用次数              1次                                           多次

    是否需要显式调用父类实现       否              否

    是否沿用父类的实现                 否               是

    分类中的实现                     类和分类都执行            覆盖类中的方法,只执行分类的实现

    值得注意的是(*load_method)(cls, SEL_load),load方法是直接使用函数指针调用,也就是走C语言函数调用的流程,不是发送消息,并不会走消息转发的流程,也就是说,如果一个类实现了load函数就会调用,如果没有实现也不会调用该类的父类load函数实现,如果父类实现了load函数的话。category调用load方法也是一样的道理。

    就是发送消息,这是和load函数实现不一样的地方,load函数的调用直接是函数指针的调用,而initialize函数是消息的转发。所以,class的子类就算没有实现initialize函数,也会调用父类的initialize函数,如果子类实现了initialize函数,则子类不会调用父类的initialize函数

    总结

    通过分别对load和initialize源代码的实现细节,我们知道了它们各自的特点,总的如下:

    1.load在被添加到runtime的时候加载,initialize是类第一次收到消息的时候被加载,load是在main函数之前,initialize是在main函数之后。

    2.load方法的调用顺序是:superClass -> class -> category;initialize方法的调用顺序是:superClass -> class。都不需要显示调用父类的方法,系统会自动调用,load方法是函数指针调用,initialize是发送消息。子类如果没有实现load函数,子类是不会调用父类的load函数的,但是子类没有实现initialize函数,则会调用父类的initialize函数。

    3.load和initialize内部实现都加了线程锁,是线程安全的,因此,这两个函数应该做一些简单的工作,不适合复杂的工作。

    4.load函数通常用来进行Method Swizzle,initialize函数则通常初始化一些全局变量,静态变量。

    相关文章

      网友评论

          本文标题:iOS 类方法load和initialize详解

          本文链接:https://www.haomeiwen.com/subject/afytectx.html