作者:一缕殇流化隐半边冰霜
神经病院Objective-C Runtime住院第二天——消息发送与转发
神经病院Objective-C Runtime入院第一天——isa和Class
神经病院Objective-C Runtime出院第三天——如何正确使用Runtime
一. [self class] 与 [super class]
下面的代码分别输出什么?
@implementation Son : Father
- (id)init {
self = [super init];
if (self) {
NSLog(@"%@", NSStringFromClass([self class]));
NSLog(@"%@", NSStringFromClass([super class]));
}
return self;
}
@end
答案: 都是输Son
解释:
-
self
和super
的区别
self
是类的一个隐藏参数,每个方法的实现的第一个参数即为self
。
super
并不是隐藏参数,它实际上只是一个“编译器标识符”,它负责告诉编译器,当调用方法时去调用父类的方法,而不是本类的方法。
在调用[super class]
的时候,runtime
会去调用objc_msgSendSuper
方法,而不是objc_msgSend
.
OBJC_EXPORT void objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */ )
/// Specifies the superclass of an instance.
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 */
};
在objc_msgSendSuper
方法中,第一个参数是一个objc_super
的结构体,这个结构体里面有两个变量,一个是接收消息的receiver
,一个是当前类的父类super_class
.
我们很容易错误的认为[super class]
是调用的[super_class class]
,其实objc_msgSendSuper
的工作原理应该是这样:
// 注意这里是从父类开始msgSend,而不是从本类开始,
objc_msgSend(objc_super->receiver, @selector(class))
/// Specifies an instance of a class. 这是类的一个实例
__unsafe_unretained id receiver;
// 由于是实例调用,所以是减号方法
- (Class)class {
return object_getClass(self);
}
由于找到了父类NSObject
里面的class
方法的IMP
,又因为传入的参数objc_super->receiver = self
;self
就是Son
,所以父类的方法class
执行IMP
之后,输出还是Son
,最后输出的两个值都是一样,都是Son
.
二. isKindOfClass 与 isMemberOfClass
下面代码输出什么?
@interface Sark : NSObject
@end
@implementation Sark
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
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]];
NSLog(@"%d %d %d %d", res1, res2, res3, res4);
}
return 0;
}
答案: 1 0 0 0
解答:
-
先来分析这两个函数的实现:
+ (Class)class { return self; } - (Class)class { return object_getClass(self); } Class object_getClass(id obj) { if (obj) return obj->getIsa(); else return Nil; } inline Class objc_object::getIsa() { if (isTaggedPointer()) { uintptr_t slot = ((uintptr_t)this >> TAG_SLOT_SHIFT) & TAG_SLOT_MASK; return objc_tag_classes[slot]; } return ISA(); } inline Class objc_object::ISA() { assert(!isTaggedPointer()); return (Class)(isa.bits & ISA_MASK); } + (BOOL)isKindOfClass:(Class)cls { for (Class tcls = object_getClass((id)self); tcls; tcls = tcls->superclass) { if (tcls == cls) return YES; } return NO; } - (BOOL)isKindOfClass:(Class)cls { for (Class tcls = [self class]; tcls; tcls = tcls->superclass) { if (tcls == cls) return YES; } return NO; } + (BOOL)isMemberOfClass:(Class)cls { return object_getClass((id)self) == cls; } - (BOOL)isMemberOfClass:(Class)cls { return [self class] == cls; }
-
首先第一题
BOOL res1 = [(id)[NSObject class] isKindOfClass:[NSObject class]]
;a.
[NSObject class]
执行完之后返回NSObject自身。b.
+(BOOL)isKindOfClass:(Class)cls
方法内部,会先去获得object_getClass
的类,而object_getClass
的源码实现会去调用当前类的obj->getIsa()
,最后ISA()
方法中获得meta class
指针。c. 接着在isKindOfClass中有一个循环,先判断class是否等于meta class,不等就继续循环判断是否等于super class,不能再继续取super class,如此循环下去。
d.
[NSObject class]
执行完之后调用isKindOfClass
,第一次先判断NSObject
和NSObject
的meta class
是否相等,根据下图我们可以看出,NSObject
的meta class
与本身不等。接着第二次循环判断:NSObject
与meta class
的superclass
是否相等,根据下图可以看出Root class(meta)
的superclass
就是Root class(class)
,也就是NSObject
本身。所以第二次循环相等,于是第一行res1
应该为YES
.
-
同理,第三行
BOOL res3 = [(id)[Sark class] isKindOfClass:[Sark class]];
a. 在
[Sark class]
执行完之后调用isKindOfClass
,第一次for
循环,Sark
的Meta Class
与[Sark class]
不等,第二次for
循环,Sark Meta class
的super class
指向的是NSObject Meta Class
和Sark Class
不相等。 第三次for
循环,NSObject Meta Class
的super class
指向的是NSObject class
与Sark Class
不相等。 第四次循环,NSObject Class
的super class
指向nil
与Sark Class
不相等。 第四次循环之后,退出循环。所以 第三行res
输出为NO
.b. 如果这里把
Sark
改成它的实例对象,[sark isKindOfClass:[Sark class]]
, 那么此时就应该输出为YES
,因为在isKindOfClass
函数中,判断sark
的isa
指向的是自己的类Sark
,所以第一次for
循环就会输出YES
. 第二行
BOOL res2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]];
,这里的isMemberOfClass
的源码实现是拿到自己的isa
指针和自己比较,判断是否相等。这里NSObject
的isa
指针指向NSObject
的Meta Class
,所以和NSObject Class
不相等,所以res2
输出NO
。同理第四行
BOOL res4 = [(id)[Sark class] isMemberOfClass:[Sark class]];
,Sark
的isa
指针指向Sark
的Meta Class
,和Sark Class
也不等,所以ret4
输出为NO
.
三. Class与内存地址
下面的代码会输出什么?
@interface Sark : NSObject
@property (nonatomic, copy) NSString *name;
- (void)speak;
@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
这道题目有两个难点:
-
obj
调用speak
方法,到底会不会崩溃。 - 如果
speak
方法不崩溃,应该输出什么?
首先谈谈隐藏参数self和_cmd的问题。
当[receiver message]
调用方法时,系统会在运行时偷偷的动态传入两个隐藏参数self
和_cmd
,之所以称之为隐藏参数,是因为在源代码中没有声明和定义这两个参数。self
就是方法接收者,_cmd
表示当前调用方法,其实它就是一个方法的选择器SEL
.
难点一: 能不能调用speak
方法?
id cls = [Sark class];
void *obj = &cls;
[(__bridge id)obj speak];
答案是可以的。obj
被转换成了一个指向Sark Class
的指针,然后使用id
转换成了objc_object
类型。obj
现在已经是一个Sark
类型的实例对象了,当然可以调用speak
的方法。
objc_class
和objc_object
的定义:
typedef struct objc_class *Class;
typedef struct objc_object *id;
@interface Object {
Class isa;
}
@interface NSObject {
Class isa OBJC_ISA_AVAILABILITY;
}
struct objc_object {
private:
isa_t isa;
}
struct objc_class : objc_object {
// Class ISA; Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags }union isa_t
{
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
uintptr_t bits;
}
难点二:如果能调用speak
,会输出什么?
很多人认为会输出sark
相关信息,这样答案就错误了。
正确的答案会输出:
my name's
内存地址每次运行都不同,但前面一定是ViewController
。
我们改变下代码,打印更多信息看一下。
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"ViewController = %@ , 地址 = %p", self, &self);
id cls = [Sark class];
NSLog(@"Sark class = %@ 地址 = %p", cls, &cls);
void *obj = &cls;
NSLog(@"Void *obj = %@ 地址 = %p", obj,&obj);
[(__bridge id)obj speak];
Sark *sark = [[Sark alloc]init];
NSLog(@"Sark instance = %@ 地址 = %p",sark,&sark);
[sark speak];
}
我们把指针地址打印出来,结果如下:
ViewController = , 地址 = 0x7fff5d52b998
Sark class = Sark 地址 = 0x7fff5d52b978
Void *obj = 地址 = 0x7fff5d52b970
my name's
Sark instance = 地址 = 0x7fff5d52b968
my name's (null)
从地址我们可以看出,随着参数的压栈,地址越来越小。
// 以下数字越高表示地址越小
// 压栈参数1: id self (0)
// 压栈参数2: SEL _cmd (1)
- (void)viewDidLoad {
// objc_msgSendSuper2[struct objc_super, SEL)
[super viewDidLoad];
// struct objc_super2 {
// id receiver 等价于 self (2)
// Class super_class 指向父类类型的指针,
id cls = [Sark class];
// (4)
void *obj = &cls;
[(__bridge id)obj speak];
}
// objc_msgSendSuper2() takes the current search class, not its superclass.
OBJC_EXPORT id objc_msgSendSuper2(struct objc_super *super, SEL op, ...)
__OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_2_0);
objc_msgSendSuper2方法参数是一个objc_super *super.
/// Specifies the superclass of an instance.
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
所以按viewDidLoad
执行时各个变量入栈顺序从高到低为self,_cmd,super_class(等同于self.class),receiver(等同于self),obj
。
第一个self
和第二个_cmd
是隐藏参数。第三个super_class
和第四个self
是[super viewDidLoad]
方法执行时候的参数。
在调用self.name
的时候,本质上是self
指针在内存想高位地址偏移一个指针。
因为在Objc中对象实质上是一个指向ClassObject地址的变量,即id obj = &ClassObject
,而对象的实例变量 void *ivar = &obj + offset(N)
- Obj : 0x7fff5d52b978
- Self: 0x7fff5d52b980
- super_class: 0x7fff5d52b988
- _cmd: 0x7fff5d52b990
- self: 0x7fff5d52b998
所以如上,obj
就是cls
的地址,向上偏移一个指针(这里指针占8
个字节)就是self
的地址,所以输出:
my name's
四. Category
下面的代码会?Compile Error / Runtime Crash / NSLog…?
@interface NSObject (Sark)
+ (void)foo;
- (void)foo;
@end
@implementation NSObject (Sark)
- (void)foo
{
NSLog(@"IMP: -[NSObject(Sark) foo]");
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
[NSObject foo];
[[NSObject new] foo];
}
return 0;
}
**答案: **
IMP: -[NSObject(Sark) foo]
IMP: -[NSObject(Sark) foo]
分析:
在加载
NSObject
的Category
中,在编译期会提示我们没有实现+(void)foo
的方法,因为在.m
文件中并没有找到+
方法的实现,只有一个-
号方法,所以会警告提示。在实际加载
Category
的时候,会把-(void)foo
加载进去,由于是实例方法,所以会放在NSObject
的实例方法链表里面。(这里涉及到Category
的知识,推荐:美团的这篇经典的深入理解Objective-C:Category)-
根据
objc_msgSend
原理,我们知道在调用[NSObject foo]
的时候,会先在NSObject
的meta-class
中去查找foo
方法的IMP
,未找到,继续在superClass
中去查找,NSObject
的meta-class
的superClass
就是本身NSObject
,于是又回到NSObject
的类方法中查找foo
方法,于是找到了,就执行foo
方法,输出:IMP: -[NSObject(Sark) foo]
-
同样在调用
[[NSObject new] foo]
的时候,会先生成一个NSObject
的对象,用这个NSObject
实例对象再去调用foo
方法的时候,会先去NSObject
的类方法里面去查找,找到,输出:IMP: -[NSObject(Sark) foo]
所以会输出两个相同的结果。