iOSIOS面试集锦iOS归纳

iOS - Runtime基础

2021-05-11  本文已影响0人  码代码的小马

Runtime合集

iOS - isa、superclass指针,元类superclass指向基类本身

1. 什么是Runtime

Runtime是一个库,位于usr/include/objc, 经常用的api位于该库下的runtime.h文件中,在使用时需要引用头文件#import <objc/runtime.h>

2. Runtime 做什么用

通过Runtime,我们可以在App运行期动态的创建对象、检查对象、修改类、对象的方法,可以说Runtime是Objective-C的运行时机制的基础

3. 消息机制的基本原理

声明一个Person类, 类包含两个对象方法(此处为了编译后查找代码方便,我把函数名命为personSleep,此处不符合代码命名规范请忽略)

@implementation Person

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

- (void)personSleep {
    NSLog(@"person is sleeping...");
}

@end

我们在外界调用eatFood,编译成cpp查看

    Person *person = [[Person alloc] init];
    [person eatFood:@"baozi"];
    [person personSleep];

cpp

    Person *person = ((Person *(*)(id, SEL))(void *)objc_msgSend)((id)((Person *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("alloc")), sel_registerName("init"));
    ((void (*)(id, SEL, NSString * _Nonnull))(void *)objc_msgSend)((id)person, sel_registerName("eatFood:"), (NSString *)&__NSConstantStringImpl__var_folders_44_1ht3l6g55dv59_5s62wsv_bm0000gn_T_ViewController_88ee85_mi_0);
    ((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("personSleep"));

把代码简化一下,我们可知编译后的[person eatFood]变成了

objc_msgSend(reciver, Selector)

objc_msgSend(reciver, Selector, org1, org2, ...)

运行期阶段:消息接收者reciver寻找Selector去执行

  1. 通过reciverisa指针找到revicerClass
  2. Classcache(方法缓存)的散列表寻找对应的IMP(方法实现)
  3. 如果2.没找到,就继续在Classmethod list(方法列表)中找对应的selector,如果找到了,填充到Class的cache中并返回selector
  4. 如果3.没找到,就继续在其父类中找
  5. 一旦找到对应的selector,直接执行reciverselectorIMP(方法实现)
  6. 若找不到对应的selector,需要消息被转发或临时向这个reciver添加selector,否则会发生崩溃

4.Runtime中的概念

4.1 objc_msgSend

所有的Objective-C方法编译后都会变成对objc_msgSend的调用,

4.2 Class

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

#if !__OBJC2__
    Class _Nullable super_class;                                //指向父类的指针
    const char * _Nonnull name;                                 //类的名称
    long version;                                               //类的版本信息,默认为0
    long info;                                                  //类的信息,供运行期使用的一些位标识
    long instance_size;                                         //该类的实例变量大小
    struct objc_ivar_list * _Nullable ivars;                    //该类的实例变量列表
    struct objc_method_list * _Nullable * _Nullable methodLists;//方法定义的列表
    struct objc_cache * _Nonnull cache;                         //方法缓存;
    struct objc_protocol_list * _Nullable protocols;            //遵守的协议列表;
#endif
}

从中可以看出,objc_class 结构体 定义了很多变量:自身的所有实例变量(ivars)、所有方法定义(methodLists)、遵守的协议列表(protocols)等。objc_class 结构体 存放的数据称为 元数据(metadata)

objc_class结构体的第一个成员变量是isa指针,isa指针保存的是所属类的结构体的实例的指针,这里保存的就是objc_class结构体的实例指针,换个名字就是对象,也就是说,Class的本质就是一个对象,我们称为 类对象

4.3 Object

在objc.h中, Object被定义成了objc_class结构体

/// Represents an instance of a class.
typedef struct objc_class *Class;

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

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

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

4.4 Meta Class

从上边我们能看出,对象的(objc_object结构体)的isa指针指向对应类对象(object_class结构体),那么类对象(object_class结构体)的isa指针又指向什么呢
object_class结构体的isa指针实际上指向的是类对象自身的meta-class(元类)

元类就是一个类对象所属的类。一个对象所属的类叫做类对象,一个类对象所属的类就是元类

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

3. 消息机制的基本原理中讲到,对象的调用过程,是通过对象的isa指针找到类对象,在类对象的methodLists中找到对应的selector

而类方法的调用过程与对象的调用差不多,流程如下:

  1. 通过类对象isa指针找到所属的meta-class(元类)
  2. meta-classmethodLists中找到对应的selector
  3. 执行对应的selector

下面看一个示例:

NSString *str = [NSString stringWithFormat:@"%d,%s", 3. @"test"];

上边的示例中,stringWithFormat被发送给了NSString类,NSString类通过isa指针找到NSString的元类,然后在该元类的方法列表中找到对应的stringWithFormat:方法,然后执行该方法

4.5 实例对象、类、元类的关系

iOS - isa、superclass指针,元类superclass指向基类本身

0c1e95adfbc84b06aa9f28f61bd2b4dc~tplv-k3u1fbpfcp-watermark.image.png

4.6 Method

object_class结构体中的methodLists(方法列表)中存放的元素就是Method(方法)

objc/runtime.h中,表示Method的‘objc_method结构体’数据结构如下

struct objc_method {
    SEL _Nonnull method_name;       //方法名
    char * _Nullable method_types;  //方法类型
    IMP _Nonnull method_imp;        //方法实现
} 
  1. SEL method_name 方法名
    SEL的定义在objc/objc.h
/// An opaque type that represents a method selector.
typedef struct objc_selector *SEL;

SEL是一个指向objc_selector的指针,但是在runtime相关头文件中并没有找到明确的定义。不过,通过测试我们可以得出:SEL只是一个保存方法名的字符串

    SEL sel = @selector(viewDidLoad);
    NSLog(@"%s", sel);
    SEL sel1 = @selector(test);
    NSLog(@"%s", sel1);

输出为:

2021-05-10 21:58:24.705590+0800 RuntimeDemo[2266:67998] viewDidLoad
2021-05-10 21:58:24.705746+0800 RuntimeDemo[2266:67998] test
  1. IMP _Nonnull method_imp 方法实现
    IMP的定义同样在objc/objc.h
/// 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用来找到函数的地址,然后执行函数

  1. char * _Nullable method_types; 方法类型
    方法类型method_types是个字符串,用来存储方法的参数类型和返回值类型

到这里,Method的结构就已经很清楚了,MethodSEL(方法名)IMP(函数指针)关联起来,当对一个对象发送消息时,会通过给出的SEL(方法名)去找到IMP(函数指针),然后执行

5. Runtime消息转发

3. 消息机制的基本原理最后一步我们提到:若找不到对应的selector,消息被转发或者临时向receiver添加这个selector对应的实现方法,否则就会崩溃

当一个方法找不到的时候,Runtime提供了消息动态解析、消息接受者重定向、消息定向等三步处理消息,具体流程如下

image

5.1 消息动态解析(动态添加方法)

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

主要用到的方法如下
动态解析的方法位于

// 位于objc/NSObject.h
+ (BOOL)resolveClassMethod:(SEL)sel ;
+ (BOOL)resolveInstanceMethod:(SEL)sel;

//位于 objc/runtime.h

/** 
 * 向一个类添加新方法,此方法需要给定名称及参数
 * 
 * @param cls 要被添加方法的类
 * @param name selector方法名称
 * @param imp 实现方法的函数指针
 * @param types 只想函数的返回值与参数类型 
 * 
 * @return 如果添加方法成功返回YES,否则返回NO
 */
OBJC_EXPORT BOOL
class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, 
                const char * _Nullable types) ;

