iOS底层 runtime 理解消息传递、转发机制

1.runtime是用来做什么的?

1.1 与runtime相关最大的,就是OC语言的动态绑定机制。

动态绑定是指一个对象发送消息后,该消息的实现(实际执行的函数)根据运行环境的不同而不同(此处只针对OC,Swift中已经不是运行时加载方法,而是和C语言类似,在编译阶段就确定了)。实现该机制,常用的就是分类(categor)、类扩展(extension)、子类(subclass)继承等我们每个人都会使用的设计模式。

正常情况下,我们使用OC的这些特性就能够解决大部分问题。但是有些情况下,为了优雅、高效的解决问题,我们有时候希望从更底层的层面进行操纵。

1.2 常用runtime实现的强大功能

OC本质上是C的扩展和封装。我们的OC代码运行时,底层调用的实际上是c语言的代码。runtime(翻译过来即运行时)就是苹果暴露给用户的一个偏底层的可以操作底层代码的API接口,是对常用的设计模式的一个必要补充。通过该接口的一些函数,我们可以直接干预消息发送过程,从而实现很多强大的功能。比如

  • (1) 实现多继承Multiple Inheritance (利用消息转发机制)
  • (2) 在分类中重写原类方法而又不失去原类方法中的功能 (利用class的Method Swizzling)
  • (3) Aspect Oriented Programming (切片编程)
  • (4) 重写class方法(Isa Swizzling)
  • (5) 给分类添加属性变量( 利用Associated Object给分类添加关联对象)
  • (6) 动态的增加方法 (利用消息转发机制,在运行时实现方法)
  • (7) NSCoding的自动归档和自动解档(利用类底层的结构查询函数,批量给所有属性自动添加相同的解档、归档方法)

2. runtime API中主要内容

2.1 对象、类的定义

从下表可以看到,本质上类是一个指向类结构体的指针,而对象是一个指向对象结构体的指针,对象结构体中存储有一个isa类,它动态的指向该对象的类。类结构体中存储有类的名字,父类名字,类的成员变量(无论是通过@property还是直接定义的成员变量都存储在这里),类的实例变量大小(我们定义实例变量的时候变量空间大小就已经确定了),类的方法链表(普通类里面存储着该类的实例方法,元类中存储中该类的类方法),协议链表,方法缓存表(我们发送消息时第一个查询的结构体)等。

**

go 复制代码
//对象结构体中存储有一个isa类
struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;//Isa标识对象的类;它指向一个结构的类定义编译。
};
//所有的对象本质上都是一个id,而id是一个指向对象结构体的指针
typedef struct objc_object *id;
//类是一个指向类结构体的指针
typedef struct objc_class *Class;` 
`//类结构体中存储有该类定义的所有相关数据;` 
`struct objc_class {`
`Class isa  OBJC_ISA_AVAILABILITY;//Isa标识对象的类;它指向一个结构的类定义编译。`
`#if !__OBJC2__`
`Class super_class;``//父类`
`const char *name;``//类名`
`long version;``//类的版本信息,默认为0`
`long info;``//类信息,供运行期使用的一些位标识`
`long instance_size;``//类的实例变量大小`
`struct objc_ivar_list *ivars;``// 类的成员变量链表`
`struct objc_method_list **methodLists;``// 方法链表`
`struct objc_cache *cache;``//方法缓存`
`struct objc_protocol_list *protocols;``//协议链表#`
`endif} `
`OBJC2_UNAVAILABLE;`

因为类也有一个isa 指针,所以类本质上也是一个对象,称为类对象。类对象Isa指针标识的类为该类的元类(meta class),每一个类都是这个元类的唯一实例对象。元类对象Isa指针标识的类为根元类,根元类(root meta Class)在整个系统中只有一个,所有的元类的isa指针都指向根元类,根元类的Isa指针标识的类为自己。具体如下所示,图中虚线代表类的isa指针指向,实线代表类的父类。根元类的父类是根类,同时根元类的实例对象也是根类(root class),这里形成了一个闭环。

isa指针指向:实例对象->类->元类->(不经过父元类)直接到根元类(NSObject的元类),根元类的isa指向自己;

2.2 Method、IMP、SEL的定义

把他们拿出来说,是因为容易他们之间存在相关性和差异,非常容易产生误解,而且他们对我们理解消息机制很有帮助,我们可以看一下方法Method的定义如下:

