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

Objective-C运行时中的类加载与初始化流程详解

2022-08-124.1k 阅读

类加载的概述

在Objective-C运行时系统中,类加载是一个至关重要的过程。它负责将类的定义从磁盘上的二进制文件加载到内存中,并进行一系列的初始化准备工作,使得程序在运行时能够正确地使用这些类。

类加载的时机

类加载并非在程序启动时就一次性完成所有类的加载,而是采用了按需加载的策略。这意味着只有当程序首次使用某个类时,才会触发该类的加载过程。例如,当创建类的实例对象、访问类的类方法或者引用类的成员变量等操作发生时,如果该类尚未加载,运行时系统就会启动类加载流程。

类加载的具体流程

加载阶段

  1. 定位类的二进制文件:运行时系统首先要找到包含类定义的二进制文件。在iOS和macOS系统中,应用程序的二进制文件通常是Mach - O格式。运行时会根据类的名称在Mach - O文件的符号表中查找对应的类定义。例如,对于自定义的MyClass类,运行时会在符号表中搜索与MyClass相关的符号信息,以确定类定义在二进制文件中的位置。
  2. 映射二进制数据到内存:一旦确定了类定义在二进制文件中的位置,运行时系统会将相关的二进制数据映射到内存中。这一步使用了操作系统的内存映射机制,将文件的一部分直接映射到进程的虚拟地址空间。这样,程序就可以像访问内存一样直接访问类的定义数据,而无需额外的文件I/O操作,大大提高了访问效率。

链接阶段

  1. 符号绑定:在类的二进制数据加载到内存后,类中可能会引用其他类、函数或者变量等外部符号。符号绑定的目的就是将这些外部符号的引用与实际的内存地址关联起来。例如,一个类中调用了另一个类的方法,在编译时这个方法调用只是一个符号引用,在链接阶段,运行时系统会找到被调用方法的实际内存地址,并将其绑定到调用处,确保程序在运行时能够正确地执行方法调用。
  2. 合并常量和变量:类中定义的常量和变量也需要在链接阶段进行处理。运行时会将类的常量数据合并到进程的常量区,将变量数据合并到合适的内存区域(如数据段或堆)。对于全局变量,运行时会为其分配内存空间,并在程序启动时进行初始化。

初始化阶段

  1. 元类的初始化:在Objective - C中,每个类都有一个对应的元类(meta - class),元类存储了类方法的相关信息。在类初始化之前,首先要对元类进行初始化。元类的初始化过程包括设置元类的isa指针(指向根元类)、superclass指针(指向父类的元类)以及注册类方法等操作。
  2. 类的初始化:元类初始化完成后,开始类的初始化。这一步会初始化类的成员变量,执行类的+load方法(如果存在)。+load方法是在类被加载到内存时就会被调用,并且只会被调用一次。例如:
@implementation MyClass
+ (void)load {
    NSLog(@"MyClass's +load method is called.");
}
@end

在程序运行时,当MyClass类被加载,上述+load方法中的日志就会被输出。 3. 父类的初始化:如果当前类有父类,在完成自身的初始化后,会递归地调用父类的初始化方法,确保父类的成员变量和相关初始化逻辑也能正确执行。这样从根类开始,逐步向下初始化所有的类,构建起完整的类继承体系。

类初始化的深入探讨

+load方法的特性

  1. 调用顺序:+load方法的调用顺序是按照类的继承体系自顶向下进行的。先调用根类的+load方法,然后依次调用子类的+load方法。对于同一层级的类,其+load方法的调用顺序与编译顺序有关,通常按照编译顺序依次调用。例如:
@interface SuperClass : NSObject
@end
@implementation SuperClass
+ (void)load {
    NSLog(@"SuperClass's +load method.");
}
@end

@interface SubClass1 : SuperClass
@end
@implementation SubClass1
+ (void)load {
    NSLog(@"SubClass1's +load method.");
}
@end

@interface SubClass2 : SuperClass
@end
@implementation SubClass2
+ (void)load {
    NSLog(@"SubClass2's +load method.");
}
@end

在程序运行加载这些类时,会先输出SuperClass's +load method.,然后按照编译顺序输出SubClass1's +load method.或者SubClass2's +load method.。 2. 线程安全性:+load方法是线程安全的,运行时系统会确保在多线程环境下,每个类的+load方法只被调用一次,不会出现重复调用或者并发调用导致的数据竞争问题。

