神经病院objc runtime入院考试

27 分钟读完

参加sunnyxx的神经病院objc runtime入院考试

1. 下面的代码输出什么?

 @implementation Son : Father
 - (id)init {
     self = [super init];
     if (self) {
         NSLog(@"%@", NSStringFromClass([self class]));
         NSLog(@"%@", NSStringFromClass([super class]));
     }
     return self;
 }
 @end

答:[self class]没什么疑问就不说了,研究一下super的调用,先看一下objc4-750中的解释:

When it encounters a method call, the compiler generates a call to one of the functions objc_msgSend, objc_msgSend_stret, objc_msgSendSuper, or objc_msgSendSuper_stret. Messages sent to an object’s superclass (using the super keyword) are sent using objc_msgSendSuper; other messages are sent using objc_msgSend. Methods that have data structures as return values * are sent using objc_msgSendSuper_stret and objc_msgSend_stret.

由上面可以看出用super调用实际是调用了 objc_msgSendSuper(struct objc_super *super, SEL op, ...), 再看一下函数中的super参数的解释:

A pointer to an objc_super data structure. Pass values identifying the context the message was sent to, including the instance of the class that is to receive the message and the superclass at which to start searching for the method implementation.

函数中的super 参数是一个objc_super的结构体类型

struct objc_super {
    __unsafe_unretained _Nonnull id receiver;
    __unsafe_unretained _Nonnull Class super_class;
};

结构体中有两个属性,一个是receiver用来存储当前的对象,一个是super_class用来存储当前对象的父类类型,所以[super class]receiver还是当前对象,那么调用的结果和[self class]也是一样的了。

虽然结果一样,但是过程有点区别,class方法在NSObject类中定义。 那么[self class]的调用是在Son类的方法列表开始找,然后逐级到父类寻找直到NSObject,而[super class]是在Father类开始找,逐级找到NSObject

2. 下面代码的结果?

BOOL res1 = [(id)[NSObject class] isKindOfClass:[NSObject class]];
BOOL res2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]];
BOOL res3 = [(id)[Sark class] isKindOfClass:[Sark class]];
BOOL res4 = [(id)[Sark class] isMemberOfClass:[Sark class]];

答:先说答案,YES/NO/NO/NO,然后看一下原因,在OC中class也是对象,是metaClass类型的,instance-class-metaClass的关系可以通过一幅图来看一下:(图片来自这里)对于本题来说NSObject就对应图中的Root class, Sark就对应图中的Superclass。 那么上边的问题返回true的条件就是方法的参数传入的是调用者class的metaClass类型,具体看一下。

  1. BOOL res1 = [(id)[NSObject class] isKindOfClass:[NSObject class]]; NSObject的metaClass类型应该是[NSObject metaClass](代指,实际获取metaClass需要用objc_getMetaClass),它又是NSObject子类,所以参数传入[NSObject class]也是YES。
  2. BOOL res2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]]; 跟第一条一样,但是传入了父类对象,所以返回NO。
  3. 剩下两条传入的参数是[Sark class], 完全不在metaClass之列,所以都返回NO。

3. 下面的代码会?Compile Error / Runtime Crash / NSLog…?

@interface NSObject (Sark)
+ (void)foo;
@end
@implementation NSObject (Sark)
- (void)foo {
    NSLog(@"IMP: -[NSObject (Sark) foo]");
}
@end
// 测试代码
[NSObject foo];
[[NSObject new] foo];

答:都会调用到-foo, 对象方法的调用是在class的方法列表中查找(不考虑cache),类方法的调用是在metaClass的方法列表中查找,找不到的话逐级到父类查找,那么跟上题类似,NSObject的metaClass的superclass就是NSObject, 所以最终还是会找到NSObject的方法列表中,成功调用。

4. 下面的代码会?Compile Error / Runtime Crash / NSLog…?

@interface Sark : NSObject
@property (nonatomic, copy) NSString *name;
@end
@implementation Sark
- (void)speak {
    NSLog(@"my name's %@", self.name);
}
@end
@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    id cls = [Sark class];
    void *obj = &cls;
    [(__bridge id)obj speak];
}
@end

答:

(1). 会不会crash?

不会, id类型在objc中的定义是: typedef struct objc_object *id; 那么

id cls = [Sark class];
void *obj = &cls;

可以理解为

objc_object *cls = [Sark class];
void *obj = &cls;

[Sark class]的返回objc_class类型,objc_class继承自objc_object, 所以用objc_object类型的cls接收也是完全可以满足objc_object的赋值要求的了。 再经过c类型和oc类型的转换,就获得了一个Sark类型的实例对象。可以正常调用实例方法。

(2). 会输出什么?

会输出什么的关键问题是对象是怎么找到它的属性的,站在巨人的肩膀上,直接看一下霜神的研究结论吧

Objc中的对象是一个指向ClassObject地址的变量,即 id obj = &ClassObject , 而对象的实例变量 void *ivar = &obj + offset(N)

所以只要找出入栈顺序,找出obj在栈中的相邻值就可以了。 viewDidLoad调用了super, objc_super的结构是

struct objc_super {
    /// Specifies an instance of a class.
    __unsafe_unretained id receiver;

    /// Specifies the particular superclass of the instance to message. 
#if !defined(__cplusplus)  &&  !__OBJC2__
    /* For compatibility with old objc-runtime.h header */
    __unsafe_unretained Class class;
#else
    __unsafe_unretained Class super_class;
#endif
    /* super_class is the first class to search */
};
#endif

得出入栈顺序是self, _cmd(SEL, viewDidLoad), super_class, self(receiver), cls, obj,输出的结果就是cls偏移一个地址(32位是4字节,64位是8字节),就取到了self的值

(3). 变种1

@interface Sark : NSObject
@property (nonatomic, copy) NSString *sex;
@property (nonatomic, copy) NSString *name;
@end
@implementation Sark
- (void)speak {
    NSLog(@"my name's %@", self.name);
}
@end
@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    id cls = [Sark class];
    void *obj = &cls;
    [(__bridge id)obj speak];
    NSLog(@"super: %@", [super class]);
}
@end

输出:

my name's ViewController
super: ViewController

按照入栈顺序,取到的值应该是super_class

(4). 变种2

@interface Sark : NSObject
@property (nonatomic, copy) NSString *name;
@end
@implementation Sark
- (void)speak {
    NSLog(@"my name's %@", self.name);
}
@end
@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    NSString *myName = @"hotchner";
    id cls = [Sark class];
    void *obj = &cls;
    [(__bridge id)obj speak];
    [(__bridge id)obj speak];
}
@end

输出:

my name's hotchner

新的入栈顺序变成了self, _cmd(SEL, viewDidLoad), super_class, self(receiver), myName, cls, obj

结语

真的是神经病院!

参考链接

  1. Objective-C对象模型及应用
  2. 神经病院Objective-C Runtime入院第一天——isa和Class
  3. 神经病院Objective-C Runtime住院第二天——消息发送与转发
  4. 神经病院Objective-C Runtime出院第三天——如何正确使用Runtime
  5. Dissecting objc_msgSend on ARM64
  6. 深入解析 ObjC 中方法的结构
  7. Objc 对象的今生今世
  8. sunnyxx视频

留下评论