Objective-C Runtime 解析(二)——NSObject的load与initialize方法

哈翔
2023-12-01

NSObject类作为Objective-C中绝大多数类的父类,向其子类提供了基本的Runtime接口与Objective-C Class的一些方法默认实现。

在NSObject中有两个类方法,load与initialize方法,由Runtime动态调用,用于配置Class或Category。(这种配置对于所有的Class实例,均有效)

在Apple文档中,它们被分类为Initializing a Class方法。

+initialize
Initializes the class before it receives its first message.
+load
Invoked whenever a class or category is added to the Objective-C runtime; implement this method to perform class-specific behavior upon loading.

load方法

在程序运行时,Runtime会将所有的Class和Category加载到内存中,这时,会调用类的load方法,通知我们Class或Category已经被加载到内存中。

 +(void)load;

在Objective-C中,初始化的顺序为

The order of initialization is as follows:

  1. All initializers in any framework you link to.
  1. All +load methods in your image.
  1. All C++ static initializers and C/C++ attribute(constructor)
    functions in your image.
  1. All initializers in frameworks that link to you.

其中对于Objective-C的Class与Category的load方法的调用顺序为:

  1. superclass在subclass前调用load
  2. category在它所扩展的类之后被调用load方法

注意,在load方法中,你可以向其他类发送消息(即方法调用),Objective-C会保证消息发送成功。但是并不保证接受消息的load函数已经被调用。

initialize方法

initialize方法是用来初始化一个Class或Category的另一个方法。它与load方法调用的不同之处在于,它会在该Class或其subclass 接受代码中第一个消息之前时候被调用。如果subclass没有重写initialize方法的话,则super class的initialize函数会被多次调用。
superclass会在subclass之前被调用initialize方法

+(void)initialize;

对于initialize方法,需要注意两点:

  1. Runtime对于initialize方法的调用是线程安全的(注意这里并没有包含并自己显示调用的情况),这意味着:
    (1)initialize会在class接受第一个消息的线程中被调用。
    (2)initialize会阻塞class的message sending,只有当initialize方法执行完毕,其余的message才会被执行。这里有dead lock的危险,因此在initialize中的代码不应过于复杂。

  2. 同一个class的initialize方法可能会被多次调用,这种情况发生在subclass没有实现initialize方法的时候或者显示调用[super initialize]时。
    为了防止initialize多次调用,可以在initialize方法中加入判断当前调用者是否为当前类本身。

+(void)initialize {
  if (self == [ClassName self]) {
    // ... do the initialization ...
  }
}

load VS initialize

load 与 initialize 方法是没有先后关系的。

它们调用的时机不一样:

  • load当runtime将类加载时被调用,而且这个时机要先于appdelegate中的
    (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
  • initialize方法发生在类第一次接受消息时调用。关于这里的第一个消息需要特别说明一下,对于 NSObject 的 runtime 机制而言,其在调用 NSObject 的 + (void)load 消息不被视为第一个消息,但是,如果像普通函数调用一样直接调用 NSObject 的 + (void)load 消息,则会引起 + (void)initialize 的调用。反之,如果没有向 NSObject 发送第一个消息,+ (void)initialize 则不会被自动调用。

load 与 initialize的继承性不一样

  • load方法针对于每一个类而言,它不会被继承。如果子类没有重写load方法,也不会调用superclass的load方法。
  • initialize 方法可以被继承,当subclass没有重写initialize方法时,如果superclass中有实现,则会调用之。
  • 对于Category,会覆盖其对应类的initialize方法。
  • 当一个类被runtime加载时,只会调用其自身的load方法,其superclass的load方法不会被调用。而当initialize被调用时,其superclass的initialize函数会被依次调用。如下代码
@interface Car : NSObject

@end

@interface AutoCar : Car

@end

@interface BMWCar : AutoCar

@end

@interface BMWCar(MyCar)

@end

@implementation Car
+(void) initialize
{
    NSLog(@"%@ %s", [self class], __FUNCTION__);
}
@end

@implementation AutoCar

+(void) initialize
{
    NSLog(@"%@ %s", [self class], __FUNCTION__);
}
@end

@implementation BMWCar
+(void) initialize
{
    NSLog(@"%@ %s", [self class], __FUNCTION__);
}
@end

- (void)viewDidLoad {
    [super viewDidLoad];
    BMWCar *myCar = [BMWCar new];
    
}

输出结果为:

Car +[Car initialize]
AutoCar +[AutoCar initialize]
BMWCar +[BMWCar initialize]

参考文章

CaryaLiu 博客
Apple文档

应用实例

Method Swizzling 和 AOP 实践
Notification Once

补充:一种另类的单例实现

突然想到了一个问题,既然load方法与initialize方法都会被Runtime自动调用一次,并且在Runtime情况下,这两个方法都是线程安全的,那么是否可以作为单例类的一个实现呢?
不废话,上代码:

@interface MySpecialObject : NSObject
+(MySpecialObject *) sharedInstance;
@end
// 利用initialize函数实现
static MySpecialObject *singalObject = nil;
@implementation MySpecialObject
+(void) initialize
{
    if (self == [MySpecialObject self] && singalObject == nil) {
        singalObject = [[MySpecialObject alloc] init];
    }
}

+(MySpecialObject *) sharedInstance
{
    return singalObject;
}


- (void)viewDidLoad {
    [super viewDidLoad];
    MySpecialObject *obj = [MySpecialObject sharedInstance];
    NSLog(@"%@", obj);
}

-(void) viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];
    MySpecialObject *obj = [MySpecialObject sharedInstance];
    NSLog(@"%@", obj);
}

