Objective-C:如何在启动时强制调用' +initialize ',而不是在类第一次使用的时候?

时间:2023-01-15 14:54:13

Problem

For certain classes, I would like to explicitly call the +initialize method when my program starts, rather than allowing the runtime system to call it implicitly at some nondeterministic point later when the class happens to first be used. Problem is, this isn't recommended.

对于某些类,我希望在程序启动时显式地调用+initialize方法,而不是允许运行时系统在第一次使用该类时的某个不确定点隐式地调用它。问题是,这是不推荐的。

Most of my classes have little to no work to do in initialization, so I can just let the runtime system do its thing for those, but at least one of my classes requires as much as 1 second to initialize on older devices, and I don't want things to stutter later when the program is up and running. (A good example of this would be sound effects — I don't want sudden delay the first time I try to play a sound.)

我大部分的类初始化没有工作要做,所以我可以让运行时系统做了那些事情,但至少我的一个类需要多达1秒初始化在旧设备,我不想事情口吃后程序启动并运行。(一个很好的例子就是声音效果——我不想在第一次尝试播放声音时突然延迟。)

What are some ways to do this initialization at startup-time?

在startup-time中有什么方法可以进行初始化?

Attempted solutions

What I've done in the past is call the +initialize method manually from main.c, and made sure that every +initialize method has a bool initialized variable wrapped in a @synchronized block to prevent accidental double-initialization. But now Xcode is warning me that +initialize would be called twice. No surprise there, but I don't like ignoring warnings, so I'd rather fix the problem.

我过去所做的就是从main手动调用+initialize方法。c,并确保每个+initialize方法都有一个包装在@synchronized块中的bool initialized变量,以防止意外的双初始化。但是现在Xcode警告我+initialize将被调用两次。这并不奇怪,但我不喜欢忽略警告,所以我宁愿解决这个问题。

My next attempt (earlier today) was to define a +preinitialize function that I call directly instead +initialize, and to make sure I call +preinitialize implicitly inside of +initialize in case it is not called explicitly at startup. But the problem here is that something inside +preinitialize is causing +initialize to be called implicitly by the runtime system, which leads me to think that this is a very unwise approach.

我的下一个尝试(今天早些时候)是定义一个+preinitialize函数,我直接调用它而不是+initialize,并确保我在+initialize内部隐式地调用+preinitialize,以防它在启动时没有被显式地调用。但是这里的问题是,+preinitialize内部的某些东西导致了+initialize被运行时系统隐式地调用,这使我认为这是一种非常不明智的方法。

So let's say I wanted to keep the actual initialization code inside +initialize (where it's really intended to be) and just write a tiny dummy method called +preinitialize that forces +initialize to be called implicitly by the runtime system somehow? Is there a standard approach to this? In a unit test, I wrote...

假设我想把实际的初始化代码保存在+initialize(真正的初始化代码)中,然后编写一个叫做+preinitialize的虚拟小方法,强制+initialize由运行时系统隐式地调用?有标准的方法吗?在单元测试中,我写道……

+ (void) preinitialize
{
  id dummy = [self alloc];
  NSLog(@"Preinitialized: %i", !!dummy);
}

...but in the debugger, I did not observe +initialize being called prior to +alloc, indicating that +initialize was not called implicitly by the runtime system inside of +preinitialize.

…但是在调试器中,我没有观察到在+alloc之前调用+初始化,这表明+初始化过程中的运行时系统没有隐式地调用+initialize。

Edit

I found a really simple solution, and posted it as an answer.

我找到了一个非常简单的解决方案,并将它作为一个答案发布了。

4 个解决方案

#1


3  

There are two problems here. First, you should never call +initialize directly. Second, if you have some piece of initialization that can take over a second, you generally shouldn't run it on the main queue because that would hang the whole program.

这里有两个问题。首先,不应该直接调用+initialize。其次,如果你有一段初始化可以占用一秒钟的时间,你通常不应该在主队列上运行它,因为这会挂起整个程序。

