对 OC 神经病院入学考试题目的理解

这是一个老话题,各种文章已经泛滥了,我为什么还要来掺和呢?个人觉得网上的那些解释存在跳跃性,不能和已有的认知联系起来,下面是我探讨这个问题的记录,对我认为比较陌生的地方力求足够详细地解释。如发现错误之处希望得到您能指正!

上代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@interface Sark : NSObject
@property (nonatomic, copy) NSString *name;
- (void)speak;
@end

@implementation Sark

- (void)speak {
NSLog(@"my name's %@", self.name);
}

@end

- (void)viewDidLoad {
[super viewDidLoad];

id cls = [Sark class];
void *obj = &cls;
[(__bridge id)obj speak];
}

问题 1

会运行时会报错吗?

如果它看起来像鸭子、游泳像鸭子、叫声像鸭子,那么它可能就是只鸭子。

我们知道,实例对象的isa指向类对象的地址。第 17 行中 cls 指向类对象,&cls 就是类对象的地址,即 obj 指向类对象的地址。将 obj 转换成 id 类型后,即是告诉编译器 obj 可以被 Runtime 当做一个实例对象。相对于常规步骤创建的实例对象,obj 的内存区域并不完整,不过 [(__bridge id)obj speak] 真正运行起来时,由于 isa 指向正确,所以并不影响方法查找,speak 方法还是可以被正确响应。

对象的实质就是指向类对象的地址的变量。

问题 2

如果不会运行报错,最终输出结果是什么?

第 9 行做输出打印,转换成汇编后[1]查看 self.name 是调用 _objc_getProperty:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
"-[Sark name]":                         ; @"\01-[Sark name]"
Lfunc_begin2:
.loc 1 21 0
.cfi_startproc
; %bb.0:
sub sp, sp, #16 ; =16
.cfi_def_cfa_offset 16
str x0, [sp, #8]
str x1, [sp]
Ltmp5:
.loc 1 21 39 prologue_end
ldr x1, [sp]
ldr x0, [sp, #8]
mov x2, #8
mov w8, #0
and w3, w8, #0x1
add sp, sp, #16 ; =16
b _objc_getProperty

objc_getProperty 的源码[2]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
id objc_getProperty(id self, SEL _cmd, ptrdiff_t offset, BOOL atomic) {
if (offset == 0) {
return object_getClass(self);
}

// Retain release world
id *slot = (id*) ((char*)self + offset);
if (!atomic) return *slot;

// Atomic retain release world
spinlock_t& slotlock = PropertyLocks[slot];
slotlock.lock();
id value = objc_retain(*slot);
slotlock.unlock();

return objc_autoreleaseReturnValue(value);
}

源码的第 7 行表明,是在 self 的基础上偏移 offset 进行取值的。

在每个 oc 函数调用过程中,self 是一个隐藏参数,是栈空间的一个临时变量,其值在每个栈空间中都不相同,但始终指向对象(也可能是类对象),那么当前的消息接收者(obj)偏移一个字节(指针)长度后指向哪里呢?obj 是栈里面的临时变量,这就需要知道和 objc 相邻的内存单元中存放的是什么。

oc 函数执行时其实有两个隐藏参数 self_cmd ,函数栈空间是从高地址向低地址生长的。当即将执行 [super viewDidLoad] 时,栈空间中的变量从高至低依次是:self_cmd(viewDidLoad)。接下来看看 [super viewDidLoad] 是如何调用的。

[super viewDidLoad] 转换成 c++ 代码[3]后:((void (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("ViewController"))}, sel_registerName("viewDidLoad"));,可以简写成 objc_msgSendSuper((__rw_objc_super){self,class_getSuperclass(objc_getClass("ViewController"))}, sel_registerName("viewDidLoad")),第二个参数为 SEL,第一个参数是结构体变量,该结构体的定义:

1
2
3
4
5
struct __rw_objc_super { 
struct objc_object *object;
struct objc_object *superClass;
__rw_objc_super(struct objc_object *o, struct objc_object *s) : object(o), superClass(s) {}
};

该结构体有两个成员变量,在上述调用时,object 被赋值为 selfsuperClass 相当于 class_getSuperclass(self.class)。下面通过示例代码来说明结构体作为栈上的临时变量,成员变量的存放情况:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 定义结构体
struct DemoStruct {
NSString *first;
NSString *second;
};

- (void)viewDidLoad {
[super viewDidLoad];

struct DemoStruct gg = (struct DemoStruct){@"abc",@"test"};
NSLog(@"gg:%p",&gg);
NSLog(@"gg.first:%p",&(gg.first));
NSLog(@"gg.second:%p",&(gg.second));
}

输出:
1
2
3
gg:0x16fc91b30
gg.first:0x16fc91b30
gg.second:0x16fc91b38

可以看出,结构体的第一个成员变量的地址就是该结构体的地址,余下的成员变量向高地址方向依次填充:

执行 [super viewDidLoad] 调用时,先构建了一个 objc_super 结构体变量存放于当前栈上,再加上 clsobj 两个临时变量,执行到 [(__bridge id)obj speak] 时,栈空间结构为:

当通过 self.name 取值时,即是 obj 指针向高地址方向偏移一个指针的位置取值,取到的就是当前 ViewController 实例对象的地址,打印:
my name's <ViewController: 0x14de09020>

后记

这道题目考察的知识点太多,涉及到函数调用传参、隐藏参数、objc 对象的内存布局、栈空间结构等等,看到他人的解释是似懂非懂,仔细一行一行地跟进时,才发现处处是细节魔鬼,当然能相处这样题目的人更牛!

  1. 通过 Xcode 的 Assembly 可以查看汇编
  2. objc 源码可以在 https://opensource.apple.com/source/objc4/ 下载
  3. 此处转换命令:xcrun -sdk iphoneos clang -rewrite-objc -fobjc-arc -framework Foundation ViewController.m -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator14.4.sdk