+initialize方法

  1. 调用时机:+initialize方法与+load方法不同,它不是在类加载时调用,而是在类或其任何子类第一次接收到消息时调用。例如:
@implementation MyClass
+ (void)initialize {
    if (self == [MyClass class]) {
        NSLog(@"MyClass's +initialize method is called.");
    }
}
@end

当程序中第一次向MyClass类或其子类发送消息时,上述+initialize方法会被调用。需要注意的是,这里通过if (self == [MyClass class])进行判断,是因为子类在调用+initialize方法时,self指向的是子类,这样可以避免子类重复调用父类的+initialize方法逻辑。 2. 调用机制:与+load方法自顶向下的调用顺序不同,+initialize方法是懒加载的,并且是基于消息发送机制。如果一个类没有实现+initialize方法,当第一次向该类发送消息时,运行时会沿着继承链向上查找,直到找到实现了+initialize方法的类并调用。例如,如果SubClass没有实现+initialize方法,而SuperClass实现了,当第一次向SubClass发送消息时,会调用SuperClass的+initialize方法。

类加载与初始化中的数据结构

objc_class结构体

在Objective - C运行时,objc_class结构体是类的核心数据结构,它包含了类的基本信息,如类名、超类指针、元类指针、实例变量列表、方法列表、协议列表等。以下是简化的objc_class结构体定义:

struct objc_class {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
    #if !defined(__arm64__)  &&  !defined(__x86_64__)
    Class _Nullable super_class                                        OBJC2_UNAVAILABLE;
    const char * _Nonnull name                                         OBJC2_UNAVAILABLE;
    long version                                                        OBJC2_UNAVAILABLE;
    long info                                                          OBJC2_UNAVAILABLE;
    long instance_size                                                  OBJC2_UNAVAILABLE;
    struct objc_ivar_list * _Nullable ivars                             OBJC2_UNAVAILABLE;
    struct objc_method_list * _Nullable * _Nullable methodLists          OBJC2_UNAVAILABLE;
    struct objc_cache * _Nonnull cache                                  OBJC2_UNAVAILABLE;
    struct objc_protocol_list * _Nullable protocols                     OBJC2_UNAVAILABLE;
    #endif
};
  1. isa指针:isa指针指向类的元类,通过isa指针,对象可以找到其所属类的元类,进而调用类方法。对于实例对象,isa指针指向实例对象所属的类;对于类对象,isa指针指向类的元类。
  2. super_class指针:指向该类的父类,如果该类是根类,super_class指针为NULL。通过super_class指针,运行时可以实现方法的动态查找和继承机制。
  3. methodLists:是一个指向方法列表的指针数组,每个元素指向一个objc_method_list结构体,该结构体包含了类的实例方法列表。通过这个结构,运行时可以快速定位到类的实例方法并进行调用。

objc_method结构体

objc_method结构体用于描述类的方法,它包含了方法的名称、方法的类型编码以及方法的实现函数指针。以下是其定义:

struct objc_method {
    SEL _Nonnull method_name                                         OBJC2_UNAVAILABLE;
    char * _Nullable method_types                                       OBJC2_UNAVAILABLE;
    IMP _Nonnull method_imp                                           OBJC2_UNAVAILABLE;
};
  1. method_name:是一个SEL类型(本质是一个typedef unsigned long SEL;),代表方法的选择器,用于唯一标识一个方法。不同类中相同名称的方法具有相同的选择器。
  2. method_types:是一个字符串,用于描述方法的参数类型和返回值类型,通过类型编码的方式表示。例如,@encode(void)表示返回值为void类型,@encode(int)表示返回值为int类型等。
  3. method_imp:是一个函数指针,指向方法的实际实现代码。当运行时找到对应的方法后,通过这个函数指针调用方法的实现。

元类的结构与作用

元类也是一个objc_class结构体实例,它与普通类的区别在于:元类存储的是类方法的相关信息。元类的isa指针指向根元类,根元类的isa指针指向自身,形成一个闭环。元类的super_class指针指向父类的元类,通过这种结构,类方法也能实现继承机制。例如,NSObject类的元类存储了+alloc+new等类方法,当自定义类继承自NSObject时,自定义类的元类会通过super_class指针继承NSObject元类的类方法。

类加载与初始化中的动态行为

