iOS之Runtime(一)

一:@@@《基础篇》@@@

二:@@@《应用篇》@@@

目录:

一、理解OC是动态语言,Runtime又是什么?
二、消息机制的基本原理
三、与Runtime交互的三种方式
四、分析Runtime中的数据结构
五、深入理解Rutime消息发送原理
六、多继承的实现思路:Runtime

题外话:计算机唯一能识别的语言是机器语言,高级编程语言不能被直接识别,需要先编译为汇编语言,再由汇编语言编译为机器语言才能被计算机识别。而 Objective-C语言不能被直接编译为汇编语言,它必须先编译为C语言,然后再编译为汇编语言,最后再由汇编语言编译为机器语言才能被计算机识别。 从OC到C语言的过渡就是由runtime来实现的。我们使用OC进行面向对象开发,但是C语言更多的是面向过程开发,这就需要将面向对象的类转变为面向过程的结构体。

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

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

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

我们常说OC是一门动态语言,就是因为它总是把一些决定性的工作(数据类型的确定)从编译阶段推迟到运行时阶段。OC代码的运行不仅需要编译器,还需要运行时系统(Runtime Sytem)来执行编译后的代码。

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

二、消息机制的基本原理

OC 的方法调用都是类似 [self methodName] 的形式,其实每次都是一个运行时消息发送过程。

第一步:编译阶段
[self methodName] 方法被编译器转化,分为两种情况:
1. 不带参数的方法被编译为:objc_msgSend(receiver,methodName)
2. 带参数的方法被编译为:objc_msgSend(recevier,methodName,org1,org2,…)
 
第二步:运行时阶段
消息接收者 recever 寻找对应的 selector(方法),也分为两种情况:
1.接收者能找到对应的selector,直接执行接收receiver对象的selector方法。
2.接收者找不到对应的selector,消息被转发或者临时向接收者添加这个selector对应的实现内容,否则崩溃。

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

三、与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"));

四、分析Runtime中的数据结构

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

要查看Runtime的代码 ,只需要我们在当前代码文件中导入如下头文件,使用组合键"Command +鼠标点击",即可进入Runtime的源码文件。

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

1. id 对应: objc_object

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

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

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                              OBJC2_UNAVAILABLE;
    const char * _Nonnull name                               OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                       OBJC2_UNAVAILABLE;
    struct objc_ivar_list * _Nullable ivars                  OBJC2_UNAVAILABLE;
    struct objc_method_list * _Nullable * _Nullable methodLists                    OBJC2_UNAVAILABLE;
    struct objc_cache * _Nonnull cache                       OBJC2_UNAVAILABLE;
    struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;
#endif

} OBJC2_UNAVAILABLE;
/* Use `Class` instead of `struct objc_class *` */
理解 objc_class 定义中的参数:

1.isa指针:

objc_object 和 objc_class 同样是结构体,而且都拥有一个isa指针。

objc_object 的isa指针指向对象的定义,那么objc_class的指针是怎么回事呢?

其实,在Runtime中Objc类本身同时也是一个对象。Runtime把类对象所属类型就叫做元类,用于描述类对象本身所具有的特征,最常见的类方法就被定义于此,所以objc_class中的isa指针指向的是元类,每个类仅有一个类对象,而每个类对象仅有一个与之相关的元类。

2. super_class 指针:

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

3. cache:

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

4. ivars:

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

5. methodLists:

methodLists 用于存放对象的所有成员方法。

6. protocols:

protocols 用于存放对象的所有协议。

3. SEL 对应: objc_selector

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 对应: objc_ivar

Ivar 代表类中实例变量的类型,是一个指向 objc_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                               OBJC2_UNAVAILABLE;
    char * _Nullable ivar_type                               OBJC2_UNAVAILABLE;
    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 对应: objc_method

Method 表示某个方法的类型,是一个指向 objc_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                                 OBJC2_UNAVAILABLE;
    char * _Nullable method_types                            OBJC2_UNAVAILABLE;
    IMP _Nonnull method_imp                                  OBJC2_UNAVAILABLE;
}     

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

6. 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);
}

注意:函数指针(IMP)的前两个参数必须是 id 和 SEL。

五、深入理解Rutime消息发送

先前讲到,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就提供了如下三种方法来处理:动态方法解析消息接受者重定向消息重定向,这三种方法的调用关系如下图:

    image.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)

下面使用一个示例来说明动态解析:MyGirl 类中声明方法却未添加实现,我们通过Runtime动态方法解析的操作为其他添加方法实现,具体代码如下:

// MyGirl.h文件
#import <Foundation/Foundation.h>

@interface MyGirl : NSObject

// 声明类方法,但未实现
+(void)myGirl:(NSString *)need;

// 声明实例方法,但未实现
-(void)eatFoods:(NSString *)foodsName;

@end
// MyGirl.m文件
#import "MyGirl.h"
#import <objc/runtime.h>  // 导入运行时

@implementation MyGirl