Put your initialization logic into a separate method so you can call it when you expect to. Optionally, put the logic into a dispatch_once block so that it's safe to call it multiple times. Consider the following example.

将初始化逻辑放入一个单独的方法中,以便您可以在需要时调用它。可选地,将逻辑放入dispatch_once块中,以便能够安全地多次调用它。考虑下面的例子。

@interface Foo: NSObject
+ (void)setup;
@end

@implementation Foo

+ (void)setup {
    NSLog(@"Setup start");

    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        NSLog(@"Setup running");
        [NSThread sleepForTimeInterval:1]; // Expensive op
    });
}
@end

Now in your application:didFinishLaunchingWithOptions: call it in the background.

现在在应用程序中:didFinishLaunchingWithOptions:在后台调用它。

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    NSLog(@"START");

    // Here, you should setup your UI into an "inactive" state, since we can't do things until
    // we're done initializing.

    dispatch_group_t group = dispatch_group_create();
    dispatch_group_async(group, dispatch_get_global_queue(0, 0), ^{
        [Foo setup];
        // And any other things that need to intialize in order.
    });

    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        NSLog(@"We're all ready to go now! Turn on the the UI. Set the variables. Do the thing.");
    });
    return YES;
}

This is how you want to approach things if order matters to you. All the runtime options (+initialize and +load) make no promises on order, so don't rely on them for work that needs that. You'll just make everything much more complicated than it needs to be.

如果顺序对你很重要的话,这就是你处理事情的方式。所有的运行时选项(+初始化和+加载)都不会对订单做出承诺,所以不要依赖它们来进行需要的工作。你会让事情变得比需要的复杂得多。

You may want to be able to check for programming errors in which you accidentally call Foo methods before initialization is done. That's best done, IMO, with assertions. For example, create an +isInitialized method that checks whatever +setup does (or create a class variable to track it). Then you can do this:

您可能希望能够检查在初始化完成之前意外调用Foo方法的编程错误。在我看来,最好是用断言。例如,创建一个+isInitialized方法,它检查任何+设置(或者创建一个类变量来跟踪它)。然后你可以这样做:

#if !defined(NS_BLOCK_ASSERTIONS)
#define FooAssertInitialized(condition) NSAssert([Foo isInitialized], @"You must call +setup before using Foo.")
#else
#define FooAssertInitialized(condition)
#endif

- (void)someMethodThatRequiresInitialization {
    FooAssertInitialized();

    // Do stuff
}

This makes it easy to mark methods that really do require initialization before use vs ones that may not.

这使得在使用之前需要进行初始化的方法很容易标记。

#2


2  

Cocoa provides a setup point earlier than +initialize in the form of +load, which is called very shortly after the program's start. This is a weird environment: other classes that rely on +load may not be completely initialized yet, and more importantly, your main() has not been called! That means there's no autorelease pool in place.

Cocoa都以+load的形式提供一个比+initialize更早的设置点,在程序启动后不久就会调用这个设置点。这是一个奇怪的环境:依赖于+load的其他类可能还没有完全初始化,更重要的是,您的main()还没有被调用!这意味着没有自动共享池。

After load but before initialize, functions marked with __attribute__((constructor)) will be called. This doesn't allow you to do much that you can't do in main() so far as I know.

在加载之后但在初始化之前,将调用__attribute__(构造函数)标记的函数。就我所知,这并不能使您完成main()中无法完成的工作。

One option would be to create a dummy instance of your class in either main() or a constructor, guaranteeing that initialize will be called as early as possible.

一种选择是在main()或构造函数中创建类的伪实例,以确保尽早调用initialize。

#3


2  

The first possible place to run class-specific code is +load, which happens when the class is added to the ObjC runtime. It's still not completely deterministic which classes' +load implementations will be called in what order, but there are some rules. From the docs:

