一道值得深入思考的iOS面试题详解

2025-05-29 0 83

前言

最近在群里看到有人发的一道面试题,题目如下:

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25
@interface spark : nsobject

@property(nonatomic,copy) nsstring *name;

@end

@implementation spark

- (void)speak {

nslog(@"my name is:%@",self.name);

}

@end

@implementation viewcontroller

- (void)viewdidload {

[super viewdidload];

id cls = [spark class];

void *obj = &cls;

[(__bridge id)obj speak];

}

问题:上述代码运行起来会:complie error?|runtime crash?|nslog ?

最终问题就是这段代码的运行结果。

过程

第一眼看这个问题,我直接就想说,这个东西啊,肯定是编译报错了、要不就是崩溃啊

所以我就跟着写了些代码,结果发现:

wtf? 怎么能运行,而且结果竟然还是

一道值得深入思考的iOS面试题详解

相信当你看到这个结果的时候会和我一样吃惊,不和逻辑啊,怎么竟然能执行成功并且还打印出来当前controller了,不符合常理啊。

解析

对于计算机而言,不存在什么魔法,如果一段代码能运行必然存在它的原理。

我们需要做的就是分析为什么能成功。

为什么调用不崩溃

我们需要了解,cls的意思。

cls在c语言里,就是一个指针,这个指针的内容指向spark类

当我们通过void *obj = &cls;这个语句执行后,获取的就是一个指向这个指针cls的指针

事实上在这一步操作实现后,obj 这个指针就已经具有object-c对象的功能了,为什么呢?接下来我们可以看看runtime实现原理了,这里我只说一点

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35
//对象

struct objc_object {

class isa objc_isa_availability;

};

//类

struct objc_class {

class isa objc_isa_availability;

#if !__objc2__

class super_class objc2_unavailable;

const char *name objc2_unavailable;

long version objc2_unavailable;

long info objc2_unavailable;

long instance_size objc2_unavailable;

struct objc_ivar_list *ivars objc2_unavailable;

struct objc_method_list **methodlists objc2_unavailable;

struct objc_cache *cache objc2_unavailable;

struct objc_protocol_list *protocols objc2_unavailable;

#endif

} objc2_unavailable;

//方法列表

struct objc_method_list {

struct objc_method_list *obsolete objc2_unavailable;

int method_count objc2_unavailable;

#ifdef __lp64__

int space objc2_unavailable;

#endif

/* variable length structure */

struct objc_method method_list[1] objc2_unavailable;

} objc2_unavailable;

//方法

struct objc_method {

sel method_name objc2_unavailable;

char *method_types objc2_unavailable;

imp method_imp objc2_unavailable;

}

引自: ios runtime详解-简书

上述简介中部分是错误的,因为这个只是在<objc/runtime.h>中的显示,但是觉得直接删除又显现不出更改。因而在此专门写出,我会在下面给出正确的解释与数据来源

?

1

2

3

4

5

6
struct objc_class : objc_object {

// class isa;

class superclass;

cache_t cache; // formerly cache pointer and vtable

class_data_bits_t bits;

}

数据来源:苹果obj4开源代码第1012行 用以替换 上述简述引用中的 objc_class

可以看到objc_object这个对象的首字段是isa 指向一个class

也就是说,我们如果有一个指向class的地址的指针,相当于这个对象就已经可以使用了,只是像他的成员变量等等的一系列值都还没有被初始化。

所以接下来用(__bridge id)obj,调用是不会产生问题的

为什么能打印出viewcontroller对象?

这个问题就是由两个小部分组成的

1. name 这个属性是什么时候赋的值?

2. viewcontroller 这个对象是什么时候被传入的?

首先我们需要先了解一下,一个类对象的数据是如何存储的。

这里我就按照上文一样引用很多的论证了,我们自己来探究

该上代码了:

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17
@interface cls : nsobject

@property(nonatomic,strong) nsstring *test;

@property(nonatomic,strong) nsstring *test1;

@end

@implementation cls

- (void)printprinter {

nslog(@"self:%p",self);

nslog(@"self.test:%p",&_test);

nslog(@"self.test1:%p",&_test1);

}

@end

接下来调用printprinter,打印一下对象指针地址:

一道值得深入思考的iOS面试题详解

