iOS底层原理系列03-Objective-C运行时机制

发布于:2025-03-15 ⋅ 阅读:(13) ⋅ 点赞:(0)

1. Runtime 架构详解与内部工作机制

OC 作为一门动态语言,其强大的灵活性主要源于其底层运行时系统(Runtime)。Runtime 系统实质上是一套 C 和汇编语言编写的 API,它作为 OC 与底层系统之间的桥梁,使得 OC 拥有了动态特性。

OC Runtime 系统的核心架构由以下几个关键组件构成:

  1. Class 结构体:用于表示类的基本单元
  2. Object 结构体:表示对象实例
  3. Method 结构体:封装方法信息
  4. IMP 函数指针:指向方法的实际实现
  5. SEL 选择器:方法的唯一标识符

在 iOS 开发中,我们日常编写的 OC 代码最终都会转换为 Runtime 的 C 函数调用。这一转换过程由编译器和运行时系统共同完成。

在这里插入图片描述

现在,让我们来看一下 Runtime 系统中最核心的几个数据结构的定义:

// objc 对象结构
struct objc_object {
    Class isa;  // 指向类的指针
};

// 类结构
struct objc_class {
    Class isa;  // 元类指针
    Class super_class;  // 父类指针
    const char *name;  // 类名
    long version;  // 版本信息
    long info;  // 类信息
    long instance_size;  // 实例大小
    struct objc_ivar_list *ivars;  // 实例变量列表
    struct objc_method_list **methodLists;  // 方法列表
    struct objc_cache *cache;  // 方法缓存
    struct objc_protocol_list *protocols;  // 协议列表
};

// 方法结构
struct objc_method {
    SEL method_name;  // 方法选择器
    char *method_types;  // 方法类型编码
    IMP method_imp;  // 方法实现函数指针
};

Runtime 系统的核心工作原理围绕着这些结构体展开,通过它们实现了 OC 语言的动态特性,包括动态类型、动态绑定以及动态加载等功能。

2. 消息传递机制

2.1 objc_msgSend实现原理

OC 中的方法调用本质上是消息传递过程。当我们编写如下代码时:

[object method:argument];

编译器会将其转换为:

objc_msgSend(object, @selector(method:), argument);

objc_msgSend 函数是 Runtime 系统的核心,它的基本实现原理如下:

  1. 获取对象的 isa 指针,找到对应的类
  2. 在类的方法缓存中查找方法
  3. 如果缓存中找不到,则在方法列表中查找
  4. 如果当前类中找不到,则沿着继承链向上查找
  5. 如果继承链上都没有找到,则进入消息转发流程
  6. 找到方法后,执行对应的 IMP 函数指针

objc_msgSend 主要使用汇编语言实现,以保证最高的性能。其伪代码逻辑大致如下:

id objc_msgSend(id self, SEL _cmd, ...) {
    if (self == nil) return nil;
    
    // 查找实现
    IMP imp = lookUpImpOrForward(self, _cmd);
    
    // 执行方法实现,传递所有参数
    return imp(self, _cmd, ...);
}

2.2 方法查找流程分析

objc_msgSend 函数开始查找方法实现时,会按照以下顺序执行查找:
在这里插入图片描述

  1. 缓存查找:首先在对象所属类的方法缓存(cache)中查找,缓存使用哈希表实现,查找速度非常快。
  2. 当前类查找:如果缓存中没有找到,则在类的方法列表(methodLists)中查找。这个过程相对较慢,因为需要遍历方法列表。
  3. 父类链查找:如果在当前类中没有找到方法,则沿着继承链向上查找父类的方法,同样遵循缓存优先的原则。
  4. 动态解析:如果继承链上都没有找到对应方法,则会触发动态方法解析流程,调用 +resolveInstanceMethod:+resolveClassMethod: 方法。
  5. 消息转发:如果动态解析未能解决问题,则进入完整的消息转发流程。

方法查找的源码实现位于 objc-runtime-new.mm 文件中的 lookUpImpOrForward 函数:

IMP lookUpImpOrForward(id obj, SEL sel, Class cls, int behavior) {
    // 缓存查找
    if (cache_getImp(cls, sel, &obj)) {
        return (IMP)imp;
    }
    
    // 类方法列表查找
    method_t *m = getMethodNoSuper_nolock(cls, sel);
    if (m) return m->imp;
    
    // 父类查找
    for (Class curClass = cls->superclass; curClass; curClass = curClass->superclass) {
        // 检查父类缓存
        if (cache_getImp(curClass, sel, &obj)) {
            return (IMP)imp;
        }
        
        // 检查父类方法列表
        method_t *m = getMethodNoSuper_nolock(curClass, sel);
        if (m) return m->imp;
    }
    
    // 方法未找到,进入转发流程
    return _objc_msgForward;
}

