runtime/消息机制

129 阅读21分钟

1、runtime

runtime 是 OC底层的一套C语言的API(引入 <objc/runtime.h> 或<objc/message.h>),编译器最终都会将OC代码转化为运行时代码,通过终端命令编译.m 文件:clang -rewrite-objc xxx.m可以看到编译后的xxx.cpp(C++文件)。

1.1 动态添加属性/方法、方法交换、归档接档、字典转模型、遍历类属性--映射解析、修改isa指针,自己实现kvo

参考:

1、https://juejin.cn/post/6844903826269405198

2、利用runtime实现消息转发机制的三次补救-https://www.jianshu.com/p/1073daee5b92

如果NSObject没有则进入消息转发(类的动态方法解析、备用接受者对象、完整的消息转发)。

image.png

1.2 针对越界问题-Array添加分类(添加方法、模拟多继承、公开私有方法、降低耦合性减少代码量) 和扩展的区别 TODO 加载时机问题、应用场景

1、方法交换--onload

如果实现onload,在main函数之前,类装载的时候就主动调用,会影响启动速度,避免耗时操作,不仅仅走onload,还会有其他一些操作。

没有实现该方法,是懒加载类,用的时候才会加载。runtime。一旦实现,就会提前加载时机到Main之前。

2、动态的添加方法/属性,挂载对象,获取类信息

  • 类簇

    点击展开内容

    类簇(Class Cluster)是一种在Objective-C和Swift编程中常用的设计模式,主要用于创建一组具有相似行为但内部实现可能不同的类的抽象。这种模式通过公开一个公共的、抽象的父类和多个私有的子类来实现,其中父类提供统一的接口,而子类负责具体的实现细节。

    类簇的目的和优势:

    1. 封装性用户只需要知道公共接口,不需要关心具体实现细节,增强了代码的封装性

    2. 易于扩展:可以在不影响现有代码的情况下增加新的子类来扩展功能。

    3. 灵活性:根据不同的需求动态选择最合适的实现,提高程序的灵活性和效率。

    示例:

    在Foundation框架中,NSNumberNSArrayNSString都是使用类簇实现的。例如,NSArray可以有多种内部实现(如单元素数组、空数组等),但对外都表现为NSArray

    NSArray *array1 = [NSArray arrayWithObject:@"apple"];
    NSArray *array2 = [NSArray array];
    NSArray *array3 = [NSArray arrayWithObjects:@"apple", @"banana", @"cherry", nil];
    
    NSArrayNSConstantArray // 常量数组
    __NSArrayI // 多个元素
    __NSArrayM // 有且只有一个元素,且init后可变数组
    __NSPlaceholderArray // alloc后都是__NSPlaceholderArray
    __NSSingleObjectArrayI // 有且只有一个元素
    __NSArrayO // init不可变数组
    

    在这个例子中,尽管array1array2array3都是NSArray的实例,它们可能分别由不同的私有子类实现,以优化存储和访问效率。

    实现类簇:

    实现类簇通常涉及以下步骤:

    1. 定义一个公共的父类:这个父类定义了所有公共接口,并作为外界交互的唯一窗口。

    2. 实现多个私有子类:这些子类继承自公共父类,但不在公共头文件中暴露。每个子类都有自己特定的实现逻辑。

    3. 在父类中根据条件动态创建子类实例:通常在父类的工厂方法中根据输入参数或其他条件判断,决定实例化哪一个子类。

    代码示例:

    
    // 公共的抽象父类
    
    @interface MyArray : NSObject
    
    - (id)objectAtIndex:(NSUInteger)index;
    
    + (instancetype)arrayWithObjects:(const id [])objects count:(NSUInteger)cnt;
    
    @end
    
    // 私有子类
    
    @interface MyEmptyArray : MyArray
    
    @end
    
    @interface MySingleObjectArray : MyArray
    
    @end
    
    @interface MyMultipleObjectsArray : MyArray
    
    @end
    
    @implementation MyArray
    
    + (instancetype)arrayWithObjects:(const id [])objects count:(NSUInteger)cnt {
    
    switch (cnt) {
    
    case 0:
    
    return [[MyEmptyArray alloc] init];
    
    case 1:
    
    return [[MySingleObjectArray alloc] initWithObject:objects[0]];
    
    default:
    
    return [[MyMultipleObjectsArray alloc] initWithObjects:objects count:cnt];
    
    }
    
    }
    
    - (id)objectAtIndex:(NSUInteger)index {
    
    [self doesNotRecognizeSelector:_cmd];
    
    return nil;
    
    }
    
    @end
    
    

    在这个简化的例子中,MyArray是一个类簇的公共父类,它根据对象数量的不同,动态地返回不同的子类实例。这样的设计使得外部代码只与MyArray交互,而不需要关心具体的数组类型,从而达到了封装和优化的目的。

  • 选择器方法未被定义

    isa-结构体-selector->父类->NSobject->消息转发

    Uibutton->uicontorl->Uiview->UIresponder->NSobject

    提供三次机会去补救。为什么在第二次更合适。forwardingTargetForSelector

    第一次:给个机会去实现这个函数。动态添加,加一个本身不存在的方法,本身就是冗余的,成本比较高,很麻烦。resolveInstanceMethod

    第二次:用新的接受者实现;最合适。TODO:why

    第三次:用其他函数实现;开销大,创建新对象,而且一直作为消息中转站,不太合适重写。--forwardInvocation

