iOS面试题·项目中用过 Runtime 吗?

移动开发
我们知道静态语言在编译时期,就已经确定了函数的具体调用,而动态语言要等到运行时期才能真正确定调用哪个函数; Objective-C 是一门动态语言,它是通过 Runtime 这个运行时机制来实现的。

前言

我们知道静态语言在编译时期,就已经确定了函数的具体调用,而动态语言要等到运行时期才能真正确定调用哪个函数; Objective-C 是一门动态语言,它是通过 Runtime 这个运行时机制来实现的。

[[248751]]

虽然说 Runtime 是相对于底层的机制,但是在项目过程中也经常用来解决一些问题。下面我们就来看看利用 Runtime 可以解决项目中什么问题。

项目中用 Runtime 实现的功能

利用关联对象为分类增加伪属性

在项目的开发中,经常会遇到要为已经存在的类添加属性。面对这种情况,我们一般都是创建一个分类,来实现为已有的类增加属性,但是由于分类结构的特殊性,在分类添加属性,并不会为我们自动创建实例变量和存储方法。

首先我们要知道,常规定义一个 @property,其实编译器会为我们做三件事情:

  • 生成实例变量 _property
  • 生成 getter 方法
  • 生成 setter 方法

但是,在分类中并不会帮我们去生成实例变量和存取方法,所以我们需要自己去实现存取方法,这里我们会通过关联对象去将键值关联到对象上面去,以下是代码示例: 

  1. @property (nonatomic, strong) NSString *title; 
  2.  
  3. - (NSString *)title { 
  4.     return objc_getAssociatedObject(self, _cmd); 
  5.  
  6. - (void)setTitle:(NSString *)title { 
  7.     objc_setAssociatedObject(self, @selector(title), title, OBJC_ASSOCIATION_RETAIN); 

这个我们暂时只讲如何通过关联对象为分类增加伪属性,至于分类为什么不会为我们自动添加实例变量和存取方法,以及关联对象的实现原理等,我们会在后面的面试题继续涉及到这一话题。

利用 Method Swizzling 交换方法

我们可以用 Method Swizzling 来交换两个方法的实现,以便达到 Hook 的效果;例如交换 ViewController 生命周期方法来实现页面埋点,或者在不影响原有的功能增加一些特殊的功能。

交换方法主要是利用到 Runtime 中的class_addMethod 、class_replaceMethod、method_exchangeImplementations 方法来实现的,以下是 Method Swizzling 代码示例: 

  1. /** 
  2.  交换方法 
  3.  */ 
  4. + (void)pxy_swizzleMethodWithOriginalSelector:(SEL)originalSelector swizzledSelector:(SEL)swizzledSelector { 
  5.     Class class = [self class]; 
  6.  
  7.     SEL originalSeletor = originalSelector; 
  8.     SEL swizzledSeletor = swizzledSelector; 
  9.  
  10.     Method originMethod = class_getInstanceMethod(class, originalSeletor); 
  11.     Method swizzledMethod = class_getInstanceMethod(class, swizzledSeletor); 
  12.  
  13.     //先尝试給源SEL添加IMP,这里是为了避免源SEL没有实现IMP的情况 
  14.     BOOL didAddMethod = class_addMethod(class, originalSeletor, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod)); 
  15.     if (didAddMethod) { 
  16.         //添加成功:说明源SEL没有实现IMP,将源SEL的IMP替换到交换SEL的IMP 
  17.         class_replaceMethod(class, swizzledSeletor, method_getImplementation(originMethod), method_getTypeEncoding(originMethod)); 
  18.     } else { 
  19.         //添加失败:说明源SEL已经有IMP,直接将两个SEL的IMP交换即可 
  20.         method_exchangeImplementations(originMethod, swizzledMethod); 
  21.     } 

利用 class_copyIvarList 实现 NSCoding 的自动归档解档

在利用 NSKeyedArchiver 归档解档对象的时候,对象 Model 需要实现 NSCoding 协议,并且要实现 encodeWithCoder、initWithCoder 两个方法,在这两个方法中要为每个属性进行 code 和 encode,不然就会 crash。

在项目开发过程中,经常会出现 Model 中的属性会变更,这个时候总是会忘记去修改对应的属性 code 和 encode,这里就会导致 crash;为了避免这个现象和让 Model 中的方法更加简洁可控,这里我们会利用 class_copyIvarList 来获取对象中的成员变量列表,然后利用 KVC 来 code 和 encode。实例代码如下:(这里我们将这个通用的代码抽象成宏,这样子在需要的 Model 中直接调用就可以了) 

  1. #define PXYNSCodingRuntime_EncodeWithCoder(Class) \ 
  2. unsigned int outCount = 0;\ 
  3. Ivar *ivars = class_copyIvarList([Class class], &outCount);\ 
  4. for (int i = 0; i < outCount; i++) {\ 
  5.     Ivar ivar = ivars[i];\ 
  6.     NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];\ 
  7.     [aCoder encodeObject:[self valueForKey:key] forKey:key];\ 
  8. }\ 
  9. free(ivars);\ 
  10.  
  11. #define PXYNSCodingRuntime_InitWithCoder(Class)\ 
  12. if (self = [super init]) {\ 
  13.     unsigned int outCount = 0;\ 
  14.     Ivar *ivars = class_copyIvarList([Class class], &outCount);\ 
  15.     for (int i = 0; i < outCount; i++) {\ 
  16.         Ivar ivar = ivars[i];\ 
  17.         NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];\ 
  18.         id value = [aDecoder decodeObjectForKey:key];\ 
  19.         if (value) {\ 
  20.             [self setValue:value forKey:key];\ 
  21.         }\ 
  22.     }\ 
  23.     free(ivars);\ 
  24. }\ 
  25. return self;\ 
  26.  
  27. // 对应调用 
  28. - (void)encodeWithCoder:(NSCoder *)aCoder { 
  29.     PXYNSCodingRuntime_EncodeWithCoder(Father) 
  30. - (nullable instancetype)initWithCoder:(NSCoder *)aDecoder { 
  31.     PXYNSCodingRuntime_InitWithCoder(Father) 

利用 objc_allocateClassPair、object_setClass 等 API 来实现 KVO Block

在项目中,会经常使用 KVO 来监听某个属性的变化。先给出系统调用的方式,添加监听后,在 observeValueForKeyPath 方法中处理变化: 

  1. - (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context; 
  2.  
  3.  - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context { 
  4.  NSLog(@"%@ 对象的 %@ 属性改变了:%@",object,keyPath,change); 
  5.  } 

但是在开发过程中,有时候想将代码增加内聚性和在 observeValueForKeyPath 减少判断,我们可以通过 Runtime 来实现一个 KVO Block,这样调用地方即处理消息的地方,代码上比较直观,简单 API 如下: 

  1. typedef void(^PXYKVOCompleteBlock)(id observer, NSString *keyPath, id oldValue, id newValue); 
  2.  
  3. /** 
  4.  添加 KVO Block 
  5.  */ 
  6. - (void)pxy_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath completeBlock:(PXYKVOCompleteBlock)completeBlock; 
  7.  
  8. /** 
  9.  移除 KVO Block 
  10.  */ 
  11. - (void)pxy_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath; 

KVO 主要是动态派生出一个中间类,然后在这个中间类处理相关通知逻辑,具体代码可以 Demo 中的 NSObject+PXYKVO 具体实现;

利用消息转发机制实现多播委托(蹦床模式)

首先,在对象收到无法处理的消息之后,会执行消息转发,消息转发有三个步骤:

  • 调用 resolveInstanceMethod 方法。动态方法解析,这里会给类使用 class_addMethod 来增加方法的机会。
  • 调用 forwardingTargetForSelector 方法,看是否有备用接收者,将消息转发给备用接收者处理。
  • 调用 methodSignatureForSelector 和 forwardInvocation 方法,进行完成的消息转发。

如果经过上面三个步骤,还不能正确处理消息,程序就会走 doesNotRecognizeSelector 方法,crash 掉。

蹦床模式:就是把一条消息 “反弹” 到另外一个对象,蹦床一般使用 forwardInvocation 来实现。

在项目开发中,事件回调一般使用:Block、Delegate、NSNotificationCenter;但是在多个模块需要监听一个事件的场景:使用通知会将项目变得不可控,因为任何一个地方都可以监听这个通知,在排查问题的时候就会变得异常困难,这个时候我们可以使用多播委托,实现一对多回调。

大致原理:实现一个管理类,将需要回调的对象注册进来,然后将事件消息发送给这个管理类,由于这个管理类是没有实现委托方法的,就不能正常处理这个消息,这个时候就会走消息转发流程;然后我们通过消息转发流程,将消息转发到注册进来的对象中去,这样子就要可以实现我们的多播委托了。

具体代码可以看 Demo 中的 PXYMulticastDelegate 多播委托实现类。

总结

Objective-C 利用 Runtime 运行时变成一门动态语言,在开发过程中,使用 Runtime 相关 API 可以实现一些很强大的功能,这里我们简单讲到使用 Runtime 完成为分类增加伪属性、利用 Method SWizzling 来 Hook 方法、实现 NSCoding 自动归档解档、实现 KVO Block、多播委托。

当然还可以实现更多的功能,比如字典模型之间的转换、页面无侵入埋点、监听 App 网络流量等等。

还有可以实现什么好玩的功能,欢迎留言,感激不尽。

责任编辑:未丽燕 来源: 简书
相关推荐

2022-07-12 12:05:22

JavaSemaphore

2016-03-03 10:07:39

ios内存管理面试总结

2020-06-04 14:40:40

面试题Vue前端

2011-03-24 13:27:37

SQL

2023-11-13 07:37:36

JS面试题线程

2024-04-01 00:00:00

Redis缓存服务消息队列

2021-08-05 05:04:50

热部署模型字节

2015-09-02 09:32:56

java线程面试

2014-09-19 11:17:48

面试题

2009-06-06 18:36:02

java面试题

2009-06-06 18:34:05

java面试题

2019-11-26 10:30:11

CSS前端面试题

2023-07-28 08:04:56

StringHeaatomic线程

2021-06-29 10:21:54

this面试前端

2023-09-04 08:28:34

JavaScripforEach 循环

2021-02-23 12:43:39

Redis面试题缓存

2013-05-29 10:23:36

Android开发移动开发Java面试题

2009-06-16 14:03:16

Hibernate面试Hibernate面试

2020-11-05 10:01:35

系统设计软件

2013-07-10 10:02:05

iOS面试题Objective-CiOS开发
点赞
收藏

51CTO技术栈公众号