// 类方法未找到(实现)时调起
+(BOOL)resolveClassMethod:(SEL)sel{
    
    if (sel==@selector(myGirl:)) {
        
        // 类方法获取类名
        Class classValue=object_getClass(self);
        
        // 类中添加方法
        class_addMethod(classValue, sel, class_getMethodImplementation(classValue, @selector(myGirlReplaceFun:)), "我的女孩");
        
        return YES;
    }
    return [super resolveClassMethod:sel];
}

+(void)myGirlReplaceFun:(NSString *)value{
    NSLog(@"类方法没有实现,调用我啦(%@)",value);
}


// 实例方法未找到(实现)时调起
+(BOOL)resolveInstanceMethod:(SEL)sel{
    
    if (sel==@selector(eatFoods:)) {
        
        // Class classNext=object_getClass(self); // 不能使用类方法获取类名
        
        // 实例方法获取类名
        Class classValue=[self class];
        
        // 类中添加方法
        class_addMethod(classValue, sel, class_getMethodImplementation(classValue, @selector(eatFoodsReplaceFun:)), "吃什么呢");
    }
    return [super resolveInstanceMethod:sel];
}

-(void)eatFoodsReplaceFun:(NSString *)foodsName{
    NSLog(@"实例方法没有实现,来吃我啦(%@)",foodsName);
}

@end


// 控制器中导入文件和调用

#import "MyGirl.h"

- (void)viewDidLoad {
    [super viewDidLoad];
    [self useRuntimeAction];
}

-(void)useRuntimeAction{
  
    // 调用类方法
    [MyGirl myGirl:@"APink😬"];
    
    // 调用实例方法
    MyGirl *girl=[[MyGirl alloc]init];
    [girl eatFoods:@"哈根达斯💓"];
}

最后运行结果:

TestModel[89579:13842797] 类方法没有实现,调用我啦(APink😬)
TestModel[89579:13842797] 实例方法没有实现,来吃我啦(哈根达斯💓)

2. 消息接收者重定向

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

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

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

下面使用一个示例来说明消息接收者的重定向:
我们创建一个MyBoy 类,声明并实现 playGameAction:、talkWithGirlfriend:两个方法,然后在视图控制器ViewController.h 里测试,关键代码如下:

// MyBoy.h 文件中
#import <Foundation/Foundation.h>

@interface MyBoy : NSObject

// 类方法
+(void)playGameAction:(NSString *)gameName;

// 实例方法
-(void)talkWithGirlfriend:(NSString *)GFName;

@end


// MyBoy.m 文件中

#import "MyBoy.h"
@implementation MyBoy

+(void)playGameAction:(NSString *)gameName{
    NSLog(@"进来的方法:(%s) 参数值是:%@",__func__,gameName);
}

-(void)talkWithGirlfriend:(NSString *)GFName{
    NSLog(@"进来的方法:(%s) 参数值是:%@",__func__,GFName);
}

@end
// 在ViewController.m 文件中
#import "ViewController.h"
#import "MyBoy.h"

@interface ViewController ()
  
@property (nonatomic,strong)MyBoy  *boyObj;

@end
  
- (void)viewDidLoad {
    [super viewDidLoad];
    [self useRuntimeNextAction];   
}
-(void)useRuntimeNextAction{
    // 调用并未声明和实现的类方法
    [ViewController performSelector:@selector(playGameAction:) withObject:@"极品飞车"];
    
    // 调用并未声明和实现的类方法
    self.boyObj = [[MyBoy alloc] init];
    [self performSelector:@selector(talkWithGirlfriend:) withObject:@"LYE"];
    
}
// 重定向类方法:返回一个类对象
+(id)forwardingTargetForSelector:(SEL)aSelector{
    if (aSelector == @selector(playGameAction:)) {
        return [MyBoy class];
    }
    return [super forwardingTargetForSelector:aSelector];
}
// 重定向实例方法:返回类的实例
-(id)forwardingTargetForSelector:(SEL)aSelector{
    if (aSelector == @selector(talkWithGirlfriend:)) {
        return self.boyObj;
    }
    return [super forwardingTargetForSelector:aSelector];
}

运行最终结果:

TestModel[90017:13933756] 进来的方法:(+[MyBoy playGameAction:]) 参数值是:极品飞车
TestModel[90017:13933756] 进来的方法:(-[MyBoy talkWithGirlfriend:]) 参数值是:LYE

注意:动态方法解析阶段返回NO时,我们可以通过forwardingTargetForSelector可以修改消息的接收者,该方法返回参数是一个对象,如果这个对象是非nil,非self,系统会将运行的消息转发给这个对象执行。否则,继续查找其他流程。

3. 消息接收者重定向

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

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

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

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

我们会发现Runtime消息转发的一个特点:一个对象可以调起它本身不具备的方法。这个过程与OC中的继承特性很相似,其实官方文档中图示也很好的说明了这个问题:


image.png

图中的Warrior通过forwardInvocation:将negotiate消息转发给了Diplomat,这就好像是Warrior使用了超类Diplomat的方法一样。所以从这个思路,我们可以在实际开发需求中模拟多继承的操作。

最后,让我们深入一点吧😁!!!

二:@@@《应用篇》@@@

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,723评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,485评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,998评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,323评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,355评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,079评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,389评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,019评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,519评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,971评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,100评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,738评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,293评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,289评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,517评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,547评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,834评论 2 345