全方位剖析iOS高级技术问题(三)之动态运行时Runtime相关问题

本文主要内容

一.Runtime数据结构
二.Runtime类对象与元类对象
三.Runtime方法缓存&消息传递详述
四.Runtime消息转发
五.Runtime Method-Swizzling
六.Runtime动态添加方法
七.Runtime动态方法解析

全方位剖析iOS高级技术问题(三)之动态运行时Runtime相关问题

一.Runtime数据结构

  • objc_object
  • objc_class
  • isa指针
  • method_t

1、objc_object

所有的对象都是id类型的,id类型对象对应到Runtime中就是objc_object结构体。objc_object包含以下内容:isa_t共用体、关于isa操作相关、弱引用相关、关联对象相关、内存管理相关

全方位剖析iOS高级技术问题(三)之动态运行时Runtime相关问题

2、objc_class

OC中使用的Class对应到Runtime中就是objc_class结构体,objc_class继承自objc_object,所以Class也是个对象。objc_class包含以下内容:Class super Class、cache_t cache、class_data_bits_t bits

全方位剖析iOS高级技术问题(三)之动态运行时Runtime相关问题

3、isa指针

  • 共用体isa_t,指针型isa的isa的值代表Class的地址,非指针型isa的isa的值的部分代表Class的地址

全方位剖析iOS高级技术问题(三)之动态运行时Runtime相关问题

  • isa的指向:关于对象,其指向类对象;关于类对象,其指向元类对象。

全方位剖析iOS高级技术问题(三)之动态运行时Runtime相关问题

4、cache_t

  • 用于快速查找方法执行函数
  • 是可增量扩展的哈希表结构
  • 是局部性原理的最佳应用

局部性原理:一般在调用方法时,往往调用的就那几个方法,这几个方法被调用的频次是最高的,如果把调用频次最高的这几个方法放到缓存中,下次的命中率会更高。

数据结构说明: key对应OC语言中的SEL。

全方位剖析iOS高级技术问题(三)之动态运行时Runtime相关问题

5、class_data_bits_t

  • class_data_bits_t主要是对class_rw_t的封装
  • class_rw_t代表了类相关的读写信息、对class_ro_t的封装
  • class_ro_t代表了类相关的只读信息

class_rw_t:

全方位剖析iOS高级技术问题(三)之动态运行时Runtime相关问题

class_ro_t:

全方位剖析iOS高级技术问题(三)之动态运行时Runtime相关问题

class_ro_t:

  • 函数四要素:名称、返回值、参数、函数体

全方位剖析iOS高级技术问题(三)之动态运行时Runtime相关问题

Type Encodings

  • const char* types;

全方位剖析iOS高级技术问题(三)之动态运行时Runtime相关问题

Runtime总体结构:

全方位剖析iOS高级技术问题(三)之动态运行时Runtime相关问题

二.Runtime类对象与元类对象

  • 类对象存储实例方法列表等信息
  • 元类对象存储类方法列表等信息

类对象和元类对象的区别和联系

  • 实例对象能通过isa指针找到它的类对象,类对象中存储实例方法列表等信息;
  • 类对象能通过isa指针找到它的元类对象,元类对象中存储类方法列表等信息;
  • 类对象和元类对象都是objc_class数据结构,由于objc_class继承自objc_object,所以才有isa指针,才能实现实例可以通过isa指针找到类对象,进而可以访问实例方法列表等信息,类对象可以通过isa指针找到元类对象,进而可以访问类方法列表等信息。

全方位剖析iOS高级技术问题(三)之动态运行时Runtime相关问题

消息传递简述

// 
void objc_msgSend(void /* id self, SEL op,... */)

全方位剖析iOS高级技术问题(三)之动态运行时Runtime相关问题

void objc_msgSendSuper(void /* struct objc_super *super, SEL op,... */)

struct objc_super {
    // Specifies an instance of a class
    // 接收者实际就是当前对象(self)
    _unsafe_unretained id receiver;
}

全方位剖析iOS高级技术问题(三)之动态运行时Runtime相关问题

注意⚠️:无论是[self class]还是[super class],当前这条消息的接收者都是当前对象self。

消息传递流程