-(void) viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];
    MySpecialObject *obj = [MySpecialObject sharedInstance];
    NSLog(@"%@", obj);
}

输出:

2016-08-13 16:13:07.951 SpecialSingalObject[1304:110175] <MySpecialObject: 0x7fc5d849f400>
2016-08-13 16:13:08.826 SpecialSingalObject[1304:110175] <MySpecialObject: 0x7fc5d849f400>
2016-08-13 16:13:09.556 SpecialSingalObject[1304:110175] <MySpecialObject: 0x7fc5d849f400>

确实,得到了同一个实例。
对于initialize 函数,的解释:
(1) if (self == [MySpecialObject self] …
是为了保证 initialize函数只有在本类而非subclass时才执行单例初始化函数。

(2)if(…&& singalObject == nil) 是为了防止initialize多次调用而产生多个实例(除了Runtime调用,我们也可以显示调用initialize方法)。经过测试,当我们将initialize方法本身作为class的第一个方法执行时,Runtime的initialize会被先调用(这保证了线程安全),然后我们自己显示调用的initialize函数再被调用。
由于initialize方法的第一次调用一定是Runtime调用,而Runtime又保证了线程安全性,因此这里只简单的检测 singalObject == nil即可。

上面的代码确实是实现了线程安全的单例模式,但是由于load与initialize方法的本意是对于Class做设置,并且考虑到load或initialize方法有可能是在程序运行的早期执行,可能我们的单例所依赖的环境尚未形成,所有我们还是不要用这种方式实现单例吧。

经过测试,当我们的MySpecialObject类中包含其他自定义class的属性时,如Book。
当我们在MySepcialObject 的init函数中初始化Book属性时,此时虽然Book类的load方法没被调用,但是我们的Book属性仍然能被正常初始化。
也许,对于load方法的调用,Runtime是在将所有的Class加载到内存中后,再统一调用的原因吗?

关于自己最后一个关于Book属性的问题,在查阅了Runtime源码之后,确实如推测那样,Runtime会在所有类都加载入内存后,在统一的调用load方法。当初在写这篇博客时,还没有去深入的分析Runtime的源码(还没有能力:)),现在回过头来看一下,这也就是对一个问题不断深入理解的过程吧~

附上相关代码, 当dyld将所有的class image都映射到内存后,会调用load images来加载class,最后,会调用load 方法:

void
load_images(const char *path __unused, const struct mach_header *mh)
{
    // Return without taking locks if there are no +load methods here.
    if (!hasLoadMethods((const headerType *)mh)) return;

    recursive_mutex_locker_t lock(loadMethodLock);

    // Discover load methods
    {
        rwlock_writer_t lock2(runtimeLock);
        prepare_load_methods((const headerType *)mh);
    }

    // Call +load methods (without runtimeLock - re-entrant)
    call_load_methods();
}
 类似资料: