小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。
以下的代码会输出什么?
@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。_cmd表示当前调用方法,其实它就是一个方法选择器SEL。 -
难点一,能不能调用
speak方法?
id cls = [Sark class];
void *obj = &cls;
答案是可以的。obj被转换成了一个指向Sark Class的指针,然后使用id转换成了objc_object类型。obj现在已经是一个Sark类型的实例对象了。当然接下来可以调用speak的方法。
- 难点二,如果能调用
speak,会输出什么呢?
很多人可能会认为会输出sark相关的信息。这样答案就错误了。
正确的答案会输出
my name is <ViewController: 0x7ff6d9f31c50>
内存地址每次运行都不同,但是前面一定是
ViewController。why?
我们把代码改变一下,打印更多的信息出来。
- (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: 0x7fb570e2ad00> , 地址 = 0x7fff543f5aa8
Sark class = Sark 地址 = 0x7fff543f5a88
Void *obj = <Sark: 0x7fff543f5a88> 地址 = 0x7fff543f5a80
my name is <ViewController: 0x7fb570e2ad00>
Sark instance = <Sark: 0x7fb570d20b10> 地址 = 0x7fff543f5a78
my name is (null)
objc_msgSendSuper2 解读
// 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, self.class, self, obj。
- 第一个
self和第二个_cmd是隐藏参数。 - 第三个
self.class和第四个self是[super viewDidLoad]方法执行时候的参数。 - 在调用
self.name的时候,本质上是self指针在内存向高位地址偏移一个指针。在32位下面,一个指针是4字节=4*8bit=32bit。 (64位不一样但是思路是一样的) - 从打印结果我们可以看到,
obj就是cls的地址。在obj向上偏移32bit就到了0x7fff543f5aa8,这正好是ViewController的地址。
所以输出为my name is <ViewController: 0x7fb570e2ad00>。
至此,Objc中的对象到底是什么呢?
实质:
Objc中的对象是一个指向ClassObject地址的变量,即id obj = &ClassObject, 而对象的实例变量void *ivar = &obj + offset(N)
加深一下对上面这句话的理解,下面这段代码会输出什么?
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"ViewController = %@ , 地址 = %p", self, &self);
NSString *myName = @"halfrost";
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: 0x7fff44404ab0> , 地址 = 0x7fff56a48a78
Sark class = Sark 地址 = 0x7fff56a48a50
Void *obj = <Sark: 0x7fff56a48a50> 地址 = 0x7fff56a48a48
my name is halfrost
Sark instance = <Sark: 0x6080000233e0> 地址 = 0x7fff56a48a40
my name is (null)
由于加了一个字符串,结果输出就完全变了,[(__bridge id)obj speak];这句话会输出“my name is halfrost”
原因还是和上面的类似。按
viewDidLoad执行时各个变量入栈顺序从高到底为self,_cmd,self.class,self,myName,obj。obj往上偏移32位,就是myName字符串,所以输出变成了输出myName了。