一道有意思的iOS面试题

前言

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

  1. @interface Spark : NSObject

  2. @property(nonatomic,copy) NSString *name;

  3. @end

  4. @implementation Spark

  5. - (void)speak {

  6. NSLog(@"My name is:%@",self.name);

  7. }

  8. @end

  9. @implementation ViewController

  10. - (void)viewDidLoad {

  11. [super viewDidLoad];

  12. id cls = [Spark class];

  13. void *obj = &cls;

  14. [(__bridge id)obj speak];

  15. }

</pre>

问题:上述代码运行起来会: Complieerror?|Runtimecrash?|NSLog?

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

过程

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

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

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

image

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

解析

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

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

  1. 为什么调用不崩溃 我们需要了解, cls的意思。

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

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

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

  1. //对象

  2. struct objc_object {

  3. Class isa OBJC_ISA_AVAILABILITY;

  4. };

  5. //类

  6. struct objc_class {

  7. Class isa OBJC_ISA_AVAILABILITY;

  8. #if !__OBJC2__

  9. Class super_class OBJC2_UNAVAILABLE;

  10. const char *name OBJC2_UNAVAILABLE;

  11. long version OBJC2_UNAVAILABLE;

  12. long info OBJC2_UNAVAILABLE;

  13. long instance_size OBJC2_UNAVAILABLE;

  14. struct objc_ivar_list *ivars OBJC2_UNAVAILABLE;

  15. struct objc_method_list **methodLists OBJC2_UNAVAILABLE;

  16. struct objc_cache *cache OBJC2_UNAVAILABLE;

  17. struct objc_protocol_list *protocols OBJC2_UNAVAILABLE;

  18. #endif

  19. } OBJC2_UNAVAILABLE;

  20. //方法列表

  21. struct objc_method_list {

  22. struct objc_method_list *obsolete OBJC2_UNAVAILABLE;

  23. int method_count OBJC2_UNAVAILABLE;

  24. #ifdef __LP64__

  25. int space OBJC2_UNAVAILABLE;

  26. #endif

  27. /* variable length structure */

  28. struct objc_method method_list[1] OBJC2_UNAVAILABLE;

  29. } OBJC2_UNAVAILABLE;

  30. //方法

  31. struct objc_method {

  32. SEL method_name OBJC2_UNAVAILABLE;

  33. char *method_types OBJC2_UNAVAILABLE;

  34. IMP method_imp OBJC2_UNAVAILABLE;

  35. }

</pre>

引自: iOS Runtime详解-简书

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

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

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

  1. 为什么能打印出ViewController对象?

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

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

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

</pre>

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

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

该上代码了:

  1. @interface Cls : NSObject

  2. @property(nonatomic,strong) NSString *test;

  3. @property(nonatomic,strong) NSString *test1;

  4. @end

  5. @implementation Cls

  6. - (void)printPrinter {

  7. NSLog(@"self:%p",self);

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

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

  10. }

  11. @end

</pre>

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

image

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

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

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

  1. [super viewDidLoad];

  2. NSString *str = @"11111";

  3. id cls = [Spark class];

</pre>

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

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

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

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

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

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

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

</pre>

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

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

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

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

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

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

  1. id cls = [Cls class];

  2. [super viewDidLoad];

</pre>

当我们进行这部操作后,会发现,执行speak方法时崩溃了,错误是 EXC_BAC_ACCESS,说明是我们引用野指针了。

由此也可以证实, [superviewDidLoad];肯定做了一些骚操作,将ViewController的 self压入了栈区。

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

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc ViewController.m -o ViewController.cpp
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,当我们调用 [superviewDidLoad]时,执行的方法中传入了参数 self,由此将 self做为一个值压入了栈中,但是 _cmd这个参数并未被使用,因此,没有被压入栈中。

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

答案

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

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

思索

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,837评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,551评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,417评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,448评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,524评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,554评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,569评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,316评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,766评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,077评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,240评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,912评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,560评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,176评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,425评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,114评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,114评论 2 352