image.png

方法没实现,但不让程序crash。强制保活--这样就有很强的处理性,比如捕捉后让用户提交下操作信息,然后回到首页。

Runtime/RunLoop特性实现-OC/Swift。 Swift不是动态语言,为什么也可以?因为底层是一套的,iOS开发的基础。 ---TODO:

image.png

2、消息机制

2.1 objc_msgSend执行流程:3大阶段-消息发送、动态方法解析、消息转发

juejin.cn/post/690676…

juejin.cn/post/690676…

  • 消息发送:receiver就是方法调用者。receiver通过isa指针找到receiverClass,receiverClass通过superclass指针找到superClass

  • 动态方法解析

    注意:我们看到实例方法和类方法的动态方法解析最后都可以去执行-(void)other{}方法,说明实例方法和类方法本质是一样的,+ - 只是OC的语法,目的是把方法存到类对象或者实例对象,只要拿到函数指针就可以直接调用,不用在乎方法存放在哪里。 image.jpeg

    动态方法解析完,会重走消息发送的流程,从在receiverClass的cache中查找方法这一步开始。所以实例方法的动态解析必须把方法添加到class对象,类方法的动态解析必须把方法添加到meta-class对象,否者动态解析就会失败

    -(void)other{
        NSLog(@"other");
    }
    //实例方法动态解析
    +(BOOL)resolveInstanceMethod:(SEL)sel{
        if (sel == @selector(test)) {
       	//动态添加  动态给test方法添加一个方法实现  这个方法会被添加到类对象的class_rw_t的方法列表中
    	Method method = class_getInstanceMethod(self, @selector(other));
        //这里是的self是类对象   注意这里西部给类对象添加方法
    	class_addMethod(self, sel, method_getImplementation(method), method_getTypeEncoding(method));
            return YES;
        }
        return [super resolveInstanceMethod:sel];
    }
    
    
    //类方法动态解析
    +(BOOL)resolveClassMethod:(SEL)sel{
        //动态添加
        if (sel == @selector(test)) {
        		//动态添加  动态给test方法添加一个方法实现  这个方法会被添加到元类对象的class_rw_t的方法列表中
            Method method = class_getInstanceMethod(self, @selector(other));
            // 这里是的self是类对象  获取到元类对象 这里一定要给元类对象添加方法 
            class_addMethod(object_getClass(self), sel, method_getImplementation(method), method_getTypeEncoding(method));
            return YES;
        
        }
        return  [super resolveClassMethod:sel];
    }
    
  • 消息转发:将消息转发给别人

image.png

> 以上转发使用的方法都有对象方法、类方法2个版本。

methodSignatureForSelector:方法签名的作用

methodSignatureForSelector:方法在Objective-C的消息转发机制中扮演着重要的角色。当一个对象接收到一个它无法响应的消息时,Objective-C的运行时系统会调用这个方法来获取该消息对应的方法签名。方法签名(NSMethodSignature)包含了方法的参数类型和返回类型的信息,这些信息对于后续的消息转发过程至关重要。