运行类特定代码的第一个可能位置是+load,这发生在将类添加到ObjC运行时时时。还不完全确定按什么顺序调用类的+load实现,但是有一些规则。从文档:

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.

    在您的映像中,所有c++静态初始化器和C/ c++ __attribute__(构造函数)函数。

  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.

    类的+load方法在其所有超类的+load方法之后被调用。

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

    一个类别+加载方法是在类自己的+load方法之后调用的。

So, two peer classes (say, both direct NSObject subclasses) will both +load in step 2 above, but there's no guarantee which order the two of them will be relative to each other.

因此,在第2步中,两个对等类(例如,两个直接的NSObject子类)都将加载+,但是不能保证它们之间的顺序是相对的。

Because of that, and because metaclass objects in ObjC are generally not great places to set and maintain state, you might want something else...

正因为如此,而且由于ObjC中的元类对象通常不是设置和维护状态的好地方,您可能需要其他东西……

A better solution?

For example, your "global" state can be kept in the (single) instance of a singleton class. Clients can call [MySingletonClass sharedSingleton] to get that instance and not care about whether it's getting its initial setup done at that time or earlier. And if a client needs to make sure it happens earlier (and in a deterministic order relative to other things), they can call that method at a time of their choosing — such as in main before kicking off the NSApplication/UIApplication run loop.

例如,您的“全局”状态可以保存在单例类的(单个)实例中。客户端可以调用[MySingletonClass sharedSingleton]来获取该实例,而不关心它是在那个时候完成初始设置,还是在更早的时候完成。如果客户端需要确保它发生得更早(和相对于其他事情的确定性顺序),他们可以在自己选择的时间调用该方法——比如在启动NSApplication/UIApplication运行循环之前的main。

Alternatives

If you don't want this costly initialization work to happen at app startup, and you don't want it to happen when the class is being put to use, you have a few other options, too.

如果您不想在应用程序启动时发生这种昂贵的初始化工作,并且您不希望它在类被使用时发生,那么您也有一些其他的选择。

  • Keep the code in +initialize, and contrive to make sure the class gets messaged before its first "real" use. Perhaps you can kick off a background thread to create and initialize a dummy instance of that class from application:didFinishLaunching:, for example.
  • 将代码保存在+initialize中,并设法确保类在第一次“真正”使用之前得到消息。例如,您可以启动一个后台线程来创建和初始化application:didFinishLaunching:类的一个虚拟实例。
  • Put that code someplace else — in the class object or in a singleton, but in a method of your own creation regardless — and call it directly at a time late enough for setup to avoid slowing down app launch but soon enough for it to be done before your class' "real" work is needed.
  • 把代码别的地方——在类对象或一个单例,但在自己创建的方法直接不管,称之为一次晚足够设置避免减慢应用启动但很快要做在你的类的“真正的”工作是必要的。

#4


1  

Answering my own question here. It turns out that the solution is embarrassingly simple.

回答我自己的问题。结果证明,解决方法简单得令人尴尬。

I had been operating under the mistaken belief that +initialize would not be called until the first instance method in a class is invoked. This is not so. It is called before the first instance method or class method is invoked (other than +load, of course).

我一直错误地认为,在调用类中的第一个实例方法之前,不会调用+initialize。事实并非如此。在调用第一个实例方法或类方法之前调用它(当然不包括+load)。

So the solution is simply to cause +initialize to be invoked implicitly. There are multiple ways to do this. Two are discussed below.

所以解决方案就是让+initialize隐式地被调用。有多种方法可以做到这一点。两个在下面讨论。

Option 1 (simple and direct, but unclear)

In startup code, simply call some method (e.g., +class) of the class you want to initialize at startup, and discard the return value:

在启动代码中,只需调用要在启动时初始化的类的某个方法(例如+class),并放弃返回值:

(void)[MyClass class];

This is guaranteed by the Objective-C runtime system to call [MyClass initialize] implicitly if it has not yet been called.

