(load和initialize)不要被你的log迷惑了你对问题的判断

本章研究的是+(void)load和+(void)initialize 的执行情况。

研究的是基类,派生类以及类目之间的细微关系

先把研究代码贴上再来看结果(请复制这两个文件,新建一个工程来验证)

头文件

#import <Foundation/Foundation.h>

//--------------------原类---------------------
@interface Person : NSObject //基类

@end

@interface Man : Person //派生类

@end

//--------------------第一个类目类---------------------
@interface Person(ctg) //基类的类目(1)

@end

@interface Man(ctg) //派生类的类目(1)

@end

//--------------------第2个类目类---------------------
@interface Person(ctgsecond) //基类的类目(2)

@end

@interface Man(ctgsecond) //派生类的类目(2)

@end

.m

@implementation Person

+ (void)load
{
    NSLog(@"基类 : %@ [--load--] call.",[self class]);
}

+ (void)initialize
{
    NSLog(@"基类 : %@ [--initialize--] call.",[self class]);
}

@end

@implementation Man

+ (void)load
{
    NSLog(@"派生类 : %@ [--load--] call.",[self class]);
}

+ (void)initialize
{
    NSLog(@"派生类 : %@ [--initialize--] call.",[self class]);
}

@end

@implementation Person(ctg)

+ (void)load
{
    NSLog(@"基类类目(1) : %@ ctg [--load--] call.",[self class]);
}

+ (void)initialize
{
    NSLog(@"基类类目(1) : %@ ctg [--initialize--] call.",[self class]);
}

@end

@implementation Man(ctg)

+ (void)load
{
    NSLog(@"派生类类目(1) : %@ ctg [--load--] call.",[self class]);
}

+ (void)initialize
{
    NSLog(@"派生类类目(1) : %@ ctg [--initialize--] call.",[self class]);
}

@end

@implementation Person(ctgsecond)

+ (void)load
{
    NSLog(@"基类类目(2) : %@ ctgsecond [--load--] call.",[self class]);
}

+ (void)initialize
{
    NSLog(@"基类类目(2) : %@ ctgsecond [--initialize--] call.",[self class]);
}

@end

@implementation Man(ctgsecond)

+ (void)load
{
    NSLog(@"派生类类目(2) : %@ ctgsecond [--load--] call.",[self class]);
}

+ (void)initialize
{
    NSLog(@"派生类类目(2) : %@ ctgsecond [--initialize--] call.",[self class]);
}

@end
1.只引用头文件,没有任何调用的情况下,输出结果:
2018-04-19 11:34:42.451202+0800 jjjjjjj[11230:10714475] 基类类目(2) : Person ctgsecond [--initialize--] call.
2018-04-19 11:34:42.451859+0800 jjjjjjj[11230:10714475] 基类 : Person [--load--] call.
2018-04-19 11:34:42.452093+0800 jjjjjjj[11230:10714475] 派生类类目(2) : Man ctgsecond [--initialize--] call.
2018-04-19 11:34:42.452206+0800 jjjjjjj[11230:10714475] 派生类 : Man [--load--] call.
2018-04-19 11:34:42.452313+0800 jjjjjjj[11230:10714475] 基类类目(1) : Person ctg [--load--] call.
2018-04-19 11:34:42.452432+0800 jjjjjjj[11230:10714475] 派生类类目(1) : Man ctg [--load--] call.
2018-04-19 11:34:42.452538+0800 jjjjjjj[11230:10714475] 基类类目(2) : Person ctgsecond [--load--] call.
2018-04-19 11:34:42.452635+0800 jjjjjjj[11230:10714475] 派生类类目(2) : Man ctgsecond [--load--] call.

看到这个,先不要往后看,自己总结一下,加深理解。并想想为什么?


从日志中,我们暂时得出了一个结论:

1.基类,原生类,类目都调用了 load.

2.initialize 只调用了类目2的。也就是基类,和类目1的都被隐藏了。

3.initialize 调用比load 要早。从日志中可以看到先调initalize。

4.load,initialize都只调用一次。


到此你是否就确定是这样的,完全正确了✅?上面的结果不完全正确,继续往下分晰。

先解决第三个结论,initalize比load优先调用的问题(此结论是错误的)。

这个问题其实就是打日志蒙避了我们对问题的判断。

先来看下官方对initalize和load的描述https://developer.apple.com/documentation/objectivec/nsobject#//apple_ref/occ/cl/NSObject

The runtime sends initialize() to each class in a program just before the class, or any class that inherits from it, is sent its first message from within the program. Superclasses receive this message before their subclasses.

