MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Objective-C类方法+load与+initialize执行机制

2024-02-152.2k 阅读

一、Objective-C 类方法概述

在 Objective-C 中,类方法是属于类本身而不是类的实例的方法。类方法通常用于执行与类相关的通用操作,比如创建类的实例、访问类级别的共享资源等。定义类方法时,在方法声明和实现中使用 + 号前缀。例如,假设有一个 Person 类,我们可以定义如下类方法:

@interface Person : NSObject
+ (instancetype)personWithName:(NSString *)name;
@end

@implementation Person
+ (instancetype)personWithName:(NSString *)name {
    return [[self alloc] initWithName:name];
}
@end

这里的 + (instancetype)personWithName:(NSString *)name 就是一个类方法。通过这个类方法,我们可以方便地创建 Person 类的实例,而无需显式地调用 allocinit 方法。使用时可以这样调用:

Person *person = [Person personWithName:@"John"];

类方法在内存管理和代码组织方面有重要作用。它们使得与类相关的操作可以直接通过类名调用,提高了代码的可读性和可维护性。同时,类方法可以用于实现单例模式,例如常见的 sharedInstance 方法:

@interface Singleton : NSObject
+ (instancetype)sharedInstance;
@end

@implementation Singleton
static Singleton *sharedSingleton = nil;
+ (instancetype)sharedInstance {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        sharedSingleton = [[self alloc] init];
    });
    return sharedSingleton;
}
@end

在这个例子中,+ (instancetype)sharedInstance 类方法保证了无论在何处调用,都只会返回同一个 Singleton 类的实例,实现了单例模式。

二、+load 方法的执行机制

  1. +load 方法的定义与调用时机 +load 方法是在类或分类被加载到内存时调用。当程序启动时,runtime 会根据可执行文件的依赖关系,按顺序加载所有需要的类和分类,并为每个类和分类调用 +load 方法。这意味着,只要类或分类被引入到项目中,不管是否使用到,其 +load 方法都会被调用。

定义 +load 方法很简单,在类或分类的实现文件中添加如下代码:

@implementation Person
+ (void)load {
    NSLog(@"Person's +load method called");
}
@end
@interface Person (Category)
@end

@implementation Person (Category)
+ (void)load {
    NSLog(@"Person's category +load method called");
}
@end
  1. +load 方法的调用顺序
    • 类的 +load 方法调用顺序:runtime 按照类的继承体系从根类开始,自上而下调用每个类的 +load 方法。例如,如果有 Animal 类作为根类,Dog 类继承自 AnimalPuppy 类继承自 Dog,那么调用顺序为 [Animal load] -> [Dog load] -> [Puppy load]
    • 分类的 +load 方法调用顺序:分类的 +load 方法在其所属类的 +load 方法之后调用。如果一个类有多个分类,那么分类的 +load 方法按照编译顺序调用。

下面通过代码示例来验证这些顺序:

@interface Animal : NSObject
@end

@implementation Animal
+ (void)load {
    NSLog(@"Animal's +load method called");
}
@end

@interface Dog : Animal
@end

@implementation Dog
+ (void)load {
    NSLog(@"Dog's +load method called");
}
@end

@interface Puppy : Dog
@end

@implementation Puppy
+ (void)load {
    NSLog(@"Puppy's +load method called");
}
@end

@interface Dog (Category1)
@end

@implementation Dog (Category1)
+ (void)load {
    NSLog(@"Dog's Category1 +load method called");
}
@end

@interface Dog (Category2)
@end

@implementation Dog (Category2)
+ (void)load {
    NSLog(@"Dog's Category2 +load method called");
}
@end

main 函数中,不需要任何额外的调用,运行程序后,控制台输出如下:

Animal's +load method called
Dog's +load method called
Puppy's +load method called
Dog's Category1 +load method called
Dog's Category2 +load method called

这与我们前面所述的调用顺序完全一致。

  1. +load 方法的特性
    • 自动调用:无需手动调用 +load 方法,runtime 会自动管理其调用过程。
    • 线程安全:runtime 保证 +load 方法是线程安全的,多个线程加载类或分类时,不会出现竞态条件。这使得我们可以在 +load 方法中进行一些初始化操作,如注册通知、初始化共享资源等,不用担心线程安全问题。
    • 不遵循继承规则:子类重写 +load 方法不会影响父类 +load 方法的调用,父类的 +load 方法依然会按照继承体系的顺序被调用。即使子类没有显式实现 +load 方法,父类的 +load 方法也会被调用。

三、+initialize 方法的执行机制

  1. +initialize 方法的定义与调用时机 +initialize 方法在类或分类第一次接收到消息时被调用。注意,这里是第一次接收到消息,而不是加载到内存时。这意味着,如果一个类在整个程序运行过程中都没有被使用(即没有接收到任何消息),那么它的 +initialize 方法不会被调用。

定义 +initialize 方法如下:

@implementation Person
+ (void)initialize {
    if (self == [Person class]) {
        NSLog(@"Person's +initialize method called");
    }
}
@end