Objective-C运行时系统保证,如果尚未调用[MyClass initialize],则隐式调用它。

Option 2 (less direct, but clearer)

Create a +preinitialize method with an empty body:

创建一个带有空体的+preinitialize方法:

+ (void) preinitialize
{
  // Simply by calling this function at startup, an implicit call to
  // +initialize is generated.
}

Calling this function at startup implicitly invokes +initialize:

在startup中调用此函数隐式调用+initialize:

[MyClass preinitialize];  // Implicitly invokes +initialize.

This +preinitialize method serves no purpose other than to document the intention. Thus, it plays well with +initialize and +deinitialize and is fairly self-evident in the calling code. I write a +deinitialize method for every class I write that has an +initialize method. +deinitialize is called from the shutdown code; +initialize is called implicitly via +preinitialize in the startup code. Super simple. Sometimes I also write a +reinitialize method, but the need for this is rare.

这个+preinitialize方法除了用来记录意图之外没有其他用途。因此,它可以很好地处理+initialize和+deinitialize,并且在调用代码中是很明显的。我为我编写的每个具有+initialize方法的类编写一个+deinitialize方法。+从关机代码调用去初始化;通过启动代码中的+preinitialize隐式调用+initialize。超级简单。有时我也编写一个+reinitialize方法,但很少需要这样做。

I am now using this approach for all my class initializers. Instead of calling [MyClass initialize] in the start up code, I am now calling [MyClass preinitialize]. It's working great, and the call stack shown in the debugger confirms that +initialize is being called exactly at the intended time and fully deterministically.

我现在对所有类初始化器都使用这种方法。我现在调用[MyClass preinitialize]而不是在启动代码中调用[MyClass initialize]。它工作得很好,调试器中显示的调用堆栈确认+initialize正被准确地在预定时间和完全确定地调用。

#1


3  

There are two problems here. First, you should never call +initialize directly. Second, if you have some piece of initialization that can take over a second, you generally shouldn't run it on the main queue because that would hang the whole program.

这里有两个问题。首先,不应该直接调用+initialize。其次,如果你有一段初始化可以占用一秒钟的时间,你通常不应该在主队列上运行它,因为这会挂起整个程序。

Put your initialization logic into a separate method so you can call it when you expect to. Optionally, put the logic into a dispatch_once block so that it's safe to call it multiple times. Consider the following example.

将初始化逻辑放入一个单独的方法中,以便您可以在需要时调用它。可选地,将逻辑放入dispatch_once块中,以便能够安全地多次调用它。考虑下面的例子。

@interface Foo: NSObject
+ (void)setup;
@end

@implementation Foo

+ (void)setup {
    NSLog(@"Setup start");

    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        NSLog(@"Setup running");
        [NSThread sleepForTimeInterval:1]; // Expensive op
    });
}
@end

Now in your application:didFinishLaunchingWithOptions: call it in the background.

现在在应用程序中:didFinishLaunchingWithOptions:在后台调用它。

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    NSLog(@"START");

    // Here, you should setup your UI into an "inactive" state, since we can't do things until
    // we're done initializing.

    dispatch_group_t group = dispatch_group_create();
    dispatch_group_async(group, dispatch_get_global_queue(0, 0), ^{
        [Foo setup];
        // And any other things that need to intialize in order.
    });

    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        NSLog(@"We're all ready to go now! Turn on the the UI. Set the variables. Do the thing.");
    });
    return YES;
}

This is how you want to approach things if order matters to you. All the runtime options (+initialize and +load) make no promises on order, so don't rely on them for work that needs that. You'll just make everything much more complicated than it needs to be.

如果顺序对你很重要的话,这就是你处理事情的方式。所有的运行时选项(+初始化和+加载)都不会对订单做出承诺,所以不要依赖它们来进行需要的工作。你会让事情变得比需要的复杂得多。