The runtime sends the initialize() message to classes in a thread-safe manner. That is, initialize() is run by the first thread to send a message to a class, and any other thread that tries to send a message to that class will block until initialize() completes.

The superclass implementation may be called multiple times if subclasses do not implement initialize()—the runtime will call the inherited implementation—or if subclasses explicitly call [super initialize]. If you want to protect yourself from being run multiple times, you can structure your implementation along these lines:

+ (void)initialize {
  if (self == [ClassName self]) {
    // ... do the initialization ...
  }
}
Because initialize() is called in a blocking manner, it’s important to limit method implementations to the minimum amount of work necessary possible. Specifically, any code that takes locks that might be required by other classes in their initialize() methods is liable to lead to deadlocks. Therefore, you should not rely on initialize() for complex initialization, and should instead limit it to straightforward, class local initialization.

对着英文和百度翻译,大至描述为(中文)

运行时发送initialize()给程序中的每个类,或者在类之前发送它的第一条消息。超类在它们的子类之前收到此消息。

运行时initialize()以线程安全的方式消息发送给类。也就是说,initialize()由第一个线程运行向一个类发送消息,任何其他尝试向该类发送消息的线程都将阻塞,直到initialize()完成。

如果子类不实现initialize()- 运行时将调用继承的实现 - 或者如果子类显式调用,则可以多次调用超类实现[super initialize]如果你想保护自己不被多次运行,你可以按照以下方式构建你的实现:

由于initialize()是以阻塞方式调用的,因此将方法实现限制在尽可能少的工作量是很重要的。特别是,任何其他类在其initialize()方法中可能需要锁的代码都可能导致死锁。因此,您不应该依赖于initialize()复杂的初始化,而应该将其限制为直接的类本地初始化。


再来看下load的描述

The load() message is sent to classes and categories that are both dynamically loaded and statically linked, but only if the newly loaded class or category implements a method that can respond.

The order of initialization is as follows:

  1. All initializers in any framework you link to.

  2. All +load methods in your image.

  3. All C++ static initializers and C/C++ __attribute__(constructor) functions in your image.

  4. All initializers in frameworks that link to you.

In addition:

  • A class’s +load method is called after all of its superclasses’ +load methods.

  • A category +load method is called after the class’s own +load method.

In a custom implementation of load() you can therefore safely message other unrelated classes from the same image, but any load() methods implemented by those classes may not have run yet.

Important

Custom implementations of the load method for Swift classes bridged to Objective-C are not called automatically.


这个不用翻译了吧,1234和附加的两点说的很清楚 all ,就是所有镜像(文件)包括framework C/C++的静成构造。等都会使NSObject的load方法调用。附加的两点很重要,所有派生类的load一定是在基类的调用之后。所有类目的load一定是在宿主类调用之后。因此,调用顺序,基类(load) > 子类(load) > 类目(load),共同点都会调用。

用swift的看下important,很重要,明确告诉你,swift桥接到oc的是不被调用的。


好了,到此我们知道load会全部被调用,而intitalize中有提到,当发送第一个消息时被触发。

下面一步步验证,先把所有的类目给注释了,只留基类和派生类。

同时把类实现的中的日志改为如下:

@implementation Person

+ (void)load
{
//    NSLog(@"基类 : %@ [--load--] call.",[self class]);
    NSLog(@"基类 : Person [--load--] call.");
}

+ (void)initialize
{
//    NSLog(@"基类 : %@ [--initialize--] call.",[self class]);
    NSLog(@"基类 : Person [--initialize--] call.");
}

@end

@implementation Man

+ (void)load
{
//    NSLog(@"派生类 : %@ [--load--] call.",[self class]);
    NSLog(@"派生类 : Man [--load--] call.");
}

+ (void)initialize
{
//    NSLog(@"派生类 : %@ [--initialize--] call.",[self class]);
    NSLog(@"派生类 : Man [--initialize--] call.");
}

@end

同样是不调用,只引用头文件,输出结果:

2018-04-19 12:11:49.481532+0800 jjjjjjj[12645:10783340] 基类 : Person [--load--] call.
2018-04-19 12:11:49.482278+0800 jjjjjjj[12645:10783340] 派生类 : Man [--load--] call.

咦!initialize呢。怎么没有输出了?不是说initalize优于load的吗?其实很多时候都是因为我们只是直观的看日志而思维定势做出的错误判断,眼见不一定为真,就是这个样子。相信到这里你大概也看出了一二,因为我们日志中有使用到一个[self class];

印证了官方说的,当发送第一个消息的时候触发。

再把Person的load改成这样.

+ (void)load