代码示例:

//
//  ViewController.m
//  RuntimeDemo
//
//  Created by Terence on 2021/5/10.
//  Copyright © 2021年 Terence. All rights reserved.
//

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

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    [self performSelector:@selector(eat)];
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(eat)) {
        class_addMethod(self.class, sel, (IMP)eatMethod, "v@:");
        return YES;
    }
    
    return [super resolveInstanceMethod:sel];
}

void eatMethod(id obj, SEL _cmd) {
    NSLog(@"eat food");
}

@end

输出结果:

2021-05-10 23:10:23.110858+0800 RuntimeDemo[3451:122697] eat food

从上边的例子中,我们可以看出,虽然我们没有实现fun方法,但是通过重写resolveInstanceMethod方法,利用class_addMethod 方法动态的添加了对象方法eatMethod,并执行,成功调用了eatMethod方法

class_addMethod方法中的特殊参数v@:,可参考苹果官方文档中关于Type Encodings的说明:Type Encodings

5.2 消息动态转发

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

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

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

注意:

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

代码示例:

@implementation Person

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

- (void)personSleep {
    NSLog(@"person is sleeping...");
}

@end
@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
   
    [self performSelector:@selector(personSleep)];
}

+ (BOOL)resolveClassMethod:(SEL)sel {
    return YES;
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    return YES;
}