这里使用 if (self == [Person class]) 进行判断是为了防止子类继承并重写 +initialize 方法时,父类的 +initialize 方法被多次调用。因为当子类第一次接收到消息时,会先调用父类的 +initialize 方法,如果不进行判断,父类的 +initialize 方法可能会在子类调用时再次被执行。

  1. +initialize 方法的调用顺序
    • 类的 +initialize 方法调用顺序:与 +load 方法类似,+initialize 方法也是按照继承体系从根类开始调用。但是,只有当某个类第一次接收到消息时,它及其父类的 +initialize 方法才会被调用。例如,假设有 Vehicle 类作为根类,Car 类继承自 VehicleSportsCar 类继承自 Car。如果首先向 SportsCar 类发送消息,那么调用顺序为 [Vehicle initialize] -> [Car initialize] -> [SportsCar initialize]
    • 分类的 +initialize 方法调用顺序:分类的 +initialize 方法在类本身的 +initialize 方法之后调用。如果一个类有多个分类,同样按照编译顺序调用分类的 +initialize 方法。

通过以下代码示例来验证调用顺序:

@interface Vehicle : NSObject
@end

@implementation Vehicle
+ (void)initialize {
    if (self == [Vehicle class]) {
        NSLog(@"Vehicle's +initialize method called");
    }
}
@end

@interface Car : Vehicle
@end

@implementation Car
+ (void)initialize {
    if (self == [Car class]) {
        NSLog(@"Car's +initialize method called");
    }
}
@end

@interface SportsCar : Car
@end

@implementation SportsCar
+ (void)initialize {
    if (self == [SportsCar class]) {
        NSLog(@"SportsCar's +initialize method called");
    }
}
@end

@interface Car (Category1)
@end

@implementation Car (Category1)
+ (void)initialize {
    if (self == [Car class]) {
        NSLog(@"Car's Category1 +initialize method called");
    }
}
@end

@interface Car (Category2)
@end

@implementation Car (Category2)
+ (void)initialize {
    if (self == [Car class]) {
        NSLog(@"Car's Category2 +initialize method called");
    }
}
@end

main 函数中发送如下消息:

SportsCar *sportsCar = [SportsCar new];

控制台输出如下:

Vehicle's +initialize method called
Car's +initialize method called
SportsCar's +initialize method called
Car's Category1 +initialize method called
Car's Category2 +initialize method called
  1. +initialize 方法的特性
    • 延迟调用:只有在类或分类第一次接收到消息时才会调用,这使得我们可以将一些初始化操作延迟到真正需要使用该类时进行,从而提高程序的启动性能。
    • 线程安全:runtime 保证 +initialize 方法在多线程环境下也是安全的。在多线程同时访问一个未初始化的类时,只有一个线程会执行 +initialize 方法,其他线程会等待该线程执行完毕。
    • 遵循继承规则:子类重写 +initialize 方法会覆盖父类的 +initialize 方法。如果子类没有显式实现 +initialize 方法,当子类第一次接收到消息时,会调用父类的 +initialize 方法。

四、+load+initialize 方法的比较

  1. 调用时机
    • +load 方法在类或分类被加载到内存时立即调用,而 +initialize 方法在类或分类第一次接收到消息时才调用。这使得 +load 方法更适合用于一些全局的、与类本身相关的初始化操作,比如注册全局的通知中心、初始化共享资源等,因为这些操作需要在程序启动时就完成。而 +initialize 方法则适用于一些与类的使用相关的初始化操作,比如初始化类的静态变量等,这样可以避免在程序启动时进行不必要的初始化,提高启动速度。
  2. 调用顺序
    • 在类的继承体系中,+load 方法按照从根类到子类的顺序依次调用,而 +initialize 方法也是按照从根类到子类的顺序调用,但只有在类第一次接收到消息时才会触发。对于分类,+load 方法在所属类的 +load 方法之后按编译顺序调用,+initialize 方法同样在所属类的 +initialize 方法之后按编译顺序调用。
  3. 线程安全
    • 两者都由 runtime 保证线程安全。在多线程环境下,+load 方法可以放心地进行一些多线程共享资源的初始化操作,而 +initialize 方法也能确保在多个线程同时访问未初始化的类时,初始化操作的正确性。
  4. 继承与重写
    • +load 方法不遵循继承规则,子类重写 +load 方法不会影响父类 +load 方法的调用。而 +initialize 方法遵循继承规则,子类重写 +initialize 方法会覆盖父类的 +initialize 方法。如果子类没有实现 +initialize 方法,当子类接收到消息时会调用父类的 +initialize 方法。

通过下面的综合示例可以更清晰地看到它们的区别:

@interface BaseClass : NSObject
@end

@implementation BaseClass
+ (void)load {
    NSLog(@"BaseClass's +load method called");
}
+ (void)initialize {
    if (self == [BaseClass class]) {
        NSLog(@"BaseClass's +initialize method called");
    }
}
@end

@interface SubClass : BaseClass
@end

