题目:
- 下面代码 ⌘+R 后会 Compile Error 、Runtime Crash 或者 NSLog 输出?
- 如果 [(__bridge id)obj speak]; 能调用成功,输出什么?
@interface LCPerson : NSObject
@property (nonatomic, copy) NSString *username;
- (void)doSomthing;
@end
@implementation LCPerson
- (void)doSomthing {
NSLog(@"%s -- %@", __func__, self.username);
}
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
id cls = [LCPerson class];
void *obj = &cls;
[(__bridge id)obj doSomthing];
}
首先,我们运行下,会有什么结果

从以上结果可以看到,不仅编译成功了,而且没有崩溃,还有 log 日志,why?
- 为什么编译不会报错?运行后为什么没有崩溃?
- 为什么打印的结果是
ViewController
对象?
下面针对上面两个问题进行解析
为什么能正常运行?
第一步
[LCPerson class]
返回的是 Class
,实际类型为 struct objc_class *
typedef struct objc_class *Class;
id
表示将其转换为一个对象指针,id 的实际类型为 struct objc_object *
struct objc_object {
Class isa;
};
typedef struct objc_object *id;
从以上分析可以看出:cls
是 id
类型指向, 即 struct objc_object *
。 但本质还是 struct objc_class *
类型
- 验证
id cls = [LCPerson class];
if (object_isClass(cls)) {
NSLog(@"cls is Class");
}
输出结果为:

上面的打印验证了刚刚的结论
第二步
&cls
取的是 cls
的地址,相当于 [LCPerson class]
现在被一个指针的指针指向。
struct objc_object {
Class isa;
};
从 objc_object
的结构我们知道,第一个属性是 isa
,指向当前所属的类。就是说,我们如果有一个指向 Class
的地址的指针,相当于这个对象就已经可以使用了。而 cls
本质上就是 LCPerson
,即 struct objc_class *
类型,void *obj = &cls;
相当于 obj
指针指向了 &cls
,即 obj
变成了 LCPerson
的实例,就可以调用对象的实例方法了。
为什么打印的结果是 ViewController
对象?
这个我们先来了解点拓展知识
参数入栈顺序
- 函数传参
先看一段代码,如下
void lcFunction(NSNumber *a, NSNumber *b, NSNumber *c) {
NSLog(@"a == %p", &a);
NSLog(@"b == %p", &b);
NSLog(@"c == %p", &c);
}
- (void)viewDidLoad {
[super viewDidLoad];
lcFunction(@(10), @(20), @(30));
}
在给函数传入参数时,参数会作为自动变量入栈

栈是由高地址向低地址分配内存,所以他们入栈的先后顺序为:a 先入栈,b 再入栈,c 最后入栈。
- 初始化结构体
同样来一段代码
typedef struct myStruct {
NSNumber *num1;
NSNumber *num2;
NSNumber *num3;
} myStruct;
- (void)viewDidLoad {
[super viewDidLoad];
myStruct test = {@(10), @(20), @(30)};
NSNumber *num4 = @(40);
NSLog(@"num4: %p", &num4);
}
初始化一个结构体的时候,这个顺序是相反的:

他们入栈的先后顺序为:num3 先入栈,num2 再入栈,num1 最后入栈。
- 隐藏参数
我们将前面题目的代码通过 clang
转换为 cpp
文件,查看源码实现。打开终端,cd
到 ViewController.m
所在的目录,执行以下命令
clang -rewrite-objc ViewController.m -o ViewController.cpp
找到 viewDidLoad
的源码,如下:
static void _I_ViewController_viewDidLoad(ViewController * self, SEL _cmd) {
((void (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("ViewController"))}, sel_registerName("viewDidLoad"));
id cls = ((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LCPerson"), sel_registerName("class"));
void *obj = &cls;
((void (*)(id, SEL))(void *)objc_msgSend)((id)obj, sel_registerName("doSomthing"));
}
代码很繁杂,看的不太清晰,我们精简如下:
static void _I_ViewController_viewDidLoad(ViewController * self, SEL _cmd) {
objc_msgSendSuper({self, class_getSuperclass(objc_getClass("ViewController"))}, sel_registerName("viewDidLoad"));
id cls = objc_msgSend(objc_getClass("LCPerson"), sel_registerName("class"));
void *obj = &cls;
objc_msgSend(obj, sel_registerName("doSomthing"));
}
可以看到,viewDidLoad
在底层会携带两个参数:self
和 _cmd
,这个大家应该很熟悉了。方法调用在底层都会转为消息发送
objc_msgSend(void /* id self, SEL op, ... */ )
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */ )
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
objc_msgSend
会传入两个隐藏参数,而 objc_msgSendSuper
需要传入一个结构体 struct objc_super *
,结构如下:
/// Specifies the superclass of an instance.
struct objc_super {
/// Specifies an instance of a class.
__unsafe_unretained _Nonnull 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 _Nonnull Class class;
#else
__unsafe_unretained _Nonnull Class super_class;
#endif
/* super_class is the first class to search */
};
上述源码中的 {self, class_getSuperclass(objc_getClass("ViewController"))}
就是在初始化一个结构体
栈结构
按照上面的规则,题目中代码的入栈顺序为:
-
self
、_cmd
先后入栈;
-
-
objc_msgSendSuper
初始化的结构体中两个参数按照从右到左的顺序先后入栈(即class_getSuperclass(objc_getClass("ViewController"))
先入栈,self
后入栈);
-
-
cls
最后入栈。
-
整体的入栈顺序为:self
、_cmd
、class_getSuperclass(objc_getClass("ViewController"))
、self
、cls
。我们可以验证下:
- (void)viewDidLoad {
[super viewDidLoad];
id cls = [LCPerson class];
void *obj = &cls;
NSLog(@"开始");
void *start = (void *)&self;
void *end = (void *)&obj;
long count = (start - end) / 0x8;
for (long i = 0; i < count; i++) {
void *address = start - i * 0x8;
if (i == 1) {
NSLog(@"%p : %s", address, *(char **)(address));
}
else {
NSLog(@"%p : %@", address, *(void **)(address));
}
}
NSLog(@"结束");
[(__bridge id)obj doSomthing];
}
打印结果如下:

栈区的顺序结果与我们预期的一样
获取 username 属性
-
正常情况下,我们通过
[[LCPerson alloc] init]
初始化方法创建一个LCPerson
实例对象,会在堆区分配内存。但现在,我们是使用栈区指针指向了LCPerson
类对象地址,使编译器误认为是LCPerson
的实例对象,首地址为0x7ffee2fc7268
(即 isa) 。 -
获取对象的属性,通过
isa
指针的地址,进行内存平移得到属性的地址,也就是说LCPerson
对象的username
属性是根据isa
的地址平移 8 字节得到。 -
上述的打印中看到
obj
这个伪对象
的地址0x7ffee2fc7268
,根据内存平移 8字节(0x7ffee2fc7268 + 0x8),得到0x7ffee2fc7270
,而根据打印的入栈顺序找到是ViewController
对象,就是题目运行结果打印的
反思
通过这个题考查的点有:
-
方法调用的本质
-
编译器是如何判断当前对象是某个类的实例
-
函数入栈&结构体入栈顺序
-
对象属性的值是怎么获取的
总结
方法调用的本质是发送消息,在底层会调用 objc_msgSend
会携带两个隐藏参数(self、_cmd);Objective-C中的对象是一个指向 Class
地址的变量,即 id obj = &Class;函数参数入栈顺序是从左到右,而结构体参数入栈是反向的,从右到左;对象属性的获取是通过地址偏移(内存平移)
网友评论