- (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(personSleep)) {
        return [[Person alloc] init];
    }
    return [super forwardingTargetForSelector:aSelector];
}

@end

打印输出:

2021-05-11 11:50:55.352147+0800 LoadInitializeDemo[47468:1985216] person is sleeping...

可以看到,虽然当前ViewController没有实现fun方法,+resolveInstanceMethod:也没有添加其它函数实现,但是我们通过forwardingTargetForSelector把当前ViewController的方法转发给了person对象去执行了

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

5.3 消息重定向

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

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

  1. +methodSignatureForSelector
  2. + forwardInvocation:
  3. doesNotRecognizeSelector:

对象方法调用的是
-methodSignatureForSelector:
-forwardingInvocation:
doesNotRecognizeSelector:

用到的方法

//获取类方法函数的参数和返回值类型,返回签名
+ (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
// 类方法消息重定向
+ (void)forwardInvocation:(NSInvocation *)anInvocation {
    NSLog(@"aInvocation: %@", anInvocation);
    
// 获取对象方法函数的参数和返回值类型,返回签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
// 对象方法消息重定向
- (void)forwardInvocation:(NSInvocation *)anInvocation {
    NSLog(@"aInvocation: %@", anInvocation);
}


}

代码示例

#import "ViewController.h"
#import "Person.h"
#import <objc/runtime.h>
@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    [ViewController performSelector:@selector(personWakeup)];

}

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

//消息接收者重定向
+ (id)forwardingTargetForSelector:(SEL)aSelector {
//为了进行下一步,消息重定向
    return [super forwardingTargetForSelector:aSelector];
}

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

// 消息重定向
+ (void)forwardInvocation:(NSInvocation *)anInvocation {
    NSLog(@"aInvocation: %@", anInvocation);
    SEL sel = anInvocation.selector;
    
    if ([Person respondsToSelector:sel]) { //判断Person类对象是否可以响应sel
        [anInvocation invokeWithTarget:Person.class]; // 若可以响应,则将消息转发给其它对象处理
    } else {
            [anInvocation doesNotRecognizeSelector:sel];//若仍然无法响应,则报错:找不到方法
    }
}

@end


打印结果:

2021-05-11 16:40:10.119025+0800 LoadInitializeDemo[93832:2252330] person will wake up...

可以看到,我们在+forwardingInvocation:方法里面让Peron对象去执行了personWakeup函数

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

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

结合之前讲的3.消息机制的基本原理,就构成了整个消息发送及转发的流程,下面我们来总结下整个流程

6. 消息发送一级转发机制总结

调用[receiver selector]后,进行的流程:

  1. 编译阶段:[receiver selector]方法被编译器转换为:
    1. objc_msgSend(receiver, selector)(不带参数)
    2. objc_msgSend(receiver,selector, org1, org2, ...)(带参数)
  2. 运行时阶段:消息接收者receiver寻找对应的selector
    1. 通过receiverisa指针找到receiverClass
    2. 在Class的cache(方法缓存)的散列表中寻找对应的IMP(方法实现)
    3. 如果在cache(方法缓存)中没有找到对应的IMP(方法实现),则继续在Class(类)methodLists中寻找对应的selector,如果找到,填充到cache(方法缓存)中,并返回selector
    4. 如果在class(类)中没有找到这个selector,就继续在它的superclass(父类)中找
    5. 一旦找到对应的selector,直接指向receiver对应的selector方法实现的IMP(方法实现)
    6. 若找不到对应的selectorRuntime系统进入消息转发机制
      3.运行时消息转发阶段:
    7. 动态解析:通过重写resolveInstanceMethodresolveClassMethod,利用class_addMethod动态添加方法
    8. 消息接收者重定向:如果上一步没有添加其它函数实现,可在当前对象中利用forwardingTargetForSelector将消息的接收者转给其它对象
    9. 消息重定向: 如果上一步没有返回值为nil,返回了一个NSMethodSignature对象(函数签名),Runtime系统就会创建一个NSIncovation对象,并通过forwardingInvocation:消息通知当前对象,给予此次消息发送最后依次寻找IMP的机会
    10. 如果methodSignationForSelector返回nil,则Runtime系统就会发出doesNotRecoginzerSelector:消息,程序也就崩溃了

参考:

  1. Objective-C Runtime 苹果官方文档
  2. Objective-C Runtime Programming Guide
  3. 『Runtime』详解(一)基础知识
上一篇下一篇

猜你喜欢

热点阅读