前言
学如逆水行舟,不进则退。所以今天我又来了,今天给大家分享的是阿狸p5一面的面试题及参考答案。希望各位朋友看完这篇文章都能都有所收获。不管是温故而知新,还是查漏补缺。废话不多说,直接给大家上干货了。
面试题
1.MVC具有什么样的优势,各个模块之间怎么通信,比如点击Button后怎么通知Model?
MVC是一种设计思想,一种框架模式,是一种把应用中所有类组织起来的策略,它把你的程序分为三块。分别是:
M(Model):实际上考虑的是“什么”问题,你的程序本质上是什么,独立于UI工作。是程序中用于处理应用程序逻辑的部分,通常负责存取数据。
C(Controller):控制你Model如何呈现在屏幕上,当它需要数据的时候就告诉Model,你帮我获取某某数据;当它需要UI展示和更新的时候就告诉View,你帮我生成一个UI显示某某数据,是Model和View沟通的桥梁。
V(View):Controller的手下,是Cotroller要使用的类,用于构建视图,通常是根据Model来创建视图的。
要了解MVC如何工作,首先需要了解这三个模块间如何通信。
[图片上传失败...(image-fe8de4-1634558454333)]
MVC通信规则
Controller to Model
可以直接单向通信。Controller需要将Model呈现给用户,因此需要知道模型的一切,还需要有同Model完全通信的能力,并且能任意使用Model的公共API
Controller to View
可以直接单向通信。Controller通过View来布局用户界面。
Model to View
永远不要直接通信。Model是独立于UI的,并不需要和View直接通信,View通过Controller获取Model数据。
View to Controller
View不能对Controller知道的太多,因此要通过简洁的方式通信。
Target action。 首先Controller会给自己留一个target,再把配套的action交给View作为联系方式。那么View接受到某些变化时,View就会发送action给target从而达到通知的目的。这里View只需要发送action,并不需要知道Controller如何执行方法。
代理。有时候View没有足够的逻辑去判断用户操作是否符合规范,他会把判断这些问题的权力委托给其他对象,他只需获得答案就行了,并不会管是谁给的答案。
DataSoure。 View没有拥有他们所显示数据的权力。View只能向Controller请求数据进行显示,Controller则获取Model数据整理排版后提供给View。
Model访问Controller
同样的Model是独立于UI存在的,因此无法直接与Controller通信,但是当Model本身信息发生了改变的时候,会通过下面的方式进行间接通信。
Notification&KVO 一种类似电台的方法,Model信息改变时会广播消息给感兴趣的人,只要Controller接受到了这个广播的时候就会主动联系Model,获取新的数据提供给View。
从上面的简单介绍中我们来简单概括一下MVC模式的优点。
1.低耦合性
2.有利于开发分工
3.有利于组件重用
4.可维护性
2.两个无限长度链表(也就是可能有环)判断有没有交点?
给定单链表,检测是否有环。如果有环,则求出进入环的第一个节点。
判断单向链表是否有环,可以采用快指针与慢指针的方式来解决。即定义一个快指针fast和一个慢指针slow,使得fast每次跳跃两个节点,slow每次跳跃一个节点。如果链表没有环的话,则slow与fast永远不会相遇(这里链表至少有两个节点);如果有环,则fast与slow将会在环中相遇。
然后获取环的入口点方法如图表示:
[图片上传失败...(image-db7715-1634558454333)]
给定两个单链表(链表自身无环),检测两个链表是否有交点,如果有返回第一个交点。
给定两个单链表(链表自身无环),检测两个链表是否有交点,如果有返回第一个交点。
检测两个单链表是否有交点是很容易的,因为如果两个链表有交点,那么这两个链表的最后一个节点必须想同,所以只需要比较最后一个节点即可。但是这种方案是无法求出第一个交点的,所以需要另辟蹊径。另外,判断是否有交点可以转换成链表是否有环的问题。让一个链表的最后一个节点指向第二个链表的头结点,这样的话,如果相交,则新的链表是存在环的,并且交点便士环的入口点。
给定两个单链表(不确定是否带环),判断是否有交点
先判断两个链表是否带环:
1.如果两个都不带环,可以用:判断两个无环单链表是否有交点。
2.连兵哥哥都带环:找到两个入环点r1,r2,环1的入环点r1,从r1开始遍历一圈,每个节点如r2比较
3.一个带环一个不带环,则肯定不想交。
给定单链表头结点,删除链表中倒数第k个结点
这个问题的关键是需要获取倒数第k个节点。如何获取呢?这里,需要两个指针p和q,p指向头结点,q指向距离头结点为k的节点。这样p和q每次遍历一个节点,当q遍历到末尾的时候,p指向的节点即为倒数第k个节点,然后删除即可。
只给定单链表中某个结点p(并非最后一个节点,即p->next!=NULL)指针,删除该结点:
只给定单链表中某个结点p(非空结点),在p前面插入一个结点。
上诉两种方法的原理都是一样的。
3.UITable View的相关优化
1.基础的优化准则(高度缓存,cell重用...)
滚动很快时,只加载目标范围内的Cell,这样按需加载,极大的提高流畅度。提前计算并缓存好高度(布局),因为heightForRow AtIndexPath:是调用最频繁的方法。异步绘制,遇到复杂界面,遇到性能瓶颈时,可能就是突破口。滑动时按需加载,这个在大量图片展示,网络加载的时候很管用!(SDWebImage已经实现异步加载,配合这条性能杠杠的)。
除了上面最主要的三个方面外,还有很多几乎大伙都很熟知的优化点:
1.正确使用reuseIdentifier来重用Cells 2.尽量使所有的view opaque,包括Cell自身 3.尽量少用或不用透明图层
4.如果Cell内现实的内容来自web,使用异步加载,缓存请求结果 5.减少subviews的数量 在heightForRow AtIndexPath:如果你需要用到它,致用一次然后缓存结果 6.尽量少用addView给Cell动态添加View,可以初始化时就添加,然后通过hide来控制是否显
4.KVO、Notification、delegate各自的优缺点,效率还有使用场景
在开发ios应用的时候,我们经常遇到一个常见的问题:在不过分耦合的前提下,controllers间怎么进行通信。在IOS应用不断的出现三种模式来实现这种通信:
1.委托delegation;
2.通知中心Notification Center;
3.键值观察key value observing, KVO
delegate的优势:
1.很严格的语法,所有能响应的时间必须在协议中有清晰的定义
2.因为有严格的语法,所以编译器能帮你检查是否实现了所有应该实现的方法,不容易遗忘和出错
3.使用delegate的时候,逻辑很清楚,控制流程可跟踪和识别
4.在一个controller中可以定义多个协议,每个协议有不同的delegate
5.没有第三方要求保持/监视通信过程,所以加入出了问题,那我们可以比较方便的定位错误代码
6.能够接受调用的协议方法的返回值,以为这delegate能够提供反馈信息给controller
delegate的缺点:
需要写的代码比较多
有一个“Notification Center”的概念,他是一个单例对象,允许当事件发生的时候通知一些对象,满足控制器与一个人一的对象进行通信的目的,这种模式的基本特征就是接收到在在该controller中发生某件事件而产生的消息,controller用一个key(通知名称),这样对于controller是匿名的,其他的使用同样的key来注册了该通知的对象能对通知的事件作出反应。
notification的优势
1.不需要写多少代码,实现比较简单
2.一个对象发出的通知,多个对象进行反应,一对多的方式实现很简单
缺点:
1.编译期不会再接茬通知是否被正确处理
2.释放注册的对象时候,需要再通知中心取消注册
3.调试的时候,程序的工作以及控制控制流程难跟踪
4.需要第三方来管理controller和观察者的联系
5.controller和观察者需要提前知道通知名称,UserInfo dictionary keys。如果这些没有在工作区间定义,那么会出现不同步的情况
6.通知发出后,发出通知的对象不能从观察者获得任何反馈。
KVO
KVO十一对象能观察另一个对象属性的值,前两种模式更适合一个controller和其他的对象进行通信,而KVO适合任何对象监听另一个对象的改变,这是一个对象与另外一个对象保持同步的一种方法。KVO只能对属性作出反应,不会用来对方法或者动作作出反应。
优点:
1.提供一个简单地方法来实现两个对象的同步
2.能对非我们创建的对象作出反应
3.能够提供观察的属性的最新值和先前值
4.用keypaths来观察属性,因此也可以观察嵌套对象
缺点:
1.观察的属性必须使用string来定义,因此编译器不会出现警告和检查
2.对属性的重构将导致观察不可用
3.复杂的“if”语句要求对象正在观察多个值,这是因为所有的观察都通过一个方法来指向KVO有显著的使用场景,当你希望监视一个属性的时候,我们选用KVO
而notification和delegate有比较相似的用处
当处理属性层的消息的事件时候,使用KVO,其他的尽量使用delegate,除非代码需要处理的东西确实很简单,那么用通知很方便。
5.如何手动通知KVO
重写Controller里面某个属性的setter方法,联动给View赋值,使用Controller监控Model里面某个值的变化,在controller的dealloc函数中用一行代码了结:removeObserver。
6.Objective-中的copy方法
对象的复制就是复制一个对象作为副本,他会开辟一块新的内存(堆内存)来存储副本对象,就像复制文件一样,即源对象和副本对象是两块不同的内存区域。对象要具备复制功能,必须实现<NSCopying>协议或者,<NSMutable Copying>协议,常用的可复制对象有:NSNumber、NSString、NSMutableString、NSNAarray、NSMutableArray、NSDictionary、NSMutableDictionary
copy:产生对象的副本是不可变的
mutableCopy:产生的对象副本是可变的
浅拷贝和深拷贝
浅拷贝值复制对象本身,对象里的属性、包含的对象不做复制
深拷贝则即复制对象本身,对像的属性也会复制一份
Foundation中支持复制的类,默认是浅复制
对象的自定义拷贝
对象拥有复制特性,必须NSCopying,NSMutableCopying协议,实现该协议的CopyWithZone:方法或MUtableCopyWithZone:方法。
浅拷贝实现
[图片上传失败...(image-b46fbf-1634558454333)]
[图片上传失败...(image-309d4d-1634558454333)]
7.runtime中,SEL和IMP的区别
方法名SEL-表示该方法的名称;
一个typs-表示该方法参数的类型;
一个IMP-指向该方法的具体实现的函数指针,说白了IMP就是实现方法。
8.autoreleasepool的使用场景和原理
[图片上传失败...(image-cc2c20-1634558454333)]
autoreleased对象什么时候释放
[图片上传失败...(image-7b0457-1634558454333)]
[图片上传失败...(image-59ad37-1634558454333)]
[图片上传失败...(image-507b2c-1634558454333)]
思考得怎么样了?相信在你心中已经有答案了。那么让我们一起来看看console输出:
[图片上传失败...(image-8da98d-1634558454333)]
跟你预想的结果有出入吗?Any way,我们一起来分析下为什么会得到这样的结果。
分析:3种场景下,我们都通过【NSString stringWithFormat:@“leichunfeng”】创建了一个autoreleased对象,这是我们实验的前提。并且,为了能够在view WillAPPear和viewDidAppear中继续访问这个对象,我们使用了一个全局的weak变量string_weak来指向它。因为_weak变量有一个特性就是它不会影响所指向对象的生命周期,这里我们正利用了这个特性。
场景1:当使用【NSStringstringWithFormat:“leichunfeng”】创建一个对象时,这个对象的引用计数为1,并且这个对象被系统自动添加到了当前的autoreleasepool中。当使用局部变量string指向这个对象时,这个对象的引用计数+1,变成了2.因为在ARC下NSStringstring本质上就是_strong NSStringstring。所以在viewDidLoad方法返回前,这个对象是一直存在的,且引用计数为2.而当viewDidLoad方法时,局部变量string被回收,指向了nil。因此,其所指向对象的引用数为-1,变成了1.
而在viewWillAppear方法中,我们仍然可以打印出这个对象的值,说明这个对象并没有被释放。咦。这不科学吧?我读书少,你表骗我。不是一直都说当函数返回的时候,函数内部产生对象就会被释放的吗?如果你这样想的话,那我只能说:骚年你太年轻了。开个玩笑,我们继续。前面我们提到了,这个对象是一个autoreleased对象,autoreleased对象是被添加到了当前最近的autoreleasepool中的,只有当这个autoreleasepool自身drain的时候,autoreleasepool中的autoreleased对象才会被release。
另外,我们注意到当在viewDidAppear中在打印这个对象的时候,对象的值变成了nil,说明此时对象已经被释放了。因此,我们可以大胆地猜测一下,这个对象一定是在viewWillAppear和viewDidAppear方法之间的某个时候被释放了,并且是由于它所在的autoreleasepool被drain的时候释放了。
你说什么就是什么咯?有本事你就证明给我看你妈是你妈。额,这个我真证明不了,不过上面的猜测我还是可以证明的,不信,你看!
在开始前,我先简单的说明一下原理,我们可以通过使用llad的watchpoint命令来设置观察点,观察全局变量string_weak_的值的变化,string_weak_变量保存的就是我们创建的autoreleased对象的地址。在这里,我们再次利用了_weak变量的另外一个特性,就是当它所指向的对象被释放时,_weak变量的值会被置为nil。了解了基本原理后,我们开始验证上面的猜测。
我们现在第35行打一个断点,当程序员运行到这个断店时,我们通过lldb命令wathchpoint set vstring weak设置观察点,观察string_weak_变量1的值的变化,如下图所示,我们将在console中看到类似的输出,说明我们已经成功的设置了一个观察点:
[图片上传失败...(image-a91078-1634558454333)]
设置好观察点后,点击Continue program execuyion按钮,继续运行程序,我们将看到如下图所示的界面:
[图片上传失败...(image-18bd68-1634558454333)]
我们先看console中的输出,注意到string_weak_变量的值由0x000079b886567d0变成了0x0000000000000000,也就是nil。说明此时它所指向的对象被释放了。另外,我们也可以注意到一个细节,那就是console中打印了两次对象的值,说明此时viewWillAppear也已经被调用了,而viewDidAppear还没有被调用。
接着,我们来看看左侧的线程堆栈。我们看到了一个非常敏感的方法调用-【NSAutoreleasePool release】,这个方法最终通过调用AutoreleasePoolPage::pop(void*)函数来负责对autoreleasepool中的autoreleased对象执行release操作。结合前面的分析,我们知道在viewDidLoad中创建的autoreleased对象在方法返回后引用计数为1,所以经过这里的release操作后,这个对象的引用计数-1,变成了0,该autoreleased对象最终被释放,猜测得正。
另外,值得一提的是,我们在代码中并没有手动添加autoreleasepool,那这个autoreleasepool究竟是哪里来的呢?看完后面的章节你就明白了。
场景2:同理,当通过【NSString stringWithFormat:“leichunfeng”】创建一个对象,这个对象的引用计数为1.而当使用局部变量string指向这个对象时,这个对象的引用计数+1,变成了2.而出了当前作用域时,局部变量string变成了nil,所以其所指向对象的引用计数变成1.另外,我们知道当出了@autoreleasepool{}的作用域时,当前autoreleasepool被drain,其中的autoreleased对象被release。所以这个对象的引用计数变成了0,对象最终被释放。
场景3:同理,当出了@autoreleasepool{}的作用域时,其中的autoreleased对象被release,对象的引用计数变成1.当出了局部变量string的作用域,即viewDidLoad方法返回时,string指向了nil,其所指向对象的引用计数变成0,对象最终被释放。
理解在这3种场景下,autoreleased对象什么时候释放对我们理解OBjective-C的内存管理机制非常有帮助。其中,场景1出现得最多,就是不需要我们手动添加@autoreleasepool{}的情况,直接使用系统维护的autoreleasepool;场景2就是需要我们手动添加@autoreleasepool{}的情况,手动干预autoreleased对象的释放时机;场景3是为了区别场景2而引入的,在这种场景下并不能达到出了@autoreleasepool{}的作用域时autoreleased对象被释放的目的。
PS:请读者参考场景1的分析过程,使用lldb命令watchpoint自行验证下在场景2和场景3下autoreleased对象的释放时机,you should give it a try yourself。
AutoreleasePoolPage
细心的读者应该已经有所察觉,我们在上面已经提到了-【NSAutoreleasePool release】方法最忠实通过调用AutoreleasePoolPage::pop(void*)函数来负责对autoreleasepool中的autoreleased对象执行release操作的。
- 那这里的AutoreleasePoolPage是什么东西呢?其实,autoreleasePool是没有单独的内存结构的,它是通过以AutoreleasePoolPage为结点的双向链表来实现的。我们打开runtime的源码工程,在NSobject。mm文件的第438-932行可以找到autoreleasepool的实现源码。通过阅读源码,我们就可以知道:
- 每一个线程的autoreleasepool其实就是一个指针的堆栈;
- 每一个指针代表一个需要release的对象挥着POOL_SENTINEL(哨兵对象,代表一个autoreleasepool的边界)
- 一个pool token就是这个pool所对应的POOL_SENTINEL的内存地址。当这个pool被pop的时候,所有内存地址在pool token之后的对象都会被release
- 这个堆栈被划分成了一个以page为结点的双向链表。pages会在必要的时候动态地增加或删除;
- Thread-local strage(线程局部存储)指向hot page,即最新添加的autoreleased对象所在的那个page。
一个空的AutoreleasePoolPage的内存结构如下图所示:
[图片上传失败...(image-26c4cd-1634558454333)]
[图片上传失败...(image-eb8531-1634558454333)]
Autorelease Pool Blocks
我们使用clang-rewrite-objc命令将下面Objective-C代码重写成C++代码:
[图片上传失败...(image-6b7b75-1634558454333)]
[图片上传失败...(image-9285ae-1634558454333)]
[图片上传失败...(image-56e4bb-1634558454333)]
[图片上传失败...(image-3aec1-1634558454333)]
因此,单个autoreleasepool的运行过程可以简单地理解为objc_autoreleasePoolPush()、{对象autorelease}和objc_autoreleasePoolPop(void*)三个过程。
push操作
上面提到的objc_autoreleasePoolPush()函数本质上就是调用的AutoreleasePoolPage的Push函数。
[图片上传失败...(image-e4117f-1634558454333)]
因此,我们接下来看看AutoreleasePoolPage的push函数的作用和执行过程。一个push操作其实就是创建一个新的autoreleasepool,对应AutoreleasePoolPage的具体实现就是往AutoreleasePoolPage中的nect位置插入一个POOL_SENTINEL,并且返回插入的POOL_SENTINEL的内存地址。这个地址也就是我们前面提到的pooltoken,在执行pop操作的时候作为函数的入参。
[图片上传失败...(image-e4c79-1634558454333)]
[图片上传失败...(image-d15b00-1634558454333)]
autoreleaseFast函数在执行一个具体的插入操作时,分别对三种情况进行了不同处理:
[图片上传失败...(image-a8977-1634558454333)]
autorelease操作
[图片上传失败...(image-447ceb-1634558454333)]
[图片上传失败...(image-aac909-1634558454333)]
[图片上传失败...(image-c510d6-1634558454333)]
pop操作
同理,前面提到的objc_autoreleasePoolpop(void*)函数本质上也是调用的AutoreleasePoolPage的pop函数。
[图片上传失败...(image-887453-1634558454333)]
下面是某个线程的autoreleasepool堆栈的内存结构图,在这个autoreleasepool堆栈中总共有两个POOL_SENTINEL,即有两个autoreleasepool。该堆栈由三个AutoreleasePoolPage结点为coldpage(),最后一个AutoreleasePoolPage结点为hotpage()。其中,前两个结点已经满了,最后一个结点中保存了最新添加的autoreleased对象objr3的内存地址。
[图片上传失败...(image-d0f493-1634558454333)]
[图片上传失败...(image-f27655-1634558454333)]
NSThread、NSRunLoop和NSAutoreleasePool
根据苹果官方文档中对NSRunLoop的描述,我们可以知道每一个线程,包括主线程,都会拥有一个专属的NSRunLoop对象,并且会在有需要的时候自动创建。
Each NSThread objct,including the application’s main thread,has an NSRunLoop object automatically created for it as needed。
同样的,根据苹果官方文档中对NSAutoreleasePool的描述,我们可知,在主线程的NSRunLoop对象(在系统级别的其他线程中应该也是如此,比如通过dispatch_get_global_queue)(DISPATCH_queue_PRIORITY_DEFAULT,0)获取到的线程)的每个event loop开始前,系统会自动创建一个autoreleasepool,并在event loop结束时drain。我们上面提到的场景1中创建的autoreleased对象就是被系统添加到了这个自动创建的autoreleasepool中,并在这个autoreleasepool被drain时得到释放。
The Application Kit creates an autorelease pool on the main thread at the beginning of every cycle of the event loop,and drains it at the end,thereby releasing any autoreleased objects generated while processing an event。
另外NSAutoreleasePool中还提到,每一个线程都会维护自己的autoreleasepool堆栈。换句话说autoreleasepool是与线程紧密相关的,每一个sutoreleasepool只对应一个线程。
Each thread (including the main threde) maintains its own stack of NSAutoreleasePool objects。
弄清楚NSThread、 NSRunLoop和NSAutoreleasePool三者之间的关系可以帮助我们从整体上了解Objective-C的内存管理机制,清楚系统在背后到底为我们做了些什么,理解整个运行机制等。
总结
看到这里,相信你应该对Objective-C的内存管理机制有了更进一步的认识。通常情况下,我们是不需要手动添加autoreleasepool的,使用线程自动维护的autoreleasepool就好了。根据苹果官方文档中Using Autorelease Pool Blocks的描述,我们知道在下面三种情况下是需要手动添加autoreleasepool的:
- 如果你编写的程序不是基于UI框架的,比如说命令行工具;
- 如果你编写的循坏中创建了大量的临时对象;
- 如果你创建了一个辅助线程
今天就给大家分享到这里了,全是手打,打了一天了,手都有抽筋了,不妨点赞关注支持一下。
更多iOS资料请关注主页加入圈子获取哦!!!
网友评论