**

arduino 复制代码
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 是一个指向结构体的指针,它包含了IMP和SEL,还包含了方法类型定义、方法的参数等。

SEL是方法的指针,但不同于C语言中的函数指针,函数指针直接保存了方法的地址,但SEL只是方法编号;

IMP是方法的具体实现函数指针,在runtime里,我们可以使用函数改变或者设置IMP来更改一个函数的具体实现,例如:

**

scss 复制代码
method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2) //交换两个方法的实现
method_setImplementation(Method _Nonnull m, IMP _Nonnull imp) // 给一个方法设置实现 
2.3 runtime中常用的的一些函数

runtime中的函数,一般按照结构体的层级结构来操纵。对类中成员进行操作的,以class开头,对方法中成员进行操作的以method开头,其他的以此类推。常见的函数如下:

**

scss 复制代码
class_getProperty(Class _Nullable cls, const char * _Nonnull name)   //获取类的所有属性列表
class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, 
                const char * _Nullable types)      //给类添加方法
class_replaceMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, 
                    const char * _Nullable types)  //替换类方法
class_addIvar(Class _Nullable cls, const char * _Nonnull name, size_t size, 
              uint8_t alignment, const char * _Nullable types)    //增加类变量
method_getImplementation(Method _Nonnull m)   //获取方法的实现
method_setImplementation(Method _Nonnull m, IMP _Nonnull imp)  //设置方法的实现
method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2)  //交换方法的实现
imp_implementationWithBlock(id _Nonnull block) //使用一个block创建一个实现
sel_getName(SEL _Nonnull sel) //获取方法的名称
sel_registerName(const char * _Nonnull str) //注册一个方法

3.消息传递、转发机制

想要合理的利用runtime中相关API接口,必须理解runtime中的消息传递、转发机制。

(1)当一个对象发送消息时,首先,底层会执行一个消息发送函数,函数长这样

**

arduino 复制代码
 objc_msgSend(void /* id self, SEL op, ... */ 

如果是使用super发送消息,函数长这样:

**

arduino 复制代码
 objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */ 

(2)底层会从该对象所属的类中(isa指针所指的类)的方法缓存列表查找对应的实现

(3)如果2找不到,会从该类的方法链表中继续查找

(4)如果3找不到,会跳转到该类的父类查找,父类步骤和子类一样。 (5)一直向上到根类,如果根类仍然找不到,就开始准备进行消息转发。转发第一步:动态消息解析。查看当前类是否实现了resolveInstanceMethod方法(如果是类方法,会看是否实现了resolveClassMethod方法)。如果该方法返回了YES,消息转发终止。我们可以在这个方法中动态添加方法实现,不实现也不要紧,只要返回YES消息发送就不会报错。

**

less 复制代码
+(BOOL)resolveClassMethod:(SEL)sel
{
    NSString * selStr = NSStringFromSelector(sel);
    if ([selStr isEqualToString:@"runTest"]) {
//注意,想要给类添加方法,必须添加到它的metaClass上,所以在class_addMethod中添加的类都要是原类!!!
//  确定metaClass的方法是objc_getMetaClass(object_getClassName(self));
        if (class_addMethod(objc_getMetaClass(object_getClassName(self)), sel,class_getMethodImplementation(objc_getMetaClass(object_getClassName(self)), @selector(runTestFunction)), "s@:")) {
            return YES;
        }
        return [super resolveClassMethod:sel];
    }
    return [super resolveClassMethod:sel];
}

(6)如果第5步返回NO,就开始消息重定向。查看是否指定了其他对象来执行该方法。具体是查看当前类是否实现了forwardingTargetForSelector方法;如果该方法返回了一个对象,就在该对象上执行该selctor方法(该对象上执行该方法时步骤与本对象一致);

**

scss 复制代码
-(id)forwardingTargetForSelector:(SEL)aSelector

(7)如果第6步返回nil,就需要进行真正的消息转发机制。具体是查看当前类是否实现了methodSignatureForSelector方法,如果该方法返回不为nil,就执行forwardInvocation方法。如果forwardInvocation实现了,消息转发终止(但不见得消息转发完成,forwardInvocation只是一个消息的分发中心,将这些不能识别的消息转发给不同的接收对象,或者转发给同一个对象,再或者将消息翻译成另外的消息,亦或者简单的"吃掉"某些消息,因此没有响应也不会报错。)。

**

csharp 复制代码
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
    if ([someOtherObject respondsToSelector:
            [anInvocation selector]])
        [anInvocation invokeWithTarget:someOtherObject];
    else
        [super forwardInvocation:anInvocation];
}

