一道值得思考的iOS面试题

栏目: IOS · 发布时间: 6年前

内容简介:最近在群里看到有人发的一道面试题,题目如下:问题:上述代码运行起来会:最终问题就是这段代码的运行结果。

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

@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了,不符合常理啊。

解析

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

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

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

clsC语言 里,就是一个指针,这个指针的内容指向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 ,调用是不会产生问题的

  1. 为什么能打印出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 ,打印一下对象指针地址:

一道值得思考的iOS面试题

可以发现,指针偏移量成员变量和指针首地址差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对象!

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

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面试题》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

微创新

微创新

德鲁•博迪、雅各布•戈登堡 / 钟莉婷 / 中信出版社 / 2014-4-5 / 42.00

好产品不一定要颠覆,微小改进就能让用户尖叫! 引爆创新领域的全新方法论 互联网时代行之有效的5大创新策略 创业者、产品经理必读的创新行动指南 《怪诞行为学》作者 丹•艾瑞里 《影响力》作者 罗伯特•西奥迪尼 全球50位最具影响力的商业思想家之一丹尼尔•平克 周鸿祎、黎万强、罗振宇、牛文文、张鹏 联袂重磅推荐 为什么iPod可以在众多mp3产品中......一起来看看 《微创新》 这本书的介绍吧!

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具

RGB HSV 转换
RGB HSV 转换

RGB HSV 互转工具