runtime 运行时得问题

OC消息机制简述
==============
参考链接:<http://blog.sina.com.cn/s/blog_72811461010119km.html>  
各种语言都有些传递函数的方法:C语言中可以使用函数指针,C++中有函数引用、仿函数等,OC里有选择器(selector)和block, OC强调的是消息传递,而非方法调用,因此可以对一个对象传递任何消息,而不需要在编译期声名这些消息的处理方法。既然编译期并不能确定方法的地址,那么运行期就需要自行定位了。OC的运行时(runtime)就是通过<code>id objc_msgSend(id theReceiver, SEL theSelector, ...)</code>这个函数来调用方法的,其中theReceiver是调用对象,theSelector则是消息名,省略号就是C语言的不定参数了。这里的消息名是SEL类型,它被定义为struct objc_selector *。不过文档中并没有透露objc_selector是什么东西,但提供了@selector指令来生成:<code>SEL selector = @selector(message);</code> @selector 是在编译期计算的,所以并不是函数调用。更进一步的测试表明,它在Mac OS X和iOS下都是一个C风格的字符串(char \*):

```
SEL selector = @selector(myMethod);
// const char *selectorName = (char *)selector;
const char *selectorName = sel_getName(selector);
printf("%s\n", selectorName); // myMethod
```
接下来我们来看一个小示例:

```
// Test.h
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>

@interface Test : NSObject

- (NSString *)intToString:(NSInteger)number;
- (NSString *)doubleToString:(double *)number;
- (NSString *)pointToString:(CGPoint)point;
- (NSString *)intsToString:(NSInteger)number1 second:(NSInteger)number2 third:(NSInteger)number3 ;
- (NSString *)doublesToString:(double)number1 second:(double)number2 third:(double)number3;
- (NSString *)combineString:(NSString *)string1 withSecond:string2 withThird:string3;

@end

```

```
// Test.m
#import "Test.h"

@implementation Test

- (NSString *)intToString:(NSInteger)number {
    return [NSString stringWithFormat:@"%ld", (long)number];
}

- (NSString *)doubleToString:(double *)number {
    return [NSString stringWithFormat:@"%f", *number];
}

- (NSString *)pointToString:(CGPoint)point {
    return [NSString stringWithFormat:@"{%f, %f}", point.x, point.y];
}

- (NSString *)intsToString:(NSInteger)number1 second:(NSInteger)number2 third:(NSInteger)number3 {
    return [NSString stringWithFormat:@"%ld, %ld, %ld", (long)number1, (long)number2, (long)number3];
}

- (NSString *)doublesToString:(double)number1 second:(double)number2 third:(double)number3 {
    return [NSString stringWithFormat:@"%f, %f, %f", number1, number2, number3];
}

- (NSString *)combineString:(NSString *)string1 withSecond:string2 withThird:string3 {
    return [NSString stringWithFormat:@"%@, %@, %@", string1, string2, string3];
}

@end
```

```
// ViewController中调用
- (IBAction)testSelector:(id)sender {
    // objc_msgSend
    Test *test = [[Test alloc] init];
    CGPoint point = {100, 100}; // CGPointMake(100, 100)
    NSLog(@"%@", objc_msgSend(test, @selector(pointToString:), point));  // 向对象test发送pointToString:消息,跟的参数是point,返回值是对象test接收消息pointToString:point后的返回值
}
```

注意,在上面的例子中,objc_msgSend()方法可能编译不通过,这时候要在XCode中设置一下:TARGET -> Build Settings -> Apple LLVM x.x - Preprocessing -> Enable Strict Checking of objc_msgSend Calls 设为NO
![](./images/1.png)

在上面的调用中,我们采用的方式是:<code>NSLog(@"%@", objc_msgSend(test, @selector(pointToString:), point)); </code>这个selector同样可以跟据字符串生成:<code>NSLog(@"%@", objc_msgSend(test, sel_registerName("pointToString:"), point));</code>
看到这里我们可以发现,这种实现方式只能确定消息名和参数数目,而参数类型和返回类型是确定不了的,所以编译器只能在编译期警告你参数类型不对,而无法阻止你传递类型错误的参数。