(8)上述步骤,如果到第7部都没有实现,系统就会报错,提示[unrecognized selector sent to instance] 注意:上述任何一步,都要在前一步骤没有完成的基础上 。

3.1、消息传递机制

在 Objective-C 中,方法调用本质是消息传递。例如 [obj doSomething] 会被转换为 objc_msgSend(obj, @selector(doSomething))。具体流程如下:

  1. 查找方法实现

    • 在对象的类的方法列表(class_rw_t 结构)中查找。
    • 若未找到,沿继承链向上查找父类。
    • 若最终未找到,触发消息转发机制

3.2、消息转发机制

消息转发分为三个阶段,开发者可在此过程中补救未实现的方法:

1. 动态方法解析(Dynamic Method Resolution)

方法+resolveInstanceMethod:+resolveClassMethod:
作用 :允许动态添加方法实现。
示例

less 复制代码
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(dynamicMethod)) {
        class_addMethod([self class], sel, (IMP)dynamicIMP, "v@:");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

void dynamicIMP(id self, SEL _cmd) {
    NSLog(@"动态添加的方法被调用");
}
2. 备用接收者(Fast Forwarding)

方法-forwardingTargetForSelector:
作用 :将消息转发给其他对象处理。
示例

less 复制代码
- (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(missingMethod)) {
        return [BackupObject new]; // 备用对象实现 missingMethod
    }
    return [super forwardingTargetForSelector:aSelector];
}
3. 完整消息转发(Normal Forwarding)

方法-methodSignatureForSelector:-forwardInvocation:
作用 :生成方法签名并封装为 NSInvocation 进行转发。
示例

less 复制代码
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if (aSelector == @selector(handleInvocation)) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    if ([anInvocation.selector isEqual:@selector(handleInvocation)]) {
        [anInvocation invokeWithTarget:[BackupObject new]];
    } else {
        [super forwardInvocation:anInvocation];
    }
}

4.如何使用runtime

基于runtime提供的数据结构,以及上述消息传递、转发机制,runtime提供了丰富的函数让我们来实现我们第1节中提到的强大的功能,我们这里简单梳理下实现方式:

  • (1) 实现多继承Multiple Inheritance (利用消息转发机制)。

我们在Warrior中头文件中定义一个方法negotiate,但是不实现它,而在forwardingTargetForSelector方法中,针对该selecotr,指定一个Diplomat对象,就可以将该方法实现交给diplomat类来实现。看起来就像是Warrior也继承了了Diplomat的方法一样(注意,像respondsToSelector:isKindOfClass: 这类方法只会考虑继承体系,不会考虑转发链。也就是说如果[Warrior respondsToSelector:negotiate]会返回NO)。

  • (2) 在分类中重写原类方法而又不失去原类方法中的功能 (利用class的Method Swizzling)

    实现方式是通过runtime中的实现交换函数method_exchangeImplementations。首先,在本类中定义另一个待交换的方法exchage_ViewDidLoad;待交换的方法中需要调用原方法,然后添加需要额外实现的功能(例如第1节中提到的数据统计方法)。在恰当的时机(一般是在load方法中),交换该两个方法的实现。实际执行代码的使用,调用原类方法会执行待交换的方法的实现,待交换的方法实现中又会调用原来的方法实现,从而保留了原来的方法的实现。

**