方法签名的主要作用包括:

  1. 提供方法参数和返回值的类型信息:方法签名包含了方法的参数类型、返回值类型以及参数个数等信息。这些信息对于正确地转发消息至关重要,因为在消息转发过程中,运行时系统需要这些信息来构造调用栈和正确地传递参数。

  2. 消息转发(Message Forwarding):在Objective-C的消息转发机制中,如果一个对象无法响应某个消息,运行时系统会尝试通过几个步骤来“挽救”这个未能处理的消息。methodSignatureForSelector:是这个过程中的第一步,通过它获取到的方法签名,运行时系统可以进一步构造一个NSInvocation对象,这个对象包含了原始消息的所有信息(包括选择器、目标对象、参数等)。然后,运行时系统会调用forwardInvocation:方法,将这个NSInvocation对象传递给你,以便你可以自定义如何处理这个未能响应的消息。

  3. 自定义消息转发:通过重写methodSignatureForSelector:forwardInvocation:方法,开发者可以实现高度自定义的消息转发逻辑。例如,可以将消息转发给另一个对象处理,或者动态地提供一个方法实现。

在你的示例代码中:


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

NSLog(@"dx--methodSignatureForSelector=1111");

if (aSelector == @selector(undo)) {

return [NSMethodSignature signatureWithObjCTypes:"v@:i"];

}

return [super methodSignatureForSelector:aSelector];

}

当接收到undo消息,但是当前对象没有实现undo方法时,这段代码会返回一个对应于undo方法的NSMethodSignature对象。这个方法签名表明undo方法没有返回值(v),有一个隐含的self参数(@)、一个隐含的_cmd参数(:),以及一个int类型的参数(i)。有了这个方法签名,运行时系统就可以构造一个NSInvocation对象,并尝试通过forwardInvocation:方法来转发这个消息。

2.2 动态调用方法-NSInvocationperformSelector:

在Objective-C中,动态调用方法(函数)主要依赖于Objective-C的运行时(runtime)特性。

最常用的方式包括使用 NSInvocationperformSelector: 系列方法。以下是这两种方式的简要说明和示例:

### 1. 使用`performSelector:`系列方法
`performSelector:`系列方法是`NSObject`的一部分,可以用来动态地调用一个对象的方法。这些方法包括:
- `- (id)performSelector:(SEL)aSelector;`
- `- (id)performSelector:(SEL)aSelector withObject:(id)object;`
- `- (id)performSelector:(SEL)aSelector withObject:(id)object1 withObject:(id)object2;`

**示例**:
```objective-c
SEL myMethod = @selector(myMethodWithArg1:arg2:);
[myObject performSelector:myMethod withObject:arg1 withObject:arg2];

```
这种方法简单易用,但它有一些限制,比如最多只能传递两个参数,且不能传递非对象类型的参数(比如基本数据类型)。

### 2. 使用`NSInvocation`
`NSInvocation`是一个更为强大且灵活的方式,它可以用来动态地构造和调用方法调用。`NSInvocation`可以传递任意数量的参数,包括非对象类型的参数。
**示例**:
```objective-c
SEL myMethod = @selector(myMethodWithArg1:arg2:);
NSMethodSignature *signature = [myObject methodSignatureForSelector:myMethod];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
[invocation setSelector:myMethod];
[invocation setTarget:myObject];

// 设置参数
NSInteger arg1 = 42;
[invocation setArgument:&arg1 atIndex:2]; // 注意:参数索引从2开始,0和1被self和_cmd占用
NSString *arg2 = @"Hello";
[invocation setArgument:&arg2 atIndex:3];
// 调用方法
[invocation invoke];

// 获取返回值
NSString *result = nil;
[invocation getReturnValue:&result];

```
`NSInvocation`提供了完整的动态方法调用功能,但使用起来比`performSelector:`系列方法更为复杂。

总结

Objective-C提供了performSelector:系列方法和NSInvocation两种主要方式来动态调用方法。performSelector:系列方法适用于简单的场景,而NSInvocation则提供了更高的灵活性和强大的功能,适用于需要动态传递多个参数或处理复杂数据类型的场景。选择哪种方式取决于具体的需求和场景。

