iOS Runtime详解

一、什么是Runtime?

我们都知道,从源代码到可执行文件需要经历三个阶段:编译链接运行
Objective-C是一门动态语言,会尽可能的将决定性的工作从编译时和链接时推迟到运行时,也就是说只有编译器是不够的,还需要一个运行时系统 (runtime system) 来执行编译后的代码。这就是 Objective-C Runtime 系统存在的意义,它是整个Objc运行框架的一块基石。
Runtime简称运行时。OC就是运行时机制,其中最主要的是消息机制。对于C语言,函数的调用在编译的时候会决定调用哪个函数。对于OC的函数,属于动态调用过程,在编译的时候并不能决定真正调用哪个函数(事实证明,在编译阶段,OC可以调用任何函数,即使这个函数并未实现,只要申明过就不会报错。而C语言在编译阶段就会报错),只有在真正运行的时候才会根据函数的名称找到对应的函数来调用。

二、Runtime源码

苹果和GNU各自维护一个开源的Runtime版本,这两个版本之间都在努力的保持一致。
1.苹果公司Runtime开源代码
2.GNU Runtime开源代码

三、Runtime底层解析

我们首先来看下runtime对象(object)类(class)方法(method)等都是这么定义的

1. 对象(object)

/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;

/// Represents an instance of a class.
// 对象
struct objc_object {
    // 对象的isa指针指向类对象
    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 指针,而Class是一个指向objc_class结构体的指针。
由此可以得出对象的本质是一个objc_object的结构体类的本质是一个objc_class的结构体

2. 类(class)

// 类对象
struct objc_class {
    // 类对象的isa指针指向元类对象
    // 元类对象的isa指针指向的是根元类
    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;

从上面源码可以看出objc_class 结构体定义了很多变量,其中包含了自身的所有实例变量(ivars)所有方法定义(methodLists)遵守的协议列表(protocols)等。objc_class 结构体 存放的数据称为元数据(metadata)
objc_class的第一个成员变量是isa指针,此isa指针指向的是本身的元类(meta class)

3. 元类(meta class)

那么什么是元类呢?
元类是编译器在创建类的同时创建的一个虚拟的类,用来存储类对象的类方法等信息的类。
类和元类的关系就和实例对象和类的关系一样:类就是实例对象所属的类,元类就是类对象所属的类
元类也是一个指向objc_class结构体的指针,元类isa指针指向的是根元类

4. 实例对象、类、元类的关系

下面用一张图来总结下这三者之间的关系

isa走位图
由图中可以看出:
实例对象中有个isa指针,这个isa指针指向实例对象所在的类,类对象中也有个isa指针,这个isa指针指向类对象所在的元类,元类对象还有个isa指针,这个isa指针指向根元类根元类中的isa指针指向的是本身

5. 方法(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;
}   

其中method_namemethod_imp分别是方法名称方法实现,那么method_types是什么呢?
method_types类型编码,为了和运行时系统协作,编译器将方法的返回类型和参数类型都编码成一个字符串,并且和方法选标关联 在一起。method_types的类型编码对照表如下:

类型编码对照表

四、消息传递

Objective-C中方法的调用通常是这样的[obj run],编译器在编译时都会转化为objc_msgSend(obj, run)进行消息发送;
如果obj为实例对象则消息传递流程:
1.找到对象所在类:通过objisa指针找到Class类。
2.从缓存中查找:从Class类中的方法缓冲区cache中查找方法(被调用过的方法都会存在方法缓冲区cache中,以便下次更快的调用),如果没有找到则进入下一步
3.从方法列表中查找:如果cache中没有,则从methodLists中查找。如果没找到则进入下一步。
4.通过继承链查找:通过Class的继承链找到父类直到根类NSObject,每次重复2,3步,如果还找不到则进入下一步。
5.动态方法解析:调用 + (BOOL)resolveInstanceMethod:(SEL)sel方法来查看是否能够返回一个selector,如果存在则返回selector。不存在进入下一步。
6.备用接收者- (id)forwardingTargetForSelector:(SEL)aSelector这个方法来询问是否有接收者可以接收这个方法。如果有接收者,则交给它处理,否则进入下一步。
7.消息的转发:如果到这一步还不能够找到相应的selector的话,就要进行完整的方法转发过程。调用方法(void)forwardInvocation:(NSInvocation *)anInvocation,如果这里还没有处理则会进入下一步。
8.奔溃:最后还是没有找到的话就只有呵呵了,这时候unrecognized selector sent to instance 0x100111df0的错误就来了。

动态方法解析

在上面方法传递过程中如果一直没找到方法会进入动态消息解析过程,在此过程中可以动态的添加方法实现。如果你添加了方法实现, 那运行时系统就会重新启动一次消息发送的过程。
动态方法解析主要在+ (BOOL)resolveInstanceMethod:(SEL)sel+ (BOOL)resolveClassMethod:(SEL)sel这两个方法中进行,通过例子我们来了解一下

@interface ViewController ()

// 声明run方法
- (void)run;

+ (void)walk;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // 调用run方法,但run方法并未被实现
    [self run];
    [ViewController walk];
}

// 对象方法未找到时调起此方法,可以再次方法中添加方法实现
+ (BOOL)resolveInstanceMethod:(SEL)sel{
    // 如果没有实现run方法
    if (sel == @selector(run)) {
        /**
         * 可以在此添加一个方法实现
         * @param cls         被添加方法的类
         * @param name        selector 方法名
         * @param imp         实现方法的函数指针
         * @param types imp   指向函数的返回值与参数类型
         * @return            如果添加方法成功返回 YES,否则返回 NO
         */
        return class_addMethod(self, sel, (IMP)runImp, "v@:");
    }else if (sel == @selector(walk)) {
        return class_addMethod(self, sel, (IMP)walkImp, "v@:");
    }
    
    return [super resolveInstanceMethod:sel];
}

// 类方法未找到时调起此方法,可以再次方法中添加方法实现
+ (BOOL)resolveClassMethod:(SEL)sel{
    // 如果没有实现run方法
    if (sel == @selector(walk)) {
        
        /**
        * 可以在此添加一个方法实现
        * @param cls         被添加方法的类的元类。⚠️这是元类
        * @param name        selector 方法名
        * @param imp         实现方法的函数指针
        * @param types imp   指向函数的返回值与参数类型
        * @return            如果添加方法成功返回 YES,否则返回 NO
        */
        return class_addMethod(objc_getMetaClass(object_getClassName(self)), sel, (IMP)walkImp, "v@:");;
    }
    return [super resolveClassMethod:sel];
}

// 方法实现
void runImp(id obj, SEL sel){
    NSLog(@"实例方法实现 %s",__func__);
}

// 方法实现
void walkImp(id obj, SEL sel){
    NSLog(@"类方法实现 %s",__func__);
}

@end

这是打印的信息

2020-09-02 15:55:13.694867+0800 RuntimeDemo[5899:162375] 实例方法实现 runImp
2020-09-02 15:55:13.695411+0800 RuntimeDemo[5899:162375] 类方法实现 walkImp

备用接收者

如果在动态消息转发过程中没有添加方法的实现,那么此时Runtime就会调用- (id)forwardingTargetForSelector:(SEL)aSelector这个方法来返回一个备用接收者,然后由这个备用接收者来实现这个方法。下面通过一个例子我们来了解一下

@interface Person : NSObject

@end

@implementation Person

- (void)run{
    NSLog(@"%s",__func__);
}

+ (void)walk{
    NSLog(@"%s",__func__);
}

@end


@interface ViewController ()

// 声明run方法
- (void)run;

+ (void)walk;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // 调用run方法,但run方法并未被实现
    [self run];
    [ViewController walk];
}

