内容简介:最近在群里看到有人发的一道面试题,题目如下:问题:上述代码运行起来会:最终问题就是这段代码的运行结果。
最近在群里看到有人发的一道面试题,题目如下:
@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? 怎么能运行,而且结果竟然还是
相信当你看到这个结果的时候会和我一样吃惊,不和逻辑啊,怎么竟然能执行成功并且还打印出来当前controller了,不符合常理啊。
解析
对于计算机而言,不存在什么魔法,如果一段代码能运行必然存在它的原理。
我们需要做的就是分析为什么能成功。
- 为什么调用不崩溃 我们需要了解,
cls
的意思。
cls
在 C语言 里,就是一个指针,这个指针的内容指向Spark类
当我们通过 void *obj = &cls;
这个语句执行后,获取的就是一个指向这个指针 cls
的指针
事实上在这一步操作实现后,obj 这个指针就已经具有Object-c对象的功能了,为什么呢?接下来我们可以看看runtime实现原理了,这里我只说一点
//对象 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_object
这个对象的首字段是isa 指向一个Class
也就是说,我们如果有一个指向Class的地址的指针,相当于这个对象就已经可以使用了,只是像他的成员变量等等的一系列值都还没有被初始化。
所以接下来用 (__bridge id)obj
,调用是不会产生问题的
- 为什么能打印出ViewController对象?
这个问题就是由两个小部分组成的
1. name 这个属性是什么时候赋的值? 2. ViewController 这个对象是什么时候被传入的? 复制代码
首先我们需要先了解一下,一个类对象的数据是如何存储的。
这里我就按照上文一样引用很多的论证了,我们自己来探究
该上代码了:
@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
,打印一下对象指针地址:
可以发现,指针偏移量成员变量和指针首地址差8个字节,每个成员变量与上一个成员变量偏移量也是8个字节。
完成到这一步,我们仍然没有发现上述两个问题是应该怎么解释。但是我们知道了,一个Object-C 对象的指针,和它的成员变量的指针肯定是连续的。这就为接下来我们的分析提供了一些思路。
下一步,我在原本的题目中增加一行代码:
[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
的指针堆栈地址
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];
我们只执行了这一步操作,那必然是这个操作产生的结果。为了验证,我们可以更改一下调用顺序
id cls = [Cls class]; [super viewDidLoad]; 复制代码
当我们进行这部操作后,会发现,执行speak方法时崩溃了,错误是 EXC_BAC_ACCESS
,说明是我们引用野指针了。
由此也可以证实, [super viewDidLoad];
肯定做了一些骚操作,将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
,当我们调用 [super viewDidLoad]
时,执行的方法中传入了参数 self
,由此将 self
做为一个值压入了栈中,但是 _cmd
这个参数并未被使用,因此,没有被压入栈中。
至此,这个问题已经被解释出来了。
答案
所有NSObject对象的首地址都是指向这个对象的所属类。这个条件是充要条件。反过来说,如果一个地址指向某个类,我们就可以把这个地址当成对象去用。所以编译是会通过的,也不会报 unrecognized selector
的错误。
打印结果会是ViewController对象的原因是因为 cls
在栈上的数据结构符合了它作为真实的类时候的数据结构, cls.name
原本地址正好是栈上ViewController对象地址,因此NSLog能打印出 <ViewController >
思索
这类问题,考察的东西很深,并且结合了很多知识点。但是当我们拿到面试题并且能进行思索的时候一定要好好的考虑,我对这道题的想法,也是在不断的试验中逐渐的完善,并且尝试了很多。其实找面试题为什么是这个答案的过程和,找代码找bug的流程都是类似的,都是排除变量,逐步探索,最终将探索过程和概念结合。
以上所述就是小编给大家介绍的《一道值得思考的iOS面试题》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。