iOS Runtime 详解(一)

2020-02-22  本文已影响0人  黎明s

>Runtime 介绍

C 语言 作为一门静态类语言,在编译阶段就已经确定了所有变量的数据类型,同时也确定好了要调用的函数,以及函数的实现。而 Objective-C 语言是一门动态语言。在编译阶段并不知道变量的具体数据类型,也不知道所真正调用的哪个函数。只有在运行时间才检查变量的数据类型,同时在运行时才会根据函数名查找要调用的具体函数。这样在程序没运行的时候,我们并不知道调用一个方法具体会发生什么。

Objective-C 扩展了 C 语言,并加入了面向对象特性和 Smalltalk 式的消息传递机制。而这个扩展的核心是一个用 C 和 编译语言 写的 Runtime 库。它是 Objective-C 面向对象和动态机制的基石。

Objective-C 是一个动态语言,这意味着它不仅需要一个编译器,也需要一个运行时系统来动态得创建类和对象、进行消息传递和转发。理解 Objective-C 的 Runtime 机制可以帮我们更好的了解这个语言,适当的时候还能对语言进行扩展,从系统层面解决项目中的一些设计或技术问题。了解 Runtime ,要先了解它的核心 - 消息传递 (Messaging)。

Runtime其实有两个版本: modernlegacy。我们现在用的 Objective-C 2.0 采用的是现行 (Modern) 版的 Runtime 系统,只能运行在 iOS 和 macOS 10.5 之后的 64 位程序中。而 macOS 较老的32位程序仍采用 Objective-C 1 中的(早期)Legacy版本的 Runtime 系统。这两个版本最大的区别在于当你更改一个类的实例变量的布局时,在早期版本中你需要重新编译它的子类,而现行版就不需要。

Runtime 基本是用 C汇编写的,可见苹果为了动态系统的高效而作出的努力。你可以在这里下到苹果维护的开源代码。苹果和GNU各自维护一个开源的 runtime 版本,这两个版本之间都在努力的保持一致。平时的业务中主要是使用官方Api,解决我们框架性的需求。

高级编程语言想要成为可执行文件需要先编译为汇编语言再汇编为机器语言,机器语言也是计算机能够识别的唯一语言,但是OC并不能直接编译为汇编语言,而是要先转写为纯C语言再进行编译和汇编的操作,从OCC语言的过渡就是由runtime来实现的。然而我们使用OC进行面向对象开发,而C语言更多的是面向过程开发,这就需要将面向对象的类转变为面向过程的结构体。

>数据结构

runtime数据结构.png

>消息机制的基本原理

基本消息发送框架.png

Objective-C 语言 中,对象方法调用都是类似[receiver selector];的形式,其本质就是让对象在运行时发送消息的过程。编译器转成消息发送objc_msgSend(receiver, selector)objc_msgSend(recevier,selector,org1,org2,…),runtime时执行的流程是这样的:

但这种实现有个问题,效率低。一个class往往只有20%的函数会被经常调用,可能占总调用次数的80%。每个消息都需要遍历一次objc_method_list并不合理。如果把经常被调用的函数缓存下来,那可以大大提高函数查询的效率。这也就是objc_class中另一个重要成员objc_cache做的事情 - 再找到selector之后,把selectormethod_name作为keymethod_imp作为value给存起来。当再次收到recevier消息的时候,可以直接在cache里找到,避免去遍历objc_method_list。从前面的源代码可以看到objc_cache是存在objc_class结构体中的。

objec_msgSend的方法定义如下:

OBJC_EXPORT id objc_msgSend(id self, SEL op, ...)

>Runtime 中的概念解析

