资讯动态

iOS Runtime机制深度解析:从objc_msgSend到消息转发实战应用

发布时间:2026/8/18 4:25:42 来源:尧图企业网站定制
1. 项目概述为什么我们需要深入理解Runtime如果你在iOS开发领域摸爬滚打超过一年却还没被“Runtime”这个词“折磨”过那你的开发经历可能是不完整的。这听起来有点夸张但事实是无论你是想解决一个诡异的崩溃实现一个优雅的AOP切面还是仅仅想理解KVO/KVC背后的魔法最终都会撞上Runtime这堵墙。很多人对它的第一印象是“底层”、“复杂”、“黑魔法”然后选择敬而远之依赖于网上零散的代码片段去实现一些高级功能。但我要告诉你这种“拿来主义”在初期或许能解决问题长期来看却会让你在排查深层Bug或设计灵活架构时寸步难行。这个内容就是为你打破这堵墙准备的。它不是一份冷冰冰的官方文档翻译也不是一堆晦涩术语的堆砌。我会从一个多年一线开发者的视角带你从“用”的角度反推“理”把Runtime那些看似高深的概念掰开揉碎成一个个你日常开发中真实遇到的场景。我们的目标很明确让你不仅能看懂Runtime相关的代码更能自信地在合适的地方使用它甚至创造性地解决那些看似无解的问题。无论你是刚刚对objc_msgSend感到好奇的中级开发者还是希望系统梳理知识体系的高级工程师这里都有你想要的干货。2. Runtime核心概念与设计思想拆解2.1 Objective-C语言的动态基石在开始摆弄那些具体的API之前我们必须先建立正确的认知Runtime不是Objective-C的一个“功能”或“库”它就是Objective-C语言本身。C语言是静态的函数调用在编译时就已经确定地址。而Objective-C之所以能实现消息转发、方法交换、动态添加属性等特性其灵魂就在于这套在程序运行时才决定如何执行的机制。你可以把Runtime想象成Objective-C对象的“操作系统”。每一个Objective-C对象在内存中除了包含我们定义的实例变量ivars还有一个至关重要的指针——isa。在传统的、我们最熟悉的Runtime版本中通常指“Legacy”或“非Tagged Pointer”情形下这个isa指针指向的是这个对象的类对象Class。而类对象本身也是一个对象它内部存储了该类的所有信息实例方法列表、属性列表、协议列表、成员变量列表以及一个指向其父类类对象的指针superclass。这就构成了一条沿着继承链向上追溯的完整信息链。当你写下[receiver message]这行代码时编译器并不会直接把它编译成一个函数调用。相反它会被转换成对objc_msgSend函数的调用objc_msgSend(receiver, selector(message))。这个函数是Runtime引擎的入口它的工作就是根据receiver的isa指针找到类对象然后在该类的方法列表以及其父类链的方法列表中查找与selector匹配的方法实现IMP一个指向函数实际地址的指针。如果找到就跳转执行如果找不到就会启动一套名为“消息转发”Message Forwarding的复杂补救流程。理解objc_msgSend的查找流程是理解所有Runtime高级技巧的基石。2.2 关键数据结构解析Class, Method, Ivar, PropertyRuntime的世界是由一系列C语言结构体定义的。虽然我们日常用Objective-C语法但底层操作的都是这些结构体。了解它们你才能看懂那些底层API。Class (objc_class)类的运行时表示。它包含isa: 指向该类的元类Meta Class。元类存储了类方法方法。这解释了为什么实例方法存在类对象里而类方法存在元类里——一切都是对象包括类本身。superclass: 指向父类的类对象构成继承链。cache: 方法缓存。为了提升objc_msgSend的查找速度最近调用过的方法的selector和IMP会被缓存到这里。这是Runtime性能优化的关键。bits: 一个指向class_data_bits_t的指针它包含了类的大部分核心数据需要通过特定的掩码如data()来获取。class_rw_t *data(): 通过bits获取的可读写数据区包含了类在运行期可变的信息如方法列表、属性列表、协议列表等。这里的“可读写”是重点它是我们进行方法交换method swizzling等操作的战场。class_ro_t *ro(): 只读数据区存储了编译时确定的原始信息如类名、实例变量布局等。Method (objc_method/Method)代表一个方法。它主要包含SEL name: 方法选择器可以理解为方法名的唯一ID。selector()返回的就是它。不同类的相同方法名其SEL是相同的这保证了多态性。const char *types: 方法类型编码。这是一个非常重要的字符串它描述了方法的返回值类型和所有参数类型。例如“v:”表示返回void参数依次是idself和SEL_cmd。在进行动态方法解析或消息转发时你必须正确理解和使用类型编码。IMP imp: 方法实现的实际函数指针。它的函数签名总是至少包含两个隐藏参数id self和SEL _cmd。Ivar (objc_ivar/Ivar)代表一个实例变量。它包含了变量名、类型编码和偏移量。通过Runtime可以获取和修改实例变量的值但直接访问Ivar会破坏封装性通常只在调试、序列化或实现某些特殊功能如模型转换时使用。Property (objc_property/objc_property_t)代表一个属性。它是getter、setter方法和关联的实例变量Ivar的合成体。通过Runtime可以获取属性的名称、特性nonatomic, strong, copy等字符串。注意现代Runtime尤其是64位系统后引入了“Non-pointer isa”和“Tagged Pointer”等优化isa指针不再单纯是一个地址而是利用多余的比特位存储了引用计数、是否有关联对象等信息。但这并不影响我们基于isa-Class-method list这个核心逻辑链的理解。在操作时Runtime API已经为我们屏蔽了这些底层差异。3. Runtime核心API详解与实战演练了解了基本原理我们就要上手了。Runtime提供了一套丰富的C语言API让我们可以在运行时“为所欲为”。下面我将这些API分为几大类并结合代码示例讲解。3.1 类与对象操作这类API允许我们动态地创建类、添加方法、添加属性或者查询类的信息。#import objc/runtime.h #import objc/message.h // 1. 动态创建一个新类 Class newClass objc_allocateClassPair([NSObject class], “MyDynamicClass”, 0); // 参数父类类名额外的字节通常为0 // 2. 为这个类添加一个实例变量 class_addIvar(newClass, “_dynamicProperty”, sizeof(id), log2(sizeof(id)), encode(id)); // 参数类变量名大小对齐方式类型编码 // 3. 为这个类添加一个方法 void dynamicMethodIMP(id self, SEL _cmd) { NSLog(“动态添加的方法被调用了”); } class_addMethod(newClass, selector(dynamicMethod), (IMP)dynamicMethodIMP, “v:”); // 参数类选择器实现类型编码 // 4. 注册这个类使其可用 objc_registerClassPair(newClass); // 5. 使用这个类 id dynamicObject [[newClass alloc] init]; [dynamicObject performSelector:selector(dynamicMethod)]; // 输出动态添加的方法被调用了 // 6. 获取类的所有属性包括从父类继承的 unsigned int count; objc_property_t *properties class_copyPropertyList([UIView class], count); for (unsigned int i 0; i count; i) { const char *propertyName property_getName(properties[i]); NSLog(“属性名%s”, propertyName); } free(properties); // 记得释放拷贝的内存实操心得class_addMethod的最后一个参数types字符串很容易写错。一个实用的技巧是先在一个已有的类里用encode()和method_getTypeEncoding获取正确的方法编码再照葫芦画瓢。动态创建的类在使用完毕后如果确定不再需要可以调用objc_disposeClassPair但在ARC环境下需极其小心确保没有任何实例存活。3.2 方法操作与交换Method Swizzling这是Runtime中最著名也最容易被误用的特性。Method Swizzling 的核心是交换两个方法的IMP实现。// 安全的 Method Swizzling 实现 (void)load { static dispatch_once_t onceToken; dispatch_once(onceToken, ^{ Class class [self class]; SEL originalSelector selector(viewWillAppear:); SEL swizzledSelector selector(my_viewWillAppear:); 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) { // 添加成功原方法在父类本类现在有了一个指向新实现的 originalSelector。 // 接下来将 swizzledSelector 的实现替换为父类的原始实现。 class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod)); } else { // 添加失败原方法已在本类实现。直接交换两个方法的实现即可。 method_exchangeImplementations(originalMethod, swizzledMethod); } }); } - (void)my_viewWillAppear:(BOOL)animated { // 在调用原始实现前后插入自定义逻辑 NSLog(“% 的 viewWillAppear: 被调用”, NSStringFromClass([self class])); [self my_viewWillAppear:animated]; // 由于方法已交换这里实际调用的是原始的 viewWillAppear: }注意事项这是重中之重在load方法中进行load方法在类被加载到运行时调用且只调用一次是进行Swizzling最安全的地方。绝对不要在initialize中做因为其调用时机不确定且可能多次调用。使用dispatch_once确保交换操作只执行一次防止多线程下的重复交换导致混乱。总是尝试class_addMethod如上例所示这是处理继承链上方法的安全做法。直接对可能来自父类的方法使用method_exchangeImplementations可能会意外地交换到父类的实现影响所有继承自该父类的子类这是灾难性的。谨慎命名像示例中的my_前缀可以避免方法名冲突和无限递归。不要滥用Method Swizzling 破坏了代码的清晰性和可预测性使得调试变得困难。它应作为最后的手段主要用于全局性的、无侵入的监控如日志、埋点或修复系统Bug。3.3 关联对象Associated Objects这是为已存在的类动态添加“属性”的标准方式。其本质是在对象上挂载一个键值对字典。// 定义关联的键通常使用静态变量的地址这能保证键的唯一性 static void *kMyAssociatedObjectKey kMyAssociatedObjectKey; // 设置关联对象 objc_setAssociatedObject(targetObject, kMyAssociatedObjectKey, associatedValue, OBJC_ASSOCIATION_RETAIN_NONATOMIC); // 内存管理语义 // OBJC_ASSOCIATION_ASSIGN: 弱引用类似assign // OBJC_ASSOCIATION_RETAIN_NONATOMIC: 强引用非原子类似strong, nonatomic // OBJC_ASSOCIATION_COPY_NONATOMIC: 拷贝非原子类似copy, nonatomic // OBJC_ASSOCIATION_RETAIN/COPY: 原子版本 // 获取关联对象 id value objc_getAssociatedObject(targetObject, kMyAssociatedObjectKey); // 移除关联对象设为nil即可 objc_setAssociatedObject(targetObject, kMyAssociatedObjectKey, nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC);使用场景为系统类如UIView、UIButton添加自定义数据块在分类Category中“模拟”添加存储属性实现轻量级的装饰器模式。3.4 消息转发机制深度剖析当objc_msgSend在类及其父类链中找不到方法实现时Runtime并不会立即崩溃而是给予对象三次补救的机会。这是一个层层递进的强大机制。第一步动态方法解析Dynamic Method Resolution// 类方法resolveInstanceMethod: 实例方法或 resolveClassMethod: 类方法 (BOOL)resolveInstanceMethod:(SEL)sel { if (sel selector(someUnknownMethod)) { // 动态地为这个selector添加一个实现 class_addMethod([self class], sel, (IMP)dynamicIMP, “v:”); return YES; // 返回YES表示已处理 } return [super resolveInstanceMethod:sel]; }这一步是给类一个机会在运行时动态添加一个方法来响应未知的消息。第二步快速转发Fast Forwarding如果第一步返回NORuntime会询问“有没有其他对象能处理这条消息”- (id)forwardingTargetForSelector:(SEL)aSelector { if (aSelector selector(someUnknownMethod)) { // 返回另一个拥有该方法的对象消息将原封不动地转发给它 return [[SomeOtherClass alloc] init]; } return [super forwardingTargetForSelector:aSelector]; }这一步通常用于实现简单的代理或组合模式将消息转给一个备用对象。第三步完整转发Normal Forwarding如果前两步都未处理就启动代价最高但最灵活的完整转发流程。Runtime先调用- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector获取方法签名。你必须返回一个有效的方法签名否则流程终止并抛出doesNotRecognizeSelector:异常。如果返回了有效签名Runtime会调用- (void)forwardInvocation:(NSInvocation *)anInvocation。NSInvocation是一个封装了目标、选择器、参数和返回值的对象。你可以在这里做任何事修改目标、修改参数、甚至完全不调用原始方法。- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { if (aSelector selector(someUnknownMethod:)) { // 手动构造或从其他类获取方法签名 return [NSMethodSignature signatureWithObjCTypes:“v:”]; // 返回void参数为id, SEL, id } return [super methodSignatureForSelector:aSelector]; } - (void)forwardInvocation:(NSInvocation *)anInvocation { if ([anInvocation selector] selector(someUnknownMethod:)) { // 可以在这里改变调用目标 id target [[SomeOtherClass alloc] init]; [anInvocation invokeWithTarget:target]; // 也可以修改参数 NSString *newArg “修改后的参数”; [anInvocation setArgument:newArg atIndex:2]; // 0:self, 1:_cmd, 2:第一个参数 [anInvocation invokeWithTarget:target]; // 甚至可以获取返回值 NSString *returnValue; [anInvocation getReturnValue:returnValue]; NSLog(“返回值%”, returnValue); } else { [super forwardInvocation:anInvocation]; } }完整转发非常强大可以实现真正的“消息代理”、AOP、甚至模拟多重继承。NSInvocation是理解这一步的关键它让消息变成了一个可以存储、修改和重放的对象。4. Runtime在真实项目中的高阶应用场景理解了API我们来看看如何用它们解决实际问题。以下场景均来自我经历的真实项目。4.1 场景一无侵入的全局行为监控与日志这是Method Swizzling最经典也最合理的用途。假设我们需要在不修改任何业务代码的情况下统计所有ViewController的生命周期和用户点击事件。// UIViewControllerLogging.h interface UIViewController (Logging) end // UIViewControllerLogging.m implementation UIViewController (Logging) (void)load { static dispatch_once_t onceToken; dispatch_once(onceToken, ^{ swizzleMethod([self class], selector(viewDidAppear:), selector(logging_viewDidAppear:)); swizzleMethod([self class], selector(viewDidDisappear:), selector(logging_viewDidDisappear:)); }); } void swizzleMethod(Class class, SEL originalSelector, SEL swizzledSelector) { // ... 使用前面提到的安全Swizzling实现 ... } - (void)logging_viewDidAppear:(BOOL)animated { // 上报日志 [MyAnalytics logEvent:“page_view” params:{“page”: NSStringFromClass([self class])}]; // 调用原始实现 [self logging_viewDidAppear:animated]; } - (void)logging_viewDidDisappear:(BOOL)animated { NSTimeInterval duration ... // 计算页面停留时长 [MyAnalytics logEvent:“page_leave” params:{“page”: NSStringFromClass([self class]), “duration”: (duration)}]; [self logging_viewDidDisappear:animated]; }对于UIButton的点击事件可以类似地SwizzlesendAction:to:forEvent:方法。这样做的好处是解耦彻底业务开发人员完全感知不到监控的存在新增页面也会自动被覆盖。4.2 场景二安全的模型转换与JSON解析手动编写-initWithDictionary:和-toDictionary方法枯燥且易错。利用Runtime可以自动完成属性映射。// NSObjectJSONModel.h - (instancetype)initWithDictionary:(NSDictionary *)dict; - (NSDictionary *)toDictionary; // NSObjectJSONModel.m - (instancetype)initWithDictionary:(NSDictionary *)dict { self [self init]; if (self [dict isKindOfClass:[NSDictionary class]]) { unsigned int count; objc_property_t *properties class_copyPropertyList([self class], count); for (int i 0; i count; i) { const char *propertyName property_getName(properties[i]); NSString *key [NSString stringWithUTF8String:propertyName]; id value dict[key]; if (value nil || [value isKindOfClass:[NSNull class]]) { continue; // 字典中无对应值跳过 } // 获取属性特性判断是否是对象类型以及内存管理语义 const char *attributes property_getAttributes(properties[i]); NSString *attributeStr [NSString stringWithUTF8String:attributes]; // 简单的类型判断实际项目需要更完善的解析 if ([attributeStr containsString:“T\”NSString\””]) { // NSString类型 if ([value isKindOfClass:[NSString class]]) { [self setValue:value forKey:key]; } else if ([value respondsToSelector:selector(stringValue)]) { [self setValue:[value stringValue] forKey:key]; } } else if ([attributeStr containsString:“T\”NSNumber\””]) { // NSNumber类型 // ... 处理数字转换 } else if ([attributeStr containsString:“T”]) { // 其他对象类型尝试KVC赋值前提是value类型匹配 try { [self setValue:value forKey:key]; } catch (NSException *exception) { NSLog(“设置属性 % 时出错: %”, key, exception); } } // 基础数据类型int, float等需要更复杂的处理此处略 } free(properties); } return self; }避坑技巧需要处理嵌套模型字典套字典对应一个自定义Model属性。可以通过判断属性类型是否是自定义类并递归调用initWithDictionary:。需要处理数组属性数组内元素也可能是模型。可以约定一个协议让模型类声明数组中元素的类如 (NSDictionary *)arrayElementClassMapping。关键点对于id或NSObject *类型的属性直接使用setValue:forKey:是危险的如果value类型不匹配可能崩溃。更安全的做法是结合类型编码进行严格的类型检查或使用NSKeyedArchiver进行深拷贝。成熟的库如YYModel、MJExtension内部都做了大量此类容错处理。4.3 场景三实现轻量级的AOP面向切面编程AOP的核心是将散布在多个地方的横切关注点如日志、权限、事务集中管理。利用消息转发我们可以实现一个简单的AOP拦截器。// AOPInterceptor.h interface AOPInterceptor : NSProxy property (nonatomic, weak) id target; // 真实对象 property (nonatomic, strong) NSMutableDictionary *preHandlerBlocks; // 方法执行前的Block property (nonatomic, strong) NSMutableDictionary *postHandlerBlocks; // 方法执行后的Block - (void)registerPreHandler:(void(^)(NSInvocation *inv))block forSelector:(SEL)sel; - (void)registerPostHandler:(void(^)(NSInvocation *inv))block forSelector:(SEL)sel; end // AOPInterceptor.m implementation AOPInterceptor - (NSMethodSignature *)methodSignatureForSelector:(SEL)sel { // 将方法签名请求转发给真实对象 return [self.target methodSignatureForSelector:sel]; } - (void)forwardInvocation:(NSInvocation *)invocation { SEL sel [invocation selector]; // 1. 执行前置处理 void (^preBlock)(NSInvocation *) self.preHandlerBlocks[NSStringFromSelector(sel)]; if (preBlock) { preBlock(invocation); } // 2. 调用原始方法 if ([self.target respondsToSelector:sel]) { [invocation invokeWithTarget:self.target]; } // 3. 执行后置处理 void (^postBlock)(NSInvocation *) self.postHandlerBlocks[NSStringFromSelector(sel)]; if (postBlock) { postBlock(invocation); } } // 使用示例 id realObject [[SomeClass alloc] init]; AOPInterceptor *proxy [AOPInterceptor alloc]; proxy.target realObject; [proxy registerPreHandler:^(NSInvocation *inv) { NSLog(“即将执行方法%”, NSStringFromSelector(inv.selector)); } forSelector:selector(doSomething)]; [proxy registerPostHandler:^(NSInvocation *inv) { NSLog(“方法执行完毕%”, NSStringFromSelector(inv.selector)); } forSelector:selector(doSomething)]; // 客户端代码通过proxy调用而非realObject [(SomeClass *)proxy doSomething];这个AOPInterceptor本质上是一个代理对象NSProxy它截获所有发送给它的消息在forwardInvocation:中插入自定义逻辑。这比Method Swizzling更灵活因为它是针对单个对象实例的不会污染全局的类方法列表且可以动态地添加或移除切面逻辑。4.4 场景四高性能的依赖注入容器在大型项目中手动管理对象依赖关系繁琐且耦合度高。可以利用Runtime自动解析和注入依赖。// DIContainer.h interface DIContainer : NSObject - (void)registerClass:(Class)class forProtocol:(Protocol *)protocol; - (id)resolve:(Protocol *)protocol; end // DIContainer.m implementation DIContainer { NSMutableDictionary *_protocolToClassMap; } - (instancetype)init { self [super init]; if (self) { _protocolToClassMap [NSMutableDictionary new]; } return self; } - (void)registerClass:(Class)class forProtocol:(Protocol *)protocol { NSString *protocolName NSStringFromProtocol(protocol); _protocolToClassMap[protocolName] class; } - (id)resolve:(Protocol *)protocol { NSString *protocolName NSStringFromProtocol(protocol); Class class _protocolToClassMap[protocolName]; if (!class) { throw [NSException exceptionWithName:NSInvalidArgumentException reason:“未注册的协议” userInfo:nil]; } // 利用Runtime分析类的所有属性查找声明了该协议类型的属性并递归注入 return [self _createInstance:class]; } - (id)_createInstance:(Class)class { id instance [[class alloc] init]; unsigned int count; objc_property_t *properties class_copyPropertyList(class, count); for (int i 0; i count; i) { const char *attributes property_getAttributes(properties[i]); NSString *attributeStr [NSString stringWithUTF8String:attributes]; // 解析属性声明例如T“MyProtocol“,,N,V_myService // 我们需要提取出协议名 MyProtocol NSRegularExpression *regex [NSRegularExpression regularExpressionWithPattern:“([^])” options:0 error:nil]; NSTextCheckingResult *match [regex firstMatchInString:attributeStr options:0 range:NSMakeRange(0, attributeStr.length)]; if (match) { NSString *protocolName [attributeStr substringWithRange:[match rangeAtIndex:1]]; Protocol *propertyProtocol NSProtocolFromString(protocolName); if (propertyProtocol) { // 递归解析依赖 id dependency [self resolve:propertyProtocol]; if (dependency) { const char *propertyName property_getName(properties[i]); NSString *key [NSString stringWithUTF8String:propertyName]; try { [instance setValue:dependency forKey:key]; } catch (NSException *exception) { NSLog(“注入属性 % 失败: %”, key, exception); } } } } } free(properties); return instance; } end // 使用 protocol NetworkServiceProtocol NSObject - (void)fetchData; end interface MyViewController : UIViewController property (nonatomic, strong) idNetworkServiceProtocol networkService; end // 在App启动时配置容器 DIContainer *container [DIContainer new]; [container registerClass:[ConcreteNetworkService class] forProtocol:protocol(NetworkServiceProtocol)]; // 创建ViewController时自动注入 MyViewController *vc [container resolve:protocol(MyViewController)]; // 需要MyViewController也注册协议 // 或者更常见的是在ViewController内部通过容器获取依赖 // self.networkService [container resolve:protocol(NetworkServiceProtocol)];这个例子展示了如何利用Runtime的属性解析能力实现一个简单的依赖注入框架。它的优势在于将对象的创建和依赖组装逻辑集中到了容器中降低了组件间的耦合使单元测试更容易可以注入Mock对象。5. Runtime实战中的疑难杂症与性能调优5.1 消息转发与性能损耗动态特性必然带来性能开销。objc_msgSend本身经过高度优化缓存命中率极高所以常规消息发送开销很小。但消息转发流程特别是forwardInvocation:则非常重。NSInvocation的创建和调用成本很高。优化建议缓存一切可缓存的结果例如在methodSignatureForSelector:中不要每次都[NSMethodSignature signatureWithObjCTypes:...]可以将创建好的方法签名缓存起来。优先使用forwardingTargetForSelector:如果可能将消息快速转发给一个确定的对象这比完整的forwardInvocation:流程快得多。避免在性能关键路径上使用动态解析对于会被频繁调用的方法绝对不要依赖resolveInstanceMethod:因为每次调用未识别的方法都会走一遍这个流程。5.2 Method Swizzling的陷阱与最佳实践除了之前提到的安全实践还有几个深坑子类与父类的Swizzling顺序如果子类和父类都Swizzle了同一个方法顺序不当会导致预期外的行为。通常应该在父类的load中先Swizzle确保子类Swizzle时操作的是已经“混合”过的方法。但最根本的解决方案是使用前面提到的、先尝试class_addMethod的安全方案。Swizzling dealloc 方法这是一个极其危险的操作。dealloc的调用时机和线程安全性由Runtime严格管理不当的Swizzling很容易导致对象生命周期混乱引发难以调试的崩溃。如果非要在dealloc中插入逻辑考虑使用关联对象监听对象的释放通过设置一个在对象释放时执行的关联对象析构函数但这同样需要小心。对同一个方法进行多次Swizzling使用dispatch_once是防止这种情况的标准做法。5.3 线程安全考量Runtime的许多底层操作如修改类结构本身并不是线程安全的。尽管load方法在单线程中执行但你的Swizzled方法可能在任何线程被调用。确保Swizzled方法的实现是线程安全的如果Swizzled方法中访问了共享数据必须使用适当的同步机制如synchronized,dispatch_queue,os_unfair_lock。避免在运行时动态修改“正在被频繁调用”的类结构例如在一个高并发的网络请求回调中动态添加方法可能会引发意想不到的竞争条件。这类操作最好在程序启动初期完成。5.4 调试与符号化当你的应用因为Runtime相关操作而崩溃时崩溃堆栈可能看起来非常诡异尤其是涉及到forwardInvocation:或Swizzling后的方法。为Swizzling后的方法取一个有意义的名称如前所述使用my_前缀。这样在崩溃报告里看到[MyViewController my_viewDidAppear:]时你立刻知道这是被Swizzle过的方法。在forwardInvocation:中记录日志记录被转发的原始选择器和目标这在调试复杂的消息转发链时非常有用。使用LLDB调试Runtime你可以使用image lookup -rn objc_msgSend来查找相关符号或者直接对class_addMethod、method_exchangeImplementations等Runtime函数设置断点。6. 现代Swift开发中的Runtime思维虽然Swift在设计上更强调静态性和安全性大量减少了Runtime的动态特性例如纯Swift类默认不支持Method Swizzling但Runtime的思维模式依然有价值。与Objective-C混编只要类继承自NSObject或用objc标记它就依然生活在Objective-C Runtime的世界里。你依然可以使用KVC、KVO、关联对象甚至谨慎地进行Method Swizzling。Swift的反射MirrorSwift提供了MirrorAPI来在运行时检查实例的属性这可以看作是Runtime的一种更安全、受限的替代品常用于调试、模型打印或简单的序列化。协议扩展与面向协议编程POPSwift的协议和扩展机制在某种程度上实现了基于接口的动态行为组合这可以替代一部分需要通过Runtime实现的动态功能且是类型安全的。理解底层兼容性当你调用一个标记为objc dynamic的Swift方法时你实际上是在创建一个可由Objective-C Runtime调用的方法入口。理解这一点有助于你理解Swift与Objective-C混编时的性能和行为。核心思想迁移即使在Swift中Runtime教给我们的最重要的思想——“接口与实现分离”、“通过消息传递进行对象通信”、“在运行时决定行为”——这些设计理念依然通过协议、闭包、泛型等现代语言特性以更安全的形式发挥着作用。你不再需要直接操作objc_method但设计一个灵活、可扩展、易测试的架构其内核逻辑与熟练运用Runtime是相通的。从我个人的经验来看Runtime就像一把锋利无比的瑞士军刀。在拙劣的开发者手中它可能把代码搞得一团糟难以维护但在理解其原理和约束的开发者手中它能优雅地解决那些用常规面向对象设计难以处理的问题。学习Runtime的最终目的不是为了炫技或滥用而是为了在真正需要的时候多一种强大而有效的解决方案同时加深你对Objective-C这门语言乃至整个Cocoa框架的理解。当你再遇到一个看似不可能的架构需求时或许Runtime就是那把开启新思路的钥匙。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价