再来看看NSObject协议提供的一些传递消息的方法:

```
- (id)performSelector:(SEL)aSelector
- (id)performSelector:(SEL)aSelector withObject:(id)anObject
- (id)performSelector:(SEL)aSelector withObject:(id)anObject withObject:(id)anotherObject
```
仔细看下,这样有一些局限性,比如参数必须是对象,最多支持2个参数。
我们先调用一下:

```
NSLog(@"%@", [test performSelector:@selector(intToString:) withObject:@123]); // 由于要求传入的是对象,所以我们这里把123转成NSNumber,另外还要把我们原来写的intToString:(NSInteger)number方法改成:
- (NSString *)intToString:(NSNumber *)number {
    return [NSString stringWithFormat:@"%ld", [number integerValue]];
}
```
但是如果要传3个,4个甚至更多的参数,NSObject原生提供的API就做不到了,所以我们可以给NSObject加一点扩展方法,假设我们想要实现这样:

```
@interface NSObject (Util)

- (id)performSelector:(SEL)aSelector withObjects:(NSArray *)objects;
@end

```
在实现这两个方法之前,而要介绍一些小东西,先看一下IMP:
A pointer to the start of a method implementation.  
<code>id (*IMP)(id, SEL, ...)</code>
也就是说这个东西实际上是指向了函数实现的地址。我们来看一个Demo:
比如我们建立两个类,一个Dog一个Cat, 它们都实现了hello方法:

```
#import <Foundation/Foundation.h>
@interface Dog : NSObject
- (void)hello;
@end

#import "Dog.h"
@implementation Dog
- (void)hello {
    NSLog(@"汪汪!");
}
@end
```

```
#import <Foundation/Foundation.h>
@interface Cat : NSObject
- (void)hello;
@end

#import "Cat.h"
@implementation Cat
- (void)hello {
    NSLog(@"苗苗!");
}
@end

```

```
#import <Foundation/Foundation.h>
#import "Dog.h"
#import "Cat.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Dog *dog = Dog.new;
        Cat *cat = Cat.new;
        SEL sel  = @selector(hello);
        IMP imp1 = [dog methodForSelector:sel];
        IMP imp2 = [cat methodForSelector:sel];
        imp1(dog, sel); // 汪汪!
        imp2(cat, sel); // 苗苗!
        // 注意:如果imp1和imp2提示出错,则需要在XCode中设置一下:
        // TARGET -> Build Settings -> Apple LLVM x.x - Preprocessing -> Enable Strict Checking of objc_msgSend Calls 设为NO
    }
    return 0;
}
```

参考链接:  
<http://www.cnblogs.com/biosli/p/NSObject_inherit_2.html>
<http://www.cnblogs.com/kesalin/archive/2012/11/14/dynamic_method_resolve.html>  
Objective-C是一门动态语言,一个函数是由一个selector(SEL),和一个implement(IML)组成的。Selector相当于门牌号,而Implement才是真正的住户(函数实现)。
和现实生活一样,门牌可以随便发(@selector(XXX)),但是不一定都找得到住户,如果找不到系统会给程序几次机会来程序正常运行,实在没出路了才会抛出异常。下图是objc_msgSend调用时,查找SEL的IML的过程。咱们以这个流程为例看看其中涉及的很有用的函数。
![](./images/2.png)
<code>resolveInstanceMethod</code>函数:  
这个函数在运行时(runtime),没有找到SEL的IML时就会执行。这个函数是给类利用class_addMethod添加函数的机会,示例:

```
// Foo.h
#import <Foundation/Foundation.h>
@interface Foo : NSObject
- (void)bar;
@end
```

```
// Foo.m
#import "Foo.h"
@implementation Foo
- (void)bar {
    NSLog(@"method bar is called");
}
@end
```
以上面的类Foo中,我们实现了一个叫bar的方法,接下来调用一下:

```
- (IBAction)testSelector:(id)sender {
    Foo *foo = [[Foo alloc] init];
    [foo bar];
    // [foo missMethod];
    [foo performSelector:@selector(missMethod)];
}
```
当代码执行到 [foo performSelector:@selector(missMethod)]; 时,程序crash掉,原因很简单,对象foo没有实现missMethod方法,这时候就会走resolveInstanceMethod方法,而系统默认实现是调用super的resolveInsanceMethod, 然后抛出异常,crash掉了。  

下面我们在Foo.m中做一点事情:

```
// Foo.m
#import "Foo.h"
#include <objc/runtime.h>

@implementation Foo

void dynamicMethodIMP(id self, SEL _cmd) {
    NSLog(@"dynamicMethodIMP");
}

- (void)bar {
    NSLog(@"method bar is called");
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    NSLog(@"Instance resolving %@", NSStringFromSelector(sel));
    if (sel == @selector(missMethod)) {
        class_addMethod([self class], sel, (IMP)dynamicMethodIMP, "v@:");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

+ (BOOL)resolveClassMethod:(SEL)sel {
    NSLog(@"Class resolving %@", NSStringFromSelector(sel));
    return [super resolveClassMethod:sel];
}

@end

```

我们发现这次程序并没有crash, 而是打印出来了 Instance resolving missMethod, dynamicMethodIMP,也就是说当执行方法[foo missMethod]的时候,由于foo对象没有实现missMethod, 方法resolveInstanceMethod:被调用,在这个方法里面,我们通过运行时动态的添加了一个方法dynamicMethodIMP, 所以dynamicMethodIMP被调用。
Objective C 中的方法其实就是至少带有两个参数(self 和 _cmd)的普通 C 函数,因此在上面的代码中提供这样一个 C 函数 dynamicMethodIMP,让它来充当对象方法 missMethod 这个SEL的动态实现。因为 missMethod 是被对象所调用,所以它被认为是一个对象方法,因而应该在 resolveInstanceMethod 方法中为其提供实现。通过调用
 class_addMethod([self class], sel, (IMP)dynamicMethodIMP, "v@:"); 就能在运行期动态地为 sel 这个 selector 添加实现:dynamicMethodIMP。class_addMethod 是运行时函数,所以需要导入头文件:<objc/runtime.h>
关于方法:  
<code>BOOL class_addMethod ( Class cls, SEL name, IMP imp, const char *types );</code>
第一个参数是类,第二个参数是selector,第三个参数是实现,即imp, 第四个参数是类型编码

```
cls    
The class to which to add a method.
name    
A selector that specifies the name of the method being added.
imp    
A function which is the implementation of the new method. The function must take at least two arguments—self and _cmd.
types    
An array of characters that describe the types of the arguments to the method. For possible values, see Objective-C Runtime Programming Guide > Type Encodings. Since the function must take at least two arguments—self and _cmd, the second and third characters must be “@:” (the first character is the return type).
```
关于oc运行时的类型编码,可以通过官方文档查看:
Xcode -> Help -> Documentation And API Reference 输入Type Encodings查看,如图:
![](./images/3.png)
![](./images/4.png)

注意在上面的示例中,程序没有crash并不是由于我们在方法<code>+ (BOOL)resolveInstanceMethod:(SEL)sel</code>中返回YES所致,而是我们确实提供了sel的实现,所以如果返回的是YES, 但并没有提供具体的实现,程序一样会crash: -[Foo missMethod]: unrecognized selector sent to instance 0x7fc3da502ff0 *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[Foo missMethod]: unrecognized selector sent to instance 0x7fc3da502ff0',那这个BOOL类型的返回值有什么用呢?作用:在它没有提供真正的实现,并且提供了消息转发机制的情况下,YES 表示不进行后续的消息转发,返回 NO 则表示要进行后续的消息转发。