struct objc_class {
    // objc_class 结构体的实例指针
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
    // 指向父类的指针
    Class _Nullable super_class                                     OBJC2_UNAVAILABLE;
    // 类的名字
    const char * _Nonnull name                                      OBJC2_UNAVAILABLE;
    // 类的版本信息,默认为 0
    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;

类对象就是一个结构体struct objc_class,这个结构体存放的数据称为元数据(metadata),该结构体的第一个成员变量也是isa指针,这就说明了Class本身其实也是一个对象,因此我们称之为类对象,类对象在编译期产生用于创建实例对象,是单例。

Object(对象)被定义为objc_object结构体,其数据结构如下:

/// Represents an instance of a class.
struct objc_object {
    // objc_object 结构体的实例指针
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};

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

这里的id被定义为一个指向objc_object 结构体的指针。从中可以看出objc_object 结构体只包含一个Class类型的isa 指针。换句话说,一个Object(对象)唯一保存的就是它所属 Class(类)的地址。当我们对一个对象,进行方法调用时,比如[receiver selector];,它会通过objc_object 结构体isa 指针去找对应的object_class 结构体,然后在object_class 结构体methodLists(方法列表)中找到我们调用的方法,然后执行。

对象(objc_object 结构体)isa 指针指向的是对应的类对象(object_class 结构体)。而类对象(object_class 结构体)isa 指针实际上指向的的是类对象自身的Meta Class(元类)

Meta Class(元类)就是一个类对象所属的类。一个对象所属的类叫做类对象,而一个类对象所属的类就叫做元类

Runtime 中把类对象所属类型就叫做Meta Class(元类),用于描述类对象本身所具有的特征,而在元类methodLists中,保存了类的方法链表,即所谓的类方法。并且类对象中的isa 指针指向的就是元类。每个类对象有且仅有一个与之相关的元类。

对象方法的调用过程,我们是通过对象的isa 指针找到对应的 Class(类);然后在Class(类)method list(方法列表)中找对应的selector。而 类方法的调用过程 和对象方法调用差不多,流程如下:

下面我们通过一张图来清晰地表示出实例对象(Object)类(Class)Meta Class(元类)的指向关系。

object-class-meta.png

通过上图我们可以看出整个体系构成了一个自闭环,struct objc_object结构体实例它的isa指针指向类对象类对象isa指针指向了元类super_class指针指向了父类的类对象,而元类super_class指针指向了父类的元类,那元类的isa指针又指向了自己。

元类(Meta Class)是一个类对象的类。

在上面我们提到,所有的类自身也是一个对象,我们可以向这个对象发送消息(即\color{#45B787}{调用类方法})。为了调用类方法,这个类的isa指针必须指向一个包含这些类方法的一个objc_class结构体。这就引出了meta-class的概念,元类中保存了创建类对象以及类方法所需的所有信息。任何NSObject继承体系下的meta-class都使用NSObjectmeta-class作为自己的所属类,而基类的meta-classisa指针是指向它自己。

@interface NSObject (Test)

+ (void)foo;

@end


@implementation NSObject (Test)

- (void) foo {
    NSLog(@"%@",NSStringFromSelector(_cmd));
    return;
}
@end

int main(int argc, char * argv[]) {
    @autoreleasepool {
        [NSObject foo];
        [[NSObject new] foo];
    }
    return 0;
}
输出结果:
foo
foo

解析:类的实例方法是存储在类的methodLists中,而类方法则是存储在元类的methodLists中,NSObject元类superclass是指向Class,当调用[NSObject foo]的时候,因为这是一个类方法调用,所以从元类中查找签名为foo的方法,没有发现,然后再沿superclass继续查找,结果在Class中查找到该方法,于是调用该方法输出。但如果将NSObject的分类换成其他类的分类(如NSString),会发现程序崩溃,这是因为签名为foo的函数在NSString中,而当我们进行类方法调用的时候,最后会查找到NSObjectClass中,但该Class中并没有对应的方法签名,于是再沿superclass向上查找,由于NSObjectsuperclassnil,于是抛出unrecognized selector

object_class 结构体methodLists(方法列表)中存放的元素就是Method(方法)。在objc/runtime.h中,表示Method(方法)objc_method 结构体的数据结构:

/// An opaque type that represents a method in a class definition.
/// 代表类定义中一个方法的不透明类型
typedef struct objc_method *Method;

struct objc_method {
    // 方法名
    SEL _Nonnull method_name                                 OBJC2_UNAVAILABLE;
    // 方法类型
    char * _Nullable method_types                            OBJC2_UNAVAILABLE;
     // 方法实现
    IMP _Nonnull method_imp                                  OBJC2_UNAVAILABLE;
}                                                            OBJC2_UNAVAILABLE;

objc_method 结构体中包含了method_name(方法名)method_types(方法类型)method_imp(方法实现)。说明SELIMP其实都是Method的属性。下面,我们来了解下这三个变量。

1. SEL(objc_selector)
Objc.h
/// An opaque type that represents a method selector.代表一个方法的不透明类型
typedef struct objc_selector *SEL;

objc_msgSend函数第二个参数类型为SEL,它是selectorObjective-C中的表示类型(Swift中是Selector类)。selector是方法选择器,可以理解为区分方法的ID,而这个ID的数据结构是SELSEL是一个指向objc_selector 结构体的指针。
runtime 相关头文件中并没有找到明确的定义。不过,通过测试我们可以得出:SEL只是一个保存方法名的字符串。

SEL sel = @selector(viewDidLoad);
NSLog(@"%s", sel);              // 输出:viewDidLoad
SEL sel1 = @selector(test);
NSLog(@"%s", sel1);             // 输出:test
@property SEL selector;

可以看到selectorSEL的一个实例。

A method selector is a C string that has been registered (or “mapped“) with the Objective-C runtime. Selectors generated by the compiler are automatically mapped by the runtime when the class is loaded.

其实selector就是个映射到方法的C字符串,你可以用Objective-C编译器命令@selector()或者Runtime系统的sel_registerName函数来获得一个SEL类型的方法选择器。

selector既然是一个string,是类似className+method的组合,命名规则有两条:

这也带来了一个弊端,我们在写C代码的时候,经常会用到函数重载,就是函数名相同,参数不同,但是这在Objective-C中是行不通的,因为selector只记了methodname,没有参数,所以没法区分不同的method
比如:

- (void)caculate(NSInteger)num;
- (void)caculate(CGFloat)num;

是会报错的。
我们只能通过命名来区别:

- (void)caculateWithInt(NSInteger)num;
- (void)caculateWithFloat(CGFloat)num;

在不同类中相同名字的方法所对应的方法选择器是相同的,即使方法名字相同而变量类型不同也会导致它们具有相同的方法选择器。

2. IMP(method_imp)
/// A pointer to the function of a method implementation. 
#if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ... */ ); 
#else
typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...); 
#endif

IMP的实质是一个函数指针,所指向的就是方法的实现。IMP用来找到函数地址,然后执行函数。在iOSRuntime中,Method通过selectorIMP两个属性,实现了快速查询方法及实现,相对提高了性能,又保持了灵活性。

IMP和SEL关系:

每一个继承于NSObject的类都能自动获得runtime的支持。在这样的一个类中有一个isa指针指向该类定义的数据结构体,这个结构体是由编译器编译时为类(需继承于NSObject)创建的。在这个结构体中有包括了指向其父类类定义的指针以及 Dispatch tableDispatch table是一张SELIMP的对应表。也就是说方法编号SEL最后还是要通过Dispatch table表寻找到对应的IMPIMP就是一个函数指针,然后执行这个方法:
1)通过方法获得方法的编号:SEL methodId=@selector(methodName);或者SEL methodId = NSSelectorFromString(methodName);
2)通过方法编号执行该编号的方法:[self performSelector:methodId withObject:nil];
3)通过方法编号获取该编号的方法名 NSString *methodName = NSStringFromSelector(methodId);
4)通过方法编号获得IMPIMP methodPoint = [self methodForSelector:methodId];
5)执行IMPvoid (*func)(id, SEL, id) = (void *)imp;func(self, methodName,param);