动态添加类与方法

  1. 动态添加类:在Objective - C运行时,可以在运行时动态添加类。使用objc_allocateClassPair函数可以创建一个新的类对,包括类和元类。例如:
// 创建一个新类
Class newClass = objc_allocateClassPair([NSObject class], "MyDynamicClass", 0);
if (newClass) {
    // 为新类添加实例变量
    class_addIvar(newClass, "myIvar", sizeof(int), log2(sizeof(int)), @encode(int));
    // 注册新类
    objc_registerClassPair(newClass);
}

上述代码创建了一个继承自NSObject的新类MyDynamicClass,并为其添加了一个int类型的实例变量myIvar,最后通过objc_registerClassPair函数将新类注册到运行时系统中。 2. 动态添加方法:可以使用class_addMethod函数在运行时为类动态添加方法。例如:

// 定义方法实现
void dynamicMethodIMP(id self, SEL _cmd) {
    NSLog(@"Dynamic method is called.");
}

// 为类添加方法
BOOL added = class_addMethod([MyDynamicClass class], @selector(dynamicMethod), (IMP)dynamicMethodIMP, "v@:");
if (added) {
    // 调用动态添加的方法
    [(id)[MyDynamicClass new] performSelector:@selector(dynamicMethod)];
}

上述代码为MyDynamicClass类动态添加了一个名为dynamicMethod的方法,并在添加成功后调用了该方法。

方法交换

方法交换是Objective - C运行时的一个强大特性,可以在运行时交换两个方法的实现。这在AOP(面向切面编程)、方法拦截等场景中非常有用。例如,要交换UIViewControllerviewDidLoad方法和自定义的myViewDidLoad方法:

@implementation UIViewController (MethodSwizzling)
+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Method originalMethod = class_getInstanceMethod([self class], @selector(viewDidLoad));
        Method swizzledMethod = class_getInstanceMethod([self class], @selector(myViewDidLoad));
        method_exchangeImplementations(originalMethod, swizzledMethod);
    });
}

- (void)myViewDidLoad {
    // 自定义的逻辑
    NSLog(@"Swizzled viewDidLoad method.");
    // 调用原始的viewDidLoad方法
    [self myViewDidLoad];
}
@end

在上述代码中,通过method_exchangeImplementations函数交换了viewDidLoad方法和myViewDidLoad方法的实现。当UIViewController及其子类的viewDidLoad方法被调用时,实际上会执行myViewDidLoad方法中的逻辑,并且在myViewDidLoad方法中通过递归调用[self myViewDidLoad]来执行原始的viewDidLoad方法逻辑。

类加载与初始化中的异常处理

类加载失败的情况

  1. 类定义缺失:如果在Mach - O文件中找不到类的定义,例如类的二进制数据被损坏或者类名拼写错误,运行时会无法加载该类。这种情况下,程序可能会抛出异常或者导致未定义行为。例如,在导入第三方库时,如果库文件损坏,其中的某些类可能无法正常加载。
  2. 依赖类未加载:如果一个类依赖于其他类,而这些依赖类尚未加载,可能会导致类加载失败。例如,SubClass继承自SuperClass,如果SuperClass没有先被加载,当加载SubClass时可能会出现问题。在Objective - C运行时,通常会自动处理这种依赖关系,优先加载依赖类,但在一些复杂的动态加载场景下,可能会出现依赖加载顺序错误的情况。

初始化异常处理

  1. +load方法异常:如果在+load方法中发生异常,由于+load方法在类加载时就会被调用,可能会导致整个类加载过程中断,进而影响到依赖该类的其他部分。例如,在+load方法中进行数据库连接操作,如果连接失败抛出异常,可能会使依赖该类进行数据库操作的功能无法正常运行。为了避免这种情况,在+load方法中应该尽量避免复杂的、可能失败的操作,或者对可能的异常进行捕获处理。
  2. +initialize方法异常:+initialize方法在类第一次接收到消息时调用,如果在该方法中发生异常,会影响到类的正常使用。例如,在+initialize方法中初始化一些全局变量时出错,可能导致后续类的方法调用出现错误。同样,在+initialize方法中也应该对可能的异常进行妥善处理,确保类的初始化能够顺利完成。

通过深入理解Objective - C运行时中的类加载与初始化流程,开发者可以更好地掌握程序的运行机制,优化代码性能,处理复杂的动态行为以及解决潜在的异常问题,从而编写出更加健壮和高效的Objective - C程序。