// 返回一个备用接收者
- (id)forwardingTargetForSelector:(SEL)aSelector{
    NSLog(@"instance method : %@", NSStringFromSelector(aSelector));
    if (aSelector == @selector(run)) {
        return [[Person alloc] init];
    }
    return [super forwardingTargetForSelector:aSelector];
}

+ (id)forwardingTargetForSelector:(SEL)aSelector{
    NSLog(@"class method : %@", NSStringFromSelector(aSelector));
    if (aSelector == @selector(walk)) {
        return [Person class];
    }
    return [super forwardingTargetForSelector:aSelector];
}

@end

下面是此次运行打印的结果

2020-09-02 18:07:40.491838+0800 RuntimeDemo[6821:230239] instance method : run
2020-09-02 18:07:40.492687+0800 RuntimeDemo[6821:230239] -[Person run]
2020-09-02 18:07:40.493125+0800 RuntimeDemo[6821:230239] class method : walk
2020-09-02 18:07:40.493510+0800 RuntimeDemo[6821:230239] +[Person walk]

可以看到虽然ViewController没有实现这两个方法,动态方法解析也没有添加这个两个方法实现,但是我们通过 forwardingTargetForSelector 把当前 ViewController的方法转发给了 Person 对象去执行了。打印结果也证明我们成功实现了转发。

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

消息转发(重定向)