scss 复制代码
+(void)exchangeOriginMethodWithMethodExchangeMethod
{
//    防止方法被多次调用后交换失效;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        SEL originSEL =  @selector(viewDidLoad);
        SEL swizzSEL = @selector(exchage_ViewDidLoad);
        Method viewDidLoad = class_getInstanceMethod([self class], originSEL);
        Method exchang_viewDidLoad = class_getInstanceMethod([self class], swizzSEL);
//        测试原来的选择子是否已经添加了方法(是否已经交换了方法);
        Boolean didAddMethod = class_addMethod([self class], originSEL,method_getImplementation(exchang_viewDidLoad),method_getTypeEncoding(exchang_viewDidLoad));
        if (!didAddMethod) {
//
            //        如果没有添加方法,就直接交换
            method_exchangeImplementations(viewDidLoad, exchang_viewDidLoad);
        }else{
//            如果已经添加了,就同时更换交换后的方法实现;
            class_replaceMethod([self class], swizzSEL, method_getImplementation(viewDidLoad), method_getTypeEncoding(viewDidLoad));
        }
    });
}
-(void)exchage_ViewDidLoad
{
    NSLog(@"%@ did load",self);
    [self exchage_ViewDidLoad];//注意,exchage_ViewDidLoad的实现现在是viewDidLoad了,所以没有循环调用
}
  • (3) Aspect Oriented Programming (切片编程,内容太多,暂不展开,可以看这里)

  • (4) 重写class方法(Isa Swizzling)

    苹果著名的KVO技术和NSNotificationCenter就使用的该方法,在我们给一个对象添加了KVO键值观察方法后

**

objectivec 复制代码
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)contex。

后台会重新创建一个NSKVONotifying_Object类,然后偷偷将原来的类的isa指针指向该类。该类中会在属性变量修改时候,调用

**

objectivec 复制代码
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context

方法,并发出相应的通知

  • (5) 给分类添加属性变量( 利用Associated Object给分类添加关联对象)

    我们可以给分类添加属性,但是分类不会自动给我们生成成员变量。因为类的成员变量在编译器已经决定了(写入了类的结构体中,具体见前面结构体的定义),但是category是在运行期才决议的。所以如果要给分类添加成员变量,需要用runtime里面函数在运行期实现。一般使用objc_setAssociatedObject和objc_getAssociatedObject函数来实现。这两个函数都是成对的出现,一个给对象添加关联对象,一个获取关联对象。具体代码如下。

**

less 复制代码
  @property(nonatomic,strong)id associatedObjcet;
-(void)setAssociatedObjcet:(id)associatedObjcet{
    objc_setAssociatedObject(self, @selector(associatedObjcet), associatedObjcet, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

-(id)associatedObjcet{
    return objc_getAssociatedObject(self, @selector(associatedObjcet));
}
  • (6) 动态的增加方法 (利用消息转发机制,在运行时实现方法)

    动态的增加方法和多重继承有些类似,都是调用的方法在类中并没有实现代码,而是在消息转发机制的某一步才动态的添加实现代码。消息转发机制本身有多步骤,所以根据需要,可以在不同的步骤实现动态添加,常见的一般在方法动态解析resolveInstanceMethod或者在消息转发forwardInvocation的时候进行。

  • (7) NSCoding的自动归档和自动解档(利用类底层的结构查询函数,批量给所有属性自动添加相同的解档、归档方法)

NSCoding其实就是对所有的属性调用encode和decode方法。使用手动操作有一个缺陷,如果属性多起来,要写好多行相似的代码,虽然功能是可以完美实现,但是看上去不是很优雅。用runtime实现的思路就比较简单,我们循环依次找到每个成员变量的名称,然后利用KVC读取和赋值就可以完成encodeWithCoder和initWithCoder了,部分代码如下:

**

ini 复制代码
 Ivar *vars = class_copyIvarList([self class], &outCount); 
for (int i = 0; i < outCount; i ++) {
 Ivar var = vars[i]; 
const char *name = ivar_getName(var); 
NSString *key = [NSString stringWithUTF8String:name];
 id value = [aDecoder decodeObjectForKey:key];
 [self setValue:value forKey:key]; 
 }
相关推荐
Unlimitedz12 分钟前
iOS GCD
macos·ios·cocoa
Java技术小馆33 分钟前
如何排查Linux系统中的CPU使用率过高问题
java·后端·面试
布多38 分钟前
AutoreleasePool:iOS 内存管理乐章中的隐秘旋律
ios·源码阅读
六月的可乐38 分钟前
【干货】前端实现文件保存总结
前端·javascript·面试
YungFan43 分钟前
SwiftUI-国际化
ios·swiftui·swift
Unlimitedz1 小时前
深入探索 iOS 卡顿优化
macos·ios·cocoa
mCell1 小时前
每秒打印一个数字:从简单到晦涩的多种实现
前端·javascript·面试
卢叁1 小时前
Swift int转String的诡异bug
ios
SuperYing2 小时前
前端候选人突围指南:让面试官主动追着要简历的五大特质(个人总结版)
前端·面试