概述
- UI 视图
- OC语言相关的面试题
- Runtime
- 内存管理
- Block
- 多线程
- RunLoop
- 网络
- 设计模式
- 架构/框架
- 算法
- 第三方库
一、UI 视图
UI面试-
1、UITableView相关的面试题
- <1>、重用机制是什么?(以子目索引条为例)
答:重用机制也就是设置了一个缓存池,在没有使用过就进行创建,添加到缓存池,只要是可以看到的界面都会被添加到缓存池,假如说:第一次有四个在当前界面展示,那么缓存池里面就是4个,如果突然变为8个,那么机会有4个被创建,4个被重用,如果再有新的,多出8个的,还会被创建,这就是缓存机制。 - <2>、数据源同步问题
-
方案:并发访问,数据拷贝
并发访问,数据拷贝 -
方案二:串行访问
串行访问
-
- <1>、重用机制是什么?(以子目索引条为例)
-
1.2、UI事件传递&响应
-
1>、事件传递的流程
事件传递
事件传递的流程// 最终哪个视图响应这个事件 - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { return nil; } // 判断某一个点击的位置是否在当前试图范围内,在的话就返回 YES, 反之返回NO - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event { return YES; }
在事件响应时候是倒序进行传递的,如果找到了就不会再向下传递,如果找不到就会把UIWindow作为返回,下面的demo以一个圆形为demo测试
-
2>、视图响应链
视图响应链
-
-
1.3、图像显示原理
图像显示原理
图像显示原理- CPU工作:Layout :UI布局和文本计算;Display:绘制;Prepare:图片编解码;Commit:提交位图;这就构成了UI视图在展示过程中CPU的全部工作
-
GPU渲染管线
GPU渲染管线
-
1.4、UI卡顿、掉帧的原因
UI卡顿、掉帧的原因
- 滑动优化方案(CPU)
- 对象的创建、调整、销毁 放到子线程去做
- 预排版(布局计算、文本计算放到子线程去做),这样主线程就会有很多的时间相应用户的交互
- 预渲染(文本等异步绘制,图片编解码等)
- 滑动优化方案(GPU)
- 纹理渲染(避免离屏渲染)
- 视图混合(减轻视图的层级以及复杂度)
- 滑动优化方案(CPU)
-
1.5、UI绘制原理&异步绘制 相关面试问题
- 1>、UI绘制原理
UI绘制原理
答:当UIView调用setNeedsDisplay
方法并没有立马调用绘制工作,而是在之后的某一时机进行UI的绘制工作-
系统的绘制流程
系统的绘制流程
-
- 1>、UI绘制原理
-
1.6、使UITableView滚动更流畅的方案或思路都有哪些?
答:这道题相对来说是比较高级的,涉及到性能优化的方面,我们可以用CPU于GPU两方面来回答这个问题,对于CPU可以采用在子线程对于对象的创建、调整、销毁或者进行预排版以及图片的解码,包括采用异步绘制方案,这些都是源于卡顿、掉帧的分析方面 -
1.7、离屏渲染
-
On-Screen Rendering 意为当前屏幕渲染,指的是GPU的渲染操作是在当前用于显示的屏幕缓冲区进行
-
Off-Screen Rendering 意为离屏渲染,指的是GPU在当前屏幕缓冲区以外新开辟一个缓冲区进行渲染操作
-
<1>、什么是离屏渲染?
答:离屏渲染的概念起源于GPU,在GPU层面上,如果说在当前屏幕缓冲区之外,新开辟一个缓冲区去进行渲染缓冲操作的话,就是离屏渲染。 -
<2>、何时会触发离屏渲染?
答:圆角的设置(当和maskToBounds一起使用时候)、设置图层蒙版、阴影、光栅化 -
<3>、为什么要避免离屏渲染?
答:当CPU和GPU进行具体的视图绘制渲染过程的当中做了大量的工作,而离屏渲染是发生在GPU层面上的,它是由于离屏渲染使GPU层面上触发了OpenGL上的多通道渲染管线,产生了额外的开销,所以我们需要避离屏渲染。
正规的回答:在触发离屏渲染的时候,会增加GPU的工作量,而增加GPU的工作量很有可能导致CPU与GPU工作耗时加起来的总耗时超出了16.7毫秒,那么就会导致UI的卡顿与掉帧。所以我们需要避免离屏渲染。
初级和中级的工程师回答:离屏渲染会创建新的缓冲区,那么会有内存上的开销,包括对于上下文的一个切换,那么就会有GPU的一个额外的开销
-
-
1.8、UIView与CALayer之间的关系是怎样的?(美团的面试题)
UIView与CALayer之间的关系
UIView与CALayer之间的关系答:UIView是专门处理事件传递与视图响应的,而CALayer 是负责UI视图的显示工作的,二者的关系用到了6大设计原则中的 单一职责原则 ,也就是二者区分工作的原因:单一职责原则
二、OC语言相关的面试题
OC语言相关的面试题-
2.1、怎么修改只读属性的变量?
答: 使用KVC,可以修改,KVC(Key-value coding)就是键值编码技术,可以通过制定的key获得想要的值value,而不是通过调用setter/getter方法访问 -
2.2、分类
-
面试题一、你用分类都做了哪些事情?
答:1、声明私有方法;2、分解体积较大的类文件,比如类比较复杂,根据功能划分;3、把 Framework的私有方法公开 -
面试题二、分类的特点是什么?
答:运行时决议,可以为系统类添加分类 -
面试题三、分类中都有可以添加哪些内容?
答:分类可以添加:实例方法、类方法、协议、属性(只声明了get和set方法,并没有为我们在分类中添加实例变量,分类可以通过关联对象技术
来添加实例变量) -
面试题四、分类的简单总结
答:(1)、分类添加的方法可以"覆盖"
原类方法(效果上是覆盖的, 实际上同名类的宿主方法还是存在的);(2)、同名分类方法谁能生效取决于编译顺序,最后被编译的分类,最优先被生效;(3)、名字相同的分类会引起编译报错,原因是是因为我们在生成具体分类的时候,经过Runtime或者编译的过程当中把我们添加的分类名称以下划线的方式拼接到宿主类上面,或者有其他更复杂的命名规则,总之名字相同和我们 定义了两个相同的同名变量含义相同 ,会引起编译报错。 -
面试题五、关联对象的本质
答:关联对象是由AssociationsManger
管理并在AssociationsHashMap
存储。所有对象的关联内容都在同一个全局容器
中。 -
面试题六、怎么清除一个关联对象所对应关联的值呢?
答:通过 setobject value 传为nil,就可以把对应 key 的值擦除
-
面试题一、你用分类都做了哪些事情?
-
2.3、扩展(Extension)
-
面试题一:一般用扩展作什么?
答:(1)、声明私有属性,这个私有属性是可以不对子类暴露的;(2)、声明私有方法,在扩展里面申明私有方法其实也就是为了方便阅读;(3)、声明私有成员变量 -
面试题二:分类和扩展的区别是什么?
答:我们可以从下面三点来回答- 第一:决议时刻:分类在运行时决议,扩展在编译时决议
- 第二:分类可以有声明有实现,扩展只以声明的形式存在,多数情况下寄生于宿主类的
.m
中 - 第三:可以为系统类添加分类,不可以为系统类添加扩展
-
面试题一:一般用扩展作什么?
-
2.4、代理(Delegate)
-
面试题一:代理是什么?
答:准确的说代理是一种软件设计模式;在iOS当中以@protocol
形式体现;传递方式一对一
-
面试题二:代理和通知的区别是什么?
答:最大的区别是:代理是一对一的传递,通知是一对多的传递
-
面试题一:代理是什么?
-
2.5、通知(NSNotification)
-
面试题一:什么是通知?
答:通知是使用观察者模式
来实现的用于跨层传递消息
的机制。传递方式是一对多
的 -
面试题二:代理和通知的区别是什么?
答:(1)、设计模式上的区别:代理是通过代理模式来实现的,通知是通过观察者模式来实现的;(2)、传递方式:代理是一对一,通知是一对多
-
面试题一:什么是通知?
-
2.6、KVO (Key - value observing)
-
面试题一:什么是KVO?
答:KVO 是 Objective-C 对观察者设计模式
的又一实现;Apple 使用了 isa 混写技术(isa-swizzling
) 来实现 KVO。 -
面试题二:KVO是如何通过 isa 混写技术来实现KVO的机制的?
答:系统在运行时,利用RuntimeAPI动态生成一个子类,并且让instance对象的isa指向这个全新的子类,同时重写 setter方法来实现 KVO 的机制的 -
面试题三:kvo 除了能观察属性外,能不能观察对象?
答:不能观察对象, 它提供一种机制,当指定的对象属性被修改后,则对象就会接受到通知,这是一个对象与另外一个对象保持同步的一种方法,即当一种对象的状态发生改变时,观察对象马上作出反应,他只能用来对属性作出反应
-
面试题一:什么是KVO?
-
2.7、main.m中都做了什么?
答:程序的入口;创建主线程的自动释放池;创建应用程序对象;并启动其主循环;运行应用程序;释放程序运行期间得所有加入自动释放池的对象;指定管理应用程序生命周期的代理 -
2.8、UIViewController的声明周期
答:当一个视图控制器被创建,并在屏幕上显示的时候。 代码的执行顺序- alloc 创建对象,分配空间。
- init (initWithNibName) 初始化对象,初始化数据。
- loadView 载入视图, 这一步不需要去干涉。除非没有使用试图为nil。
- viewDidLoad 加载完毕,可以进行自定义数据以及动态创建其他控件
当使用到self.view且self.view为nil调用 - viewWillAppear 视图将出现在屏幕之前,马上这个视图即将显示在屏幕上。
- viewDidAppear 视图已在屏幕上渲染完成。
- viewWillDisappear 视图将被从屏幕上移除之前执行。
- viewDidDisappear 视图已经被从屏幕上移除。
- dealloc 视图被销毁。
-
2.9、如果区分深拷贝和浅拷贝?
答:(1)、是否开辟了新的内存空间(深拷贝开辟了新的内存空间,内容相同;浅拷贝没有开辟新的内存空间,仅仅是拷贝了一份内存地址,指向同一块内存地址);(2)、是否影响了引用计数(深拷贝不会影响引用计数,浅拷贝会增加引用计数) -
2.10、copy 的使用
- 可变对象的 copy 和 mutableCopy 都是深拷贝
- 不可变对象的copy是浅拷贝,mutableCopy 是深拷贝
- copy方法返回的都是不可变对象
-
2.11、
@property (nonatomic, copy) NSMutableArray *array;
会产生什么样的后果?
答:如果赋值过来的是 NSMutableArray,copy之后是NSArray;如果赋值过来的是NSArray,copy之后是 NSArray。我们可以看到上述问题是 NSMutableArray,那么使用 copy 之后就会成为不可变NSArray,而且是深拷贝,如果我们再去调用 NSMutableArray 的插入,删除等等操作就会产生 crash -
2.12、MRC下如何重写retain修饰变量的setter方法?
答:如下代码@property (nonatomic, retain) id obj; - (void)setObj:(id)obj { if (_obj != obj) { [_obj release]; _obj = [obj retain]; } }
提示:
if (_obj != obj) { }
判断的原因是:如果传递进来的obj对象恰巧是原来的对象的话,先对原来的对象进行 release 操作 ,也是对传递进来的对象进行 release 操作,那么很有可能 obj 对象被我们无辜的释放掉了,那么这个时候我们再通过 obj 指针访问一个废弃的对象就会造成程序异常 crash,所以说这里进行一个不等的判断是为了对异常的处理
三、Runtime 面试题
-
3.1、消息传递的过程
消息传递的过程
- 根据一个方法先去缓存类里面寻找是否有对应方法名称的实现,如果找到了直接利用函数指针直接调用函数,完成本次的查找。
- 如果缓存里面没有的话就去该对象的类对象的方法列表里面去查找是否有该方法,如果找到了直接利用函数指针直接调用函数,完成本次的查找。
- 如果当前的类对象的方法列表里面没有改方法的话,那么就去需要通过 superClass 逐级查找其父类的类方法里面是否有该方法,如果找到了直接利用函数指针直接调用函数,完成本次的查找。
- 如果逐级找都找不到,最后是nil,那么就进入消息的转发流程
-
3.2、消息转发的流程
消息转发的流程
-
3.3、动态解析
动态解析
-
3.4、面试题一
[obj foo] 和 objc_msgSend()函数之间有什么关系?
答: [obj foo] 在编译器的处理之后就变成了 objc_msgSend()函数 -
3.5、面试题二
runtime如何通过Selector找到对应的 IMP 地址的?
答:回答是上面的消息传递 -
3.6、面试题三
- 能否向
编译后的类
中增加实例变量?
答:不可以, 编译后的类我们是不能为其增加实例变量的 - 能否向
动态添加的类
中增加实例变量?
答:可以,因为我们在动态添加这个类的过程当中,只要在调用它的那个注册方法之前去完成实例变量的添加
- 能否向
四、内存管理
-
4.1、内存布局
- stack(栈区): 方法的调用
- heap(堆区):通过 allloc 等分配的对象
- bss:未初始化的全局变量和静态变量等
- data:已经初始化的全局变量等
- text:程序的代码,加载到内存中的
-
4.2、内存管理方案
答:iOS 针对不同场景有不同的内存管理方案,如下三个方面- TaggedPointer:比如一些小对象NSNumber,我们采用 TaggedPointer 这种内存管理方案
- NONPOINTER_ISA:对于 arm64 位架构下的iOS应用程序采用的 NONPOINTER_ISA 这种内存管理方案,解释:在64位下,NONPOINTER_ISA 的 ISA其实是占 64 个bit位的,实际上有32位或者40位就够用了,剩余的bit位是浪费的,苹果为了提高内存的利用率,苹果在剩余的bit位中存储了一些内存管理方面的数据内容,所以这种叫非指针型的ISA
-
散列表:散列表其实是一个很复杂的数据结构,其中包括了
弱引用表
和引用计数表
- 扩展:散列表其实是多张 SideTables,目的是为了提高查找的效率
-
4.3、散列表内存管理了方案涉及到的数据结构
- Spinlock_t 自旋锁:是一种
忙等
的锁,所谓忙等指的是如果当前锁已经被其他线程获取,那么当前的线程会不断的探索当前锁是否被释放,如果释放掉自己就第一时间去获取这个锁。自旋锁适用于轻量的访问,比如SideTable表的引用计数的+1和-1 的操作 - RefcountMap 引用计数表:其实是一个哈希表,通过指针找到一个对象的引用计数,这个过程也是一个哈希查找。引用计数表是通过哈希表来来实现的,那么引用计数表为什么要通过哈希表来实现呢?答:实际上是为了提高查找的效率。提高查找效率的本质原因是:插入和获取是通过同一个哈希算法或者哈希函数来决定的,那么就避免了for循环遍历
- weak_table_t 弱引用表:也是一张哈希表
- Spinlock_t 自旋锁:是一种
-
4.4、MRC 和 ARC 的面试问题
-
MRC:是通过手动引用计数来进行对象的内存管理,下面标红的是 MRC 特有的方法,ARC 调用的话会引起编译报错
- alloc 分配内存空间
-
retain
:引用计数+1 -
release
:因用计数 -1 -
retainCount
:获取当前对象的引用计数值 -
autorelease
:如果我们调用了一个对象的引用计数方法的话,那么当前这个对象会在 autorelease pool 计数的时候,调用它的release操作进行引用计数 -1 - dealloc: 在MRC我们调用 dealloc的话,需要显示调用 super dealloc 来显示或者释放父类的 相关成员变量
-
ARC:是利用
自动引用计数
来管理内存- 编译器在自动为我们插入 retain 和 release 操作之外,其实 ARC 是
LLVM
和Runtime
协作的结果; - ARC 禁止手动调用
retain
、release
、retainCount
、dealloc
- ARC 中新增
weak
、strong
属性关键字
- 编译器在自动为我们插入 retain 和 release 操作之外,其实 ARC 是
-
MRC 和 ARC 区别是哪些 ?
答:从各自的特点入手- 比如:MRC是手动管理内存,ARC 是通过 Runtime 和 LLVM 共同协作来自动引用计数的管理,MRC可以调用引用计数的相关方法,ARC是不能调用的,比如
retain
、release
、retainCount
、dealloc
- 比如:MRC是手动管理内存,ARC 是通过 Runtime 和 LLVM 共同协作来自动引用计数的管理,MRC可以调用引用计数的相关方法,ARC是不能调用的,比如
-
-
4.5、引用计数管理
alloc
、retain
、release
、retainCount
、dealloc
实现原理分析- alloc:经过一些列的函数封装和调用,最终调用了 C 函数的 calloc。此时的引用计数并没有设置为 1,但是我们通过 retainCount获取引用计数却是 1
- retain:我们在进行retain操作的时候,系统是如何查找它对应的引用计数的?答:是通过两次哈希查找来找到它对应的引用计数值,然后进行相应的
+1
操作 - release:通过两次哈希查找来找到它对应的引用计数值,然后进行相应的 、
-1
操作 - retainCount:
-
4.6、系统是怎样把一个 weak 变量添加到它对一个的弱引用表当中的?
系统是怎样把一个 weak 变量添加到它对一个的弱引用表当中的
答:一个被声明为
__weak
的对象指针,经过编译器的编译之后被调用相应的objc_initWeak()
,然后经过一些列的函数调用栈,最终在weak_register_no_lock()
函数当中,进行一个弱引用变量的添加,那么决堤添加的位置是通过一个哈希算法来进行位置 查找的,如果说我们查找的对应位置当中,已经有了当前对象对应的弱引用数组 ,我们就把新的弱引用变量添加到数组当中 ,如果没有的话我们重新创建一个弱引用数组,然后在第0
个位置 添加上我们最新的弱引用指针,后面的都初始化为0
或者为nil
。 -
4.7、一个对象被废弃或者释放之后,weak 对象是怎么处理的呢?
答:清除 weak 变量,同时设置指向为 nil,总结:当一个对象被 dealloc 之后,那么在 dealloc的内部会调用它的弱引用清除的相关函数,然后在这个函数的内部实现当中根据当前对象指针查找弱引用表,把当前对象相对应的 弱引用都拿出来,是一个数组,那么遍历这个数组当中所有的弱引用指针,分别置为 nil -
4.8、自动释放池
自动释放池
-
面试题一:什么是自动释放池或者自动释放池的一个实现结构是怎样的?
答: 自动释放池是以栈
为节点通过双向链表
组合而成的,并且和线程
是一一对应的 -
面试题二:请问该对象 array 是在什么时候被释放的呢?(今日头条的面试题)
- (void)viewDidLoad { [super viewDidLoad];\ NSMutableArray *array = [NSMutableArray array]; NSLog(@"%@",array); }
答:在当次 runloop将要结束的时候调用 AutorereleasePoolPage 的 pop() 方法的时候把对应的array对象调用它的release函数或者方法,然后对它进行释放
-
面试题三:
AutorereleasePool
的实现原理是怎样的?
答:AutorereleasePool` 的实现原理就是以栈为节点,通过双向链表形式组合而成的一个数据结构。 -
面试题四:
AutorereleasePool
为何可以嵌套使用呢?
答:多次嵌套其实就是插入哨兵对象 -
面试题五:AutorereleasePool使用的一个场景?
答:在 for 循环中 alloc 图片数据等内存消耗较大的场景手动插入 AutorereleasePool。每次 for 循环都进行内存的释放来降低内存的峰值,防止一些内存消耗过大导致的问题
-
-
4.9、循环引用
-
循环引用的分类:自循环引用、相互循环引用、多循环引用
- 自循环引用:一个对象里面有一个成员变量
id __strong objc
,并且强持有它的成员变量,如果我么给 objc 赋值为 原对象的话,那么久造成了一个自循环引用
自循环引用 -
相互循环引用:两个对象的变量之间进行相互强引用对方的对象
相互循环引用 -
多循环引用
多循环引用
- 自循环引用:一个对象里面有一个成员变量
-
面试题一:如何破除循环引用?
答:避免循环引用(其中一个变量使用weak)、在合适的时机手动断环 -
面试题二:破除循环引用具体实施的方案是什么?
答:以下三种方案-
__weak(代理和block我们都会使用到)
-
__block(一般使用在block产生的循环引用),在MRC和ARC下是有区别的
- 在MRC下,__block修饰对象不会增加其引用计数,避免了循环引用。
- 在ARC下,__block修饰的对象会被强引用,无法避免循环引用,需手动解环。
-
__unsafe_unretained
- 修饰对象不会增加引用计数,避免了循环引用
- 如果修饰对象在某一时机被释放,会产生悬垂指针!,如果再通过这个指针访问原对象的话就会由于悬垂指针的原因导致内存泄漏,所以一般不建议使用 __unsafe_unretained 去解除循环引用,因为会发生后期不可预见的问题
-
-
-
4.10、什么是 ARC?
答:ARC 是由 LLVM 编译器 和 Runtime 共同协作为我们实现引用计数的管理 -
4.11、为什么weak指针指向的对象在废弃之后会被自动置为 nil ?
答:在对象被废弃之后,dealloc() 的释放方法的内部实现当中调用清除弱引用的一个方法,然后在清除弱引用的方法当中通过哈希算法来查找被废弃对象在弱引用表中的位置来提取它所对应的的弱引用指针对应列表的一个数组 ,然后进行 for 循环遍历,把每一个弱引用指针都置为 nil -
4.12、苹果是如何实现 AutoreleasePool 的?
答:AutoreleasePool 是以栈为节点,通过双向链表来组成的一种数据结构
五、Block
-
5.1、什么是 Block?
答:Block是将函数
及其执行上下文
封装起来的对象
。也可以说 Block 是一个OC对象
,封装了函数
和执行上下文
。提示:我们可以使用:
clang -rewrite-objc file.m
来查看对应编译之后的文件内容,如果报错fatal error: 'UIKit/UIKit.h' file not foundm #import <UIKit/UIKit.h>
,我,我们可以在 file.m 之前添加-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk
-
5.2、什么是 Block 调用?
答: Block 调用即是函数的调用
。 -
5.3、截获变量相关的面试题
-
查看下面的面试(滴滴出行的面试题)
int multiplier = 6; int(^JKBlock)(int) = ^int(int num) { return num * multiplier; }; multiplier = 2; NSLog(@"结果是 = %d",JKBlock(5));
局部变量截获的特点
- 局部变量:基本数据类型(对于
基本数据
类型的局部变量
截获其值) 和对象类型(对于对象
类型的局部变量连同所有权修饰符
一起截获) - 静态局部变量:以指针形式截获静态局部变量
- 静态全局变量:不截获
- 全局变量:不截获
- 局部变量:基本数据类型(对于
-
-
5.4、__block修饰符相关的面试题
- 面试题一:我们在什么情况下需要使用
__block
修饰符呢?
答:一般情况下,对被截获的变量进行赋值
操作需要添加__block修饰符
提示:
使用
不等于赋值
-
面试题坑一:下面我们是否需要对 array 进行
__block
修饰符呢?答:不需要,我们仅仅时使用 array,并不是对其赋值NSMutableArray *array = [NSMutableArray array]; void(^JKBlock)(void) = ^{ [array addObject:@"2"]; }; JKBlock();
-
面试题坑二:下面我们是否需要对 array 进行
__block
修饰符呢?答:需要,这里是对 array 赋值,不使用__block
修饰符 会报错__block NSMutableArray *array = nil; void(^JKBlock)(void) = ^{ array = [NSMutableArray array]; }; JKBlock();
提示:
栈
上的__block 变量
是指向它自身的 -
- 面试题一:我们在什么情况下需要使用
-
5.5、Block内存管理方面的面试题
-
面试题一:__forwarding存在的意义?
答:不论在任何内存位置,都可以顺利访问同一个__block变量,如果栈上的block进行了copy,那么栈上的 __forwarding 指向的是堆上的 block,堆上的__forwarding指向的是自身,无论改哪里的变量,__forwarding 指向的都是堆上的block -
面试题二:下面的代码有什么问题吗?
__block JKBlock *blockSelf = self; _blk = ^int(int num) { // var = 2; int result = num * blockSelf.var; return result; }; _blk(6);
答:上面的代码在MRC下是不会产生循环引用的,在ARC是会产生循环引用,引起内存泄漏。我们可以采用断环的方式来解决循环引用,如下代码,就是我们很长一段时间或者永远都不会调用这个block的话, 这个循环引用的环会一直存在。
__block JKBlock *blockSelf = self; _blk = ^int(int num) { // var = 2; int result = num * blockSelf.var; blockSelf = nil; return result; }; _blk(6);
-
面试题三:为什么 Block 会产生循环引用?
答:一方面:如果说当前Block对当前对象的某些成员变量截获的话,那么这个Block会对当前成员变量产生一个强引用,而当前Block又对当前对象有一个强引用,就产生了一个自循环引用的 一种循环引用问题。我们可以声明其为 __weak 变量来解除其循环引用。另外如果我们定义了 __block修饰也会产生循环引用,但是是区分场景的,MRC下是不会产生循环引用,在ARC下会产生循环引用,在ARC下我们可以通过断环的方式来解除循环引用,但是有一个弊端,如果这个Block一直得不到调用,循环引用是没办法解除的 。 -
面试题四:怎么理解 Block 截获变量的特性?(从截获变量类型分类来回答)
答:对于基本数据类型的局部变量,对其值进行截获,对于对象类型的局部变量,对其进行一个强引用,连同其所有权修饰符共同进行截获 ;而对于静态的局部变量 ,对其指针进行截获;对于全局变量和静态全局变量是不产生截获的
-
六、多线程
-
6.1、在iOS当中,为我们提供了哪几种多线程方案呢?
答:GCD、NSOperation、NSThread(常用于常驻线程)、pthread(c语言写的,一般不用) -
6.2、有关死锁的面试题
-
面试题一:同步串行,下面会产生死锁 这是头条的一个面试题,死锁的原因:由队列引起的循环等待,viewDidLoad 和 BLock 在同一个队列,这样就产生了队列的循环等待
- (void)viewDidLoad { dispatch_async(dispatch_get_main_queue(), ^{ [self dosomething]; }); }
-
面试题二:同步串行,自定义串行队列,下面代码不会产生死锁,因为 viewDidLoad 在主队列,Block在串行队列,主队列的代码执行过程中穿插串行队列的代码,执行完接着执行viewDidLoad主队列的代码,由于 viewDidLoad 和 Block 不存在队列等待,所以不会产生死锁
- (void)viewDidLoad { [super viewDidLoad]; // 自己创建的串行队列 dispatch_queue_t queue = dispatch_queue_create("com.jk.queue", DISPATCH_QUEUE_SERIAL); dispatch_sync(queue, ^{ [self dosomething]; }); }
-
面试题三:美团公司的面试题,下面的代码会依次执行:1、2、3、4、5 的打印
- (void)viewDidLoad { [super viewDidLoad]; NSLog(@"------1-------"); dispatch_sync(dispatch_get_global_queue(0, 0), ^{ NSLog(@"------2-------"); dispatch_sync(dispatch_get_global_queue(0, 0), ^{ NSLog(@"------3-------"); }); NSLog(@"------4-------"); }); NSLog(@"------5-------"); }
-
面试题四:异步串行,不会产生死锁
- (void)viewDidLoad { [super viewDidLoad]; dispatch_async(dispatch_get_main_queue(), ^{ [self dosomething]; }); }
-
面试题五:异步并发,腾讯公司的面试题,打印结果是 1、3
- (void)viewDidLoad { [super viewDidLoad]; dispatch_async(dispatch_get_global_queue(0, 0), ^{ NSLog(@"-------1-------"); [self performSelector:@selector(log) withObject:nil afterDelay:0]; NSLog(@"-------3-------"); }); } - (void)log { NSLog(@"------2----------"); }
解释:performSelector 的执行是要放到 RunLoop里面的,GCD 的dispatch_async 创建出来是没有 RunLoop的
performSelector:withObject:afterDelay:这句代码的本质是往Runloop中添加定时器
DCG子线程默认没有启动Runloop
可以添加下面的代码来启动RunLoop[[NSRunLoop currentRunLoop]addPort:[[NSPort alloc]init] forMode:NSDefaultRunLoopMode]; [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
-
-
6.3、怎么利用GCD实现多读单写呢?或者一个多读单写的模型你会如何实现?(滴滴和美团都考察过的一道面试题)
#import "UserCenter.h" @interface UserCenter() { // 定义一个并发队列 dispatch_queue_t concurrent_queue; // 用户数据中心,可能多个线程需要数据访问 NSMutableDictionary *userCenterDic; } @end // 多读单写模型 @implementation UserCenter - (instancetype)init { self = [super init]; if (self) { // 通过宏定义 DISPATCH_QUEUE_CONCURRENT 创建一个并发队列 concurrent_queue = dispatch_queue_create("com.jk.queue", DISPATCH_QUEUE_CONCURRENT); // 创建数据容器 userCenterDic = [NSMutableDictionary dictionary]; } return self; } - (id)objectForKey:(NSString *)key { __block id obj; // 同步读取指定数据 dispatch_sync(concurrent_queue, ^{ obj = [userCenterDic objectForKey:key]; }); return obj; } - (void)setObject:(id)obj forKey:(NSString *)key { // 异步栅栏调用设置数据 dispatch_barrier_sync(concurrent_queue, ^{ [userCenterDic setObject:obj forKey:key]; }); } @end
-
6.4、使用GCD实现这个需求,A、B、C 三个任务并发,完成后执行任务 D? (爱奇艺公司的面试真题)
- (void)viewDidLoad { [super viewDidLoad]; dispatch_queue_t queue = dispatch_queue_create("com.jk.queue", DISPATCH_QUEUE_CONCURRENT); dispatch_group_t group = dispatch_group_create(); dispatch_group_async(group, queue, ^{ for (int i = 0; i < 100; i++) { NSLog(@"---group1=== %d------", i); } }); dispatch_group_async(group, queue, ^{ for (int i = 0; i < 100; i++) { NSLog(@"---group2=== %d------", i); } }); dispatch_group_notify(group, dispatch_get_main_queue(), ^{ // 前面的事情做完,做后面的事情 NSLog(@"-----3----"); }); }
-
6.5、NSOperation 相关的面试题
- NSOperation的介绍:需要配合NSOperationQueue使用来实现多线程方案,它可以:添加依赖、控制执行任务的状态、最大并发量
- 面试题一:NSOperation都要有哪些任务执行的状态?
答:isReady
(当前的任务是否处于就绪状态)、isExecuting
(代表当前任务是否处于执行中的状态)、isFinshed
(当前任务是否执行完成)、isCancelled
(代表当前任务是否已取消) - 面试题二:我们要如何控制 NSOperation 的状态?
答:如果只重写main
方法,底层控制变更任务执行完成状态,以及任务退出(底层为我们控制状态,我们不需要负责)。如果只重写了start
方法,自行控制任务状态。 - 面试题三:系统是怎样移除一个
isFinshed=YES
的NSOperation
的呢?
答:系统是通过KVO的方式来移除 NSOperationQueue 当中所对应的 NSOperation 的 ,来达到可以销毁或者退出 NSOperation 这个对象
七、RunLoop
-
7.1、什么是RunLoop?
答:RunLoop是通过内部维护的事件循环
(没有消息需要处理时,休眠以避免资源占用,用户态->内核态;有消息处理时,立刻被唤醒,内核态->用户态 ) 来对事件/消息进行管理
的一个对象。 -
7.2、为什么
main()
函数可以保持一直运行的状态,而不退出呢?
答:在 main() 函数中所调用 UIApplicationMain 函数内部 会启动主线程的 RunLoop,而 RunLoop又是对事件循环的一种维护机制,可以做到在有事做的时候可以去做事,没有事情做的时候 可以通过用户态到内核态的一个切换。 从而避免资源的一个占用,然而当前的线程处于一个休眠的状态 -
7.3、RunLoop和mode以及Source、Timer、Observer是什么样的一个关系呢?
答:一对多的关系
Source0:需要手动唤醒线程,Source1:具备唤醒线程的能力
-
7.4、有没有使用过CommonMode , 如果用过的话,你对它是怎么理解的?
答:CommonMode 不是真实存在的一种mode,它是同步Source/Timer/Observer
到多个Mode中的一种技术方案。 -
7.5、当一个处于休眠状态的 RunLoop,我们可以通过哪些方式来唤醒它呢?
答:(1)、Source1来唤醒当前线程,(2)、Timer事件的回调到了,(3)、外部手动的唤醒。 -
7.6、RunLoop与NSTimer,滑动 TableView 的时候我们的定时器还会生效吗?
答:不会生效,这个时候mode
由kCFRunLoopDefaultMode
变成了UITrackingRunLoopMode
,我们可以把NSTimer添加到 NSRunLoopCommonModes 里面,如下代码NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:@selector(clickTimeIndexPath) userInfo:nil repeats:YES]; [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes]; /* UITrackingRunLoopMode和NSDefaultRunLoopMode是真正存在的模式 [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode]; [[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode]; NSRunLoopCommonModes:n并不是一个真的模式,它只是一个标记 timer 能在 _commandModes 数组中存放的模式下工作 [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes]; */
-
7.7、runloop的mode作用是什么?
答:runloop的mode 常用的有两种mode: NSDefaultRunLoopMode与UITrackingRunLoopMode,在某一种mode下执行自己的source0、source1、timer,不同mode下互不影响,分工明确,使用起来更灵活、流畅 -
7.8、线程与RunLoop之间是怎么样的关系?
答:线程 和 RunLoop 是一一对应的,自己创建的线程默认是没有 RunLoop的 -
7.9、怎样实现一个常驻线程呢?
答: 1>、为当前线程开启一个RunLoop;2>、向该RunLoop里面添加一个 Port/Source/Observer/Timer等维持 RunLoop 的事件循环;3>、启动该 RunLoop。我们要注意添加的mode和运行的mode要是同一个mode,否则的话有可能 外部使用的一个while循环导致一个死循环 -
7.10、RunLoop是如何做到有事做事,没事休息的?
答:使我们在调用 CFRunLoop的Run的相关方法之后,会调用系统的一个函数 mach_msg() ,从而产生了一个用户态到核心态的切换,然后当前线程处于休眠状态,从而做到 有事做事,没事休息
-
7.11、怎样保证子线程数据回来更新UI的时候不打断用户的滑动操作?
答:在用户进行滑动的过程当中,当前的RunLoop是运行在UITrackingRunLoopMode下的,而我们的网络请求一般是放在子线程当中进行的,而子线程返会给主线程的数据,我们要抛给主线程更新UI,这个时候我们可以把子线程抛回给主线程进行UI更新的这块逻辑可以给它包装起来。提交到主线程的NSDefaultRunLoopMode模式下,这样的话我们抛回来的任务当前用户滑动的过程当中处于UITrackingRunLoopMode模式下面,我们分配到NSDefaultRunLoopMode模式下面提交的任务就不会执行,而我们的手停止滑动之后,当前线程的mode就切换到了NSDefaultRunLoopMode模式下面了,这个时候会处理我们通过子线程上报给主线程的任务,然后进行 UI 的更新,就不会打断用户的滑动操作
八、网络
-
8.1、什么是HTTP?
答:首先回答 :请求报文和响应报文的结构组成
请求报文
响应报文 -
8.2、HTTP的请求方式都有哪些?
答:GET、POST、HEAD、PUT、DELETE、OPTIONS -
8.3、GET 和 POST 方式的区别 ?
答:分以下两种- 初级工程师的答案:1>、GET请求以 ?分割拼接到URL后面,POST请求参数在 boay 里面;2>、GET参数请求长度限制在 2048个字符,POST一般没有该限制;3>、GET请求不安全,POST 请求比较安全
- 高级工程师的答案:标准的答案应该从
语义的角度来回答
- GET:获取资源(安全的、幂等的、可缓存的)
- POST:处理资源 (不安全的、非幂等的、不可缓存的)
提示
- 安全性:不应该引起 Server 端的任何状态变化 (GET、HEAD、OPTIONS)
- 幂等的:同一个请求方法执行多次和执行一次的效果完全相同。(GET、PUT、DELETE)
- 缓存:请求是否可以被缓存(GET、HEAD)
-
8.4、你都了解哪些状态码,他们的含义是什么?
答:分以下五种- 1XX:
- 2XX:200(响应成功)
- 3XX:302(发生了网络的重定向)
- 4XX:401和404(客户端本身发起的请求存在某些问题)
- 5XX:502和501(server端本身是有异常的)
-
8.5、HTTP建立的流程是什么? (三次握手和四次挥手)
-
8.6、HTTP 都有哪些特点呢?
答:特点如下:- 1、无连接:HTTP连接具有一个建立连接和释放连接的过程
- 2、无状态:我们在多次发送 HTTP请求的时候,如果是同一个用户的话,对于Server来说是不知道同一个用户的,那么这就表达出了 HTTP 无状态的特点
-
8.7、Charles抓包原理是怎样的?
答:利用了HTTP请求中间人攻击的漏洞来进行实现的,HTTP中间人攻击具体指的是:客户端与服务端的连接中间多了一个中间人的中间枢纽
-
8.8、HTTPS 与 HTTP 之间有怎样的区别?
答:HTTPS = HTTP + SSL/TLS,也就是说 HTTPS比HTTP多了安全的模块,IP网络层,TCP传输层,HTTP应用层,SSL/TLS 是位于传输层之上,应用层之下的 一个中间层 ,这时我们可以说 HTTPS 是安全的 HTTP
-
8.9、HTTPS建立连接流程是怎样的?(美团的面试题)主要考察建立连接安全是怎么保障的
HTTPS建立连接流程
答:首先客户端向服务端发送一个报文,报文包含三部分:TLS版本号、支持的加密算法(可能多种加密算法,供服务端选择一个)、random number C,之后服务端会告诉客户端一个握手的消息,包含最终商定的加密算法、random number S、Server证书,客户端收到这些消息后首先会验证 server证书,来判定当前的server是否是一个合法的server, 其实也就是对server端证书的公钥进行验证,之后会组装会话秘钥
,组装会话秘钥实际上是通过随机数C、S、包括客户端产生的一个预主秘钥
来进行会话秘钥的组装;此时客户端会发送一个报文到服务端,通过server端的公钥对预主秘钥进行加密传输,之后server端再通过 私钥解密 得到对应的预主秘钥,之后在server端再通过随机数C和S,包括通过 私钥解密 得到对应的预主秘钥 这三个内容进行会话秘钥的组装合成;之后再由客户端向服务端发送一个加密的握手消息,然后再由服务端返回客户端一个加密的握手消息来验证过我们的安全通道是否已经正确建立完成 。拓展
- 会话秘钥:random number C + random number S + 预主秘钥,通过一定的算法合成起来就是 会话秘钥,会话秘钥代表的是对称加密的一种结果
-
8.10、HTTPS建立连接过程当中都是用了哪些加密手段?为什么?
答:连接过程使用非对称加密,非对称加密是很耗时的,但是可以保证安全,后续传输过程当中应该使用对称性加密,目的是为了减少耗时带来的性能损耗拓展:
-
非对称加密:
发送方的数据使用公钥加密,接收方使用私钥解密,加密和解密使用的钥匙是不一样的。 -
对称加密:
发送方对发送的内容使用对称秘钥
加密 ,加密结果通过TCP连接传输给接收方,之后通过同一个对称秘钥对解密的结果进行解密,最后拿到最终的明文数据,加密个解密使用的是同一把钥匙。缺点是:对称秘钥需要通过TCP连接进行传输,很可能发生中间人攻击,对秘钥进行一个劫持,所以对称加密不能保证一个绝对的安全,反之非对称加密是安全的,是要公钥在网络传输过程中进行传递的,而私钥是保留在可靠方或者 服务端,不在网络上发生传递,也就不涉及到劫持和暴露的可能,对于安全的保障来说,非对称加密比对称加密好很多的
-
-
8.11、TCP 和 UDP 相关的面试题
- 基本的介绍:TCP 和 UDP 都属于传输协,TCP:传输控制协议,UDP:用户数据报协议
- 面试题一:你是否了解 UDP 协议呢?
答:我们从两个方面入手:特点和功能- 特点:无连接(不需要提前做好连接)、尽最大努力交付(不保证可靠传输)、面向报文(既不合并,也不拆分)
- 功能:复用、分用、差错检测
- 面试题二:你对 TCP 是怎样理解的呢 ?
答:我们从两个方面入手:特点和功能- 特点:面向连接(数据传输开始之前,需要建立连接,比如三次握手;数据传输结束之后,需要释放连接,比如四次挥手)、可靠传输、面向字节流、流量控制、拥塞控制
- 功能:
-
8.12、HTTP 连接为什么要三次握手?
HTTP 连接为什么要三次握手
答:解决同步超时的场景,比如我们的客户端给服务端发送 syn同步报文超时了,客户端会再次发送超时超时重连的报文,在服务端收到之后,返回给 客户端一个syn的Ack报文,如果是两次握手,那么就结束了。但是这个时候超时的那个syn报文服务端收到了,那么它就会认为客户端想要再次建立连接,会给客户端发送 syn的Ack报文,那么就会有两次的连接了,如果有第三次握手的话就可以解决这个问题,比如如果刚才超时的那个报文客户端收到之后,就可以不发送第三次握手,那么服务端就会认为此连接无效,所以说还是要三次握手来解决这个超时报文的问题。
-
8.13、HTTP 断开连接为什么要四次挥手?
答:客户端发送 FIN 关闭报文,服务端回复确认报文,这时候客户端到服务端就关闭了,这称为半关闭状态。如果之后有数据的话,服务端是可以向客户端发送数据的,但是客户端是不可以发送数据的,之后在某个时机,服务端会发送一个FIN的ACK的终止报文断开由服务端到客户端的连接,客户端再回复一个ACK的确认报文。这里有两个方向的断开连接是因为客户端与服务端所建立的TCP连接通道是全双工的,而全双工的概念是一条通道是双方或者两个端点都可以发送和接收。也正是由于全双工才需要双方的连接释放,也即是我们的四次挥手。 -
8.14、TCP 是怎样保证可靠传输的呢?
答:主要是从以下四个方面,可靠传输在TCP层面是通过停止等待协议
(无差错情况、超时重传、确认丢失、确认迟到)来实现的- 无差错
- 不丢失
- 不重复
- 按序到达
-
8.15、DNS 解析相关的面试题
-
面试题一:你是否了解 DNS 解析呢?(美团、头条、腾讯等公司的面试题)
DNS 解析
答:所谓DNS解析,其实是域名到IP地址的映射,DNS解析请求采用UDP数据报,并且是明文传输。客户端向服务端发送网络请求的时候,需要先经历一个DNS解析的过程,也就是域名到IP地址的映射的过程,客户端会通过DNS协议向DNS服务器请求对相应域名的一个解析,然后DNS服务器把解析的结果的IP地址返回给客户端,再有客户端向对应的 服务端(IP Server) 发送相应的网络请求
- 拓展一:DNS的查询方式:递归查询和迭代查询
-
拓展二:DNS解析存在哪些常见的问题?
答:DNS劫持问题 和 DNS解析转发问题
DNS劫持 - 拓展三:DNS劫持 与 HTTP 是关系是怎样的?
答:没有关系,原因是:DNS解析发生在HTTP建立连接之前,DNS解析使用的UDP数据报,端口号53 - 拓展四:怎么解决DNS劫持?
答:httpDNS(使用HTTP协议向DNS服务器的80端口进行请求) 和 长连接
-
-
8.16、Session 和 Cookie 相关的面试题
-
面试题一:什么是 Cookie ?
Cookie传递的过程
答:Cookie 主要是用来记录用户的状态,区分用户;状态保存在客户端
。 -
面试题二:怎样修改 Cookie ?
答:新Cookie覆盖旧Cookie,覆盖原则:name、path、domain等需要与原Cookie一致 -
面试题三:怎样删除 Cookie ?
答:新Cookie覆盖旧Cookie,覆盖原则:name、path、domain等需要与原Cookie一致,设置Cookie的expires=过去的一个时间点,或者maxAg=0 -
面试题四:怎样保证 Cookie 的安全呢 ?
答:方案一:对 Cookie 进行加密处理;方案二:只在https协议上携带 Cookie (更优的方案);方案三:设置Cookie为httpOnly,防止跨站脚本攻击 -
面试题五:什么是 Session ?
答:Cookie 也是用来记录用户的状态,区分用户;状态放在服务端
。 -
面试题六:Session 和 Cookie 的关系是怎样的 ?
答:Seesion需要依赖于 Cookie机制来实现
-
面试题七:TCP 与 UDP 有什么区别 ?
答:从特点和功能入手,比如:TCP是面向连接的,并且支持可靠传输 以及面向字节流 ,包括TCP提供了流量的控制和拥塞的控制,而UDP只是简单的提供了复用、分用、差错检测 基本的传输层的功能 ,UDP还是无连接的 。
-
九、设计模式
设计模式-
9.1、你都了解或者知道哪些设计原则 ?以及你对这些设计原则怎么理解的?
答:六大设计原则:单一职责原则、依赖倒置原则、开闭原则、里氏替换原则、接口隔离原则、迪米特法则- 单一职责原则:一个类只负责一件事,比如系统的 UIView(只负责事件传递和事件响应 ) 和 CALayer(负责动画以及视图的展示或者说显示 )
- 开闭原则:对修改关闭,对扩展开放
- 接口隔离原则:使用多个专门的协议,而不是一个庞大臃肿的协议,比如我们常用的UITableView的 delegate(处理事件回调的代理事件) 和 dataSource(获取数据源) 就是利用的接口隔离原则,协议中的方法应当尽量少
- 依赖倒置原则:抽象不应该依赖于具体的实现,具体的实现可以依赖于抽象。
- 里氏替换原则:KVO就使用了里氏替换原则,父类可以被子类无缝替换,且原有功能不受影响
- 迪米特法则:一个对象应该对其他的对象有尽可能少的了解。这样就体现出了
高聚合
、低耦合
。
-
9.2、适配器分为哪几种?
答:适配器分:对象适配器 和 类适配器 -
9.3、什么是命令模式?
答:命令模式是用来做行为参数化的,命令模式的作用是:降低代码重合度的
十、 架构 / 框架
架构 / 框架-
10.1、图片缓存框架的面试题
-
面试题一:怎样设计一个图片缓存的框架呢?
图片缓存的框架 -
面试题二:图片通过什么方式进行读写?过程是怎样的?
答:回答第一个:以图片URL的单向Hash值作为 Key,来存储到我们对应的图片框架中,图片读取的流程如下
图片读取的流程 - 面试题三:内存的设计上需要考虑哪些问题?
答:一方面:内存存储空间的大小Size(图片不能是无限大,占用空间),另一方面淘汰策略(很早的图片清除掉) - 面试题四:磁盘设计需要考虑哪些问题?
答:磁盘的空间很大,读取的效率很低,可以考虑:存储方式、大小限制(如100M)、淘汰策略(如:某一张图片存储时间距今已经超过七天) - 面试题五:网络部分的设计需要考虑哪些问题呢?
答:图片请求的最大并发了量、请求超时策略(我们可以通过超时重试的机制再次加载图片)、请求的优先级 - 面试题六:对于不同格式的图片,解码采用什么方式来做?
答:应用策略模式对不同的图片格式进行解码 - 面试题七:在哪个阶段做图片解码处理?
答:磁盘读取后 或者 网络请求返回后
-
-
10.2、阅读时长统计框架(爱奇艺面试真题)
-
面试题一:怎么样设计一个时长统计的框架呢?
时长统计的框架
答:首先分为两大模块:记录器(对每一条时长进行统计)、记录管理者
- 记录器:页面式(页面的push到pop)、流式:feed流的阅读时长、自定义式:比如微博横滑式的新闻播放
- 记录管理者:记录缓存、磁盘存储模块来处理一些异常的情景、上传器:我们记录的时长进行一个上传操作
- 面试题二:为何要有不同类型的记录器,你的考虑是什么?
答:基于不同分类场景提供的关于记录的封装、适配 - 面试题三:记录的数据会由于某种原因丢失,你是怎样处理的?
答:第一种:定时写磁盘(比如每隔多长时间进行一次磁盘的写入,没有条数的限制),第二种:限制内存缓存条数(如 10 条),超过该条数,即写入磁盘 - 面试题四:记录上传器关于延迟上传的具体场景有哪些?
答:前后台切换、从无网到有网的切换
-
-
10.3、请问你了解MVVM吗?请简单的描述下
MVVM
答:model:数据层、View:视图层、ViewModel:业务逻辑层
-
10.4、客户端整体架构面试题
-
面试题一:客户端的整体架构图
客户端的整体架构图 -
面试题二:业务之间的解耦通信方式,
答:OpenURL 和 依赖注入( 依赖注入式把业务的代码写到中间层,其他的业务想要使用,就去调用中间层,来避免业务之间有直接的来联系)
依赖注入
-
十一、算法
-
11.1、字符串反转
-
面试题一:给定字符串:"
字符串反转思路Hello, World
",实现将其反转,输出结果:dlroW, olleH
答:思路:从两头设置指针,在遍历的过程中一直交换begin和end指针所指的值,直到begin>=end结束交换
定义一个CharReverse
类,书写反转的方法#import "CharReverse.h" @implementation CharReverse void char_reverse(char *cha) { // 指向第一个字符 char *begin = cha; // 指向最后一个字符 char *end = cha + strlen(cha) - 1; while (begin < end) { // 交换前后两个字符,同时移动指针 char temp = *begin; *(begin++) = *end; *(end--) = temp; } } @end
具体的使用
// 字符换反转 char ch[] = "Hello, World"; char_reverse(ch); NSLog(@"字符串反转的结果是:%s",ch);
打印结果是:字符串反转的结果是:dlroW ,olleH
-
-
11.2、链表反转(美团和链家的面试真题)
链表反转
答:定义类 ReverseList,代码如下
-
.h文件
#import <Foundation/Foundation.h> struct Node { // 代表节点数据 int data; // 代表链表的下一个节点 struct Node * _Nonnull next; }; NS_ASSUME_NONNULL_BEGIN @interface ReverseList : NSObject // 链表反转 struct Node *reverseList(struct Node *head); // 构造一个链表 struct Node *constructList(void); // 打印链表中的数据 void printList(struct Node *head); @end NS_ASSUME_NONNULL_END
-
.m件
#import "ReverseList.h" @implementation ReverseList // 链表反转 struct Node *reverseList(struct Node *head) { // 定义遍历指针 struct Node *p = head; // 反转后的链表头部 struct Node *newH = NULL; // 遍历链表 while (p != NULL) { // 记录下一个节点 struct Node *temp = p->next; // 当前节点的next指向新链表头部 p->next = newH; // 更改新链表头部为当前节点 newH = p; // 移动p指针 p = temp; } // 返回反转后的链表头结点 return newH; } // 构造一个链表 struct Node *constructList(void) { // 头结点定义 struct Node *head = NULL; // 记录当前尾结点 struct Node *cur = NULL; for (int i = 0; i < 5; i++) { struct Node *node = malloc(sizeof(struct Node)); node->data = I; // 头结点为空,新节点即为头结点 if (head == NULL) { head = node; } else { // 当前节点的next为新节点 cur->next = node; } // 设置当前的节点为新的节点 cur = node; } return head; } // 打印链表中的数据 void printList(struct Node *head) { struct Node *temp = head; while (temp != NULL) { printf("node is = %d \n", temp->data); temp = temp->next; } } @end
实际的使用
struct Node *head = constructList(); printList(head); NSLog(@"-----------\n"); struct Node *newHead = reverseList(head); printList(newHead);
打印结果
node is = 0 node is = 1 node is = 2 node is = 3 node is = 4 2020-06-17 11:36:09.346645+0800 JKOCTest[11525:594310] ----------- node is = 4 node is = 3 node is = 2 node is = 1 node is = 0
-
-
11.3、有序数组合并(头条的面试题)
有序数组合并
排序的思想:利用两个指针变量一直对比,直到结束,中间是合并结果
排序的思想
定义一个类MergeSortedList
-
MergeSortedList.h
#import <Foundation/Foundation.h> NS_ASSUME_NONNULL_BEGIN @interface MergeSortedList : NSObject /// 有序数组的合并,将有序数组 a 和 b 的值合并到一个数组result当中,且仍然保持有序 /// @param a 被合并数组的第一个 /// @param aLen 第一个数组的长度 /// @param b 被合并数组的第二个 /// @param bLen 第二个数组的长度 /// @param result 合并后的结果数组 void mergeList(int a[], int aLen, int b[], int bLen, int result[]); @end NS_ASSUME_NONNULL_END
-
MergeSortedList.m
#import "MergeSortedList.h" @implementation MergeSortedList void mergeList(int a[], int aLen, int b[], int bLen, int result[]) { // 遍历数组 a 的指针 int p = 0; // 遍历数组 b 的指针 int q = 0; // 记录当前存储的位置 int i = 0; // 任一数组对应位置 没有 到达边界则进行遍历 while (p < aLen && q < bLen) { // 如果 a 数组对应位置的值小于b数组对应位置的值 if (a[p] <= b[q]) { // 存储a数组的值 result[i] = a[p]; // 移动a数组的遍历指针 p++; } else { // 存储b数组的值 result[i] = b[q]; // 移动b数组的遍历指针 q++; } // 指向合并结果的下一个存储的位置 I++; } // 如果 a 数组有剩余 while (p < aLen) { // 将a数组剩余部分拼接到合并结果的后面 result[i] = a[p++]; I++; } // 如果 a 数组有剩余 while (q < bLen) { // 将b数组剩余部分拼接到合并结果的后面 result[i] = b[q++]; I++; } } @end
实际的使用
// 有序数组归并 int a[5] = {1,4,6,7,8}; int b[8] = {2,3,5,6,8,10,11,12}; // 用于存储归并结果 int result[13]; // 归并操作 mergeList(a, 5, b, 8, result); // 打印归并结果 printf("merge result is:"); for (int i = 0; i < 13; i++) { printf("%d ", result[I]); }
打印结果:
merge result is:1 2 3 4 5 6 6 7 8 8 10 11 12
-
-
11.4、Hash算法(爱奇艺考察过Hash算法的运用)
-
面试题:在一个字符串中找到第一个只出现一次的字符?
答:分析:如输入:abaccdeff
,则输出 b。算法思路:字符(char) 是一个长度为 8 的数据类型,因此总共有可能 256 中可能;每个字母根据其 ASCII 码值作为数组的下标对应数组的一个数字;数组中存储的是每个字符出现的次数。例如:给定值是字母 a,对应ASCII值是97,数组索引下标为97。
定义类 HashFind -
HashFind.h 代码
#import <Foundation/Foundation.h> NS_ASSUME_NONNULL_BEGIN @interface HashFind : NSObject // 查找第一个出现一次的字符 char findFirstChar(char *cha); @end NS_ASSUME_NONNULL_END
-
HashFind.m 代码
#import "HashFind.h" @implementation HashFind // 查找第一个出现一次的字符 char findFirstChar(char *cha) { // 空字符 char result = "\0"; // 定义一个数组 用来存储各个字母出现的次数 int array[256]; // 对数组进行初始化操作 for (int i = 0; i < 256; i++) { array[i] = 0; } // 定义一个指针,指向当前字符串头部 char *p = cha; // 遍历每个字符 while (*p != '\0') { // 在字母对应存储位置 进行出现次数 +1 操作 array[*(p++)]++; } // 将p指针重新指向字符串头部 p = cha; // 遍历每个字母出现的次数 while (*p != '\0') { // 遇到第一个出现次数为 1 的字符,打印结果 if (array[*p] == 1) { result = *p; break; } // 反之继续向后遍历 p++; } return result; } @end
实际的使用
// 查找第一个只出现一次的字符 char cha[] = "abaccgdeffb"; char fc = findFirstChar(cha); printf("this char is %c \n", fc);
打印结果:
this char is g
-
-
11.5、查找两个子视图的共同父视图(头条、美团、阿里、腾讯 的面试题 )
答:思路:找出各自所有的父视图,各自放到一个数组里面,数组倒叙对比其父视图,直到遇到不一样的父视图结束
法定义一个类:CommonSuperFind-
CommonSuperFind.h 代码
#import <Foundation/Foundation.h> #import <UIKit/UIKit.h> NS_ASSUME_NONNULL_BEGIN @interface CommonSuperFind : NSObject // 查找两个视图的共同父视图 - (NSArray<UIView *> *)findCommonSuperView:(UIView *)view other:(UIView *)viewOther; @end NS_ASSUME_NONNULL_END
-
CommonSuperFind.m 代码
#import "CommonSuperFind.h" @implementation CommonSuperFind - (NSArray <UIView *> *)findCommonSuperView:(UIView *)viewOne other:(UIView *)viewOther { NSMutableArray *result = [NSMutableArray array]; // 查找第一个视图的所有父视图 NSArray *arrayOne = [self findSuperViews:viewOne]; // 查找第二个视图的所有父视图 NSArray *arrayOther = [self findSuperViews:viewOther]; int i = 0; // 越界限制条件 while (i < MIN((int)arrayOne.count, (int)arrayOther.count)) { // 倒序方式获取各个视图的父视图 UIView *superOne = [arrayOne objectAtIndex:arrayOne.count - i - 1]; UIView *superOther = [arrayOther objectAtIndex:arrayOther.count - i - 1]; // 比较如果相等 则为共同父视图 if (superOne == superOther) { [result addObject:superOne]; I++; } // 如果不相等,则结束遍历 else{ break; } } return result; } - (NSArray <UIView *> *)findSuperViews:(UIView *)view { // 初始化为第一父视图 UIView *temp = view.superview; // 保存结果的数组 NSMutableArray *result = [NSMutableArray array]; while (temp) { [result addObject:temp]; // 顺着superview指针一直向上查找 temp = temp.superview; } return result; } @end
-
-
11.6、求无序数组中的当中位数(头条公司的笔试真题)
答:两个思路:一:排序算法 + 中位数;二:利用快速排序思想(分治思想)-
排序算法 + 中位数:
- 排序算法:先使用 冒泡排序、快速排序、堆排序 等对无序数组进行排序
- 中位数:当n为奇数时:(n + 1) / 2\;当n为偶数时:(n/2 + (n/2 + 1)) / 2
-
利用快速排序思想(分治思想)
快排思想定义一个 MedianFind 类
-
MedianFind.h
#import <Foundation/Foundation.h> NS_ASSUME_NONNULL_BEGIN @interface MedianFind : NSObject // 无序数组中位数查找 int findMedian(int a[], int aLen); @end NS_ASSUME_NONNULL_END
-
MedianFind.m
#import "MedianFind.h" @implementation MedianFind // 无序数组中位数查找 int findMedian(int a[], int aLen) { int low = 0; int high = aLen - 1; int mid = (aLen - 1) / 2; int div = PartSort(a, low, high); while (div != mid) { if (mid < div) { // 左半区间找 div = PartSort(a, low, div - 1); } else { // 右半区间找 div = PartSort(a, div + 1, high); } } return a[mid]; } int PartSort(int a[], int start, int end) { int low = start; int high = end; // 选取关键字 int key = a[end]; while (low < high) { ++low; } // 左边找比 key 小的值 while (low < high && a[high] >= key) { --high; } if (low < high) { // 找到之后交换左右的值 int temp = a[low]; a[low] = a[high]; a[high] = temp; } int temp = a[high]; a[high] = a[end]; a[end] = temp; return low; } @end
实际的运用
int list[10] = {3,1,2,4,5,10,8,6,7,}; int median = findMedian(list, 10); printf("the median is %d \n", median);
打印结果:5
-
-
十二、第三方库
第三方库-
12.1、AFNetworking
-
面试题一:AFN 的整体框架是怎样的呢?
答:框架图如下,会话是:NSURLSession,
AFN框架图
AFN结构图 -
面试题二:AFN主要类关系图
AFN主要类关系图 - 面试题三:AFURLSessionManager 主要负责哪些工作呢?
答:主要是以下四个方面- 创建和管理 NSURLSession,以及调用系统的API来生成 NSURLSessionTask(对应一个网络请求)
- 实现 NSURLSessionDelegate 等协议的代理方法,用来处理我们在网络请求的 过程当中所涉及到的一些,比如说:重定向、认证挑战的处理,以及最核心的部分网络响应数据处理的部分。
- 引入 AFSecurityPolicy 保证请求安全,比如说我们在发送一个HTTPS请求的时候,会涉及到一些证书校验,公钥验证等相关内容,这部分都是由 AFSecurityPolicy 来完成的
- 引入 AFNetworkReachabilityManager 监控网络状态,根据网络的变化做出一些相关的处理
-
-
12.2、SDWebImage
-
面试题一:你是否对 SDWebImage 的整体框架有了解?
答:架构图如下,它只要是封装了一些UIKit分类的方法
SDWebImage 的架构图 - 面试题二:SDWebImage的加载流程是什么?
答:先从内存中查找,内存没有的话从磁盘中查找,磁盘没有的话从网络下载图片
-
-
12.3、ReactiveCocoa (函数响应式的编程框架)
- 面试题一:ReactiveCocoa 是否了解呢?
答:RAC首先是一个函数响应式编程的第三方库,里面主要是订阅
和消息
两个模块
ReactiveCocoa中的核心类:RACSignal -
面试题二:怎样理解信号呢?
信号串
答:信号代表一连串的状态,在信号改变时,对应的订阅者RACSubscriber就会收到通知执行相应的指令
- 面试题一:ReactiveCocoa 是否了解呢?
-
12.4、AsyncDisplayKit 提升iOS界面性能的一个框架
- 面试题一:AsyncDisplayKit 主要是为了解决哪些为题而产生的一个框架呢?或者问 AsyncDisplayKit 这个框架主要做了什么?
答:主要解决了三方面的问题- 第一:主要解决了 Layout 布局的耗时运算(文本宽高计算、视图布局计算),它主要是把操作从主线程迁移到子线程
- 第二:Rendering渲染(主要是文本渲染、图片解码、图形绘制)
- 第三:UIKit Objects(对象创建、对象调整、对象销毁)
- 总结:主要是通过减轻主线程的压力来把更多的事情挪到子线程去做就挪到子线程去做,主要是三方面:第一方面是要是UI布局,涉及到文本宽高计算、视图布局计算;第二部分有关渲染:文本渲染、图片解码、图形绘制;第三部分:是有关对象创建、对象调整、对象销毁
-
面试题二:AsyncDisplayKit 的基本原理是怎样的?
AsyncDisplayKit 的基本原理
答:比如说我们对 UIView的一些相关修改,其实最后都落地到对 ASNode 属性的修改和提交 ,之后我们也模仿了系统的 CAAnimation 提交我们 CALayer 的 setNeedispaly形式的,把我们对 ASNode 的修改,同样也会提交到一个全局容器当中,这样第二步我们就可以监听 RunLoop 的 beforewaiting 这样一个通知 来注册一个观察者实现对这个通知的观察;然后RunLoop
发送我们刚才所说的beforewaiting这样一个通知的时候,我们的SDK就会成从全局容器中把对应的ASNode提取出来,然后把我们 ASNode 对应的属性设置,最终一次性设置给 UIView,那这样的话就可以实现我们 ASDK 的核心逻辑
- 面试题一:AsyncDisplayKit 主要是为了解决哪些为题而产生的一个框架呢?或者问 AsyncDisplayKit 这个框架主要做了什么?
网友评论