2.3 消息转发完整流程

当 Runtime 系统在对象的继承链上都未能找到对应的方法实现时,会启动消息转发机制,这是 OC 实现动态特性的重要一环。完整的消息转发流程分为三个阶段:

在这里插入图片描述

第一阶段:动态方法解析

首先,Runtime 系统会调用 +resolveInstanceMethod:+resolveClassMethod: 方法(取决于是实例方法还是类方法),给对象所属类一次动态添加方法实现的机会。

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(dynamicMethod)) {
        class_addMethod(self, sel, (IMP)dynamicMethodIMP, "v@:");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

// 动态添加的方法实现
void dynamicMethodIMP(id self, SEL _cmd) {
    NSLog(@"动态添加的方法实现");
}

如果方法解析成功返回 YES,那么消息发送过程会重新开始,再次查找方法实现。

第二阶段:快速转发

如果动态方法解析未能解决问题,则进入快速转发阶段。Runtime 系统会调用 -forwardingTargetForSelector: 方法,询问对象是否有其他对象可以处理这个消息。

- (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(someMethod)) {
        // 返回能够处理该消息的对象
        return alternativeObject;
    }
    return [super forwardingTargetForSelector:aSelector];
}

这一阶段的性能相对较高,因为 Runtime 系统会直接将消息重定向到目标对象,而不会创建完整的消息转发机制。

第三阶段:标准转发

如果前两个阶段都未能解决问题,则进入标准转发阶段。这一阶段会进行更完整的消息转发处理,但性能开销较大。

首先,Runtime 系统会调用 -methodSignatureForSelector: 方法获取方法签名:

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if (aSelector == @selector(someMethod)) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return [super methodSignatureForSelector:aSelector];
}

然后,根据方法签名创建 NSInvocation 对象,并调用 -forwardInvocation: 方法:

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    SEL selector = [anInvocation selector];
    
    if ([alternativeObject respondsToSelector:selector]) {
        [anInvocation invokeWithTarget:alternativeObject];
    } else {
        [super forwardInvocation:anInvocation];
    }
}

如果所有转发流程都失败了,则会调用 -doesNotRecognizeSelector: 方法,通常会导致应用崩溃并抛出异常。

2.4 方法缓存优化机制

为了提高方法调用的性能,OC Runtime 系统在类结构中实现了方法缓存机制。每个类都有一个 cache_t 类型的 cache 字段,用于存储曾经调用过的方法。

struct cache_t {
    struct bucket_t *_buckets;
    mask_t _mask;
    mask_t _occupied;
};

struct bucket_t {
    cache_key_t _key;  // SEL
    IMP _imp;  // 实现函数指针
};

方法缓存使用了哈希表实现,查找效率为 O(1),大大提升了频繁调用方法的性能。缓存机制的基本原理是:

  1. 空间换时间:使用额外的内存空间来存储已经查找过的方法,避免重复的方法查找过程。
  2. 哈希算法:通过 SEL 选择器计算哈希值,快速定位到对应的桶(bucket)。
  3. 动态扩容:当缓存中的元素数量达到阈值时,会触发扩容操作,重新分配更大的内存空间。
  4. 局部性原理:利用程序的局部性原理,即同一个方法可能会被连续多次调用,或者某些方法会被频繁调用。

当类接收到新消息时,会先在缓存中查找,如果找到则直接使用缓存的实现;如果未找到,则会在方法列表中查找,并将结果存入缓存,以便下次快速访问。

3. Runtime API实现

3.1 动态方法解析

动态方法解析是 OC Runtime 系统提供的一种在运行时添加方法实现的机制。通过实现 +resolveInstanceMethod:+resolveClassMethod: 方法,可以在对象接收到未实现的消息时动态添加方法实现。

核心 API 是 class_addMethod,它允许我们在运行时向类添加方法:

BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types);

参数说明:

  • cls:要添加方法的类
  • name:方法的选择器(SEL)
  • imp:方法的实现函数指针(IMP)
  • types:方法的类型编码,描述参数和返回值类型

实际应用示例:

@implementation MyClass

