当前位置: 首页 > 科技观察

iOS面试题·项目中是否使用过Runtime?

时间:2023-03-15 14:49:37 科技观察

前言我们知道静态语言在编译时就已经确定了函数的具体调用,而动态语言要到运行时才能真正确定调用哪个函数;Objective-C是一门动态语言,它运行通过定时机制实现的Runtime。Runtime虽然是相对于底层的一种机制,但是在项目过程中经常会用到它来解决一些问题。下面我们就来看看使用Runtime可以解决项目中的哪些问题。项目中Runtime实现的功能是使用关联对象为分类添加伪属性。在项目开发中,经常会遇到给已有类添加属性的情况。面对这种情况,我们一般会创建一个类来给已有的类添加属性,但是由于类结构的特殊性,给类添加属性并不会自动为我们创建实例变量和存储方法。首先我们要知道,在定义一个@property的时候,编译器会帮我们做三件事:生成实例变量_property生成getter方法生成setter方法但是在分类中,它们不会帮我们生成实例变量和访问方法,所以我们需要自己实现访问方法。这里我们将通过关联对象,将键值与对象关联起来。以下是代码示例:@property(nonatomic,strong)NSString*title;-(NSString*)title{returnobjc_getAssociatedObject(self,_cmd);}-(void)setTitle:(NSString*)title{objc_setAssociatedObject(self,@selector(title),title,OBJC_ASSOCIATION_RETAIN);}我们暂时只讲如何通过关联对象的属性给分类添加伪属性,至于为什么分类不自动为我们添加实例变量和访问方法,以及关联对象的实现原理等,我们会在后面的面试题中继续涉及到这个话题。使用MethodSwizzling交换方法,我们可以使用MethodSwizzling来交换两个方法的实现,从而达到Hook的效果;比如交换ViewController的生命周期方法实现页面嵌入,或者在不影响原有功能的情况下增加一些特殊的功能。swap方法主要是通过Runtime中的class_addMethod、class_replaceMethod、method_exchangeImplementations方法实现的。以下是MethodSwizzling代码的示例:class_getInstanceMethod(class,originalSeletor);MethodswizzledMethod=class_getInstanceMethod(class,swizzledSeletor);//首先尝试在sourceSEL中加入IMP,这里是为了避免sourceSEL的IMP情况BOOLdidAddMethod=class_addMethod(class,originalSeletor,method_getImplementation(swizzledMethod),method_getTypeEncoding(swizzledMethod));if(didAddMethod){//添加成功:说明源SEL没有实现IMP,将源SEL的IMP替换为交易所的IMPSELclass_replaceMethod(class,swizzledSeletor,method_getImplementation(originMethod),method_getTypeEncoding(originMethod));}else{//添加失败:说明源SEL已经有IMP,只是交换两个SEL的IMPmethod_exchangeImplementations(originMethod,swizzledMethod);}}使用class_copyIvarList实现NSCoding的自动返回使用NSKeyedArchiver对对象进行归档和解归档时,对象Model需要实现NSCoding协议,实现encodeWithCoder和initWithCoder方法。在这两种方法中,必须对每个属性进行code和encode,否则会crash在项目开发过程中,Model中的属性经常变化。这时候总是忘记修改相应的属性code和encode,会导致crash;为了避免这种现象,让Model中的方法更加简洁易控制,这里我们将使用class_copyIvarList获取对象中的成员变量列表,然后使用KVC进行编码编码。示例代码如下:(这里我们把这个通用代码抽象成一个宏,以便在需要的Model中直接调用)#definePXYNSCodingRuntime_EncodeWithCoder(Class)\unsignedintoutCount=0;\Ivar*ivars=class_copyIvarList([Classclass],&outCount);\for(inti=0;i*)changecontext:(void*)context{NSLog(@"%@对象的%@属性发生了变化:%@",object,keyPath,change);}但是在开发过程中,有时候我们想增加代码的内聚性,减少observeValueForKeyPath中的判断,我们可以通过Runtime实现一个KVOBlock,这样调用的地方是处理消息的地方,代码更直观,简单的API如下:typedefvoid(^PXYKVOCompleteBlock)(idobserver,NSString*keyPath,idoldValue,idnewValue);/**AddKVOBlock*/-(void)pxy_addObserver:(NSObject*)observerforKeyPath:(NSString*)keyPathcompleteBlock:(PXYKVOCompleteBlock)completeBlock;/**移除KVOBlock*/-(void)pxy_removeObserver:(NSObject*)observerforKeyPath:(NSString*)keyPath;KVO主要是动态派生一个中间类,然后在这个中间类中处理相关的通知逻辑,具体代码可以通过Demo中的NSObject+PXYKVO实现;使用消息转发机制实现组播委托(trampoline模式)。首先,对象收到无法处理的消息后,会进行消息转发。消息转发分三步:调用resolveInstanceMethod方法动态方法分析,这里会使用class_addMethod给类添加方法。调用forwardingTargetForSelector方法查看是否有备选接收者,将消息转发给备选接收者处理。调用methodSignatureForSelector和forwardInvocation方法完成消息转发。如果经过以上三步仍不能正确处理消息,程序就会走doesNotRecognizeSelector方法而崩溃。Trampoline模式:就是将消息“弹跳”到另一个对象。Trampoline一般使用forwardInvocation实现。在项目开发中,一般使用事件回调:Block、Delegate、NSNotificationCenter;但是在多个模块需要监听一个事件的场景下:使用通知会让项目变得不可控,因为这个通知可以在任何地方监听,在排查问题的时候,就会变得异常困难。这时候我们可以使用多播委托来实现一对多的回调。总的原则:实现一个管理类,注册需要回调的对象,然后将事件消息发送给管理类。由于管理类没有实现delegate方法,所以无法正常处理消息,此时消息会被发送到Forwarding流程;然后我们通过消息转发流程将消息转发给注册的对象,这样就可以实现我们的多播委托。具体代码见Demo中的PXYMulticastDelegate组播委托实现类。总结Objective-C使用Runtime成为一种动态语言。在开发过程中,使用Runtime相关的API可以实现一些非常强大的功能。这里简单说说使用Runtime为类别添加伪属性和使用MethodSWizzling对Hook方法,实现NSCoding自动归档和解压,实现KVOBlock,多播委托。当然还可以实现更多的功能,比如字典模型之间的转换、非侵入式埋点页面、App网络流量监控等等。如果有什么有趣的功能可以实现,欢迎留言,非常感谢。