{

//    NSLog(@"基类 : %@ [--load--] call.",[self class]);

    NSLog(@"基类 : Person [--load--] call.");

    [self class];//这里是多加的。

}

输出:

2018-04-19 12:16:48.358661+0800 jjjjjjj[12887:10792739] 基类 : Person [--load--] call.

2018-04-19 12:16:48.359189+0800 jjjjjjj[12887:10792739] 基类 : Person [--initialize--] call.

2018-04-19 12:16:48.359328+0800 jjjjjjj[12887:10792739] 派生类 : Man [--load--] call.

这样结果出来了吧,肯定是load先执行,执行后打印第一句,接着碰到了[self class];触发了initalize,所以打印了第二句。再到子类。

到这里基本上很清楚了,load 的调用优于initalize.

接来下要验证的就是load的基类,派生类,类目之间的加载顺序和官方所说的是不是一至。

条件是把所有NSLog中的[self class]删除,只留最正常的字串,然后把所有的initalize先注释。只保留load方法。

2018-04-19 12:24:51.621677+0800 jjjjjjj[13239:10808682] 基类 : Person [--load--] call.
2018-04-19 12:24:51.622240+0800 jjjjjjj[13239:10808682] 派生类 : Man [--load--] call.
2018-04-19 12:24:51.622402+0800 jjjjjjj[13239:10808682] 基类类目(1) : Person ctg [--load--] call.
2018-04-19 12:24:51.622510+0800 jjjjjjj[13239:10808682] 派生类类目(1) : Man ctg [--load--] call.
2018-04-19 12:24:51.622611+0800 jjjjjjj[13239:10808682] 基类类目(2) : Person ctgsecond [--load--] call.
2018-04-19 12:24:51.622703+0800 jjjjjjj[13239:10808682] 派生类类目(2) : Man ctgsecond [--load--] call.

由此可以看到是基类>派生类>类目,但问题来了,类目中有多个的时候,顺序又是怎么样的呢,又是如何解定的呢。再看下下面的输出:

2018-04-19 12:26:31.253841+0800 jjjjjjj[13338:10812283] 基类 : Person [--load--] call.
2018-04-19 12:26:31.254501+0800 jjjjjjj[13338:10812283] 派生类 : Man [--load--] call.
2018-04-19 12:26:31.254721+0800 jjjjjjj[13338:10812283] 基类类目(2) : Person ctgsecond [--load--] call.
2018-04-19 12:26:31.254852+0800 jjjjjjj[13338:10812283] 基类类目(1) : Person ctg [--load--] call.
2018-04-19 12:26:31.254944+0800 jjjjjjj[13338:10812283] 派生类类目(1) : Man ctg [--load--] call.
2018-04-19 12:26:31.255052+0800 jjjjjjj[13338:10812283] 派生类类目(2) : Man ctgsecond [--load--] call.
2018-04-19 12:30:22.524783+0800 jjjjjjj[13514:10820000] 基类 : Person [--load--] call.
2018-04-19 12:30:22.525861+0800 jjjjjjj[13514:10820000] 派生类 : Man [--load--] call.
2018-04-19 12:30:22.526573+0800 jjjjjjj[13514:10820000] 派生类类目(2) : Man ctgsecond [--load--] call.
2018-04-19 12:30:22.526770+0800 jjjjjjj[13514:10820000] 基类类目(2) : Person ctgsecond [--load--] call.
2018-04-19 12:30:22.526910+0800 jjjjjjj[13514:10820000] 基类类目(1) : Person ctg [--load--] call.
2018-04-19 12:30:22.527059+0800 jjjjjjj[13514:10820000] 派生类类目(1) : Man ctg [--load--] call.

明显看到是类目2 >类目1,到底做了什么?其实我只是把实现部分的类目2的代码搬到了类目 1,第二个输出是我把派生类的类目搬到了代码的最前面。

因此最和我们得到的结论是(load)加载顺序

1.基类>派生类>类目.

2.类目间没有派生顺序关系,全靠编译器编译连接来确定顺序,因此对我们编码而言可以认为是不可能控,你不可能说,我把代码搬到最前面不就OK了?但当你的项目过大的情况下,经常用派生的话,就不好保证了。


可以用一个队列来描述:

队头0.            基类

     1              派生类(可以有多个子类也一样)

     2              看那个类目的代码先被连接。

     ..

      .

     N            最一个被连接的类目


上面是load的加载顺序,下面再来看下initalize的加载顺序。

同样把所有load给屏掉所有的initalize方法开启。源代码的编码顺序调回到 

Persion

Man

Person(ctg)

Man(ctg)

Person(ctgsecond)