注意:如果方法没有传入参数时:void (*func)(id, SEL) = (void *)imp;func(self, methodName);。如果方法传入一个参数时:void (*func)(id, SEL,id) = (void *)imp;func(self, methodName,param);。如果方法传入俩个参数时:void (*func)(id, SEL,id,id) = (void *)imp;func(self, methodName,param1,param2);

想更深入了解 IMP 的小伙伴请戳这里

3. char *method_types

方法类型method_types是个字符串,用来存储方法的参数类型和返回值类型。

Objective-C运行时通过跟踪它的isa 指针检查对象时,它可以找到一个实现许多方法的对象。然而,你可能只调用它们的一小部分,并且每次查找时,搜索所有选择器的类分派表没有意义。所以类实现一个缓存,每当你搜索一个类分派表,并找到相应的选择器,它把它放入它的缓存。所以当objc_msgSend查找一个类的选择器,它首先搜索类缓存。这是基于这样的理论:如果你在类上调用一个消息,你可能以后再次调用该消息。

为了加速消息分发, 系统会对方法和对应的地址进行缓存,就放在上述的objc_cache,所以在实际运行中,大部分常用的方法都是会被缓存起来的,Runtime系统实际上非常快,接近直接执行内存地址的程序速度。

进行一次发送消息会在相关的类对象中搜索方法列表,如果找不到则会沿着继承树向上一直搜索知道继承树根部(通常为NSObject),如果还是找不到并且消息转发都失败了就回执行doesNotRecognizeSelector:方法报unrecognized selector错。那么消息转发到底是什么呢?接下来将会逐一介绍最后的三次机会。

