参考文章:
iOS 面试题·项目中用过 Runtime 吗?
iOS 模块分解—「Runtime面试、工作」看我就ok了!
Objective-C 利用 Runtime 运行时变成一门动态语言,在开发过程中,使用 Runtime 相关 API 可以实现一些很强大的功能:
一、使用 Runtime 完成为分类增加伪属性
二、利用 Method SWizzling 来 Hook 方法
我们可以用 Method Swizzling 来交换两个方法的实现,以便达到 Hook 的效果;例如交换 ViewController 生命周期方法来实现页面埋点,或者在不影响原有的功能增加一些特殊的功能。
交换方法主要是利用到 Runtime 中的class_addMethod、class_replaceMethod、method_exchangeImplementations方法来实现的
三、实现 NSCoding 自动归档解档:class_copyIvarList
在利用 NSKeyedArchiver 归档解档对象的时候,对象 Model 需要实现 NSCoding 协议,并且要实现encodeWithCoder、initWithCoder两个方法,在这两个方法中要为每个属性进行 code 和 encode,不然就会 crash。
在项目开发过程中,经常会出现 Model 中的属性会变更,这个时候总是会忘记去修改对应的属性 code 和 encode,这里就会导致 crash;为了避免这个现象和让 Model 中的方法更加简洁可控,这里我们会利用 class_copyIvarList 来获取对象中的成员变量列表,然后利用 KVC 来 code 和 encode。
#define PXYNSCodingRuntime_EncodeWithCoder(Class) \
unsigned int outCount = 0;\
Ivar *ivars = class_copyIvarList([Class class], &outCount);\
for (int i = 0; i < outCount; i++) {\
Ivar ivar = ivars[i];\
NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];\
[aCoder encodeObject:[self valueForKey:key] forKey:key];\
}\
free(ivars);\
#define PXYNSCodingRuntime_InitWithCoder(Class)\
if (self = [super init]) {\
unsigned int outCount = 0;\
Ivar *ivars = class_copyIvarList([Class class], &outCount);\
for (int i = 0; i < outCount; i++) {\
Ivar ivar = ivars[i];\
NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];\
id value = [aDecoder decodeObjectForKey:key];\
if (value) {\
[self setValue:value forKey:key];\
}\
}\
free(ivars);\
}\
return self;\
// 对应调用
- (void)encodeWithCoder:(NSCoder *)aCoder {
PXYNSCodingRuntime_EncodeWithCoder(Father)
}
- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder {
PXYNSCodingRuntime_InitWithCoder(Father)
}
四、实现 KVO Block
利用 objc_allocateClassPair、object_setClass 等 API 来实现
在项目中,会经常使用 KVO 来监听某个属性的变化。先给出系统调用的方式,添加监听后,在 observeValueForKeyPath 方法中处理变化:
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { NSLog(@"%@ 对象的 %@ 属性改变了:%@",object,keyPath,change);
}
但是在开发过程中,有时候想将代码增加内聚性和在observeValueForKeyPath减少判断,我们可以通过 Runtime 来实现一个 KVO Block,这样调用地方即处理消息的地方,代码上比较直观,简单 API 如下:
typedef void(^PXYKVOCompleteBlock)(id observer, NSString *keyPath, id oldValue, id newValue);
/**
添加 KVO Block
*/ - (void)pxy_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath completeBlock:(PXYKVOCompleteBlock)completeBlock;
/**
移除 KVO Block
*/ - (void)pxy_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
KVO 主要是动态派生出一个中间类,然后在这个中间类处理相关通知逻辑,具体代码可以 Demo 中的NSObject+PXYKVO具体实现;
五、多播委托(蹦床模式)
在对象收到无法处理的消息之后,会执行消息转发,消息转发有三个步骤:
1、调用resolveInstanceMethod方法。动态方法解析,这里会给类使用class_addMethod来增加方法的机会。
2、调用forwardingTargetForSelector方法,看是否有备用接收者,将消息转发给备用接收者处理。
3、调用methodSignatureForSelector和forwardInvocation方法,进行完成的消息转发。
如果经过上面三个步骤,还不能正确处理消息,程序就会走doesNotRecognizeSelector方法,crash 掉。
蹦床模式:就是把一条消息 “反弹” 到另外一个对象,蹦床一般使用forwardInvocation来实现。
在项目开发中,事件回调一般使用:Block、Delegate、NSNotificationCenter;
但是在多个模块需要监听一个事件的场景:使用通知会将项目变得不可控,因为任何一个地方都可以监听这个通知,在排查问题的时候就会变得异常困难,这个时候我们可以使用多播委托,实现一对多回调。
大致原理:实现一个管理类,将需要回调的对象注册进来,然后将事件消息发送给这个管理类,由于这个管理类是没有实现委托方法的,就不能正常处理这个消息,这个时候就会走消息转发流程;然后我们通过消息转发流程,将消息转发到注册进来的对象中去,这样子就要可以实现我们的多播委托了。
具体代码可以看 Demo 中的PXYMulticastDelegate多播委托实现类。
六、字典模型之间的转换
七、页面无侵入埋点
Method swizzling实现无侵入埋点