iOS 高级面试题--答案(下)
最近准备复习一下面试题,看到了J_Knight_的一篇出一套 iOS 高级面试题尝试着回答一下题目,由于水平有限,如有错误的地方,请大家多多指教。
网络题
22. App网络层有哪些优化策略?
- 优化DNS解析和缓存
- 对传输的数据进行压缩,减少传输的数据
- 使用缓存手段减少请求的发起次数
- 使用策略来减少请求的发起次数,比如在上一个请求未着地之前,不进行新的请求
- 避免网络抖动,提供重发机制
23. TCP为什么要三次握手,四次挥手?
三次握手:
- 客户端向服务端发起请求链接,首先发送SYN报文,SYN=1,seq=x,并且客户端进入SYN_SENT状态
- 服务端收到请求链接,服务端向客户端进行回复,并发送响应报文,SYN=1,seq=y,ACK=1,ack=x+1,并且服务端进入到SYN_RCVD状态
- 客户端收到确认报文后,向服务端发送确认报文,ACK=1,ack=y+1,此时客户端进入到ESTABLISHED,服务端收到用户端发送过来的确认报文后,也进入到ESTABLISHED状态,此时链接创建成功
四次挥手:
- 客户端向服务端发起关闭链接,并停止发送数据
- 服务端收到关闭链接的请求时,向客户端发送回应,我知道了,然后停止接收数据
- 当服务端发送数据结束之后,向客户端发起关闭链接,并停止发送数据
- 客户端收到关闭链接的请求时,向服务端发送回应,我知道了,然后停止接收数据
为什么需要三次握手: 为了防止已失效的连接请求报文段突然又传送到了服务端,因而产生错误,假设这是一个早已失效的报文段。但server收到此失效的连接请求报文段后,就误认为是client再次发出的一个新的连接请求。于是就向client发出确认报文段,同意建立连接。假设不采用“三次握手”,那么只要server发出确认,新的连接就建立了。由于现在client并没有发出建立连接的请求,因此不会理睬server的确认,也不会向server发送数据。但server却以为新的运输连接已经建立,并一直等待client发来数据。这样,server的很多资源就白白浪费掉了。
为什么需要四次挥手: 因为TCP是全双工通信的,在接收到客户端的关闭请求时,还可能在向客户端发送着数据,因此不能再回应关闭链接的请求时,同时发送关闭链接的请求
24. 对称加密和非对称加密的区别?分别有哪些算法的实现?
对称加密,加密的加密和解密使用同一密钥。
非对称加密,使用一对密钥用于加密和解密,分别为公开密钥和私有密钥。公开密钥所有人都可以获得,通信发送方获得接收方的公开密钥之后,就可以使用公开密钥进行加密,接收方收到通信内容后使用私有密钥解密。
对称加密常用的算法实现有AES,ChaCha20,DES,不过DES被认为是不安全的;非对称加密用的算法实现有RSA,ECC
25. HTTPS的握手流程?为什么密钥的传递需要使用非对称加密?双向认证了解么?
HTTPS的握手流程,如下图,摘自图解HTTP
- 客户端发送Client Hello 报文开始SSL通信。报文中包含客户端支持的SSL的版本,加密组件列表。
- 服务器收到之后,会以Server Hello 报文作为应答。和客户端一样,报文中包含客户端支持的SSL的版本,加密组件列表。服务器的加密组件内容是从接收到的客户端加密组件内筛选出来的
- 服务器发送Certificate报文。报文中包含公开密钥证书。
- 然后服务器发送Server Hello Done报文通知客户端,最初阶段的SSL握手协商部分结束
- SSL第一次握手结束之后,客户端以Client Key Exchange报文作为会议。报文中包含通信加密中使用的一种被称为Pre-master secret的随机密码串
- 接着客户端发送Change Cipher Space报文。该报文会提示服务器,在次报文之后的通信会采用Pre-master secret密钥加密
- 客户端发送Finished 报文。该报文包含链接至今全部报文的整体校验值。这次握手协商是否能够成功,要以服务器是否能够正确揭秘该报文作为判定标准
- 服务器同样发送Change Cipher Space报文。
- 服务器同样发送Finished报文。
- 服务器和客户端的Finished报文交换完毕之后,SSL连接建立完成,从此开始HTTP通信,通信的内容都使用Pre-master secret加密。然后开始发送HTTP请求
- 应用层收到HTTP请求之后,发送HTTP响应
- 最后有客户端断开连接
为什么密钥的传递需要使用非对称加密?
答:使用非对称加密是为了后面客户端生成的Pre-master secret密钥的安全,通过上面的步骤能得知,服务器向客户端发送公钥证书这一步是有可能被别人拦截的,如果使用对称加密的话,在客户端向服务端发送Pre-master secret密钥的时候,被黑客拦截的话,就能够使用公钥进行解码,就无法保证Pre-master secret密钥的安全了
双向认证了解么?(这里我真想说一句不了解)
答:上面的HTTPS的通信流程只验证了服务端的身份,而服务端没有验证客户端的身份,双向认证是服务端也要确保客户端的身份,大概流程是客户端在校验完服务器的证书之后,会向服务器发送自己的公钥,然后服务端用公钥加密产生一个新的密钥,传给客户端,客户端再用私钥解密,以后就用此密钥进行对称加密的通信
26. HTTPS是如何实现验证身份和验证完整性的?
使用数字证书和CA来验证身份,首先服务端先向CA机构去申请证书,CA审核之后会给一个数字证书,里面包裹公钥、签名、有效期,用户信息等各种信息,在客户端发送请求时,服务端会把数字证书发给客户端,然后客户端会通过信任链来验证数字证书是否是有效的,来验证服务端的身份。
使用摘要算法来验证完整性,也就是说在发送消息时,会对消息的内容通过摘要算法生成一段摘要,在收到收到消息时也使用同样的算法生成摘要,来判断摘要是否一致。
27. 如何用Charles抓HTTPS的包?其中原理和流程是什么?
流程:
- 首先在手机上安装Charles证书
- 在***设置中开启Enable SSL Proxying
- 之后添加需要抓取服务端的地址
原理:
Charles作为中间人,对客户端伪装成服务端,对服务端伪装成客户端。简单来说:
- 截获客户端的HTTPS请求,伪装成中间人客户端去向服务端发送HTTPS请求
- 接受服务端返回,用自己的证书伪装成中间人服务端向客户端发送数据内容。
具体流程如下图,图片来自扯一扯HTTPS单向认证、双向认证、抓包原理、反抓包策略
28. 什么是中间人攻击?如何避免?
中间人攻击就是截获到客户端的请求以及服务器的响应,比如Charles抓取HTTPS的包就属于中间人攻击。
避免的方式:客户端可以预埋证书在本地,然后进行证书的比较是否是匹配的
计算机系统题
29. 了解编译的过程么?分为哪几个步骤?
- 预编译:主要处理以“#”开始的预编译指令。
- 编译:
- 词法分析:将字符序列分割成一系列的记号。
- 语法分析:根据产生的记号进行语法分析生成语法树。
- 语义分析:分析语法树的语义,进行类型的匹配、转换、标识等。
- 中间代码生成:源码级优化器将语法树转换成中间代码,然后进行源码级优化,比如把 1+2 优化为 3。中间代码使得编译器被分为前端和后端,不同的平台可以利用不同的编译器后端将中间代码转换为机器代码,实现跨平台。
- 目标代码生成:此后的过程属于编译器后端,代码生成器将中间代码转换成目标代码(汇编代码),其后目标代码优化器对目标代码进行优化,比如调整寻址方式、使用位移代替乘法、删除多余指令、调整指令顺序等。
- 汇编:汇编器将汇编代码转变成机器指令。
- 静态链接:链接器将各个已经编译成机器指令的目标文件链接起来,经过重定位过后输出一个可执行文件。
- 装载:装载可执行文件、装载其依赖的共享对象。
- 动态链接:动态链接器将可执行文件和共享对象中需要重定位的位置进行修正。
- 最后,进程的控制权转交给程序入口,程序终于运行起来了。
作为一个开发者,有一个学习的氛围跟一个交流圈子特别重要,这是一个我的点击加入群聊iOS交流群:789143298 ,不管你是小白还是大牛欢迎入驻 ,分享BAT,阿里面试题、面试经验,讨论技术, 大家一起交流学习成长!
![]()
30. 静态链接了解么?静态库和动态库的区别?
静态链接是指将多个目标文件合并为一个可执行文件,直观感觉就是将所有目标文件的段合并。需要注意的是可执行文件与目标文件的结构基本一致,不同的是是否“可执行”。 静态库:链接时完整地拷贝至可执行文件中,被多次使用就有多份冗余拷贝。 动态库:链接时不复制,程序运行时由系统动态加载到内存,供程序调用,系统只加载一次,多个程序共用,节省内存。
31. 内存的几大区域,各自的职能分别是什么?
- 栈区:有系统自动分配并释放,一般存放函数的参数值,局部变量等
- 堆区:有程序员分配和释放,若程序员未释放,则在程序结束时有系统释放,在iOS里创建出来的对象会放在堆区
- 数据段:字符串常量,全局变量,静态变量
- 代码段:编译之后的代码
32. static和const有什么区别?
const是指声明一个常量 static修饰全局变量时,表示此全局变量只在当前文件可见 static修饰局部变量时,表示每次调用的初始值为上一次调用的值,调用结束后存储空间不释放
33. 了解内联函数么?
内联函数是为了减少函数调用的开销,编译器在编译阶段把函数体内的代码复制到函数调用处
34. 什么时候会出现死锁?如何避免?
死锁是指两个或两个以上的线程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。 发生死锁的四个必要条件:
- 互斥条件:一个资源每次只能被一个线程使用。
- 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:线程已获得的资源,在未使用完之前,不能强行剥夺。
- 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系。
只要上面四个条件有一个条件不被满足就能避免死锁
35. 说一说你对线程安全的理解?
在并发执行的环境中,对于共享数据通过同步机制保证各个线程都可以正确的执行,不会出现数据污染的情况,或者对于某个资源,在被多个线程访问时,不管运行时执行这些线程有什么样的顺序或者交错,不会出现错误的行为,就认为这个资源是线程安全的,一般来说,对于某个资源如果只有读操作,则这个资源无需同步就是线程安全的,若有多个线程进行读写操作,则需要线程同步来保证线程安全。
36. 列举你知道的线程同步策略?
- OSSpinLock 自旋锁,已不再安全,除了这个锁之外,下面写的锁,在等待时,都会进入线程休眠状态,而非忙等
- os_unfair_lock atomic就是使用此锁来保证原子性的
- pthread_mutex_t 互斥锁,并且支持递归实现和条件实现
- NSLock,NSRecursiveLock,基本的互斥锁,NSRecursiveLock支持递归调用,都是对pthread_mutex_t的封装
- NSCondition,NSConditionLock,条件锁,也都是对pthread_mutex_t的封装
- dispatch_semaphore_t 信号量
- @synchronized 也是pthread_mutex_t的封装
37. 有哪几种锁?各自的原理?它们之间的区别是什么?最好可以结合使用场景来说
- 自旋锁:自旋锁在无法进行加锁时,会不断的进行尝试,一般用于临界区的执行时间较短的场景,不过iOS的自旋锁OSSpinLock不再安全,主要原因发生在低优先级线程拿到锁时,高优先级线程进入忙等(busy-wait)状态,消耗大量 CPU 时间,从而导致低优先级线程拿不到 CPU 时间,也就无法完成任务并释放锁。这种问题被称为优先级反转。
- 互斥锁:对于某一资源同时只允许有一个访问,无论读写,平常使用的NSLock就属于互斥锁
- 读写锁:对于某一资源同时只允许有一个写访问或者多个读访问,iOS中pthread_rwlock就是读写锁
- 条件锁:在满足某个条件的时候进行加锁或者解锁,iOS中可使用NSConditionLock来实现
- 递归锁:可以被一个线程多次获得,而不会引起死锁。它记录了成功获得锁的次数,每一次成功的获得锁,必须有一个配套的释放锁和其对应,这样才不会引起死锁。只有当所有的锁被释放之后,其他线程才可以获得锁,iOS可使用NSRecursiveLock来实现
数据结构&算法题
38. 链表和数组的区别是什么?插入和查询的时间复杂度分别是多少?
链表和数组都是一个有序的集合,数组需要连续的内存空间,而链表不需要,链表的插入删除的时间复杂度是O(1),数组是O(n),根据下标查询的时间复杂度数组是O(1),链表是O(n),根据值查询的时间复杂度,链表和数组都是O(n)
39. 哈希表是如何实现的?如何解决地址冲突?
哈希表是也是通过数组来实现的,首先对key值进行哈希化得到一个整数,然后对整数进行计算,得到一个数组中的下标,然后进行存取,解决地址冲突常用方法有开放定址法和链表法。runtime源码的存放weak指针哈希表使用的就是开放定址法,Java里的HashMap使用的是链表法。
40. 排序题:冒泡排序,选择排序,插入排序,快速排序(二路,三路)能写出那些?
这里简单的说下几种快速排序的不同之处,随机快排,是为了解决在近似有序的情况下,时间复杂度会退化为O(n2),双路快排是为了解决快速排序在大量数据重复的情况下,时间复杂度会退化为O(n2),三路快排是在大量数据重复的情况下,对双路快排的一种优化。
- 冒泡排序
extension Array where Element : Comparable{
public mutating func bubbleSort() {
let count = self.count
for i in 0..<count {
for j in 0..<(count - 1 - i) {
if self[j] > self[j + 1] {
(self[j], self[j + 1]) = (self[j + 1], self[j])
}
}
}
}
}
- 选择排序
extension Array where Element : Comparable{
public mutating func selectionSort() {
let count = self.count
for i in 0..<count {
var minIndex = i
for j in (i+1)..<count {
if self[j] < self[minIndex] {
minIndex = j
}
}
(self[i], self[minIndex]) = (self[minIndex], self[i])
}
}
}
- 插入排序
extension Array where Element : Comparable{
public mutating func insertionSort() {
let count = self.count
guard count > 1 else { return }
for i in 1..<count {
var preIndex = i - 1
let currentValue = self[i]
while preIndex >= 0 && currentValue < self[preIndex] {
self[preIndex + 1] = self[preIndex]
preIndex -= 1
}
self[preIndex + 1] = currentValue
}
}
}
- 快速排序
extension Array where Element : Comparable{
public mutating func quickSort() {
func quickSort(left:Int, right:Int) {
guard left < right else { return }
var i = left + 1,j = left
let key = self[left]
while i <= right {
if self[i] < key {
j += 1
(self[i], self[j]) = (self[j], self[i])
}
i += 1
}
(self[left], self[j]) = (self[j], self[left])
quickSort(left: j + 1, right: right)
quickSort(left: left, right: j - 1)
}
quickSort(left: 0, right: self.count - 1)
}
}
- 随机快排
extension Array where Element : Comparable{
public mutating func quickSort1() {
func quickSort(left:Int, right:Int) {
guard left < right else { return }
let randomIndex = Int.random(in: left...right)
(self[left], self[randomIndex]) = (self[randomIndex], self[left])
var i = left + 1,j = left
let key = self[left]
while i <= right {
if self[i] < key {
j += 1
(self[i], self[j]) = (self[j], self[i])
}
i += 1
}
(self[left], self[j]) = (self[j], self[left])
quickSort(left: j + 1, right: right)
quickSort(left: left, right: j - 1)
}
quickSort(left: 0, right: self.count - 1)
}
}
- 双路快排
extension Array where Element : Comparable{
public mutating func quickSort2() {
func quickSort(left:Int, right:Int) {
guard left < right else { return }
let randomIndex = Int.random(in: left...right)
(self[left], self[randomIndex]) = (self[randomIndex], self[left])
var l = left + 1, r = right
let key = self[left]
while true {
while l <= r && self[l] < key {
l += 1
}
while l < r && key < self[r]{
r -= 1
}
if l > r { break }
(self[l], self[r]) = (self[r], self[l])
l += 1
r -= 1
}
(self[r], self[left]) = (self[left], self[r])
quickSort(left: r + 1, right: right)
quickSort(left: left, right: r - 1)
}
quickSort(left: 0, right: self.count - 1)
}
}
- 三路快排
// 三路快排
extension Array where Element : Comparable{
public mutating func quickSort3() {
func quickSort(left:Int, right:Int) {
guard left < right else { return }
let randomIndex = Int.random(in: left...right)
(self[left], self[randomIndex]) = (self[randomIndex], self[left])
var lt = left, gt = right
var i = left + 1
let key = self[left]
while i <= gt {
if self[i] == key {
i += 1
}else if self[i] < key{
(self[i], self[lt + 1]) = (self[lt + 1], self[i])
lt += 1
i += 1
}else {
(self[i], self[gt]) = (self[gt], self[i])
gt -= 1
}
}
(self[left], self[lt]) = (self[lt], self[left])
quickSort(left: gt + 1, right: right)
quickSort(left: left, right: lt - 1)
}
quickSort(left: 0, right: self.count - 1)
}
}
41. 链表题:如何检测链表中是否有环?如何删除链表中等于某个值的所有节点?
- 如何检测链表中是否有环?
public class ListNode {
public var val: Int
public var next: ListNode?
public init(_ val: Int) {
self.val = val
self.next = nil
}
}
extension ListNode {
var hasCycle: Bool {
var slow:ListNode? = self
var fast = self.next
while fast != nil {
if slow! === fast! {
return true
}
slow = slow?.next
fast = fast?.next?.next
}
return false
}
}
- 如何删除链表中等于某个值的所有节点?
func remove(with value:Int, from listNode:ListNode?) -> ListNode? {
let tmpNode = ListNode(0)
tmpNode.next = listNode
var currentNode = tmpNode.next
var persiousNode:ListNode? = tmpNode
while currentNode != nil {
if let nodeValue = currentNode?.val, nodeValue == value {
persiousNode?.next = currentNode?.next
}else {
persiousNode = currentNode
}
currentNode = currentNode?.next
}
return tmpNode.next
}
42. 数组题:如何在有序数组中找出和等于给定值的两个元素?如何合并两个有序的数组之后保持有序?
- 如何在有序数组中找出和等于给定值的两个元素?LeetCode第167题
func twoSum(_ numbers: [Int], _ target: Int) -> [Int] {
var i = 0, j = numbers.count - 1
while i < j {
let sum = numbers[i] + numbers[j]
if sum == target {
return [i + 1, j + 1]
}else if sum > target {
j -= 1
}else {
i += 1
}
}
return []
}
- 如何合并两个有序的数组之后保持有?LeetCode第88题
func merge(_ nums1: inout [Int], _ m: Int, _ nums2: [Int], _ n: Int) {
for i in stride(from: n + m - 1, to: n - 1, by: -1) {
nums1[i] = nums1[i - n]
}
var i = 0, j = 0
while i < m && j < n {
if nums1[n + i] > nums2[j] {
nums1[i + j] = nums2[j]
j += 1
}else {
nums1[i + j] = nums1[n + i]
i += 1
}
}
while i < m {
nums1[i + j] = nums1[n + i]
i += 1
}
while j < n {
nums1[i + j] = nums2[j]
j += 1
}
}
43. 二叉树题:如何反转二叉树?如何验证两个二叉树是完全相等的?
- 如何翻转二叉树?LeetCode第226题
func invertTree(_ root: TreeNode?) -> TreeNode? {
guard let root = root else { return nil }
(root.left, root.right) = (root.right, root.left)
invertTree(root.left)
invertTree(root.right)
return root
}
- 如何验证两个二叉树是完全相等的?
func isSameTree(_ p: TreeNode?, _ q: TreeNode?) -> Bool {
guard let pNode = p ,let qNode = q else { return q == nil && p == nil }
return pNode.val == qNode.val && isSameTree(pNode.left, qNode.left) && isSameTree(pNode.right, qNode.right)
}
推荐👇:
020 持续更新,精品小圈子每日都有新内容,干货浓度极高。
结实人脉、讨论技术 你想要的这里都有!
抢先入群,跑赢同龄人!(入群无需任何费用)
-
(直接搜索群号:789143298,快速入群)
-
点击此处,与iOS开发大牛一起交流学习
申请即送:
BAT大厂面试题、独家面试工具包,
资料免费领取,包括 数据结构、底层进阶、图形视觉、音视频、架构设计、逆向安防、RxSwift、flutter,
准备面试是一方面,对于非面试的iOS开发者来说更适用于检验自己,发起进阶之路。另外知识点是琐碎的,但是真的能全部弄懂并把琐碎的知识点融会贯通,构建起自己的知识体系,你就升级了。