可以发现,指针偏移量成员变量和指针首地址差8个字节,每个成员变量与上一个成员变量偏移量也是8个字节。

完成到这一步,我们仍然没有发现上述两个问题是应该怎么解释。但是我们知道了,一个object-c 对象的指针,和它的成员变量的指针肯定是连续的。这就为接下来我们的分析提供了一些思路。

下一步,我在原本的题目中增加一行代码:

?

1

2

3

4

5
[super viewdidload];

nsstring *str = @"11111";

id cls = [spark class];

为啥要增加这行代码呢,这步是经过深(瞎)思(j)熟(b)虑(试),主要是考虑到函数内部的参数生成必然会需要地方存储,但这部分存储地址,我们是不知晓的,它的实现是被系统隐藏的。而我们的代码又没有明显的设置相关代码,那么必然是由这些条件实现的。所以当我们增加了这一行代码后,不出意外的,打印结果变了

2018-11-29 20:49:39.254021+0800 test[1961:92498] my name is:11111

变成了 我们 上述的值,这一切都和猜想的差不多

于是一个基本设想就出来了:

因为栈上的地址结构和原本类的需求地址结构高度重合了,同时所有地址都能访问到对应的值。我们通过栈的默认行为生成了一个spark对象!

为了验证,我们打印一下cls和str的指针堆栈地址

?

1
nslog(@"cls address:%p str address:%p",&cls,&str);

2018-11-29 21:03:30.490989+0800 test[2129:122769] cls address:0x7ffeebf4fa00 str address:0x7ffeebf4fa08

我们可以看到他们之间相差也正好是8,而且正好和对象结构体定义的一模一样。所以这也正好能说明我们上述的打印结果my name is:11111为什么会发生。

注:这个存在的原因是因为函数内部变量采用的小端模式,也就是将参数地址由栈区从高地址依次向低地址分配,所以我们打印cls地址会比str要小。

由此,第一个小问题就解决了,答案是因为我们在生成堆栈参数的时候,拼凑出了spark对象的地址数据结构格式,和真正的对象地址数据结构一样,所以self.name就是在生成cls的那一刻起内存地址就已经被赋值了。

接下来到下一个问题了viewcontroller 是什么时候传入的?

在这一步里我们只能把目光向cls对象生成前执行的操作来看,[super viewdidload];我们只执行了这一步操作,那必然是这个操作产生的结果。为了验证,我们可以更改一下调用顺序

?

1

2

3
id cls = [cls class];

[super viewdidload];

当我们进行这部操作后,会发现,执行speak方法时崩溃了,错误是exc_bac_access,说明是我们引用野指针了。
由此也可以证实,[super viewdidload];肯定做了一些骚操作,将viewcontroller的self压入了栈区。

接下来我们就需要探究究竟做了什么操作,我们可以用如下的命令行代码将viewcontroller.m重写成c++代码,然后观看发生了什么。

?

1
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc viewcontroller.m -o viewcontroller.cpp

?

1

2
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"));

我们可以发现原本这个方法里面会传入两个参数一个是self,一个是_cmd,当我们调用[super viewdidload]时,执行的方法中传入了参数self,由此将self做为一个值压入了栈中,但是_cmd这个参数并未被使用,因此,没有被压入栈中。

至此,这个问题已经被解释出来了。

答案

所有nsobject对象的首地址都是指向这个对象的所属类。这个条件是充要条件。反过来说,如果一个地址指向某个类,我们就可以把这个地址当成对象去用。所以编译是会通过的,也不会报unrecognized selector的错误。

打印结果会是viewcontroller对象的原因是因为cls在栈上的数据结构符合了它作为真实的类时候的数据结构,cls.name原本地址正好是栈上viewcontroller对象地址,因此nslog能打印出<viewcontroller >

思索

这类问题,考察的东西很深,并且结合了很多知识点。但是当我们拿到面试题并且能进行思索的时候一定要好好的考虑,我对这道题的想法,也是在不断的试验中逐渐的完善,并且尝试了很多。其实找面试题为什么是这个答案的过程和,找代码找bug的流程都是类似的,都是排除变量,逐步探索,最终将探索过程和概念结合。

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对快网idc的支持。

收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。

快网idc优惠网 建站教程 一道值得深入思考的iOS面试题详解 https://www.kuaiidc.com/89297.html

相关文章

发表评论
暂无评论