消息转发流程简图.png
1.动态方法解析(消息动态解析)

首先,Objective-C运行时会调用+resolveInstanceMethod:或者+resolveClassMethod:,让你有机会提供一个函数实现。前者在对象方法未找到时调用,后者在类方法未找到时调用。我们可以通过重写这两个方法,添加其他函数实现,并返回YES, 那运行时系统就会重新启动一次消息发送的过程。

主要用的的方法如下:

// 类方法未找到时调起,可以在此添加方法实现
+ (BOOL)resolveClassMethod:(SEL)sel;
// 对象方法未找到时调起,可以在此添加方法实现
+ (BOOL)resolveInstanceMethod:(SEL)sel;

/** 
 * class_addMethod    向具有给定名称和实现的类中添加新方法
 * @param cls         被添加方法的类
 * @param name        selector 方法名
 * @param imp         实现方法的函数指针
 * @param types imp   指向函数的返回值与参数类型
 * @return            如果添加方法成功返回 YES,否则返回 NO
 */
BOOL class_addMethod(Class cls, SEL name, IMP imp, 
                const char * _Nullable types);

举个例子:

#import "ViewController.h"
#include "objc/runtime.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 执行 fun 函数
    [self performSelector:@selector(fun)];
}

// 重写 resolveInstanceMethod: 添加对象方法实现
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(fun)) { // 如果是执行 fun 函数,就动态解析,指定新的 IMP
        class_addMethod([self class], sel, (IMP)funMethod, "v@:");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

void funMethod(id obj, SEL _cmd) {
    NSLog(@"funMethod"); //新的 fun 函数
}

@end
输出结果:
funMethod

从上边的例子中,我们可以看出,虽然我们没有实现fun方法,但是通过重写resolveInstanceMethod:,利用class_addMethod方法添加对象方法实现funMethod方法并执行。从打印结果来看,成功调起了funMethod 方法。

我们注意到 class_addMethod 方法中的特殊参数 v@:,具体可参考官方文档中关于Type Encodings的说明:传送门

2.备用接收者(消息接受者重定向)

如果上一步中+resolveInstanceMethod:或者+resolveClassMethod:没有添加其他函数实现,运行时就会进行下一步——消息接受者重定向。

如果当前对象实现了-forwardingTargetForSelector: 或者 +forwardingTargetForSelector:方法,Runtime就会调用这个方法,允许我们将消息的接受者转发给其他对象。

其中用到的方法:

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

注意:
1.类方法和对象方法消息转发第二步调用的方法不一样,前者是+forwardingTargetForSelector:方法,后者是-forwardingTargetForSelector:方法。
2.这里+resolveInstanceMethod:或者+resolveClassMethod:无论是返回YES还是NO,只要其中没有添加其他函数实现,运行时都会进行下一步。

举个例子:

#import "ViewController.h"
#include "objc/runtime.h"

@interface Person : NSObject

- (void)fun;

@end

@implementation Person

- (void)fun {
    NSLog(@"fun");
}

@end



@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 执行 fun 方法
    [self performSelector:@selector(fun)];
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    return YES; // 为了进行下一步 消息接受者重定向
}

