Runtime-iOS运行时基础篇

转自:https://www.jianshu.com/p/d4b55dae9a0d  php

本文主要整理了Runtime的相关知识。对于一个iOS开发者来讲,掌握Runtime的重要性早已不言而喻。OC可以做为一门优秀的动态特性语言,在其背后默默工做着的就是Runtime。在网上也看过不少资料,最终我仍是但愿在一些关键的知识点上可以融入本身的理解,从简单的问题出发,一步一步理解和学以至用。html

 
iOS运行时Runtime.png

相关文章:iOS运行时Runtime应用java

目录:

1、怎么理解OC是动态语言,Runtime又是什么?
2、理解消息机制的基本原理
3、与Runtime交互的三种方式
4、分析Runtime中的数据结构
5、深刻理解Rutime消息发送原理
6、多继承的实现思路:Runtime
7、最后总结数据结构

1、怎么理解OC是动态语言,Runtime又是什么?

静态语言:如C语言,编译阶段就要决定调用哪一个函数,若是函数未实现就会编译报错。app

动态语言:如OC语言,编译阶段并不能决定真正调用哪一个函数,只要函数声明过即便没有实现也不会报错。ide

咱们常说OC是一门动态语言,就是由于它老是把一些决定性的工做从编译阶段推迟到运行时阶段。OC代码的运行不只须要编译器,还须要运行时系统(Runtime Sytem)来执行编译后的代码。函数

Runtime是一套底层纯C语言API,OC代码最终都会被编译器转化为运行时代码,经过消息机制决定函数调用方式,这也是OC做为动态语言使用的基础。性能

2、理解消息机制的基本原理

OC的方法调用都是相似[receiver selector]的形式,其实每次都是一个运行时消息发送过程。学习

第一步:编译阶段
[receiver selector]方法被编译器转化,分为两种状况:
1.不带参数的方法被编译为:objc_msgSend(receiver,selector)
2.带参数的方法被编译为:objc_msgSend(recevier,selector,org1,org2,…)测试

第二步:运行时阶段
消息接收者recever寻找对应的selector,也分为两种状况:
1.接收者能找到对应的selector,直接执行接收receiver对象的selector方法。
2.接收者找不到对应的selector,消息被转发或者临时向接收者添加这个selector对应的实现内容,不然崩溃。

说明:OC调用方法[receiver selector],编译阶段肯定了要向哪一个接收者发送message消息,可是接收者如何响应决定于运行时的判断。

3、与Runtime的交互

Runtime的官方文档中将OC与Runtime的交互划分三种层次:OC源代码NSObject方法Runtime 函数。这其实也是按照与Runtime交互程度从低到高排序的三种方式。

1.OC源代码(Objec-C Source Code)

咱们已经说过,OC代码会在编译阶段被编译器转化。OC中的类、方法和协议等在Runtime中都由一些数据结构来定义。因此,咱们平时直接使用OC编写代码,其实这已是在和Runtime进行交互了,只不过这个过程对于咱们来讲是无感的。

2.NSObject方法(NSObject Methods)

Runtime的最大特征就是实现了OC语言的动态特性。做为大部分Objective-C类继承体系的根类的NSObject,其自己就具备了一些很是具备运行时动态特性的方法,好比respondsToSelector:方法能够检查在代码运行阶段当前对象是否能响应指定的消息,因此使用这些方法也算是一种与Runtme的交互方式,相似的方法还有以下:

-description://返回当前类的描述信息 -class //方法返回对象的类; -isKindOfClass: 和 -isMemberOfClass: //检查对象是否存在于指定的类的继承体系中 -respondsToSelector: //检查对象可否响应指定的消息; -conformsToProtocol: //检查对象是否实现了指定协议类的方法; -methodForSelector: //返回指定方法实现的地址。 

3.使用Runtime函数(Runtime Functions)

Runtime系统是一个由一系列函数和数据结构组成,具备公共接口的动态共享库。头文件存放于/usr/include/objc目录下。在咱们工程代码里引用Runtime的头文件,一样可以实现相似OC代码的效果,一些代码示例以下:

//至关于:Class class = [UIView class]; Class viewClass = objc_getClass("UIView"); //至关于:UIView *view = [UIView alloc]; UIView *view = ((id (*)(id, SEL))(void *)objc_msgSend)((id)viewClass, sel_registerName("alloc")); //至关于:UIView *view = [view init]; ((id (*)(id, SEL))(void *)objc_msgSend)((id)view, sel_registerName("init")); 