如果经过前面两步Runtime 系统还是找不到相应的方法实现而无法响应消息,那么就会进入消息转发流程:
首先它会发送-methodSignatureForSelector:消息获得函数的参数和返回值类型。如果 methodSignatureForSelector:返回了一个 NSMethodSignature 对象(函数签名),Runtime 系统就会创建一个 NSInvocation 对象,并通过 forwardInvocation:消息通知当前对象,给予此次消息发送最后一次寻找 IMP 的机会。如果 methodSignatureForSelector: 返回 nil。则 Runtime 系统会发出doesNotRecognizeSelector: 消息,程序也就崩溃了。
下面我们通过一个例子来了解一下

@interface Person : NSObject

@end

@implementation Person

- (void)run{
    NSLog(@"%s",__func__);
}

+ (void)walk{
    NSLog(@"%s",__func__);
}

- (void)run:(NSString *)type{
    NSLog(@"%s %@",__func__, type);
}

@end


@interface ViewController ()

// 声明run方法
- (void)run;

- (void)run:(NSString *)type;

+ (void)walk;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // 调用run方法,但run方法并未被实现
    [self run];
    [self run:@"slowly"];
    [ViewController walk];
}

// 获取方法函数的参数和返回值类型,返回签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    if (aSelector == @selector(run)) {
        //签名,进入forwardInvocation
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }else if (aSelector == @selector(run:)) {
        //签名,进入forwardInvocation
        return [NSMethodSignature signatureWithObjCTypes:"v@:@"];
    }
    return [super methodSignatureForSelector:aSelector];
}

// 消息转发(重定向)
- (void)forwardInvocation:(NSInvocation *)anInvocation{
    SEL sel = anInvocation.selector;
    NSLog(@"- forwardInvocation %@", NSStringFromSelector(sel));
    Person *p = [[Person alloc] init];
    
    // 第一种方式 调用时候传的是什么参数就是什么参数
    if ([p respondsToSelector:sel]) {
        [anInvocation invokeWithTarget:p];
    }else {
        // 若仍然无法响应,则报错:找不到响应方法
        [self doesNotRecognizeSelector:sel];
    }
    
//    // 第二种方式 可以自定义传参
//    NSMethodSignature *signature = [p methodSignatureForSelector:sel];
//    NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
//    invocation.target = p;
//    invocation.selector = sel;
//    if (sel == @selector(run:)) {
//        NSString *runType = @"fast";
//        //注意:设置参数的索引时不能从0开始,因为0已经被self占用,1已经被_cmd占用
//        [invocation setArgument:&runType atIndex:2];
//    }
//    [invocation invoke];
    
}

+ (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    if (aSelector == @selector(walk)) {
        //签名,进入forwardInvocation
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return [super methodSignatureForSelector:aSelector];
}

+ (void)forwardInvocation:(NSInvocation *)anInvocation{
    SEL sel = anInvocation.selector;
    NSLog(@"+ forwardInvocation %@", NSStringFromSelector(sel));
    if ([Person respondsToSelector:sel]) {
        [anInvocation invokeWithTarget:objc_getClass(object_getClassName([Person class]))];
    }else {
        // 若仍然无法响应,则报错:找不到响应方法
        [self doesNotRecognizeSelector:sel];
    }
}

消息转发的实现有两种方式第一种调用时候传的是什么参数转发的就是什么参数,第二种可以自定义参数值,你想要什么参数就传什么参数。让我们来看下两种方式的打印结果
第一种方式

2020-09-06 11:36:44.051691+0800 RuntimeDemo[1377:38366] - forwardInvocation run
2020-09-06 11:36:44.052208+0800 RuntimeDemo[1377:38366] -[Person run]
2020-09-06 11:36:44.052624+0800 RuntimeDemo[1377:38366] - forwardInvocation run:
2020-09-06 11:36:44.052965+0800 RuntimeDemo[1377:38366] -[Person run:] slowly
2020-09-06 11:36:44.053331+0800 RuntimeDemo[1377:38366] + forwardInvocation walk
2020-09-06 11:36:44.053691+0800 RuntimeDemo[1377:38366] +[Person walk]

可以看到第四行这里打印的是slowly。
第二种方式

2020-09-06 11:43:33.036825+0800 RuntimeDemo[1404:40952] - forwardInvocation run
2020-09-06 11:43:33.037358+0800 RuntimeDemo[1404:40952] -[Person run]
2020-09-06 11:43:33.037811+0800 RuntimeDemo[1404:40952] - forwardInvocation run:
2020-09-06 11:43:33.038203+0800 RuntimeDemo[1404:40952] -[Person run:] fast
2020-09-06 11:43:33.039525+0800 RuntimeDemo[1404:40952] + forwardInvocation walk
2020-09-06 11:43:33.040117+0800 RuntimeDemo[1404:40952] +[Person walk]

可以看到第四行这里打印的是fast。
所以,可以根据实际开发中的需求来确定使用哪种方式。

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