作者:一缕殇流化隐半边冰霜
神经病院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.
image.png
-
同理,第三行
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 <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: 0x7f9472502940>
内存地址每次运行都不同,但前面一定是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 = <ViewController: 0x7fdf75d07260> , 地址 = 0x7fff5d52b998
Sark class = Sark 地址 = 0x7fff5d52b978
Void *obj = <Sark: 0x7fff5d52b978> 地址 = 0x7fff5d52b970
my name's <ViewController: 0x7fdf75d07260>
Sark instance = <Sark: 0x618000200190> 地址 = 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。
入栈顺序.png
第一个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 <ViewController: 0x7f9472502940>
四. 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] -
所以会输出两个相同的结果。











网友评论