Man(ctgsecond)

的顺序.

再来看下输出:(在viewContorller的viewDidLoad中用下面这句进行测试)

NSStringFromClass([Man class]);

输出:
2018-04-19 12:42:51.044889+0800 jjjjjjj[14061:10842317] 基类类目(2) : Person ctgsecond [--initialize--] call.
2018-04-19 12:42:51.045010+0800 jjjjjjj[14061:10842317] 派生类类目(2) : Man ctgsecond [--initialize--] call.

把代码的实现顺序调整为

Person(ctg)

Man(ctg)

Person(ctgsecond)

Man(ctgsecond)

Persion

Man

同样是输出上面的.

Person(ctgsecond)

Man(ctgsecond)

Person(ctg)

Man(ctg)

Persion

Man

输出为:

2018-04-19 12:47:58.326180+0800 jjjjjjj[14317:10852934] 基类类目(1) : Person ctg [--initialize--] call.

2018-04-19 12:47:58.326301+0800 jjjjjjj[14317:10852934] 派生类类目(1) : Man ctg [--initialize--] call.

可见任何类目的initalize会复盖基类和派生类的方法。至于调用的是那个类目的initalize,按代码的顺序来看,个人理解应该是类目中最后一个被连接的,但这个我这里不做决策,光从代码的顺序来看是不能断定的,不要以为代码放前,放后就一定是连接前后。要看编译器的。但可以肯定的是和连接顺序有关。

因此initalize,在基类,派生类,类目,中只选择一个来触发。其它不触发。

当调用的是子类的inital时都会触发父类的initalize方法。

总结:

        1.所有类的load加载顺序是 基类(load) > 所有子类(load)[多个子类遵守编译连接加载顺序] > 所有类目(load)[所有类目遵守编译加载顺序].

        2.只要程序运,不需要任何调用就会触发。

        3.load中的执行的是运行时不安全的,也是非线程安全的。尽量少用。

        4.load中不要处理阻塞性耗时代码,会影响整个app的启动时间,如:

+ (void)load

{

    NSLog(@"Man [--load--] call.");

    for (int i = 0 ;i < 100;i++) {

        [NSThread sleepForTimeInterval:5];

        NSLog(@"慢慢走");

    }

}

        不要以为我的代码执行load也就是10ms,不碍事,我曾经碰到过吐血的项目,大侠喜欢用load,满天飞的load,基本上是一个类,他都要写个load都初始默认值,做一些初始工作。咱来算算,1个10ms,100个类也就是100个load就是100*10ms =1 s,当启到到完成,在load上就要耗掉 1 s, 有些人说,笑死人了,1 s,有什么好优化的。嗯。那可见平时也不怎么追求质量。

        5.initalize的执行顺序 类目(category)[多个类目之间遵守编译连接顺序中的最后一个] > 派生类/基类,这里为啥派生类和基类是并级的,因为initalize方法不是必然调用的,只有用到才call.见第6点。

        6.只有当类第一次有发送消息(这个怎么理解百度一下吧)如 [Class class], [Class foo];都会触发运行时的obj_sendxxxx来发送消息。大家可以试试访问属性触发不?理论上应该也触发,因为是getter方法。重要一点就是如何只使用基类,则不会触发子类的方法这个很好理解,如果有面向对象基础的话。如果是使用的派生类,则会先调用基类再调用派生类的,不需在在派生类中显式的使用[supper initalize].重点来了。如果当我们一个基本派生了N个子类。每当有使用子类的地方就会触发一次父类的initalize方法。因此官网给出了一段代码if (self == [Classname self]) 加以控制。

        7.类目中的initalize会复盖原生类的initalize方法这点也好理解,常写类目的朋友如果类目中写了一个原生类的方法,则运行时只会调用类目中的方法而原来的方法被隐藏了。但是子类的方法不会复盖,这点又不像多态,因为多态的话,子类会复盖父类的,除非你用supper,否则不会调用了。但恰恰这个initalize却不遵守这个规则,只要是子类有调用,父类一律被调用。

       8.load的加载顺序一定是优先于initalize。

       9.每个load,initalize在app生命周期里有且只有一次被调用。

       10.load不是运行时安全,也不是线程安全的,而initalize是运行时,并且线程安全的,官方有说碰到initalize就会被阻塞直至initalize完成。

       11.不管那一种情况,都不建议大家过多用load,initalize来搞小动作,如果要做,就认认真真搞清楚思路再来做。否则会给你带来不必要的麻烦。


说明:转载请说明出处。

         希望大家多多关我的博客https://blog.csdn.net/fengsh998






  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

边缘998

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值