// 动态方法解析
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(dynamicMethod)) {
        class_addMethod(self, 
                       sel, 
                       (IMP)dynamicMethodImplementation, 
                       "v@:");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

@end

// C 函数作为方法实现
void dynamicMethodImplementation(id self, SEL _cmd) {
    NSLog(@"这是动态添加的方法实现");
}

这种机制常用于实现 @dynamic 属性的存取方法、模拟多重继承以及实现消息转发等高级功能。

3.2 方法替换与交换技术

OC Runtime 系统提供了方法替换和交换的 API,这些技术常用于实现方法拦截、AOP(面向切面编程)以及为已有方法添加功能等场景。

3.2.1 方法替换(Method Swizzling)

最常用的方法交换 API 是 method_exchangeImplementations

void method_exchangeImplementations(Method m1, Method m2);

这个函数会交换两个方法的实现,使得调用一个方法时会执行另一个方法的实现,反之亦然。

使用示例:

@implementation UIViewController (Tracking)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];
        
        // 获取原始方法和自定义方法
        SEL originalSelector = @selector(viewDidAppear:);
        SEL swizzledSelector = @selector(swizzled_viewDidAppear:);
        
        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
        
        // 先尝试添加方法实现
        BOOL didAddMethod = class_addMethod(class,
                                           originalSelector,
                                           method_getImplementation(swizzledMethod),
                                           method_getTypeEncoding(swizzledMethod));
        
        if (didAddMethod) {
            // 如果添加成功,说明原方法不存在,则替换新增方法的实现
            class_replaceMethod(class,
                               swizzledSelector,
                               method_getImplementation(originalMethod),
                               method_getTypeEncoding(originalMethod));
        } else {
            // 如果添加失败,说明原方法已存在,则交换两个方法的实现
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    });
}

// 自定义方法实现
- (void)swizzled_viewDidAppear:(BOOL)animated {
    // 调用原始实现
    [self swizzled_viewDidAppear:animated];
    
    // 添加自定义行为
    NSLog(@"视图控制器 %@ 已显示", self);
}

@end

方法替换的常见应用场景:

  • 为系统方法添加额外功能(如日志记录、性能监控)
  • 修复系统库的 bug
  • 实现全局的 AOP 功能
  • 在不修改原始类的情况下扩展功能

3.2.2 方法替换的实现原理

method_exchangeImplementations 内部实现相对简单,它交换两个 Method 结构体中的 IMP 指针:

void method_exchangeImplementations(Method m1, Method m2) {
    if (!m1 || !m2) return;
    
    rwlock_writer_t lock(runtimeLock);
    
    IMP imp1 = method_getImplementation(m1);
    IMP imp2 = method_getImplementation(m2);
    
    method_setImplementation(m1, imp2);
    method_setImplementation(m2, imp1);
}

需要注意的是,方法交换操作应当在 +load 方法中进行,因为这个方法在类被加载时调用,可以确保在应用启动后、任何其他代码执行前完成方法交换。

3.3 分类与扩展底层区别

OC 中的分类(Category)和扩展(Extension)是两种常用的类扩展机制,虽然在语法上相似,但在 Runtime 底层实现上有着本质区别。

3.3.1 分类(Category)

分类在底层由 category_t 结构体表示:

struct category_t {
    const char *name;  // 分类名
    classref_t cls;  // 分类所属的类
    struct method_list_t *instanceMethods;  // 实例方法列表
    struct method_list_t *classMethods;  // 类方法列表
    struct protocol_list_t *protocols;  // 协议列表
    struct property_list_t *instanceProperties;  // 属性列表
    // Fields below this point are available with MAC_OS_X_VERSION_10_7 or later
    struct property_list_t *_classProperties;  // 类属性列表
};

分类的关键特点:

  1. 生命周期:分类在运行时动态加载,编译后会生成独立的目标文件(.o 文件)。
  2. 加载过程:在 Runtime 初始化过程中,系统会调用 _objc_init 函数,进而调用 map_images_read_images 函数,将分类中的方法、属性、协议等附加到原始类中。
  3. 方法冲突:如果分类中的方法与原类方法同名,分类方法会"覆盖"原类方法。这里的"覆盖"是通过方法列表的合并顺序实现的,分类的方法被放在原类方法列表的前面,导致在查找时优先找到分类方法。
  4. 多个分类:如果多个分类中存在同名方法,后编译的分类会"覆盖"先编译的分类(与编译顺序有关,可在 Build Phases -> Compile Sources 中查看)。
  5. 限制:分类不能添加实例变量,因为这会改变对象的内存布局,影响已创建的对象。

3.3.2 扩展(Extension)

扩展在底层没有单独的数据结构表示,它本质上是一种特殊的匿名分类,但只在编译期生效。