`performSelector:`方法族在Objective-C中用于动态执行选择器(selector)。这些方法本身并不直接决定执行代码的线程,而是在调用它们的当前线程上执行。
换句话说,如果你在主线程中调用`performSelector:`方法,那么选择器指定的方法就会在主线程上执行;同样,如果在后台线程中调用,那么执行也会在该后台线程中进行。
不过,`NSObject`提供了几个特定的`performSelector:`方法,可以用来在不同的线程上执行选择器,或者在当前线程延迟执行,这些方法包括:
- `performSelector:onThread:withObject:waitUntilDone:`:在指定的线程上执行选择器。
- `performSelectorInBackground:withObject:`:在新的后台线程上执行选择器。
- `performSelector:withObject:afterDelay:`:在当前线程延迟执行选择器。

1. **在主线程上执行**:
[self performSelectorOnMainThread:@selector(myMethod) withObject:nil waitUntilDone:NO]; // 这会在主线程上执行`myMethod`方法,无论当前代码在哪个线程中执行。

2. **在后台线程上执行**```objective-c
   [self performSelectorInBackground:@selector(myMethod) withObject:nil];
   ```

   这会创建一个新的后台线程,并在该线程上执行`myMethod`方法。

3. **延迟执行**```objective-c
   [self performSelector:@selector(myMethod) withObject:nil afterDelay:2.0];
   ```

   这会在当前线程延迟2秒后执行`myMethod`方法。