// 消息接受者重定向
- (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(fun)) {
        return [[Person alloc] init];
        // 返回 Person 对象,让 Person 对象接收这个消息
    }
    
    return [super forwardingTargetForSelector:aSelector];
}
输出结果:
fun

可以看到,虽然当前ViewController没有实现fun方法,+resolveInstanceMethod:也没有添加其他函数实现。但是我们通过forwardingTargetForSelector把当前ViewController的方法转发给了Person对象去执行了。打印结果也证明我们成功实现了转发。

我们通过forwardingTargetForSelector可以修改消息的接收者,该方法返回参数是一个对象,如果这个对象是不是nil,也不是self,系统会将运行的消息转发给这个对象执行。否则,继续进行下一步——消息重定向流程。

3.完整消息转发(消息重定向)

如果经过消息动态解析、消息接受者重定向,Runtime系统还是找不到相应的方法实现而无法响应消息,Runtime系统会利用-methodSignatureForSelector:或者 +methodSignatureForSelector:方法获取函数的参数和返回值类型。

如果methodSignatureForSelector:返回了一个 NSMethodSignature 对象(函数签名)Runtime系统就会创建一个NSInvocation对象,并通过forwardInvocation:消息通知当前对象,给予此次消息发送最后一次寻找IMP的机会。如果methodSignatureForSelector:返回nil。则Runtime系统会发出doesNotRecognizeSelector:消息,程序也就崩溃了。所以我们可以在forwardInvocation:方法中对消息进行转发。

注意:类方法和对象方法消息转发第三步调用的方法同样不一样。

用到的方法:

// 获取类方法函数的参数和返回值类型,返回签名
+ (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;

// 类方法消息重定向
+ (void)forwardInvocation:(NSInvocation *)anInvocation;

// 获取对象方法函数的参数和返回值类型,返回签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;

// 对象方法消息重定向
- (void)forwardInvocation:(NSInvocation *)anInvocation;

举个例子:

#import "ViewController.h"
#include "objc/runtime.h"

@interface Person : NSObject

- (void)fun;

@end

@implementation Person

- (void)fun {
    NSLog(@"fun");
}

@end


@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 执行 fun 函数
    [self performSelector:@selector(fun)];
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    return YES; // 为了进行下一步 消息接受者重定向
}

- (id)forwardingTargetForSelector:(SEL)aSelector {
    return nil; // 为了进行下一步 消息重定向
}

// 获取函数的参数和返回值类型,返回签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if ([NSStringFromSelector(aSelector) isEqualToString:@"fun"]) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    
    return [super methodSignatureForSelector:aSelector];
}

// 消息重定向
- (void)forwardInvocation:(NSInvocation *)anInvocation {
    SEL sel = anInvocation.selector;   // 从 anInvocation 中获取消息
    
    Person *p = [[Person alloc] init];

    if([p respondsToSelector:sel]) {   // 判断 Person 对象方法是否可以响应 sel
        [anInvocation invokeWithTarget:p];  // 若可以响应,则将消息转发给其他对象处理
    } else {
        [self doesNotRecognizeSelector:sel];  // 若仍然无法响应,则报错:找不到响应方法
    }
}
@end
输出结果:
fun

可以看到,我们在-forwardInvocation:方法里面让Person 对象去执行了fun函数。

既然-forwardingTargetForSelector:-forwardInvocation:都可以将消息转发给其他对象处理,那么两者的区别在哪?区别就在于-forwardingTargetForSelector:只能将消息转发给一个对象。而-forwardInvocation:可以将消息转发给多个对象。

以上就是 Runtime 消息转发的整个流程。

参考链接:
https://www.jianshu.com/p/6ebda3cd8052
https://www.jianshu.com/p/633e5d8386a8
https://www.imooc.com/article/38310
官方文档:
runtime源码地址(开源)
苹果官方Runtime编程指南
objc_msgSend
objc_msgSend_fpret
objc_msgsend_stret
objc_msgsendsuper
objc_msgsendsuper_stret
大神入口:
http://yulingtianxia.com/blog/2016/06/15/Objective-C-Message-Sending-and-Forwarding/

上一篇 下一篇

猜你喜欢

热点阅读