再来看消息转发中用到的NSMethodSignature:
An NSMethodSignature(方法签名) object records(记录了) type information for the arguments and return value of a method. It is used to forward(传递,分发) messages that the receiving object does not respond to—most notably(尤其,显著地) in the case of distributed objects. You typically create an NSMethodSignature object using NSObject’s <code>- (NSMethodSignature \*)methodSignatureForSelector:(SEL)aSelector</code> instance method (on OS X v10.5 and later you can also use signatureWithObjCTypes:).It is then used to create an NSInvocation object, which is passed as the argument to a <code>- (void)forwardInvocation:(NSInvocation \*)anInvocation </code> message to send the invocation on to whatever other object can handle the message. In the default case, NSObject invokes <code>- (void)doesNotRecognizeSelector:(SEL)aSelector</code>, which raises an exception. For distributed objects, the NSInvocation object is encoded using the information in the NSMethodSignature object and sent to the real object represented by the receiver of the message.    
An NSMethodSignature object presents its argument types by index with the <code>- (const char \*)getArgumentTypeAtIndex:(NSUInteger)index</code> method. The hidden arguments for every method, self and _cmd, are at indices 0 and 1, respectively. The arguments normally specified in a message invocation follow these. In addition to the argument types, an NSMethodSignature object offers the total number of arguments with <code> @property(readonly) NSUInteger numberOfArguments </code>, the total stack frame length occupied by all arguments with <code> @property(readonly) NSUInteger frameLength </code> (this varies with hardware architecture), and the length and type of the return value with <code> @property(readonly) NSUInteger methodReturnLength </code> and <code> @property(readonly) const char \*methodReturnType </code>. Finally, applications using distributed objects can determine if the method is asynchronous with the <code>- (BOOL)isOneway</code> method.     
NSMethodSignature顾名思义应该就是“方法签名”,类似于C++中的编译器时的函数签名。
官方定义该类为对方法的参数、返回类似进行封装,协同NSInvocation实现消息转发。通过消息转发可以用B实现A的方法。

下面我们来实现一个消息转发的小示例:

```
// Foo.h
#import <Foundation/Foundation.h>
@interface Proxy : NSObject
- (void)missMethod;
@end
@interface Foo : NSObject
@end
```

```
// Foo.m
#import "Foo.h"
#include <objc/runtime.h>

@implementation Proxy
- (void)missMethod {
    NSLog(@"missMethod is called in Proxy");
}
@end

@implementation Foo
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    return [Proxy instanceMethodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    SEL sel = [anInvocation selector];
    NSLog(@"forwardInvocation for selector %@", NSStringFromSelector(sel));
    
    Proxy *proxy = [[Proxy alloc] init];
    if ([proxy respondsToSelector:sel]) {
        [anInvocation invokeWithTarget:proxy];
    } else {
        [super forwardInvocation:anInvocation];
    }
}
@end
```

```
// 调用
- (IBAction)testSelector:(id)sender {
    Foo *foo = [[Foo alloc] init];
    // [foo missMethod];
    [foo performSelector:@selector(missMethod)];
}
```

程序执行结果:  
forwardInvocation for selector missMethod  
missMethod is called in Proxy

如果我们把forwardInvocation:加进去,即:

```
// Foo.m
#import "Foo.h"
#include <objc/runtime.h>

@implementation Proxy

- (void)missMethod {
    NSLog(@"missMethod is called in Proxy");
}

@end

@implementation Foo

void dynamicMethodIMP(id self, SEL _cmd) {
    NSLog(@"dynamicMethodIMP");
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    NSLog(@"Instance resolving %@", NSStringFromSelector(sel));
    if (sel == @selector(missMethod)) {
        class_addMethod([self class], sel, (IMP)dynamicMethodIMP, "v@:");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

+ (BOOL)resolveClassMethod:(SEL)sel {
    NSLog(@"Class resolving %@", NSStringFromSelector(sel));
    return [super resolveClassMethod:sel];
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    return [Proxy instanceMethodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    SEL sel = [anInvocation selector];
    NSLog(@"forwardInvocation for selector %@", NSStringFromSelector(sel));
    
    Proxy *proxy = [[Proxy alloc] init];
    if ([proxy respondsToSelector:sel]) {
        [anInvocation invokeWithTarget:proxy];
    } else {
        [super forwardInvocation:anInvocation];
    }
}

@end

```
运行程序调用missMethod方法,我们发现消息转发没有进行, 消息转发只有在对象无法正常处理消息时才会调用,而在这里我在动态方法决议中为 selector 提供了实现,使得对象可以处理该消息,所以消息转发不会继续了。[官方文档](https://developer.apple.com/library/mac/documentation/Cocoa/Reference/Foundation/Classes/NSObject_Class/Reference/Reference.html#//apple_ref/occ/clm/NSObject/resolveInstanceMethod:)中说:If you implement resolveInstanceMethod: but want particular selectors to actually be forwarded via the forwarding mechanism, you return NO for those selectors. 文档里的说法其实并不准确,只有在 resolveInstanceMethod 的实现中没有真正为 selector 提供实现,并返回 NO 的情况下才会进入消息转发流程;否则绝不会进入消息转发流程,程序要么调用正确的动态方法,要么 crash。在上面的例子中,如果把代码 <code>class_addMethod([self class], sel, (IMP)dynamicMethodIMP, "v@:");</code>注释掉,消息转发同样会进行。

友情链接:  
[官方运行时Demo](http://www.opensource.apple.com/source/objc4/objc4-532/runtime/)  
[Objective-C Runtime Programming Guide](https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Introduction/Introduction.html#//apple_ref/doc/uid/TP40008048)

下面我们回过头看一下我们准备做的扩展:

```
@interface NSObject (Util)

- (id)performSelector:(SEL)aSelector withObjects:(NSArray *)objects;
@end
```

```
Test.m
- (NSString *)combineString:(NSString *)string1 withSecond:(NSString *)string2 withThird:(NSString *)string3 {
    return [NSString stringWithFormat:@"%@, %@, %@", string1, string2, string3];
}
```

```
// 调用:
Test *test = [[Test alloc] init];
NSLog(@"%@", [test performSelector:@selector(combineString:withSecond:withThird:) withObjects:@[@"1", @"2", @"3"]]);
```


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
### 回答1: Unity 3D运行时编辑器插件Runtime Editor 3.5.0是一款非常优秀的工具,它可以让开发者在游戏运行时对游戏进行修改和编辑,使游戏开发更加便捷和高效。该插件支持在Unity编辑器中实时编辑、预览和调试游戏,而且还有非常友好的界面,易于上手使用。此外,它也支持一系列常用操作,如:拖拽物体、创建物体、编辑材质、修改脚本、编辑动画等等。总之,使用这款插件可以充分发挥开发者的想象力和创造力,大大提升游戏开发的效率和质量。同时,它也是一款开放源代码的插件,在社区得到了广泛的应用和支持。唯一需要注意的是,在使用该插件时需要注意版本的适配性,不同版本的Unity可能会有一定的兼容性问题。总体来说,对于有一定开发经验的游戏开发者来说,Runtime Editor 3.5.0是一款非常实用的工具,可以让他们更加专注于游戏的创意和实现,而不是一些繁琐的操作。 ### 回答2: Unity 3D 运行时编辑器插件 Runtime Editor 3.5.0 是一个强大的工具,它为 Unity 3D 用户提供了更加灵活、高效的编辑体验。该插件可以让用户在运行游戏的过程中随时对场景进行编辑,添加、删除和调整物体、组件、材质等元素,以及实时预览游戏效果。 Runtime Editor 3.5.0 提供了许多实用的功能和工具,使得用户能够更快速、方便地完成场景的制作。其中一些重要的功能包括: 1. 选择工具:用户可以通过选择工具来选择物体、组件、材质等元素,并可以对其进行编辑和调整。 2. 创建工具:用户可以通过创建工具来创建新的物体、组件、材质等元素,并可以对其进行定制化的设置和编辑。 3. 面板工具:用户可以在编辑器面板中访问和编辑该插件提供的所有功能和工具,并可以根据需要自定义和布置面板。 4. 预设工具:用户可以通过预设工具来快速添加和调整场景中的预设,以及对其进行设置和编辑。 另外,该插件还包括了许多其他实用的功能和工具,如实时查看场景数据、调整游戏场景视角、导出场景数据等,大大提高了 Unity 3D 用户的工作效率和创作体验。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值