You may want to be able to check for programming errors in which you accidentally call Foo methods before initialization is done. That's best done, IMO, with assertions. For example, create an +isInitialized method that checks whatever +setup does (or create a class variable to track it). Then you can do this:

您可能希望能够检查在初始化完成之前意外调用Foo方法的编程错误。在我看来,最好是用断言。例如,创建一个+isInitialized方法,它检查任何+设置(或者创建一个类变量来跟踪它)。然后你可以这样做:

#if !defined(NS_BLOCK_ASSERTIONS)
#define FooAssertInitialized(condition) NSAssert([Foo isInitialized], @"You must call +setup before using Foo.")
#else
#define FooAssertInitialized(condition)
#endif

- (void)someMethodThatRequiresInitialization {
    FooAssertInitialized();

    // Do stuff
}

This makes it easy to mark methods that really do require initialization before use vs ones that may not.

这使得在使用之前需要进行初始化的方法很容易标记。

#2


2  

Cocoa provides a setup point earlier than +initialize in the form of +load, which is called very shortly after the program's start. This is a weird environment: other classes that rely on +load may not be completely initialized yet, and more importantly, your main() has not been called! That means there's no autorelease pool in place.

Cocoa都以+load的形式提供一个比+initialize更早的设置点,在程序启动后不久就会调用这个设置点。这是一个奇怪的环境:依赖于+load的其他类可能还没有完全初始化,更重要的是,您的main()还没有被调用!这意味着没有自动共享池。

After load but before initialize, functions marked with __attribute__((constructor)) will be called. This doesn't allow you to do much that you can't do in main() so far as I know.

在加载之后但在初始化之前,将调用__attribute__(构造函数)标记的函数。就我所知,这并不能使您完成main()中无法完成的工作。

One option would be to create a dummy instance of your class in either main() or a constructor, guaranteeing that initialize will be called as early as possible.

一种选择是在main()或构造函数中创建类的伪实例,以确保尽早调用initialize。

#3


2  

The first possible place to run class-specific code is +load, which happens when the class is added to the ObjC runtime. It's still not completely deterministic which classes' +load implementations will be called in what order, but there are some rules. From the docs:

运行类特定代码的第一个可能位置是+load,这发生在将类添加到ObjC运行时时时。还不完全确定按什么顺序调用类的+load实现,但是有一些规则。从文档:

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.

    在您的映像中,所有c++静态初始化器和C/ c++ __attribute__(构造函数)函数。

  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.

    类的+load方法在其所有超类的+load方法之后被调用。

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

    一个类别+加载方法是在类自己的+load方法之后调用的。

So, two peer classes (say, both direct NSObject subclasses) will both +load in step 2 above, but there's no guarantee which order the two of them will be relative to each other.

因此,在第2步中,两个对等类(例如,两个直接的NSObject子类)都将加载+,但是不能保证它们之间的顺序是相对的。

Because of that, and because metaclass objects in ObjC are generally not great places to set and maintain state, you might want something else...

正因为如此,而且由于ObjC中的元类对象通常不是设置和维护状态的好地方,您可能需要其他东西……

A better solution?

For example, your "global" state can be kept in the (single) instance of a singleton class. Clients can call [MySingletonClass sharedSingleton] to get that instance and not care about whether it's getting its initial setup done at that time or earlier. And if a client needs to make sure it happens earlier (and in a deterministic order relative to other things), they can call that method at a time of their choosing — such as in main before kicking off the NSApplication/UIApplication run loop.

例如,您的“全局”状态可以保存在单例类的(单个)实例中。客户端可以调用[MySingletonClass sharedSingleton]来获取该实例,而不关心它是在那个时候完成初始设置,还是在更早的时候完成。如果客户端需要确保它发生得更早(和相对于其他事情的确定性顺序),他们可以在自己选择的时间调用该方法——比如在启动NSApplication/UIApplication运行循环之前的main。

Alternatives

If you don't want this costly initialization work to happen at app startup, and you don't want it to happen when the class is being put to use, you have a few other options, too.

