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

前言

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

@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/runtime.h>中的显示,但是觉得直接删除又显现不出更改。因而在此专门写出,我会在下面给出正确的解释与数据来源

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 这个对象是什么时候被传入的?

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

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

该上代码了:

@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的流程都是类似的,都是排除变量,逐步探索,最终将探索过程和概念结合。

总结

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

(0)

相关推荐

  • 最新ios面试试题以及解决思路分析

    很多IOS面试都是笔试或者直接上机操作,我们整理了最新的被问到最多的试题类型,来看下: 使用了第三方库, 有看他们是怎么实现的吗? 例:SD.YY.AFN.MJ等! <1>.SD为例: 1.入口 setImageWithURL:placeholderImage:options: 会先把 placeholderImage 显示,然后 SDWebImageManager 根据 URL 开始处理图片. 2.进入 SDWebImageManagerdownloadWithURL:delegate:op

  • IOS面试大全之常见算法

    这篇文字给大家分享了IOS面试中熟悉常见的算法,下面来一起看看吧. 1. 对以下一组数据进行降序排序(冒泡排序)."24,17,85,13,9,54,76,45,5,63" int main(int argc, char *argv[]) { int array[10] = {24, 17, 85, 13, 9, 54, 76, 45, 5, 63}; int num = sizeof(array)/sizeof(int); for(int i = 0; i < num-1; i

  • 12个iOS技术面试题及答案总结

    前言 随着移动互联网科技不断的发展和创新,如今无论是公司还是开发者或设计师个人而言,面试都是一项耗时耗钱的项目,而面对iOS开发者及设计师在面试时可能会遇到的问题进行了筛选与汇总.下面我们一起来一下看看吧. 一.如何绘制UIView? 绘制一个UIView最灵活的方法就是由它自己完成绘制.实际上你不是绘制一个UIView,而是子类化一个UIView并赋予绘制自己的能力.当一个UIView需要执行绘制操作时,drawRect:方法就会被调用,覆盖此方法让你获得绘图操作的机会.当drawRect:方

  • iOS面试中如何优雅回答Block导致循环引用的问题

    前言 说到循环引用问题,最最最常遇到的,不是在项目中,而是在面试中.如果面试官问你开发中是否遇到过retain cycle,你如果说没遇到过,估计已经很难跟面试官继续友好的沟通下去了. 但是这个问题怎么回答呢,网络上千篇一律的答案-->使用Block的时候遇到过,使用__weakSelf 代替 self 等等,可以说这个答案没啥错,但是所有人都回答的一样,并不能突出我们的逼格,无法让面试官知道我们在这方面有过研究,有闪光点. 对于开发者来说,喜欢探索,喜欢挖掘不懂的知识,在面试官眼里会加分不少.

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

    前言 最近在群里看到有人发的一道面试题,题目如下: @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 view

  • Promise面试题详解之控制并发

    前言 在写这篇文章的时候我有点犹豫,因为先前写过一篇类似的,一道关于并发控制的面试题,只不过那篇文章只给出了一种解决方案,后来在网上又陆续找到两种解决方案,说来惭愧,研究问题总是浅尝辄止,所以今天便放在一起,借着这道面试题再重新梳理一下. 题目是这样的: 有 8 个图片资源的 url,已经存储在数组 urls 中(即urls = ['http://example.com/1.jpg', -., 'http://example.com/8.jpg']),而且已经有一个函数 function loa

  • C语言sizeof和strlen的指针和数组面试题详解

    目录 一.概念 sizeof: strlen: 二.例题及解析 2.1 一维数组 2.2 字符数组 2.3 二维数组 三.总结 一.概念 sizeof: sizeof操作符的结果类型为size_t,(它在头文件用typedfe定义为unsigned int类型),计算的是分配空间的实际字节数.sizeof是运算符,可以以类型.函数.做参数 . strlen: strlen结果类型也为size_t(size_t strlen( const char *string )),但strlen是计算的空间

  • IOS开发 UIAlertController详解及实例代码

     IOS开发 UIAlertController详解 在iOS 8.0后,苹果弃用了UIAlertView和UIActionSheet,转而使用UIAlertController把之前的UIAlertView和UIActionSheet整合在一起.新版的API变得简洁了不少几行代码就可实现之前一大片代码的功能 UIAlertController* alert = [UIAlertController alertControllerWithTitle:@"My Alert" messag

  • IOS 中UIApplication详解及实例

    IOS 中UIApplication详解及实例 以前刚学iPhone开发时,觉得UIApplication这个东西特NB,特神秘,比如它居然能打开一个URL,而且还是用一个很神秘的方法得到实例: [UIApplication sharedApplication] 它对我的神秘感一直保持到今天下午.今天下午负责UI设计的同事在设计,我没有素材,比较清闲,于是发个狠,专门看了一下UIApplication这个类.果然是难者不会,会者不难,看完之后,这个类的神秘感一下子没了.下面让我来揭开它的神秘面纱

  • iOS runtime forwardInvocation详解及整理

     iOS runtime forwardInvocation详解 代码: TestModel - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { if(aSelector == @selector(testMethod)) { return [NSMethodSignature signatureWithObjCTypes:"v@:"]; } return nil; } -(void)forwardInv

  • IOS接入facebook详解

    facebook 接入 1.在 facebook 后台 配置 ios 平台, 获取 fb appid store id 可以先随便找一个已存在的填入进去. 2.CocoaPods 引入几个核心库 pod 'FBSDKCoreKit', '~> 9.0.1' pod 'FBSDKLoginKit', '~> 9.0.1' pod 'FBSDKShareKit', '~> 9.0.1' pod 工程构建出 静态库丢到 /Users/XXX/Library/Developer/Xcode/De

  • C语言经典指针笔试题详解

    目录 题目一(有关传值调用与非法访问) 题目二 (返回栈空间地址的问题 ) 题目三 (区别传值调用的传址调用) 题目四 (free释放的时机)

  • C语言中指针和数组试题详解分析

    目录 数组题: 程序一(一维数组): 字符数组 程序二(字符数组): 程序三(字符数组): 程序四(字符数组): 程序五(字符数组): 二维数组 程序六( 二维数组): 指针题 程序七( 指针): 程序八( 指针): 程序九( 指针): 程序十( 指针): 程序十( 图): 程序十一( 指针): 程序十二( 指针): 程序十三( 指针): 指针 和 数组 试题解析 小编,在这里想说一下,c语言的最后一节 C预处理,可能还需要一些时间,因为小编,昨天才下载了虚拟机 和 linux 系统,还没开始安

  • C语言栈与队列面试题详解

    目录 1.括号匹配问题 2.用队列实现栈 3.用栈实现队列 4.设计循环队列 1.括号匹配问题 链接直达: 有效的括号 题目: 思路: 做题前,得先明确解题方案是啥,此题用栈的思想去解决是较为方便的,栈明确指出后进先出.我们可以这样设定: 遇到左括号“ ( ”.“ [ ”.“ { ”,入栈. 遇到右括号“ ) ”.“ ] ”.“ } ”,出栈,跟左括号进行匹配,不匹配就报错. 上两句话的意思就是说我去遍历字符串,如果遇到左括号,就入栈:遇到右括号,就出栈顶元素,并判断这个右括号是否与栈顶括号相匹

随机推荐