全方位剖析iOS高级技术问题(三)之动态运行时Runtime相关问题

问题1:如下输出结果是什么?

#import "Mobile.h"
@interface Phone: Mobile

@end

@implementation Phone

- (id)init {
    self = [super init];
    if (self) {
        // 结果: Phone Phone
        NSLog(@"%@", NSStringFromClass([self class]));
        NSLog(@"%@", NSStringFromClass([self class]));
    }
    return self;
}

答:在消息传递过程中,[self class]的消息接收者是当前对象,即Phone。Phone的实例在初始化后,通过[self class]打印类信息或类名称,通过实例对象(instance of Subclass)的isa指针找到Phone的类对象(Subclass(class)),类对象中没有class方法,再通过superClass找到父类Mobile对象(Superclass(class)),仍然没有class方法,继续查找直到根类对象中实现class。通过[super class]打印类信息或类名称,实际的接收者也还是当前对象Phone,而[super class]的含义是跨越了类对象的查找直接到父类(Superclass(class))开始查找,直到查找到根类对象NSObject。

三.Runtime方法缓存&消息传递详述

第一步、缓存查找

实际上就是根据给定的方法选择器SEL,查找对应bucket_t中的IMP,即方法实现。涉及到Hash哈希查找,首先根据给定的方法选择器,通过Hash哈希函数映射出bucket_t在数据中的位置。

全方位剖析iOS高级技术问题(三)之动态运行时Runtime相关问题

第二步、当前类中查找

  • 对于已排序好的列表,采用二分查找算法查找方法对应执行函数
  • 对于没有排序的列表,采用一般遍历查找方法对应执行函数

第三步、父类逐级查找

全方位剖析iOS高级技术问题(三)之动态运行时Runtime相关问题

消息传递总结

消息传递过程中首先依次看缓存是否命中当前类方法列表是否命中逐级父类方法列表是否命中,然后缓存是否命中是通过哈希查找方式查找;当前类方法列表是否命中涉及对于已排序好的列表,采用二分查找算法查找方法对应执行函数,对于没有排序的列表,采用一般遍历查找方法对应执行函数;逐级父类方法列表是否命中根据superclass指针逐级查找。

全方位剖析iOS高级技术问题(三)之动态运行时Runtime相关问题

四.Runtime消息转发

对于实例方法的消息转发流程:

  • 1.首先系统会回调resolvelnstanceMethod:类方法(类方法消息转发回调resolveClassMethod:方法),参数为SEL类型的方法选择器,返回类型为BOOL类型,相当于告诉系统:我们要不要解决当前实例方法的实现,如果这一步返回YES,或者给予此方法选择器所对应方法实现的实现时,相当于通知系统当前消息已处理,会结束此消息转发流程;
  • 2.如果这一步返回NO,会回调forwardingTargetForSelector:实例方法,参数为SEL类型的方法选择器,返回类型为id类型,相当于告诉系统:这个选择器或这此实例方法的调用应该由哪个对象进行处理。转发对象是谁。如果指定了转发目标,系统会把这条消息转发给返回的转发目标,结束本次消息转发流程;
  • 3.如果没有返回任何转发目标,即返回nil。此时会调用methodSignatureForSe lector:实例方法,参数为SEL类型的方法选择器,方法的返回值类型为NS MethodSignature对象,该对象是对方法选择器返回值的类型和它的参数个数、参数类型的封装,如果该方法返回方法签名,系统会接着调用forwardInvocation:方法,如果该方法能处理消息,则结束本次消息转发流程;
  • 4.如果methodSignatureForSelector:方法返回为nil或者forwardInvocat ion:无法处理消息则被标记为”消息无法处理”,即“unrecognized selector sent to instance”。

全方位剖析iOS高级技术问题(三)之动态运行时Runtime相关问题

消息转发验证代码

RuntimeObject.h

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface RuntimeObject : NSObject

- (void)test;

@end

RuntimeObject.m

#import "RuntimeObject.h"

@implementation RuntimeObject

+ (BOOL)resolveInstanceMethod:(SEL)sel {

    // 如果是test方法,打印日志
    if (sel == @selector(test)) {
        NSLog(@"resolveInstanceMethod:");
        return NO;
    } else {
        // 返回父类的默认调用
        return [super resolveClassMethod:sel];
    }
}