如果您不想在应用程序启动时发生这种昂贵的初始化工作,并且您不希望它在类被使用时发生,那么您也有一些其他的选择。

  • Keep the code in +initialize, and contrive to make sure the class gets messaged before its first "real" use. Perhaps you can kick off a background thread to create and initialize a dummy instance of that class from application:didFinishLaunching:, for example.
  • 将代码保存在+initialize中,并设法确保类在第一次“真正”使用之前得到消息。例如,您可以启动一个后台线程来创建和初始化application:didFinishLaunching:类的一个虚拟实例。
  • Put that code someplace else — in the class object or in a singleton, but in a method of your own creation regardless — and call it directly at a time late enough for setup to avoid slowing down app launch but soon enough for it to be done before your class' "real" work is needed.
  • 把代码别的地方——在类对象或一个单例,但在自己创建的方法直接不管,称之为一次晚足够设置避免减慢应用启动但很快要做在你的类的“真正的”工作是必要的。

#4


1  

Answering my own question here. It turns out that the solution is embarrassingly simple.

回答我自己的问题。结果证明,解决方法简单得令人尴尬。

I had been operating under the mistaken belief that +initialize would not be called until the first instance method in a class is invoked. This is not so. It is called before the first instance method or class method is invoked (other than +load, of course).

我一直错误地认为,在调用类中的第一个实例方法之前,不会调用+initialize。事实并非如此。在调用第一个实例方法或类方法之前调用它(当然不包括+load)。

So the solution is simply to cause +initialize to be invoked implicitly. There are multiple ways to do this. Two are discussed below.

所以解决方案就是让+initialize隐式地被调用。有多种方法可以做到这一点。两个在下面讨论。

Option 1 (simple and direct, but unclear)

In startup code, simply call some method (e.g., +class) of the class you want to initialize at startup, and discard the return value:

在启动代码中,只需调用要在启动时初始化的类的某个方法(例如+class),并放弃返回值:

(void)[MyClass class];

This is guaranteed by the Objective-C runtime system to call [MyClass initialize] implicitly if it has not yet been called.

Objective-C运行时系统保证,如果尚未调用[MyClass initialize],则隐式调用它。

Option 2 (less direct, but clearer)

Create a +preinitialize method with an empty body:

创建一个带有空体的+preinitialize方法:

+ (void) preinitialize
{
  // Simply by calling this function at startup, an implicit call to
  // +initialize is generated.
}

Calling this function at startup implicitly invokes +initialize:

在startup中调用此函数隐式调用+initialize:

[MyClass preinitialize];  // Implicitly invokes +initialize.

This +preinitialize method serves no purpose other than to document the intention. Thus, it plays well with +initialize and +deinitialize and is fairly self-evident in the calling code. I write a +deinitialize method for every class I write that has an +initialize method. +deinitialize is called from the shutdown code; +initialize is called implicitly via +preinitialize in the startup code. Super simple. Sometimes I also write a +reinitialize method, but the need for this is rare.

这个+preinitialize方法除了用来记录意图之外没有其他用途。因此,它可以很好地处理+initialize和+deinitialize,并且在调用代码中是很明显的。我为我编写的每个具有+initialize方法的类编写一个+deinitialize方法。+从关机代码调用去初始化;通过启动代码中的+preinitialize隐式调用+initialize。超级简单。有时我也编写一个+reinitialize方法,但很少需要这样做。

I am now using this approach for all my class initializers. Instead of calling [MyClass initialize] in the start up code, I am now calling [MyClass preinitialize]. It's working great, and the call stack shown in the debugger confirms that +initialize is being called exactly at the intended time and fully deterministically.

我现在对所有类初始化器都使用这种方法。我现在调用[MyClass preinitialize]而不是在启动代码中调用[MyClass initialize]。它工作得很好,调试器中显示的调用堆栈确认+initialize正被准确地在预定时间和完全确定地调用。