iOS面试:360面试题记录与总结
昨天进行了360的视频面试,很遗憾的没有进。很多问题的答案就在嘴边,可是就是说不出来,很遗憾。总结下来还是自己的准备不够充分。下面把还记得的问题放上来,并发誓下次遇到以下题目一定答得上来。
(ps:下面的回答都是我的个人总结,很有可能会有遗漏,所以有问题可以帮我指出一下,谢谢)
面试题
第一题问题
说说oc语言和别的语言的区别。
第一题分析
遇到这道题目的时候,我就说了c语言和oc的区别。直到面试官提示了一下,问我为什么大家都说oc是一门动态的语言,我才意识到该说一下对runtime的理解了。
第一题答案
oc语言相当于c语言,它是面向对象的,所以自然具有面向对象的语言特性,如封装、继承、多态。
封装:隐藏对象的属性和实现细节,仅对外公开接口,控制在程序中属性的读取和修改的访问级别。
继承:继承就是子类可以使用从父类继承的属性和方法。
多态:接口的多种不同的实现方式即为多态。(如果一个语言只支持类而不支持多态,只能说明它基于对象的,而不是面向对象的。)
再说说为什么说oc是动态的语言。
runtime是一套底层的 C 语言 API,其为 iOS 内部的核心之一,我们平时编写的 OC 代码,底层都是基于它来实现的。
举个例子:
[receiver message];
// 底层运行时会被编译器转化为:
objc_msgSend(receiver, selector)
之所以说它是动态的,是因为它会将一些工作放在代码运行时才处理而并非编译时。也就是说,有很多类和成员变量在我们编译的时候是不知道的,而在运行时,我们所编写的代码会转换成完整的确定的代码运行。
第二题问题
既然你说runtime会把方法调用转化成objc_msgSend(receiver, selector)
,那说说消息机制的流程。
第二题分析
因为第一题的时候我有提到消息机制,所以面试官才会这样问。但是这道题目我还是胸有成竹的。
第二题答案
首先说一下类的实例的结构体中其实只有一个指向其类对象的指针。
typedef struct objc_object *id;
struct objc_object {
Class isa;
};
再说一下类对象结构体中有super_class、methodLists、cache等元素。
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;
然后可以开始表演了:
1.首先,编译器将代码[receiver message];
转化为objc_msgSend(receiver, selector)
。
2.然后会receiver的isa指针找到obj对应的class。在class中先去cache中通过SEL查找对应函数method。
3.若cache中未找到。再去methodList中查找,若methodlist中未找到,则去superClass中查找。若能找到,则将method加入到cache中,以方便下次查找,并通过method中的函数指针跳转到对应的函数中去执行。
4.如果都没有找到,runtime 会调用 resolveInstanceMethod: 或 resolveClassMethod: 来给我们一次动态添加方法实现的机会。我们需要用 class_addMethod 函数完成向特定类添加特定方法实现的操作。
(如果了解元类的概念,也可以扯一点类方法的调用流程,其实类似)
第三题问题
既然你前面一直有提到方法,那说说SEL与IMP的差别吧。
第三题分析
啊?我有提到IMP吗?这下完了,我该怎么编。。。所以在面试的时候我只说了SEL。
第三题答案
SEL是“selector”的一个类型,表示方法的名字。就像这样,SEL message = @selector(message);
而IMP是指向函数的指针,我把他理解为它就是一个函数,就是即将要执行的那个函数。
这两者估计就是通过一张表来相对应的。相当于说SEL是key,而IMP是value。这个我真的不清楚了。
第四题问题
说一下你对内存管理的理解吧。
第四题分析
就是这道题,明明都知道,却不知如何开口,僵硬了很久。
第四题答案
先放出内存管理四大原则:
1.自己生成的对象自己持有:alloc/new/copy等
2.非自己生成的对象自己也能持有:retain
3.不再需要自己持有的对象时释放:release
4.自己不持有的对象不能释放
然后说一下必须得提的引用计数:�系统通过引用计数来管理内存。系统判断对象要不要回收的唯一依据就是计数器是否为0。当一个对象被初始化的时候,他的引用计数是1,如果调用retain方法,引用计数会变为2,这时调用两次release引用计数会变为0,对象会被释放并调用dealloc方法。
可以提一下内存管理的范围:任何继承NSObject的对象,对其他的基本数据类型无效。那是因为对象和其他数据类型在系统中的存储空间不一样。前者放在堆中,后者主要在栈中。
最后在提一下ARC环境中,系统仍然是通过引用计数来管理内存,但是当我们不再需要对象时不需要自己手动release,系统会帮我们释放。
第五题问题
有哪些情况会出现内存泄漏。
第五题分析
这道题目我就只说出了一个循环引用,别的一下子没想到,估计面试官很不满意。(反思:这道题目在一家公司的笔试中以简答题的形式出现过,我当时也只是写了循环引用,结束后立马忘记了这件事了所以说自己不重视也是很大的问题)
第五题答案
1.block导致的循环引用
block中导致的内存泄漏常常是因为强引用互相之间持有而发生了循环引用无法释放。
self.block = ^{
[self doSomething];
};
这是最简单的情况,self持有block,block又持有self。解决方法是__weak __typeof(self) weakSelf = self;
。这样就打破了循环引用了。
2.NSTimer导致的内存泄漏
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(log) userInfo:nil repeats:YES];
像我这样初始化一个timer对象时,self就强引用timer,timer又强引用target,导致了循环引用。其实解决方法很简单,就是[self.timer invalidate];
。但是这里需要注意的是不能在控制器的dealloc方法中调用停止方法,因为如果timer不停止,就根本进不去dealloc方法。
注意
这里还有一点要提一下,那就是为什么不能和解决block循环应用的方法一样,用_weak方法。这样timer不就弱引用self了吗。像下面这样,美滋滋。
__typeof(self) weakSelf = self;
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:weakSelf selector:@selector(log) userInfo:nil repeats:YES];
在block中,block是对变量进行捕获,意思是对使用到的变量进行拷贝操作,注意是拷贝的不是对象,而是变量自身,拿上面的来说,block中只是对变量weakSelf拷贝了一份,也就是说,block中也定义了一个weak对象,相当于,在block的内存区域中,定义了一个__weak blockWeak对象,然后执行了blockWeak = weakSelf。所以这里并没有引起对象的引用计数的变化,所以没有问题。
回到NSTimer中,根据target的说明:
The timer maintains a strong reference to this object until it (the timer) is invalidated
这里的target是需要强引用的,也就是说就算你传过去一个weakSelf,系统也会把它转换成强引用:__strong strongSelf = weakSelf
。因此解决不了问题。
3.感谢RITL的补充。
__weak __typeof__(self) weakSelf = self;
_observer = [[NSNotificationCenter defaultCenter] addObserverForName:@"testKey" object:nil queue:nil usingBlock:^(NSNotification *note) {
__typeof__(self) strongSelf = weakSelf;
[strongSelf dismissModalViewControllerAnimated:YES];
}];
这是通知方法导致的循环引用,原因和上面的block类似。解决方法也是一样的。
4.声明delegate时如果使用了strong或者retain,十有八九会出现循环引用,所以delegate一定要用weak。
5.C对象是需要自己手动free的,所以忘记释放可能会导致内存泄漏。
第六题问题
前面你有提到可以用_weak来解决block中的循环引用,还有别的方法吗。
第六题答案
1.在ARC 环境下且 iOS 5.0 以上的版本,完全可以用 _weak解决强引用循环的问题。
2.在ARC环境下但是要兼容 iOS 4.x 的版本,用 _unsafe_unretained 解决强引用循环的问题。
_unsafe_unretained 修饰的变量就相当于一个普通的指针,但是需要注意的是它指向的对象被释放时,这个指针不会被设置 nil,就成了野指针,就不能再用它了,会崩溃的。所以,除非是要兼容 4.x 系统,否则就别用了。
3.在 MRC 环境下,用 _block 解决强引用循环的问题。
因为在 ARC 下,_block 修饰的变量在 Block 块中会被 retain,而在 MRC 下,不会 retain。但是在 MRC 下用 _block 有一个问题,不会对已经释放的对象自动置为 nil,这时候在某些异步调用的情况下,当 Block 块中的代码被执行时,就可能出现野指针的情况而引发问题。这种情况我们可以通过 malloc_zone_from_ptr 这个函数来检查指针是否为野指针。示例代码如下:
__block typeof(self) blockSelf = self;
self.block = ^{
if (!malloc_zone_from_ptr(blockSelf)) {
return;
}
[blockSelf doSomething];
}
第七题问题
控制器Apush到B后,有哪些方法可以让B的数据传递到A。
第七题分析
这道题我就答了block、delegate、通知。然而这些都不重要,面试官问这道题的目的其实就是为了引出下一题的!!
第七题答案
其实我的说这些已经差不多够了,如果硬要再说几个的话,KVO,或者NSUserdefault持久化数据也是可以的。(由于这些比较简单,我就不写在这里浪费篇幅了。)
第八题问题
说说KVO的原理吧。
第八题分析
在上一题因为我没说KVO,所以他问我KVO不行吗,我说也可以啊,然后就有这一题了。都是套路。不过还好我看过一些原理,不然这题也是编不出来的。。
第八题答案
首先KVO是oc对观察者设计模式的一种实现。它是基于runtime实现的。
下面是底层实现流程:(以Teacher类为例,teacher为实例,有一个name属性)
1.当teacher被观察时,teacher的 isa 指针从指向原来的Teacher,被KVO机制修改为指向系统新创建的子类 NSKVONotifying_Teacher类。这个时候,teacher就是NSKVONotifying_Teacher的实例了。
2.在NSKVONotifying_Teacher类中,系统会重写setName方法,并在setName方法中调用下面两个方法。
[self willChangeValueForKey:@"name"];
[self didChangeValueForKey:@"name"];
3.然后就会调用observeValueForKey:ofObject:change:context:
方法了。
这张图很好得说明了问题。
(ps:所以说,只有调用set方法的时候才会触发KVO,如果直接
_name = @"YQ"
,是不会触发KVO的,因为你没有用set方法)
第九题问题
现在在TableViewCell上有一个UILabel,UILabel上写是倒计时,倒计时用NSTimer实现,现在滚动tableView,UILabel上的text会变吗。
第九题分析
这道题目是回答的最顺的。因为以前有研究过RunLoop。
第九题答案
不会变,因为tableView滚动的事件属于UITrackingRunLoopMode,而timer默认加入到kCFRunLoopDefaultMode中,在每次调用 RunLoop 的主函数时,只能指定其中一个 Mode,这个Mode被称作 CurrentMode。所以当滚动时,定时器就不起作用了。解决办法为修改timer的模式为NSRunLoopCommonModes。
具体可以看这篇文章:RunLoop入门 看我就够了
第十题问题
frame和bouns的区别。什么时候frame和bouns的高宽不相等。
第十题分析
这道题目又是一脸懵逼。。以前好像没碰到过这种事情吧。。总不能傻在那里吧,我就说了他们的区别。。
第十题题答案
frame: view在父view坐标系统中的位置和大小。(参照点是,父亲的坐标系统)
bounds:view在本地坐标系统中的位置和大小。(参照点是,本地坐标系统,就相当于ViewB自己的坐标系统,以0,0点为起点)
然后我的理解是,父视图如果设置了自己的bouns,会影响到子视图的位置。比如父视图view.bounds = CGRectMake(-30, -30, 200, 200);
就会使得view的左上角的坐标从(0,0)变成(-30,-30),也就是说,view的子视图如果设置childView.frame = CGRectMake(0, 0, 100, 100);
,那么childView会向右下方向各偏移30像素。
然而什么时候高宽不相等。。。表示还是不清楚
感谢Hall_of_fame的补充。
下面是我根据Hall_of_fame的评论做的试验:
代码很简单,初始化视图时候打印一次,点击屏幕旋转视图后再打印一次,注意,只是旋转父视图,子视图没动。
旋转后.png 10题第一次输出.png 10题第二次输出.png可以发现旋转后只有父视图的frame发生了变化。
结论:
视图旋转只影响视图本身以及子视图的视觉效果,视图旋转改变了其在父视图中的位置但并未改变自身尺寸,也没有改变子视图在其坐标系的位置。另外旋转是围绕center为中心进行的,只有旋转的视图自身frame发生改变,bounds和center不受影响,子视图的坐标系均不受影响。
再次感谢Hall_of_fame。
第十一题问题
说说进程和线程的区别。
第十一题答案
1.线程是指进程内的一个执行单元。
2.一个程序就是一个进程,而一个进程至少有一个线程。
3.同一进程的所有线程共享该进程的所有资源。
4.不仅进程之间可以并发执行,同一个进程的多个线程之间也可以并发执行。
第十二题问题
NSOprationQueue 与 GCD 你平时一般用哪个。NSOprationQueue 与 GCD 的区别。
第十二题分析
这道题目我回答的是GCD ,然后他就问我GCD怎么实现任务顺序执行或者说依赖执行,怎么知道任务什么时候完成。额,我说这两个一般都用NSOprationQueue 比较好实现的。然而面试官抓着不放GCD,我表示我不会额。。。
第十二题答案
1.GCD是底层的C语言构成的API,而NSOperationQueue是进行过封装的,是面向对象的。
2.GCD的执行速度比NSOperationQueue快。
3.在NSOperationQueue可以随时取消已经设定要准备执行的任务,而GCD如果要停止已经加入queue的block比较麻烦。
4.NSOperation能够方便地设置依赖关系,我们可以让一个Operation依赖于另一个Operation,这样的话尽管两个Operation处于同一个并行队列中,但前者会直到后者执行完毕后再执行;
5.使用NSOperation可以方便知道Operation何时结束,比GCD更加有效地掌控我们执行的后台任务。
使用场合:
任务之间不太相互依赖:GCD
任务之间有依赖或要监听任务的执行情况:NSOperationQueue
总结
以上就是这次面试的大数题目,还有一些题目因为比较简单,所以现在也忘记了题目是什么。总的来说,这次面试也不是很难,没有问比较深的知识,主要还是自己准备不够才导致失败。
还有一点就是自己不太会的知识尽量避免提到,不然被面试官听到很有可能他就有了兴趣。