iOS RuntimeiOS 开发之 runtimeiOS开发

iOS Runtime详解

2020-09-06  本文已影响0人  当沉默已成习惯

一、什么是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。
所以,可以根据实际开发中的需求来确定使用哪种方式。

上一篇下一篇

猜你喜欢

热点阅读