Objective-C:如何在启动时强制调用 `+initialize` 而不是在 class 刚好第一次使用时调用?

Objective-C: How to force a call to `+initialize` at startup rather than later when the class happens to used for the first time?

问题

对于某些 classes,我想在我的程序启动时显式调用 +initialize 方法,而不是允许运行时系统在稍后 class刚好第一次被使用。问题是,不推荐这样做。

我的大多数 classes 在初始化时几乎没有工作要做,所以我可以让运行时系统为它们做它的事情,但至少我的 classes 之一在较旧的设备上需要多达 1 秒的时间来初始化,我不希望程序启动和 运行 之后出现卡顿。 (一个很好的例子就是声音效果——我不想在第一次尝试播放声音时突然延迟。)

有哪些方法可以在启动时进行初始化?

尝试过的解决方案

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

我的下一次尝试(今天早些时候)是定义一个我直接调用 +initialize+preinitialize 函数,并确保我在 [=13] 中隐式调用 +preinitialize =] 以防在启动时未明确调用它。但这里的问题是 +preinitialize 内部的某些东西导致 +initialize 被运行时系统隐式调用,这让我认为这是一种非常不明智的做法。

所以假设我想将实际的初始化代码保留在 +initialize 中(它真正想要的地方)并且只写一个名为 +preinitialize 的小虚拟方法强制 +initialize以某种方式被运行时系统隐式调用?有没有标准的方法?在单元测试中,我写了...

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

...但是在调试器中,我没有观察到 +initialize+alloc 之前被调用,这表明 +initialize 没有被 +initialize 内部的运行时系统隐式调用=20=].

编辑

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

运行 class-specific 代码的第一个可能位置是 +load,这发生在将 class 添加到 ObjC 运行 时。哪个 classes' +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.

  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.

因此,两个对等 classes(例如,都是直接 NSObject subclasses)将在上面的步骤 2 中都 +load,但不能保证哪个他们两个的顺序是相对的。

正因为如此,并且因为 ObjC 中的 metaclass 对象通常不是设置和维护状态的好地方,您可能需要其他东西...

更好的解决方案?

例如,您的 "global" 状态可以保存在单例 class 的(单个)实例中。客户端可以调用 [MySingletonClass sharedSingleton] 来获取该实例,而不关心它是在当时还是更早完成初始设置。如果客户需要确保它更早发生(并且相对于其他事物以确定的顺序),他们可以在他们选择的时间调用该方法——例如在 main 开始 [=22] 之前=]/UIApplication 运行 循环.

备选方案

如果您不希望在应用程序启动时进行这种代价高昂的初始化工作,并且不希望它在 class 投入使用时发生,您还有其他一些选择,也是。

  • 将代码保留在 +initialize 中,并努力确保 class 在首次 "real" 使用之前收到消息。例如,也许您可​​以启动一个后台线程来创建和初始化来自 application:didFinishLaunching: 的 class 的虚拟实例。
  • 将该代码放在其他地方 - 在 class 对象或单例中,但无论如何在你自己创建的方法中 - 并在足够晚的时间直接调用它以进行设置以避免减慢应用程序启动,但很快就可以在您的 class' "real" 工作需要之前完成。

Cocoa以+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的形式提供了早于+initialize的设置点!这意味着没有自动释放池。

load之后但在initialize之前,将调用标有__attribute__((constructor))的函数。据我所知,这不允许您做很多在 main() 中做不到的事情。

一种选择是在 main() 或构造函数中创建 class 的虚拟实例,以保证尽早调用 initialize

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

将您的初始化逻辑放入一个单独的方法中,以便您可以在需要时调用它。或者,将逻辑放入 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

现在在你的application: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;
}

如果顺序对您很重要,这就是您想要处理事情的方式。所有 运行 时间选项(+initialize+load)都没有按顺序承诺,所以不要依赖它们来完成需要它的工作。你只会让一切变得比它需要的复杂得多。

您可能希望能够检查在初始化完成之前意外调用 Foo 方法的编程错误。 IMO,最好用断言来完成。例如,创建一个 +isInitialized 方法来检查 +setup 所做的一切(或创建一个 class 变量来跟踪它)。那么你可以这样做:

#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
}

这样可以很容易地标记出确实需要在使用前进行初始化的方法与不需要的方法。

在这里回答我自己的问题。事实证明,解决方案简单得令人尴尬。

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

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

选项一(简单直接,但不清楚)

在启动代码中,只需调用您要在启动时初始化的 class 的某些方法(例如 +class),并丢弃 return 值:

(void)[MyClass class];

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

选项 2(不太直接,但更清晰)

创建具有空主体的 +preinitialize 方法:

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

在启动时调用此函数会隐式调用 +initialize:

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

+preinitialize 方法仅用于记录意图。因此,它与 +initialize+deinitialize 配合得很好,并且在调用代码中相当 self-evident。我为每个 class 写了一个 +deinitialize 方法,我写了一个 +initialize 方法。 +deinitialize 从关闭代码调用; +initialize 在启动代码中通过 +preinitialize 隐式调用。超级简单。有时我也写一个+reinitialize方法,但很少需要这个。

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