3、分析Runtime中数据结构

OC代码被编译器转化为C语言,而后再经过运行时执行,最终实现了动态调用。这其中的OC类、对象和方法等都对应了C中的结构体,并且咱们均可以在Rutime源码中找到它们的定义。

那么,咱们如何来查看Runtime的代码呢?其实很简单,只须要咱们在当前代码文件中引用头文件:

#import <objc/runtime.h> #import <objc/message.h> 

而后,咱们须要使用组合键"Command +鼠标点击",便可进入Runtime的源码文件,下面咱们继续来一一分析OC代码在C中对应的结构。

1.id—>objc_object

id是一个指向objc_object结构体的指针,即在Runtime中:

///A pointer to an instance of a class. typedef struct objc_object *id; 

下面是Runtime中对objc_object结构体的具体定义:

///Represents an instance of a class. struct objc_object { Class _Nonnull isa OBJC_ISA_AVAILABILITY; }; 

咱们都知道id在OC中是表示一个任意类型的类实例,从这里也能够看出,OC中的对象虽然没有明显的使用指针,可是在OC代码被编译转化为C以后,每一个OC对象其实都是拥有一个isa的指针的。

2.Class - >objc_classs

class是一个指向objc_class结构体的指针,即在Runtime中:

typedef struct objc_class *Class; 

下面是Runtime中对objc_class结构体的具体定义:

//usr/include/objc/runtime.h struct objc_class { Class _Nonnull isa OBJC_ISA_AVAILABILITY; #if !OBJC2 Class Nullable super_class OBJC2UNAVAILABLE; const char * Nonnull name OBJC2UNAVAILABLE; long version OBJC2_UNAVAILABLE; long info OBJC2_UNAVAILABLE; long instance_size OBJC2_UNAVAILABLE; struct objc_ivar_list * Nullable ivars OBJC2UNAVAILABLE; struct objc_method_list * Nullable * _Nullable methodLists OBJC2UNAVAILABLE; struct objc_cache * Nonnull cache OBJC2UNAVAILABLE; struct objc_protocol_list * Nullable protocols OBJC2UNAVAILABLE; #endif } OBJC2_UNAVAILABLE; 

理解objc_class定义中的参数:

isa指针:

咱们会发现objc_class和objc_object一样是结构体,并且都拥有一个isa指针。咱们很容易理解objc_object的isa指针指向对象的定义,那么objc_class的指针是怎么回事呢?
其实,在Runtime中Objc类自己同时也是一个对象。Runtime把类对象所属类型就叫作元类,用于描述类对象自己所具备的特征,最多见的类方法就被定义于此,因此objc_class中的isa指针指向的是元类,每一个类仅有一个类对象,而每一个类对象仅有一个与之相关的元类。

super_class指针:

super_class指针指向objc_class类所继承的父类,可是若是当前类已是最顶层的类(如NSProxy),则super_class指针为NULL

cache:

为了优化性能,objc_class中的cache结构体用于记录每次使用类或者实例对象调用的方法。这样每次响应消息的时候,Runtime系统会优先在cache中寻找响应方法,相比直接在类的方法列表中遍历查找,效率更高。

ivars:

ivars用于存放全部的成员变量和属性信息,属性的存取方法都存放在methodLists中。

methodLists:

methodLists用于存放对象的全部成员方法。

3.SEL

SEL是一个指向objc_selector结构体的指针,即在Runtime中:

/// An opaque type that represents a method selector. typedef struct objc_selector *SEL; 

SEL在OC中称做方法选择器,用于表示运行时方法的名字,然而咱们并不能在Runtime中找到它的结构体的详细定义。Objective-C在编译时,会依据每个方法的名字、参数序列,生成一个惟一的整型标识(Int类型的地址),这个标识就是SEL。

注意
1.不一样类中相同名字的方法对应的方法选择器是相同的。
2.即便是同一个类中,方法名相同而变量类型不一样也会致使它们具备相同的方法选择器。

一般咱们获取SEL有三种方法:
1.OC中,使用@selector(“方法名字符串”)
2.OC中,使用NSSelectorFromString(“方法名字符串”)
3.Runtime方法,使用sel_registerName(“方法名字符串”)

4.Ivar

Ivar表明类中实例变量的类型,是一个指向ojbcet_ivar的结构体的指针,即在Runtime中:

/// An opaque type that represents an instance variable. typedef struct objc_ivar *Ivar; 

下面是Runtime中对objc_ivar结构体的具体定义:

struct objc_ivar { char * Nullable ivar_name OBJC2UNAVAILABLE; char * Nullable ivar_type OBJC2UNAVAILABLE; int ivar_offset OBJC2_UNAVAILABLE; #ifdef LP64 int space OBJC2_UNAVAILABLE; #endif } 

咱们在objc_class中看到的ivars成员列表,其中的元素就是Ivar,我能够经过实例查找其在类中的名字,这个过程被称为反射,下面的class_copyIvarList获取的不只有实例变量还有属性:

Ivar *ivarList = class_copyIvarList([self class], &count); for (int i= 0; i<count; i++) { Ivar ivar = ivarList[i]; const char *ivarName = ivar_getName(ivar); NSLog(@"Ivar(%d): %@", i, [NSString stringWithUTF8String:ivarName]); } free(ivarList); 

5.Method

Method表示某个方法的类型,即在Runtime中:

/// An opaque type that represents a method in a class definition. typedef struct objc_method *Method; 

咱们能够在objct_class定义中看到methodLists,其中的元素就是Method,下面是Runtime中objc_method结构体的具体定义:

struct objc_method { SEL Nonnull method_name OBJC2UNAVAILABLE; char * Nullable method_types OBJC2UNAVAILABLE; IMP Nonnull method_imp OBJC2UNAVAILABLE; } OBJC2_UNAVAILABLE; 

理解objc_method定义中的参数:
method_name:方法名类型SEL
method_types: 一个char指针,指向存储方法的参数类型和返回值类型
method_imp:本质上是一个指针,指向方法的实现
这里其实就是SEL(method_name)与IMP(method_name)造成了一个映射,经过SEL,咱们能够很方便的找到方法实现IMP。

5.IMP

IMP是一个函数指针,它在Runtime中的定义以下:

/// A pointer to the function of a method implementation. typedef void (IMP)(void / id, SEL, ... */ ); 

IMP这个函数指针指向了方法实现的首地址,当OC发起消息后,最终执行的代码是由IMP指针决定的。利用这个特性,咱们能够对代码进行优化:当须要大量重复调用方法的时候,咱们能够绕开消息绑定而直接利用IMP指针调起方法,这样的执行将会更加高效,相关的代码示例以下:

void (*setter)(id, SEL, BOOL); int i; setter = (void (*)(id, SEL, BOOL))[target methodForSelector:@selector(setFilled:)]; for ( i = 0 ; i < 1000 ; i++ ) setter(targetList[i], @selector(setFilled:), YES); 

注意:这里须要注意的就是函数指针的前两个参数必须是id和SEL。

4、深刻理解Rutime消息发送

咱们在分析了OC语言对应的底层C结构以后,如今能够进一步理解运行时的消息发送机制。先前讲到,OC调用方法被编译转化为以下的形式:

id _Nullable objc_msgSend(id _Nullable self, SEL _Nonnull op, ...) 

其实,除了常见的objc_msgSend,消息发送的方法还有objc_msgSend_stret,objc_msgSendSuper,objc_msgSendSuper_stret等,若是消息传递给超类就使用带有super的方法,若是返回值是结构体而不是简单值就使用带有stret的值。

运行时阶段的消息发送的详细步骤以下

  1. 检测selector 是否是须要忽略的。好比 Mac OS X 开发,有了垃圾回收就不理会retain,release 这些函数了。
  2. 检测target 是否是nil 对象。ObjC 的特性是容许对一个 nil对象执行任何一个方法不会 Crash,由于会被忽略掉。
  3. 若是上面两个都过了,那就开始查找这个类的 IMP,先从 cache 里面找,若能够找获得就跳到对应的函数去执行。
  4. 若是在cache里找不到就找一下方法列表methodLists。
  5. 若是methodLists找不到,就到超类的方法列表里寻找,一直找,直到找到NSObject类为止。
  6. 若是还找不到,Runtime就提供了以下三种方法来处理:动态方法解析消息接受者重定向消息重定向,这三种方法的调用关系以下图:
     
