几道iOS面试题(五)
- 讲一讲内存的几块区域以及职能
- 讲一讲哈希表,以及解决冲突的方式
- 讲一讲HTTPS的握手流程?
- 讲一讲为什么TCP要三次握手、四次挥手
- 讲一讲Charles抓HTTPS包原理
- 讲一讲GCD 和 NSOperation 区别及各自应用场景
- 讲一讲App 启动优化策略
- 讲一讲代码编译过程
- 讲一讲静态链接、静态库和动态库
- 讲一讲线程有哪几种锁
讲一讲内存的几块区域以及职能
-
栈区
栈区(stack)由
系统自动分配并释放
,主要存放一些基本类型的变量
和对象引用类型
。方法调用的实参也是保存在栈区的。栈是系统数据结构,对应线程/进程是唯一的。优点是快速高效,缺点是有限制,数据不灵活。由编译器自动分配释放。
-
堆区
由程序员分配和释放,操作不当有可能会出现内存泄露。主要存放用new构造的对象和数组。
程序结束的时候,可能会由操作系统回收,比如iOS中alloc都是存放在堆中,优点是灵活方便,数据适应面广泛,但是效率有一定降低,堆空间的分配总是动态的,不同堆分配的内存无法互相操作。虽然程序结束的时候所有的数据空间都会被释放回系统,但是精确的申请内存,释放内存匹配是良好程序的基本要素。
-
全局区(静态区)
全局变量和静态变量是放在一起的,初始化的全局变量和静态变量存放在一块区域,未初始化的全局变量和静态变量在相邻的另一块区域,程序结束后由系统释放。
注意:全局区又可分为未初始化全局区:.bss段和初始化全局区:data段。
举例:int a;为初始化的 int a = 10 ;已初始化的。
-
文字常量区
存放常量
字符串
,程序结束后由系统释放。 -
代码区
存放函数的
二进制代码
讲一讲哈希表,以及解决冲突的方式
散列表(Hash table,也叫哈希表)是一种
线性表
的存储结构。哈希表由一个直接寻址表和一个哈希函数组成。哈希函数h(k)将元素关键字k作为自变量,返回元素的存储下标。
假设有一个长度为7的哈希表,哈希函数h(k)=k%7。元素集合{14,22,3,5}的存储方式如下图。
比如h(k)=k%7, h(0)=h(7)=h(14)=...
解决冲突:
-
开放寻址(open addressing)
- 开放寻址法:如果哈希函数返回的位置已经有值,则可以向后探查新的位置来存储这个值。
- 线性探查:如果位置i被占用,则探查i+1, i+2,……
- 二次探查:如果位置i被占用,则探查i+12,i-12,i+22,i-22,……
- 二度哈希:有n个哈希函数,当使用第1个哈希函数h1发生冲突时,则尝试使用h2,h3,……
-
开链(chaining)
哈希表每个位置都连接一个链表,当冲突发生时,冲突的元素将被加到该位置链表的最后。
常见的哈希函数
除法哈希法: h(k) = k % m
乘法哈希法: h(k) = floor(m (Akey%1))
全域哈希法: ha,b(k) = ((a*key + b) mod p) mod m a,b=1,2,...,p-1
哈希表的应用
集合和字典。
讲一讲HTTPS握手流程
HTTPS: 采用
对称加密
和非对称加密
结合的方式来保护浏览器和服务端之间的通信安全。
-
对称加密
对称加密就是指,加密和解密使用同一个密钥的加密方式。
常见的对称加密算法有
DES
、3DES
、Blowfish
、IDEA
、RC4
、RC5
、RC6
和AES
。 -
非对称加密
与对称加密算法不同,非对称加密算法需要两个密钥:公开密钥(public key)和私有密钥(private key)。
常见的非对称加密算法有:
RSA
、ECC
(移动设备用)、Diffie-Hellman
、El Gamal
、DSA
(数字签名用)。
流程
为什么TCP要三次握手,四次挥手?
三次握手的最主要目的是保证连接是双工
的、可靠
的,更多的是通过重传机制
来保证的。
为什么不能两次握手:防止已失效的连接请求又传送到服务器端,因而产生错误。
假设改为两次握手,client端发送的一个连接请求在服务器滞留了,这个连接请求是无效的,client已经是closed的状态了,而服务器认为client想要建立一个新的连接,于是向client发送确认报文段,而client端是closed状态,无论收到什么报文都会丢弃。
而如果是两次握手的话,此时就已经建立连接了。服务器此时会一直等到client端发来数据,这样就浪费掉很多server端的资源。
TCP看似复杂,其实可以归纳为以下5种报文:
- SYN
- Data(唯一携带用户数据)
- FIN
- Reset
- ACK
其中1、2、3分别为建立连接、数据传输、断开连接,这三种报文对方接收到一定要ACK确认,为何要确认,因为这就是可靠传输
的依赖的机制。如果对方在超时时间内不确认,发送方会一直重传
,直到对方确认为止、或到达重传上限次数而Reset连接。
4、5 为重置连接报文、确认ACK报文,这两种报文对方接收到要ACK确认吧?不需要!自然发送方也不会重传这2种类型的报文。
三次握手
- 第一次握手:Client将标志位SYN置为1(表示要发起一个连接),随机产生一个值seq=J,并将该数据包发送给Server,Client进入
SYN_SENT
状态,等待Server确认。 - 第二次握手:Server收到数据包后由标志位SYN=1,知道Client请求建立连接。Server将标志位SYN和ACK都置为1,ack=J+1,随机产生一个值seq=K,并将该数据包发送给Client以确认连接请求,Server进入
SYN_RCVD
状态。 - 第三次握手:Client收到确认后,
检查ack是否为J+1
,ACK是否为1
,如果正确则将标志位ACK置为1,ack=K+1,并将该数据包发送给Server,Server检查ack是否为K+1,ACK是否为1,如果正确则连接建立成功,Client和Server进入ESTABLISHED
状态,完成三次握手,随后Client与Server之间可以开始传输数据了。
四次挥手
由于TCP连接时全双工
的,因此,每个方向都必须要单独
进行关闭。
这一原则是当一方完成数据发送任务后,发送一个FIN来终止这一方向的连接,收到一个FIN只是意味着这一方向上没有数据流动了,即不会再收到数据了,但是在这个TCP连接上仍然能够发送数据,直到这一方向也发送了FIN。首先进行关闭的一方将执行主动关闭,而另一方则执行被动关闭,
- 第一次挥手:Client发送一个FIN,用来关闭Client到Server的数据传送,Client进入
FIN_WAIT_1
状态。 - 第二次挥手:Server收到FIN后,发送一个ACK给Client,确认序号为收到序号+1(与SYN相同,一个FIN占用一个序号),Server进入
CLOSE_WAIT
状态。 - 第三次挥手:Server发送一个FIN,用来关闭Server到Client的数据传送,Server进入
LAST_ACK
状态。 - 第四次挥手:Client收到FIN后,Client进入TIME_WAIT状态,接着发送一个ACK给Server,确认序号为收到序号+1,Server进入
CLOSED
状态,完成四次挥手。
tips:
-
为什么需要TIME_WAIT
TIMEWAIT状态也称为
2MSL等待状态
。-
为实现TCP这种全双工(full-duplex)连接的可靠释放 这样可让TCP再次发送最后的ACK以防这个
ACK丢失
(另一端超时并重发最后的FIN)。这种2MSL等待的另一个结果是这个TCP连接在2MSL等待期间,定义这个连接的插口(客户的IP地址和端口号,服务器的IP地址和端口号)不能再被使用。这个连接只能在2MSL结束后才能再被使用。
-
为使旧的数据包在网络因过期而消失
每个具体TCP实现必须选择一个报文段最大生存时间MSL(Maximum Segment Lifetime)。它是任何报文段被丢弃前在网络内的最长时间。
-
-
为什么建立连接是三次握手,而关闭连接却是四次挥手呢?
这是因为服务端在
LISTEN
状态下,收到建立连接请求的SYN报文后,把ACK和SYN放在一个报文里发送给客户端。而关闭连接时,当收到对方的FIN报文时,仅仅表示对方不再发送数据了但是还能接收数据,我们也未必全部数据都发送给对方了,所以我们不可以立即close
,也可以发送一些数据给对方后,再发送FIN报文给对方来表示同意现在关闭连接,因此,我们的ACK和FIN一般都会分开发送。
讲一讲Charles抓HTTPS包原理
Charles本身是一个协议代理工具
。
- 对于HTTP请求,因为数据本身没经过再次加密,因此作为代理可以知道所有客户端发送到服务端的请求内容以及服务端返回给客户端的数据内容,这也就是抓包工具能够将数据传输内容直接展现出来的原因。
- 对于HTTPS请求,Charles需要做的事情是对客户端
伪装服务端
,对服务端伪装客户端
- 截获真实客户端的HTTPS请求,伪装客户端向真实服务端发送HTTPS请求
- 接受真实服务器响应,用Charles自己的证书伪装服务端向真实客户端发送数据内容
讲一讲GCD 和 NSOperation 区别及各自应用场景
GCD 和 NSOperation的区别主要表现在以下几方面:
-
GCD是一套 C 语言API,执行和操作简单高效,因此NSOperation底层也通过GCD实现,这是他们之间最本质的区别。因此如果希望
自定义任务
,建议使用NSOperation。 -
依赖关系
NSOperation可以设置操作之间的依赖(可以跨队列设置),GCD无法设置依赖关系,不过可以通过
同步
来实现这种效果。 -
KVO
NSOperation容易判断操作当前的状态(是否执行,是否取消等)对此GCD无法通过KVO进行判断。
-
优先级
NSOperation可以设置自身的优先级,但是优先级高的不一定先执行,GCD只能设置队列的优先级,如果要区分block任务的优先级,需要很复杂的代码才能实现。
-
继承
NSOperation是一个抽象类。实际开发中常用的是它的两个子类:NSInvocationOperation和NSBlockOperation。同样我们可以自定义NSOperation,GCD执行任务可以自由组装,没有继承那么高的代码复用度。
-
效率
直接使用GCD效率确实会更高效,NSOperation会多一点开销。但是通过NSOperation可以获得依赖,优先级,继承,键值对观察这些优势,相对于多的那么一点开销确实很划算,鱼和熊掌不可得兼,取舍在于开发者自己。
-
可以随时取消准备执行的任务
当然已经在执行的不能取消,GCD没法停止已经加入queue 的 block(虽然也能实现,但是需要很复杂的代码)
基于GCD简单高效,更强的执行能力,操作不太复杂的时候,优先选用GCD。
而比较复杂的任务可以自己通过NSOperation实现。
讲一讲App 启动优化策略
-
冷启动:
App点击启动前,此时App的进程还不在系统里。
需要系统新创建一个进程分配给App。(这是一次完整的App启动过程)
-
热启动:
App在冷启动后用户将App退回后台,此时App的进程还在系统里。 用户重新返回App的过程。(热启动做的事较少)
冷启动阶段:
main()
函数执行前(pre-main阶段)main()
函数执行后(从main
函数执行,到设置self.window.rootViewController
执行完成)- 首屏渲染完成后(从
self.window.rootViewController
执行完成到didFinishLaunchWithOptions
方法作用域结束)
优化思路:
-
减少使用 +load() 方法
- 方案一:如果可能的话,将+load中的内容,放到
渲染完成后
做。 - 方案二:使用
+initialize()
的方法代替+load()。注意把逻辑移动到+initialize()时,要注意避免+initialize()的重复调用问题,可以使用dispatch_once()
让逻辑只执行一次。
+load()方法会在main()函数调用前就调用,而+initialize()是在类第一次使用时才会调用
- 方案一:如果可能的话,将+load中的内容,放到
-
合并多个动态库
苹果公司建议使用更少的动态库,并且建议在使用动态库的数量较多时,尽量将多个动态库进行合并。数量上,苹果公司最多可以支持
6
个非系统动态库合并为一个 -
优化类、方法、全局变量
减少加载启动后不会去使用的类或方法,少用全局变量。
-
优化首屏渲染前的功能初始化
main函数执行后到首屏渲染完成前,只处理首屏渲染相关业务。首屏渲染外的其他功能放到首屏渲染完成后去初始化。
-
优化主线程耗时操作,防止屏幕卡顿。
首先检查首屏渲染前,主线程上的耗时操作。将耗时操作滞后或异步处理。通常的耗时操作有:
网络加载
、编辑
、存储图片
和文件
等资源。针对耗时操作做相对应的优化即可。
讲一讲代码编译过程
-
词法分析阶段
读入源程序,对构成源程序的字符流进行扫描和分解,识别出单词,
-
语法分析阶段
机器通过词法分析,将单词序列分解成不同的语法短语,确定整个输入串能够构成语法上正确的程序。
-
语义分析阶段
检查源程序上有没有语义错误,在代码生成阶段收集类型信息
-
中间代码生成阶段
在进行了上述的语法分析和语义分析阶段的工作之后,有的编译程序将源程序变成一种内部表示形式
-
代码优化
这一阶段的任务是对前一阶段产生的中间代码进行变换或进行改造,目的是使生成的目标代码更为高效,即省时间和省空间
-
目标代码生成
这一阶段的任务是把中间代码变换成特定机器上的绝对指令代码或可重定位的指令代码或汇编指令代码。
讲一讲静态链接、静态库和动态库
静态链接是指将多个目标文件合并为一个可执行文件
,直观感觉就是将所有目标文件的段合并。
需要注意的是可执行文件与目标文件的结构基本一致,不同的是是否可执行
。
-
静态库
链接时完整地拷贝至可执行文件中,被多次使用就有多份冗余拷贝。
-
动态库
链接时不复制,程序运行时由系统动态加载到内存,供程序调用,系统只加载一次,多个程序共用,节省内存。
讲一讲线程有哪几种锁
-
自旋锁
自旋锁在无法进行加锁时,会不断的进行尝试,一般用于临界区的执行时间较短的场景,不过iOS的自旋锁
OSSpinLock
不再安全,主要原因发生在低优先级线程拿到锁时,高优先级线程进入忙等(busy-wait)状态,消耗大量 CPU 时间,从而导致低优先级线程拿不到 CPU 时间,也就无法完成任务并释放锁。这种问题被称为优先级反转
。 -
互斥锁
对于某一资源同时只允许有一个访问,无论读写,平常使用的
NSLock
,@synchronized
就属于互斥锁。 -
读写锁
对于某一资源同时只允许有一个写访问或者多个读访问,iOS中
pthread_rwlock
就是读写锁。 -
条件锁
在满足某个条件的时候进行加锁或者解锁,iOS中可使用
NSConditionLock
来实现。 -
递归锁
可以被一个线程多次获得,而不会引起死锁。它记录了成功获得锁的次数,每一次成功的获得锁,必须有一个配套的释放锁和其对应,这样才不会引起死锁。只有当所有的锁被释放之后,其他线程才可以获得锁,iOS可使用
NSRecursiveLock
来实现
转载自:https://juejin.cn/post/6986587773859790856