iOS-Runtime基础篇

Runtime的特性主要是消息(方法)传递,如果消息(方法)在对象中找不到,就进行转发,具体怎么实现的呢。我们从下面几个方面探寻Runtime的实现机制。

  • Runtime介绍
  • Runtime中的数据结构
  • Runtime消息传递
  • Runtime消息转发
Runtime介绍

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

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

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

Runtime中的数据结构

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

#import 
#import 
#import 

接下来我们来看一下Runtime主要用到的结构体
一、objc_object

/// Represents an instance of a class.
struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};
/// A pointer to an instance of a class.
typedef struct objc_object *id;

每个实例内都有一个isa指针,该指针指向创建该实例的Class

二、objc_class

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;
super_class:指向objc_class类所继承的父类的指针。但是如果当前类已经是最顶层的类(如:NSObject),则super_class指针为NULL
name:类的名字
version:版本
info:信息
instance_size:实例大小
ivars:实例变量列表,存取方法都存放在methodLists中
methodLists:方法列表
cache:用于记录每次使用类或者实例对象调用的方法。这样每次响应消息的时候,Runtime系统会优先在cache中寻找响应方法,相比直接在类的方法列表中遍历查找,效率更高。存储的方式是以方法名为key、方法实现为value的Map中
protocols:遵守的协议列表

objc_object中的isa指针指向的结构体就是创建该实例的objc_class

同时,可以很明显的发现,在objc_class中也存在着一个isa指针,那么这个isa指针指向的又是哪里呢。其实,在Runtime中Objc类本身同时也是一个对象。Runtime把类对象所属类型就叫做元类(Meta-Class),用于描述类对象本身所具有的特征,最常见的类方法就被定义于此,所以objc_class中的isa指针指向的是元类,每个实例仅有一个类对象,而每个类对象仅有一个与之相关的元类。具体的关系我们可以参考下图。

iOS-Runtime基础篇_第1张图片

三、objc_method

struct objc_method {
    SEL _Nonnull method_name                                
    char * _Nullable method_types                            
    IMP _Nonnull method_imp                                  
}  
method_name:方法名,类型SEL
method_types: 一个char指针,指向存储方法的参数类型和返回值类型
method_imp:本质上是一个指针,指向方法的实现

这里其实就是SEL(method_name)IMP(method_name)形成了一个映射,通过SEL,我们可以很方便的找到方法实现IMP

四、SEL

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

SEL在OC中称作方法选择器,用于表示运行时方法的名字。OC在编译时,会依据每一个方法的名字、参数序列,生成一个唯一的整型标识(Int类型的地址),这个标识就是SEL

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

五、IMP

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

IMP这个函数指针指向了方法实现的首地址,当OC发起消息后,最终执行的代码是由IMP指针决定的。

六、objc_category

struct objc_category {
    char * _Nonnull category_name                            OBJC2_UNAVAILABLE;
    char * _Nonnull class_name                               OBJC2_UNAVAILABLE;
    struct objc_method_list * _Nullable instance_methods     OBJC2_UNAVAILABLE;
    struct objc_method_list * _Nullable class_methods        OBJC2_UNAVAILABLE;
    struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;
}                                                            OBJC2_UNAVAILABLE;
name:是指 class_name 而不是 category_name。
cls:要扩展的类对象,编译期间是不会定义的,而是在Runtime阶段通过name对 应到对应的类对象。
instanceMethods:category中所有给类添加的实例方法的列表。
classMethods:category中所有添加的类方法的列表。
protocols:category实现的所有协议的列表。
instanceProperties:表示Category里所有的properties,这就是我们可以通过objc_setAssociatedObject和objc_getAssociatedObject增加实例变量的原因,不过这个和一般的实例变量是不一样的。

从上面的objc_category的结构体中可以看出,分类中可以添加实例方法,类方法,甚至可以实现协议,添加属性。
平时所说的category中添加属性的操作就是利用Runtime的objc_setAssociatedObjectobjc_getAssociatedObject来的

Runtime消息传递

在了解完Runtime的数据结构后,我们来看个具体例子,了解下Runtime的消息传递流程是什么样的。比如:[obj foo]的执行流程

  • 通过objisa指针,找到对应的Class
  • Classcache中查找fooIMP,有则执行IMP
  • 没有则到Classmethod list中查找foo,找到执行IMP
  • 还没有则到superClass中找,有则执行IMP
  • 还是没有的话,则会走Runtime消息转发流程
Runtime消息转发

首先,我们先看一张消息转发的流程图:

iOS-Runtime基础篇_第2张图片
消息转发流程图.png

一、动态方法解析

所谓动态解析,我们可以理解为通过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)

下面以一个例子来说明动态解析。
Person类中有+ (void)run:(int)time;- (void)eat:(NSString *)food;的方法声明但是没有实现,我们通过动态方法解析为其添加实现,具体代码如下:

// ViewController.m

#import "ViewController.h"
#import "Person.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    
    Person *person = [[Person alloc] init];
    [person eat:@"Apple"];
    [Person run:10];
}

@end
// Person.h

#import 

@interface Person : NSObject
// 只有声明,没有实现
+ (void)run:(int)time;
// 只有声明,没有实现
- (void)eat:(NSString *)food;

@end
// Person.m

#import "Person.h"
#import "objc/runtime.h"
#import "objc/message.h"

@implementation Person