    消息转发流程图.png

1.动态方法解析(Dynamic Method Resolution)

所谓动态解析,咱们能够理解为经过cache和方法列表没有找到方法时,Runtime为咱们提供一次动态添加方法实现的机会,主要用到的方法以下:

//OC方法: //类方法未找到时调起,可于此添加类方法实现 + (BOOL)resolveClassMethod:(SEL)sel //实例方法未找到时调起,可于此添加实例方法实现 + (BOOL)resolveInstanceMethod:(SEL)sel //Runtime方法: /** 运行时方法:向指定类中添加特定方法实现的操做 @param cls 被添加方法的类 @param name selector方法名 @param imp 指向实现方法的函数指针 @param types imp函数实现的返回值与参数类型 @return 添加方法是否成功 */ BOOL class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, const char * _Nullable types) 

下面使用一个示例来讲明动态解析:Perosn类中声明方法却未添加实现,咱们经过Runtime动态方法解析的操做为其余添加方法实现,具体代码以下:

//Person.h文件 @interface Person : NSObject //声明类方法,但未实现 + (void)haveMeal:(NSString *)food; //声明实例方法,但未实现 - (void)singSong:(NSString *)name; @end 
//Person.m文件 #import "Person.h" #import <objc/runtime.h> @implementation Person //重写父类方法:处理类方法 + (BOOL)resolveClassMethod:(SEL)sel{ if(sel == @selector(haveMeal:)){ class_addMethod(object_getClass(self), sel, class_getMethodImplementation(object_getClass(self), @selector(zs_haveMeal:)), "v@"); return YES; //添加函数实现,返回YES } return [class_getSuperclass(self) resolveClassMethod:sel]; } //重写父类方法:处理实例方法 + (BOOL)resolveInstanceMethod:(SEL)sel{ if(sel == @selector(singSong:)){ class_addMethod([self class], sel, class_getMethodImplementation([self class], @selector(zs_singSong:)), "v@"); return YES; } return [super resolveInstanceMethod:sel]; } + (void)zs_haveMeal:(NSString *)food{ NSLog(@"%s",__func__); } - (void)zs_singSong:(NSString *)name{ NSLog(@"%s",__func__); } 
//TestViewController.m文件 //测试:Peson调用并未实现的类方法、实例方法,并无崩溃 Person *ps = [[Person alloc] init]; [Person haveMeal:@"Apple"]; //打印:+[Person zs_haveMeal:] [ps singSong:@"纸短情长"]; //打印:-[Person zs_singSong:] 

注意1:咱们注意到class_addMethod方法中的特殊参数“v@”,具体可参考这里
注意2:成功使用动态方法解析还有个前提,那就是咱们必须存在能够处理消息的方法,好比上述代码中的zs_haveMeal:与zs_singSong:

2.消息接收者重定向

咱们注意到动态方法解析过程当中的两个resolve方法都返回了布尔值,当它们返回YES时方法便可正常执行,可是若它们返回NO,消息发送机制就进入了消息转发(Forwarding)的阶段了,咱们可使用Runtime经过下面的方法替换消息接收者的为其余对象,从而保证程序的继续执行。

//重定向类方法的消息接收者,返回一个类 - (id)forwardingTargetForSelector:(SEL)aSelector //重定向实例方法的消息接受者,返回一个实例对象 - (id)forwardingTargetForSelector:(SEL)aSelector 

下面使用一个示例来讲明消息接收者的重定向:
咱们建立一个Student类,声明并实现takeExam:、learnKnowledge:两个方法,而后在视图控制器TestViewController(一个继承了UIViewController的自定义类)里测试,关键代码以下:

//Student.h文件 @interface Student : NSObject //类方法:参加考试 + (void)takeExam:(NSString *)exam; //实例方法:学习知识 - (void)learnKnowledge:(NSString *)course; @end 
// Student.m文件 @implementation Student + (void)takeExam:(NSString *)exam{ NSLog(@"%s",__func__); } - (void)learnKnowledge:(NSString *)course{ NSLog(@"%s",__func__); } @end 
//TestViewConroller.m文件 //重定向类方法:返回一个类对象 + (id)forwardingTargetForSelector:(SEL)aSelector{ if (aSelector == @selector(takeExam:)) { return [Student class]; } return [super forwardingTargetForSelector:aSelector]; } //重定向实例方法:返回类的实例 - (id)forwardingTargetForSelector:(SEL)aSelector{ if (aSelector == @selector(learnKnowledge:)) { return self.student; } return [super forwardingTargetForSelector:aSelector]; } //在TestViewConroller的viewDidLoad中测试: //调用并未声明和实现的类方法 [TestViewController performSelector:@selector(takeExam:) withObject:@"语文"]; //调用并未声明和实现的类方法 self.student = [[Student alloc] init]; [self performSelector:@selector(learnKnowledge:) withObject:@"天文学知识"]; //正常打印: // +[Student takeExam:] // -[Student learnKnowledge:] 

注意:动态方法解析阶段返回NO时,咱们能够经过forwardingTargetForSelector能够修改消息的接收者,该方法返回参数是一个对象,若是这个对象是非nil,非self,系统会将运行的消息转发给这个对象执行。不然,继续查找其余流程。

3.消息重定向

当以上两种方法没法生效,那么这个对象会由于找不到相应的方法实现而没法响应消息,此时Runtime系统会经过forwardInvocation:消息通知该对象,给予这次消息发送最后一次寻找IMP的机会:

- (void)forwardInvocation:(NSInvocation *)anInvocation; 

其实每一个对象都从NSObject类中继承了forwardInvocation:方法,可是NSObject中的这个方法只是简单的调用了doesNotRecongnizeSelector:方法,提示咱们错误。因此咱们能够重写这个方法:对不能处理的消息作一些默认处理,也能够将消息转发给其余对象来处理,而不抛出错误。

咱们注意到anInvocation是forwardInvocation惟一参数,它封装了原始的消息和消息参数。正是由于它,咱们还不得不重写另外一个函数:methodSignatureForSelector。这是由于在forwardInvocation: 消息发送前,Runtime系统会向对象发送methodSignatureForSelector消息,并取到返回的方法签名用于生成NSInvocation对象。

下面使用一个示例来从新定义转发逻辑:在上面的TestViewController添加以下代码:

-(void)forwardInvocation:(NSInvocation *)anInvocation{ //1.从anInvocation中获取消息 SEL sel = anInvocation.selector; //2.判断Student方法是否能够响应应sel if ([self.student respondsToSelector:sel]) { //2.1若能够响应,则将消息转发给其余对象处理 [anInvocation invokeWithTarget:self.student]; }else{ //2.2若仍然没法响应,则报错:找不到响应方法 [self doesNotRecognizeSelector:sel]; } } //须要从这个方法中获取的信息来建立NSInvocation对象,所以咱们必须重写这个方法,为给定的selector提供一个合适的方法签名。 - (NSMethodSignature*)methodSignatureForSelector:(SEL)aSelector{ NSMethodSignature *methodSignature = [super methodSignatureForSelector:aSelector]; if (!methodSignature) { methodSignature = [NSMethodSignature signatureWithObjCTypes:"v@:*"]; } return methodSignature; } 

而后再在视图控制器里直接调用Student的方法以下:

//self是当前的TestViewController,调用了本身并不存在的learnKonwledge:方法 [self performSelector:@selector(learnKnowledge:) withObject:@"天文学”]; //正常打印: //-[Student learnKnowledge:] 

总结:

1.从以上的代码中就能够看出,forwardingTargetForSelector仅支持一个对象的返回,也就是说消息只能被转发给一个对象,而forwardInvocation能够将消息同时转发给任意多个对象,这就是二者的最大区别。

2.虽然理论上能够重载doesNotRecognizeSelector函数实现保证不抛出异常(不调用super实现),可是苹果文档着重提出“必定不能让这个函数就这么结束掉,必须抛出异常”。(If you override this method, you must call super or raise an invalidArgumentException exception at the end of your implementation. In other words, this method must not return normally; it must always result in an exception being thrown.)

3.forwardInvocation甚至可以修改消息的内容,用于实现更增强大的功能。

6、多继承的实现思路:Runtime

咱们会发现Runtime消息转发的一个特色:一个对象能够调起它自己不具有的方法。这个过程与OC中的继承特性很类似,其实官方文档中图示也很好的说明了这个问题:

 
forwarding.png

图中的Warrior经过forwardInvocation:将negotiate消息转发给了Diplomat,这就好像是Warrior使用了超类Diplomat的方法同样。因此从这个思路,咱们能够在实际开发需求中模拟多继承的操做。

7、最后总结:

以上就是iOS运行时的基础知识部分了,理解Runtime的工做原理,下一篇iOS运行时Runtime应用,将总结其在实际开发中的使用。

做者:梧雨北辰 连接:https://www.jianshu.com/p/d4b55dae9a0d 来源:简书 简书著做权归做者全部,任何形式的转载都请联系做者得到受权并注明出处。
相关文章
相关标签/搜索