### 总结
`performSelector:`方法族本身在调用它们的当前线程上执行选择器。
但通过使用特定的`performSelector:`变体,可以实现在指定线程上执行选择器,或者在当前线程延迟执行选择器。
这为在不同线程间的方法调用提供了便利,但需要注意线程安全和性能问题。
  • 消息转发机制

    juejin.cn/post/684490…

    一个对象在发送消息的时候首先在cache里找,如果找不到就在该类的struct objc_method_list列表中去搜索,如果找到则直接调用相关方法的实现,如果没有找到就会通过super_class指针沿着继承树向上去搜索。如果找到就跳转,如果到了继承树的根部(通常为NSObject)还没有找到。那就会调用NSObjec的一个方法doesNotRecognizeSelector:,这样就会报unrecognized selector 错误。

    其实在调用doesNotRecognizeSelector:方法之前还会进行消息转发---还有三次机会来补救。也就是常说的OC消息转发的三次补救措施。

    www.jianshu.com/p/1073daee5…

    image.jpeg

  • 消息传递机制

    在iOS开发中,消息传递是组件间通信的重要方式。delegateblockKVO(Key-Value Observing)、responder chain以及RxSwift是常见的几种消息传递机制,它们各自有不同的使用场景和优缺点。

    消息传递机制:delegate、block、KVO、responder chain、rx区别和场景

    1. Delegate(代理)

    • 场景:一对一的通信,当一个对象需要将事件通知给另一个对象时使用。常见于UIKit中的视图控制器和视图之间的交互,如UITableViewDelegate

    • 优点:清晰的定义了哪些方法是可以被代理对象实现的,易于理解和使用。

    • 缺点需要定义协议,增加了代码量;只能单向通信。

    2. Block(闭包)

    • 场景:在完成某项任务(如异步操作)后执行一段代码。常用于回调和配置

    • 优点:代码紧凑,易于理解;可以捕获和修改上下文中的变量。

    • 缺点:容易造成循环引用(retain cycle);对于复杂的异步操作,嵌套的block可能导致“回调地狱”。

    3. KVO(Key-Value Observing)

    • 场景监听对象属性值的变化。适用于模型和视图之间的自动同步,如一个属性值改变需要更新UI。

    • 优点:自动通知依赖的对象属性变化,减少了样板代码。

    • 缺点:基于字符串的键路径,容易出错且不安全;需要手动管理观察者的添加和移除,容易遗漏。

    4. Responder Chain(响应者链)

    • 场景事件处理,如触摸事件、摇动事件等。适用于事件需要由多个对象处理的情况。

    • 优点 :iOS自动管理响应者链,开发者只需正确实现响应方法

    • 缺点:适用范围有限,主要用于UI事件的传递。

    5. RxSwift(响应式编程)

    • 场景:基于响应式编程范式,适用于处理异步事件和数据流。特别适合于复杂的数据绑定和事件处理场景。

    • 优点:可以简化复杂的异步操作和事件处理代码;提供了丰富的操作符用于数据处理和转换。

    • 缺点:学习曲线陡峭,概念多;增加了应用的复杂性和包大小。

  • 6.nsnotification

    总结

    • Delegate适用于一对一的通信,需要明确的协议和方法。

    • Block适合于回调场景,尤其是异步操作。

    • KVO用于监听对象属性的变化,实现数据和UI的自动同步。

    • Responder Chain主要用于处理UI事件的传递。

    • RxSwift适用于复杂的数据流和事件处理,基于响应式编程范式。

    选择合适的消息传递机制取决于具体的应用场景和开发需求。理解每种机制的特点和适用场景,可以帮助开发者更高效地进行iOS应用开发。

  • 底层Mach消息机制

    Objective-C的底层运行在苹果操作系统上,包括iOS和macOS,这些操作系统的核心是XNU内核,而XNU内核的一个重要组成部分是Mach微内核。

    Mach提供了一系列底层机制,包括进程间通信(IPC),而Mach消息(Mach messages)是实现IPC的主要方式。

    Objective-C运行时(runtime)在某些情况下会使用Mach消息机制,尤其是在进程间或线程间通信时。

    Mach消息机制概述

    Mach消息机制是一种基于消息传递的通信方式,允许在Mach端口(Mach ports)之间发送和接收消息。每个Mach端口可以看作是一个通信通道的端点,而端口本身是由Mach微内核管理的。通过Mach消息,可以实现数据的传输、信号的发送以及对远程过程调用(RPC)的支持。

    Mach消息的组成

    一个Mach消息包含以下几个主要部分:

    • 消息头(Header):包含了消息的基本信息,如消息的类型、优先级、发送者和接收者的端口等。

    • 消息体(Body):包含了实际传输的数据。消息体可以包含简单的数据类型,也可以包含更复杂的对象,如端口引用或指向其他数据结构的指针。

    • 尾部(Trailer):可选部分,通常用于传递额外的信息,如安全相关的信息。

    Mach消息的发送和接收

    发送Mach消息的基本步骤如下:

    1. 创建一个消息结构,填充消息头和消息体。

    2. 使用mach_msg函数发送消息,指定目标端口。

    3. 等待接收方处理消息。

    接收Mach消息的基本步骤如下:

    1. 在接收端,监听指定的Mach端口。

    2. 当消息到达时,使用mach_msg函数接收消息。

    3. 处理消息内容。

    Objective-C运行时和Mach消息

    Objective-C运行时使用Mach消息进行底层的进程间通信和线程间通信。例如,Grand Central Dispatch(GCD)在实现线程间的任务调度时,就可能使用到Mach消息机制。此外,Objective-C的异常处理、垃圾回收(在支持垃圾回收的系统上)等功能也可能涉及Mach消息的使用。

    总结

    Mach消息机制是XNU内核提供的一种底层进程间通信方式,Objective-C运行时在需要进行底层通信时会使用到这一机制。虽然开发者在使用Objective-C进行应用开发时不需要直接操作Mach消息,但了解这一机制有助于更好地理解操作系统和Objective-C运行时的工作原理。--TODO:

2.3 performSelector的原理以及用法-TODO

juejin.cn/search?quer…

juejin.cn/post/696358…

点击展开内容

performSelector: 方法是 Objective-C 中 NSObject 类的一部分,它提供了一种动态调用方法的机制。这种机制允许开发者在运行时根据方法名的字符串形式来调用方法,而不是在编译时静态地绑定方法。这背后的原理是 Objective-C 的动态消息派发(Dynamic Message Dispatch)机制。

原理

Objective-C 是一门动态语言,它的方法调用基于运行时的消息派发。当你调用一个对象的方法时,实际上是向这个对象发送了一个消息,这个消息包含了方法的选择器(Selector)。运行时系统会查找这个选择器对应的方法实现,并执行它。如果找不到,运行时系统会通过一系列步骤尝试“修复”这个未能处理的消息,例如消息转发(Message Forwarding)。