+ (BOOL)resolveClassMethod:(SEL)sel
{
    if (sel == @selector(run:))
    {
        //添加函数实现
        class_addMethod(object_getClass(self),
                        sel,
                        class_getMethodImplementation(object_getClass(self), @selector(resolve_run:)),
                        "v@");
        
        return YES;
    }
    
    return [class_getSuperclass(self) resolveClassMethod:sel];
}

+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    if (sel == @selector(eat:))
    {
        //添加函数实现
        class_addMethod([self class],
                        sel,
                        class_getMethodImplementation([self class], @selector(resolve_eat:)),
                        "v@");
        
        return YES;
    }
    
    return [super resolveInstanceMethod:sel];;
}

+ (void)resolve_run:(int)time
{
    NSLog(@"run %d minute", time);
}

- (void)resolve_eat:(NSString *)food
{
    NSLog(@"eat %@", food);
}

@end

执行结果如下:
eat Apple
run 10 minute

注意:

  1. 我们注意到class_addMethod方法中的特殊参数“v@”,具体可参考苹果文档Type Encodings有详细的解释。
  2. 成功使用动态方法解析还有个前提,那就是我们必须存在可以处理消息的方法,比如上述代码中的resolve_run:resolve_eat:

二、消息接受者重定向

当动态解析过程中没有对方法进行实现的话,那么消息发送机制就进入了消息转发(Forwarding)的阶段了,我们可以使用Runtime通过下面的方法替换消息接收者的为其他对象,从而保证程序的继续执行。

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

还是上面的例子,我们在Person中对eat:方法不进行动态解析,让它进入下一步(接受者重定向),转由Student类处理这个事件。

// Person.m
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    return [super resolveInstanceMethod:sel];;
}

- (id)forwardingTargetForSelector:(SEL)aSelector
{
    if (aSelector == @selector(eat:))
    {
        return [Student new];
    }
    
    return [super forwardingTargetForSelector:aSelector];
}

- (void)resolve_eat:(NSString *)food
{
    NSLog(@"eat %@", food);
}
// Student.h
#import 

@interface Student : NSObject

- (void)eat:(NSString *)food;

@end
// Student.m
#import "Student.h"

@implementation Student

- (void)eat:(NSString *)food
{
    NSLog(@"student eat %@", food);
}

@end

执行结果如下:
student eat Apple

可以看到我们通过forwardingTargetForSelector把当前Person的方法转发给了Student去执行了。打印结果也证明我们成功实现了转发。

注意:
在这里只会对实例方法进行接受者重定向,而不会对类方法进行接受者重定向

三、完整消息重定向

如果在上一步还不能处理未知消息,则唯一能做的就是启用完整的消息转发机制了。
首先它会发送- methodSignatureForSelector:消息获得函数的参数和返回值类型。如果- methodSignatureForSelector:返回nilRuntime则会发出- doesNotRecognizeSelector:消息,程序这时也就挂掉了。如果返回了一个函数签名,Runtime就会创建一个NSInvocation 对象并发送- forwardInvocation:消息给目标对象。
下面看一个例子,还是之前的eat:方法,Person类没有实现,我们通过完整消息重定向的方式让TeacherFarmer两个类来实现这个方法。

// Person.m
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    return [super resolveInstanceMethod:sel];
}

- (id)forwardingTargetForSelector:(SEL)aSelector
{
    return [super forwardingTargetForSelector:aSelector];
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
    if (aSelector == @selector(eat:))
    {
        return [NSMethodSignature signatureWithObjCTypes:"v@:*"];
    }
    
    return [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation
{
    SEL sel = anInvocation.selector;
    
    Teacher *teacher = [Teacher new];
    Farmer *farmer = [Farmer new];
    
    if ([teacher respondsToSelector:sel] || [farmer respondsToSelector:sel])
    {
        if ([teacher respondsToSelector:sel])
        {
            [anInvocation invokeWithTarget:teacher];
        }
        
        if ([farmer respondsToSelector:sel])
        {
            [anInvocation invokeWithTarget:farmer];
        }
    }
    else
    {
        [self doesNotRecognizeSelector:sel];
    }
}
// Teacher.h
#import 

@interface Teacher : NSObject

- (void)eat:(NSString *)food;

@end

// Teacher.m
#import "Teacher.h"

@implementation Teacher

- (void)eat:(NSString *)food
{
    NSLog(@"Teacher eat %@", food);
}

@end
// Farmer.h
#import 

@interface Farmer : NSObject

- (void)eat:(NSString *)food;

@end

// Farmer.m
#import "Farmer.h"

@implementation Farmer

- (void)eat:(NSString *)food
{
    NSLog(@"Farmer eat %@", food);
}

@end

打印结果如下:
Teacher eat Apple
Farmer eat Apple

总结:

  1. 从以上的代码中就可以看出,- forwardingTargetForSelector仅支持一个对象的返回,也就是说消息只能被转发给一个对象,而- forwardInvocation可以将消息同时转发给任意多个对象,这就是两者的最大区别。
  2. 虽然理论上可以重载doesNotRecognizeSelector函数实现保证不抛出异常(不调用super实现),但是苹果文档着重提出“一定不能让这个函数就这么结束掉,必须抛出异常”。

参考自:
Runtime-iOS运行时基础篇
iOS Runtime详解

你可能感兴趣的:(iOS-Runtime基础篇)