- (id)forwardingTargetForSelector:(SEL)aSelector {

    NSLog(@"forwardingTargetForSelector:");
    return nil;
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {

    if (aSelector == @selector(test)) {
        NSLog(@"methodSignatureForSelector:");
        // v代表返回值是void类型的
        // @代表第一个参数类型是id,即self
        // :代表第二个参数是SEL类型的,即@selector(test)
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    } else {
        return [super methodSignatureForSelector:aSelector];
    }
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {

    NSLog(@"forwardInvocation:");
}
@end

AppDelegate.h中调用

#import "RuntimeObject.h"

RuntimeObject *obj = [[RuntimeObject alloc] init];
// 调用test方法,只有声明,没有实现
[obj test];
结果:
2022-08-10 20:17:21.320828+0800 01-Runtime消息转发[66889:15765818] resolveInstanceMethod:
2022-08-10 20:17:21.320945+0800 01-Runtime消息转发[66889:15765818] forwardingTargetForSelector:
2022-08-10 20:17:21.321040+0800 01-Runtime消息转发[66889:15765818] methodSignatureForSelector:
2022-08-10 20:17:21.321153+0800 01-Runtime消息转发[66889:15765818] forwardInvocation:

五.Runtime Method-Swizzling`

1、objc_object

什么是Method-Swizzling?

全方位剖析iOS高级技术问题(三)之动态运行时Runtime相关问题

方法交换验证代码

RuntimeObject.h

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface RuntimeObject : NSObject

- (void)test;
- (void)otherTest;

@end

RuntimeObject.m

#import "RuntimeObject.h"

@implementation RuntimeObject

+ (void)load {

    // 获取test方法实现结构
    Method test = class_getInstanceMethod(self, @selector(test));
    // 获取otherTest方法实现结构
    Method otherTest = class_getInstanceMethod(self, @selector(otherTest));

    // 交换两个方法的实现
    method_exchangeImplementations(test, otherTest);
}

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

- (void)otherTest {

    // 在调用此方法前,已经将两个方法的实现进行了交换
    // 实际上是调用test的具体实现
    [self otherTest];
    NSLog(@"otherTest");
}
@end
结果:
2022-08-10 20:18:03.814878+0800 01-Runtime消息转发[67020:15767056] test
2022-08-10 20:18:03.814996+0800 01-Runtime消息转发[67020:15767056] otherTest

六.Runtime动态添加方法`

动态添加方法验证代码

RuntimeObject.m

void testImp(void) {
    NSLog(@"test invoke");
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {

    // 如果是test方法,打印日志
    if (sel == @selector(test)) {
        NSLog(@"resolveInstanceMethod:");

        // 动态添加test方法的实现
        class_addMethod(self, @selector(test), testImp, "v@:");

        return YES;
    } else {
        // 返回父类的默认调用
        return [super resolveClassMethod:sel];
    }
}
结果:
2022-08-10 20:32:33.993726+0800 01-Runtime消息转发[69545:15784461] resolveInstanceMethod:
2022-08-10 20:32:34.034447+0800 01-Runtime消息转发[69545:15784461] test invoke

问题:是否使用过performSelector:系统方法?

实际上是考察Runtime动态添加方法的特性!

Xcode内部全局搜索快捷键:shift+command+o

七.Runtime动态方法解析`

问题:是否使用过@dynamic编译器关键字?

声明的属性在实现当中标识为@dynamic时,相当于getter和setter方法在运行时添加,而非在编译时声明好。

  • 动态运行时语言将函数决议推迟到运行时
  • 编译时语言在编译期进行函数决议

本文总结

问题1:[obj foo]和objc_msgSend()函数之间有什么关系?

[obj foo] 《===》 objc_msqSend(self, @selector(foo))

问题2:runtime如何通过Selector找到对应的IMP地址的?

缓存是否命中当前类方法列表是否命中逐级父类方法列表是否命中

问题3:能否向编译后的类中增加实例变量?

编译前的类已经完成实例变量的布局,runtime数据结构中的class_ro_t,编译后无法修改!但可以在动态添加的类中增加实例变量