Swift & Objective-C

+load vs. +initialize: Timing and Inheritance Traps

Why does method-swizzling code always go in +load? Can’t we put it in the similar-looking +initialize instead?

4 min read
Cover image for +load vs. +initialize: Timing and Inheritance Traps

Why does method-swizzling code always go inside +load? Can’t we put it in the similar-looking +initialize instead?

+load and +initialize both look like methods called once when a class is ready, but their timing, invocation mechanism, and inheritance rules differ completely. They are also a common Objective-C interview topic, and misunderstanding them can lead to confusion over “Why is this code running twice?”—worth clarifying once.


Quick comparison

+load +initialize
Invocation timing When the class loads into the runtime (before main) Immediately before the class receives its first message (lazy)
Invocation mechanism Direct function-pointer call Through objc_msgSend
Category Class and category implementations are both called separately The category implementation overrides the class implementation
Inheritance Only the class that implements it is called Inherited—may be called multiple times because of subclasses
If unused? Still called Never called if the class is unused

Let’s unpack the parts that the table alone doesn’t make intuitive.


+load: Before main, unconditionally

+load is called when the binary containing the class or category is loaded into the runtime—that is, before the main function even runs. It runs even if the app never uses that class.

Its invocation mechanism is unusual. It is called directly via a function pointer, without passing through objc_msgSend. Normal override rules therefore do not apply.

  • If a subclass does not implement +load, the parent implementation is not called on its behalf.
  • The class’s +load and the category’s +load are both called separately—this is not overriding.
  • Ordering is guaranteed: the parent class’s +load runs before the child’s, and the class’s +load runs before the category’s.

These properties align perfectly with swizzling. Even when +load is implemented in a category, it does not interfere with the original class’s +load and reliably runs exactly once at the earliest point in the app’s lifetime.

There is a cost: +load is charged directly to app startup time. Every class’s +load runs sequentially before main, so heavy work here delays the first screen. That is why Apple has advised avoiding +load where possible for years. In fact, inside +load, other classes outside the image containing self may not have loaded yet, limiting what you can safely do.

Diagram showing when +load and +initialize are called on the app launch timeline
The only branching point is whether the call goes through msgSend

+initialize: Lazily, just before the first message

+initialize uses the opposite strategy. The runtime calls it immediately before the class receives its first message. If the app never uses the class, it is never called. It is a lazy initialization point that adds no startup cost.

Because it goes through objc_msgSend, normal method inheritance rules apply. This is where the famous trap appears.

@implementation Animal
+ (void)initialize {
    NSLog(@"initialize: %@", self);
}
@end

@interface Dog : Animal
@end
@implementation Dog
@end

When the first message is sent to Dog, the log looks like this.

initialize: Animal
initialize: Dog

Animal’s +initialize runs twice: once for Animal itself, and once because Dog, which does not implement +initialize, inherits and runs the parent implementation. That is why the conventional implementation of +initialize includes a class check.

+ (void)initialize {
    if (self == [Animal class]) {
        // only initialize the  Animal real class’s share here
    }
}

For reference, +initialize is thread-safe by itself because the runtime invokes it while holding a per-class lock. There is no need to layer dispatch_once on top.


Practical selection criteria

The decision rule is simple.

  • Tasks that must happen unconditionally and as early as possible, such as swizzling or class registration+load (keep it minimal)
  • Preparation needed only when the class is used+initialize (self check required)
  • Most initialization → neither, really; dispatch_once a singleton or lazy property is sufficient

Swift does not have this dilemma. It does not provide anything equivalent to +load. There is no official mechanism for inserting global executable code before main. Instead, type properties (static let) provide lazy, thread-safe initialization at the language level, replacing the role of +initialize. This is one reason Swift is structurally advantageous for app startup performance.

Illustration depicting before-main +load and lazy +initialize as a racetrack
One starts running before the race; the other sleeps until its first call

Summary

  • +load means before main, unconditionally, with a direct function-pointer call—both class and category implementations are called.
  • +initialize means just before the first message, lazily, through msgSend—unused classes are never called.
  • +initialize may run multiple times because of inheritance, so if (self == [MyClass class]) checks are conventional.
  • Why swizzling belongs in +load: earliest timing, category-independent invocation, and guaranteed one-time execution
  • Overusing +load directly delays app startup—defer heavy initialization with lazy loading

Viewed alongside the other runtime articles (objc_msgSend, message forwarding, swizzling, and KVO), it becomes clear that the difference between +load and +initialize ultimately hinges on one axis: whether the call goes through msgSend. Once you understand the invocation path, you no longer need to memorize the rules.

Continue reading