performSelector: 方法利用了这种动态消息派发机制,允许开发者在运行时根据选择器动态地调用方法。这种方式增加了代码的灵活性,但也降低了类型安全性,因为编译器无法在编译时检查方法的存在性或参数类型的匹配性。

用法

performSelector: 方法有几个变体,包括但不限于:

  • performSelector:

  • performSelector:withObject:

  • performSelector:withObject:withObject:

这些方法允许你调用不同参数数量的方法 。如果方法接受的参数多于两个,或者参数类型不是对象,那么你需要使用 NSInvocation 或其他机制来调用。

示例

假设有一个类 MyClass,它有一个方法 doSomething

@interface MyClass : NSObject

  • (void)doSomething;

@end

@implementation MyClass

  • (void)doSomething {

NSLog(@"Did something!");

}

@end

你可以这样使用 performSelector: 来调用 doSomething 方法:

MyClass *myObject = [[MyClass alloc] init];

[myObject performSelector:@selector(doSomething)];

如果方法带有一个参数:


- (void)doSomethingWithParameter:(NSString *)parameter;

调用方式如下:


[myObject performSelector:@selector(doSomethingWithParameter:) withObject:@"Hello"];

注意事项

  • 使用 performSelector: 系列方法时,需要注意内存管理(尤其是在非 ARC 环境下),因为编译器可能无法正确推断所有权语义。

  • 由于类型安全问题,过度依赖 performSelector: 可能会使代码难以理解和维护。

  • 在某些情况下(如选择器表示的方法返回基本数据类型),直接使用 performSelector: 方法可能会导致编译器警告或运行时错误。在这种情况下,应考虑使用 NSInvocation 或其他替代方案。

2.4 如何查到一个方法的

imp是一个函数指针由编译器生成的, 它指向方法的实现。可以通过imp直接调用方法。

IMP imp = [myObject methodForSelector:@selector(myMethod)];
imp(myObject, @selector(myMethod)); 
myObject是一个对象,myMethod是该对象的一个方法。methodForSelector:方法返回一个IMP,即该方法的实现,你可以通过imp直接调用该方法。
当你发起一个 ObjC 消息之后,最终它会执行的那段代码,就是由这个函数指针指定的。而 IMP 这个函数指针就指向了这个方法的实现。
既然得到了执行某个实例某个方法的入口,我们就可以绕开消息传递阶段,直接执行方法的实现,以达到更好的性能(在 MantleMTLModelAdapter.m 中可以看到这方面的应用)。

你会发现 IMP 指向的方法与 objc_msgSend 函数类型相同,参数都包含 id 和 SEL 类型。每个方法名都对应一个 SEL 类型的方法选择器.

而每个实例对象中的 SEL 对应的方法实现肯定是唯一的通过一组 id 和 SEL 参数就能确定唯一的方法实现地址;反之亦然。

(1) 使用 @selector() 生成的选择子不会因为类的不同而改变(即使方法名字相同而变量类型不同也会导致它们具有相同的方法选择子),其内存地址在编译期间就已经确定了。也就是说向不同的类发送相同的消息时,其生成的选择子是完全相同的。

(2) 通过 @selector(方法名) 就可以返回一个选择子,通过 (void *)@selector(方法名), 就可以读取选择器的地址。

(3) 推断出的 selector 的特性:

Objective-C 为我们维护了一个巨大的选择子表

在使用 @selector() 时会从这个选择子表中根据选择子的名字查找对应的 SEL。如果没有找到,则会生成一个 SEL 并添加到表中。

在编译期间会扫描全部的头文件和实现文件将其中的方法以及使用 @selector() 生成的选择子加入到选择子表中。

selector 的唯一性是在运行时再进行检查的,那如果selector同名怎么处理

在Objective-C中,selector是方法的唯一标识符。如果两个方法具有相同的selector,编译器会发出警告,但不会阻止编译。运行时系统会根据方法的实现来调用正确的方法。如果类中存在多个同名方法,运行时系统会根据方法的签名(参数类型和返回类型)来区分它们。如果无法区分,会导致运行时错误。