@implementation SubClass
+ (void)load {
    NSLog(@"SubClass's +load method called");
}
+ (void)initialize {
    if (self == [SubClass class]) {
        NSLog(@"SubClass's +initialize method called");
    }
}
@end

@interface BaseClass (Category)
@end

@implementation BaseClass (Category)
+ (void)load {
    NSLog(@"BaseClass's category +load method called");
}
+ (void)initialize {
    if (self == [BaseClass class]) {
        NSLog(@"BaseClass's category +initialize method called");
    }
}
@end

main 函数中:

SubClass *subClass = [SubClass new];

控制台输出:

BaseClass's +load method called
SubClass's +load method called
BaseClass's category +load method called
BaseClass's +initialize method called
SubClass's +initialize method called
BaseClass's category +initialize method called

从输出结果可以清晰地看到 +load+initialize 方法在调用时机和顺序上的不同。

五、实际应用场景

  1. +load 方法的应用场景
    • 全局初始化:例如在一个网络请求框架中,可以在 +load 方法中配置网络请求的基本参数,如设置 API 根地址、初始化网络会话等。这样在程序启动时,网络框架就已经准备好可以使用,而无需在每次使用网络请求时进行重复的初始化操作。
    • 注册通知:如果有一些全局的通知需要在程序启动时注册,可以在类的 +load 方法中完成。比如在一个应用程序中,有一个用于监听应用进入后台和前台的通知,就可以在相关类的 +load 方法中注册这些通知。
@implementation AppLifecycleObserver
+ (void)load {
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationDidEnterBackground:) name:UIApplicationDidEnterBackgroundNotification object:nil];
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationWillEnterForeground:) name:UIApplicationWillEnterForegroundNotification object:nil];
}
+ (void)applicationDidEnterBackground:(NSNotification *)notification {
    NSLog(@"Application entered background");
}
+ (void)applicationWillEnterForeground:(NSNotification *)notification {
    NSLog(@"Application will enter foreground");
}
@end
  1. +initialize 方法的应用场景
    • 类级别的懒加载:当一个类有一些静态变量或者需要在第一次使用时进行初始化的资源时,可以在 +initialize 方法中进行懒加载。例如,有一个日志记录类,其中有一个静态的日志文件对象,只有在第一次使用日志记录功能时才需要创建该文件对象,就可以在 +initialize 方法中实现。
@implementation Logger
static NSFileHandle *logFileHandle;
+ (void)initialize {
    if (self == [Logger class]) {
        NSString *logFilePath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject stringByAppendingPathComponent:@"log.txt"];
        NSFileManager *fileManager = [NSFileManager defaultManager];
        if (![fileManager fileExistsAtPath:logFilePath]) {
            [fileManager createFileAtPath:logFilePath contents:nil attributes:nil];
        }
        logFileHandle = [NSFileHandle fileHandleForWritingAtPath:logFilePath];
    }
}
+ (void)logMessage:(NSString *)message {
    NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
    [formatter setDateFormat:@"yyyy-MM-dd HH:mm:ss"];
    NSString *dateString = [formatter stringFromDate:[NSDate date]];
    NSString *logMessage = [NSString stringWithFormat:@"%@ - %@\n", dateString, message];
    [logFileHandle writeData:[logMessage dataUsingEncoding:NSUTF8StringEncoding]];
}
@end

在这个例子中,Logger 类的 +initialize 方法在第一次调用 Logger 类的任何方法(如 logMessage:)时会被调用,从而初始化日志文件对象。这样避免了在程序启动时就创建日志文件对象,只有在真正需要记录日志时才进行初始化。

六、注意事项

  1. +load+initialize 方法中避免复杂操作 虽然 +load+initialize 方法提供了方便的初始化时机,但应尽量避免在这些方法中进行复杂的计算或长时间的操作。+load 方法在程序启动时调用,如果其中有复杂操作,会影响程序的启动速度。+initialize 方法在类第一次接收到消息时调用,复杂操作可能会导致首次使用该类时出现卡顿。
  2. +initialize 方法中的 self 判断+initialize 方法中,一定要使用 if (self == [ClassName class]) 这样的判断,以防止父类的 +initialize 方法在子类调用时被重复执行。如果不进行这样的判断,可能会导致一些资源被重复初始化,引发意想不到的问题。
  3. 分类对 +load+initialize 方法的影响 当使用分类时,要注意分类的 +load+initialize 方法会在类本身的相应方法之后调用。如果分类的 +load+initialize 方法中修改了类的一些全局状态,可能会影响到类本身及其他分类的行为。因此,在编写分类的 +load+initialize 方法时,要充分考虑其对整个类体系的影响。

通过深入理解 +load+initialize 方法的执行机制,我们可以在 Objective-C 开发中更合理地进行类的初始化和资源管理,提高程序的性能和稳定性。在实际项目中,根据不同的需求和场景,正确选择使用 +load+initialize 方法,能够优化代码结构,使程序更加健壮和高效。无论是全局初始化、懒加载,还是处理类与分类之间的关系,这两个方法都为我们提供了强大的工具,只要善加利用,就能为开发带来诸多便利。