扩展的关键特点:

  1. 生命周期:扩展在编译期就与原类合并,不会生成独立的目标文件。
  2. 合并过程:编译器会将扩展中的内容直接合并到原类的定义中,就像这些内容本来就写在原类中一样。
  3. 实例变量:扩展可以添加实例变量,因为它在编译期就确定了对象的内存布局。
  4. 访问控制:扩展可以为原类添加私有方法和属性,这些方法和属性在类的实现文件中可见,但对外不可见。
  5. 限制:扩展只能为有源码的类添加功能,不能对系统的类或第三方库的类使用扩展。

3.3.3 底层实现区别总结

  1. 存在形式:分类由 category_t 结构体表示,而扩展没有单独的结构体。
  2. 加载时机:分类在运行时动态加载,扩展在编译期就与原类合并。
  3. 实例变量:分类不能添加实例变量,扩展可以添加实例变量。
  4. 方法覆盖:分类可以覆盖原类方法,多个分类之间按编译顺序覆盖;扩展的方法和原类方法等价,不存在覆盖关系。
  5. 源码要求:分类可以为任何类添加功能,扩展只能为有源码的类添加功能。

4. Runtime 操作的线程安全策略

OC Runtime 系统在设计中考虑了多线程环境,但并非所有 Runtime 操作都是线程安全的。了解 Runtime 的线程安全策略对于开发多线程应用至关重要。

4.1 Runtime 系统中的锁机制

Runtime 系统主要通过以下几种锁机制保证线程安全:

  1. 全局 Runtime 锁(RuntimeLock):保护全局数据结构,如类列表、方法缓存等。
  2. 自旋锁(spinlock):用于保护方法缓存等轻量级操作。
  3. 读写锁(rwlock):允许多读单写,用于优化某些常见的读取操作。
  4. OSSpinLock/os_unfair_lock:用于保护对象的 isa 指针等关键字段。

4.2 线程安全的 Runtime 操作

以下 Runtime 操作在内部实现了线程安全保障:

  1. 类注册与初始化objc_registerClassPairobjc_getClass 等函数。
  2. 消息发送objc_msgSend 及其变体是线程安全的。
  3. 方法查找class_getInstanceMethodclass_getClassMethod 等。
  4. 属性访问class_getPropertyprotocol_getProperty 等。
  5. 对象分配与释放object_getClassobject_setClass 等。

4.3 非线程安全的 Runtime 操作

以下 Runtime 操作需要开发者自行保证线程安全:

  1. 动态添加方法class_addMethod 应避免在多线程环境中同时向同一个类添加方法。
  2. 方法交换method_exchangeImplementations 不应在多线程环境中并发执行。
  3. 关联对象操作objc_setAssociatedObjectobjc_getAssociatedObject 在并发访问同一个对象的同一个键时可能不安全。
  4. 动态创建类objc_allocateClassPair 不应在多线程环境中并发创建具有相同名称的类。
  5. Category 加载:不应在运行时动态加载 Category,这可能导致未定义行为。

4.4 线程安全案例

在 +load 方法中执行方法交换+load 方法在主线程的应用启动时调用,是执行方法交换的安全时机。

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        // 执行方法交换
        Method originalMethod = class_getInstanceMethod(self, @selector(originalMethod));
        Method swizzledMethod = class_getInstanceMethod(self, @selector(swizzledMethod));
        method_exchangeImplementations(originalMethod, swizzledMethod);
    });
}

使用 dispatch_once 确保初始化代码只执行一次

+ (void)initialize {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        // 执行一次性初始化代码
        // 如添加方法、创建动态类等
    });
}

为动态类操作添加线程同步

static OSSpinLock lock = OS_SPINLOCK_INIT;

// 线程安全地添加方法
OSSpinLockLock(&lock);
class_addMethod(cls, selector, implementation, types);
OSSpinLockUnlock(&lock);

避免运行时修改系统类:修改系统类的行为可能导致与系统框架的并发冲突,应谨慎使用。

优先使用高级 API:尽可能使用 OC 的高级 API,它们通常已实现内部线程安全。

总结

OC Runtime 系统是这门语言动态特性的基石,Runtime 系统的核心价值在于它赋予了静态编译语言以动态特性,使得 OC 能够支持反射、动态加载、消息转发等高级功能。这些特性为 iOS 和 macOS 开发提供了强大的工具,也为框架开发者提供了极大的灵活性。

然而,与强大的能力相伴的往往是复杂性和潜在风险。过度依赖 Runtime 系统的动态特性可能导致代码难以理解和维护,甚至带来性能和安全隐患。在实际开发中,我们应当在灵活性和可维护性之间寻找适当的平衡点,合理使用 Runtime 系统提供的能力。