iOS进阶面试题(二)

前10题面试题见iOS进阶面试题(一)

面试题.png

11.用过 Swift 吗?如何评价 String index 的设计?

解答:Q1略过
Q2:如何评价 String index 的设计 ?
字符串的索引:

  • 可扩展的字符群集可以组成一个或者多个 Unicode 标量。这意味着不同的字符以及相同字符的不同表示方式可能需要不同数量的内存空间来存储。所以 Swift 中的字符在一个字符串中并不一定占用相同的内存空间数量。因此在没有获取字符串的可扩展的字符群的范围时候,就不能计算出字符串的字符数量。如果您正在处理一个长字符串,需要注意characters属性必须遍历全部的 Unicode 标量,来确定字符串的字符数量。

  • 另外需要注意的是通过characters属性返回的字符数量并不总是与包含相同字符的NSString的length属性相同。NSString的length属性是利用 UTF-16 表示的十六位代码单元数字,而不是 Unicode 可扩展的字符群集。

  • 前面提到,不同的字符可能会占用不同数量的内存空间,所以要知道Character的确定位置,就必须从String开头遍历每一个 Unicode 标量直到结尾。因此,Swift 的字符串不能用整数(integer)做索引。

  • 使用startIndex属性可以获取一个String的第一个Character的索引。使用endIndex属性可以获取最后一个Character的后一个位置的索引。因此,endIndex属性不能作为一个字符串的有效下标。如果String是空串,startIndex和endIndex是相等的。

  • 通过调用 String 的 index(before:) 或 index(after:) 方法,可以立即得到前面或后面的一个索引。您还可以通过调用 index(_:offsetBy:) 方法来获取对应偏移量的索引,这种方式可以避免多次调用 index(before:) 或 index(after:) 方法。

        let stringIndexStr = "hello word!"
        let starIndexC = stringIndexStr[stringIndexStr.startIndex]//获取字符串第一个索引的 字符
        print("startIndexValue:\(stringIndexStr.startIndex)")//打印 startIndexValue:Index(_base: Swift.String.UnicodeScalarView.Index(_position: 0), _countUTF16: 1)
        
        print("starIndexCharacter:\(starIndexC)")//打印 starIndexCharacter:h
        
        //endIndex属性不能作为一个字符串的有效下标,运行时会崩溃
        //let endIndexC = stringIndexStr[stringIndexStr.endIndex]//试图获取越界索引对应的 Character,将引发一个运行时错误。
        //print("endIndexCharacter:\(endIndexC)")
 

12.假设 iPhone 上有一个与服务器的 TCP 连接,此时 iPhone 忽然断网,服务器能在短时间内知会 iPhone 的离线吗?

解答

  • 一般来说不能
  1. 中断连接可以是客户端,也可以是服务端。
    假设由客户端中断连接,即客户端发起FIN报文中断连接请求,告诉服务器“我没有数据发给你了”。这时,客户端进入FIN_WAIT_1状态。服务器收到FIN报文后,如果还有数据没有发送完成,则不会急着关闭SOCKET,会继续发送数据。这时,服务器会发送ACK告诉客户端“我收到你的断开请求了,但是我还没有准备好,请等我消息”。这时,客户端就进入FIN_WAIT_2状态,继续等待服务器的FIN报文。当服务器确认数据已经发送完成,则向客户端发送FIN报文,告诉客户端“好了,我这边的数据发完了,准备断开连接”。客户端收到FIN报文后就知道断开连接,但是它不相信网络,怕服务器不知道断开,所以发送ACK后进入TIME_WAIT状态。如果服务器没有收到ACK报文,则客户端会继续重传ACK,服务器收到ACK报文后就知道可以断开连接了。客户端等待2MSL后依然没有收到回复,则证明服务器已正常关闭。那好,我客户端也可以关闭连接了。OK,TCP连接就这样关闭了。

  2. 在服务器的TCP连接没有完全断开之前不允许重新监听,某些情况下可能是不合理的。
    【例】
    服务器需要处理大量的客户端的连接,每个连接的生存时间可能很短,但是每秒都有很多客户端来请求。这个时候,如果由服务器主动关闭连接,比如某些客户端不活跃,就需要被服务器主动清理掉,这时会产生大量的TIME_WAIT连接。由于我们的请求量很大,就可能导致TIME_WAIT的连接数很多而导致服务器的端口不够用,无法处理新的连接。

3.如何解决TIME_WAIT状态引起的bind失败?
答:使用setsockopt()设置socket描述符的选项SO_REUSEADDR为1,表示允许创建端口号相同,但是IP地址不同的多个socket描述符。即,在server代码的socket函数和bind函数调用之间插入如下代码:
int opt = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

最后, 我们来简要说说另外一种网络结构, 假设把pc1和pc2直接用网线相连, 建立起世界最小局域网, 并形成tcp连接。如果在客户端和服务端都没有心跳机制,那么实验结果如下
1. 如果断掉其中的网线, 客户端和服务端都没有感知。
2. 客户端突然断电, 则服务端没有感知。
3.服务端突然断电, 则客户端没有感知。

13.为什么 Wireshark 不能直接抓取 SSL 的原始数据?

  1. 必要的加密解密基础知识

1)对称加密算法:就是加密和解密使用同一个密钥的加密算法。因为加密方和解密方使用的密钥相同,所以称为称为对称加密,也称为单钥加密方法。

优点是:加密和解密运算速度快,所以对称加密算法通常在消息发送方需要加密大量数据时使用;

缺点是:安全性差,如果一方的密钥遭泄露,那么整个通信就会被破解。另外加密之前双方需要同步密钥;

常用对称加密算法有:DES、3DES、TDEA、Blowfish、RC2、RC4、RC5、IDEA、SKIPJACK、AES等;

2)非对称加密算法:而非对称加密算法需要两个密钥来进行加密和解密,这两个秘钥是公开密钥(public key,简称公钥)和私有密钥(private key,简称私钥)。

公钥和私钥是一对:公钥用来加密,私钥解密,而且公钥是公开的,私钥是自己保存的,不需要像对称加密那样在通信之前要先同步秘钥。

有点是:安全性更好,私钥是自己保存的,不需要像对称加密那样在通信之前要先同步秘钥。

缺点是:加密和解密花费时间长、速度慢,只适合对少量数据进行加密。

常用的非对称加密算法有:RSA、Elgamal、Rabin、D-H、ECC等;

3)HASH算法:也称为消息摘要算法。将任意长度的二进制值映射为较短的固定长度的二进制值,该二进制值称为哈希值。

常用于检验数据的完整性,检验数据没有被篡改过。常见的又 MD5(MD系列),SHA-1(SHA系列)

HTTPS 使用到了上面全部三种加密算法。

  1. HTTPS 的作用

HTTPS简单而言,即使建立在SSL/TLS协议之上的HTTP。不使用SSL/TLS的HTTP通信,就是不加密的通信。所有信息明文传播,带来了三大风险。
(1) 窃听风险(eavesdropping):第三方可以获知通信内容。
(2) 篡改风险(tampering):第三方可以修改通信内容。
(3) 冒充风险(pretending):第三方可以冒充他人身份参与通信。

SSL/TLS协议是为了解决这三大风险而设计的,希望达到:
(1) 所有信息都是加密传播,第三方无法窃听。
(2) 具有校验机制,一旦被篡改,通信双方会立刻发现。
(3) 配备身份证书,防止身份被冒充。

互联网是开放环境,通信双方都是未知身份,这为协议的设计带来了很大的难度。而且,协议还必须能够经受所有匪夷所思的攻击,这使得SSL/TLS协议变得异常复杂。

4. 基本的运行过程
HTTPS 的基本运行过程:
1)利用对称加密算法来加密网页内容,那么如何保证对称加密算法的秘钥的安全呢?
2)使用非对称加密算法来获得对称加密算法的秘钥,从而保证了对称加密算法的秘钥的安全,也就保证了对称加密算法的安全
这里这样安排使用的原理是,利用了对称加密算法和非对称加密算法优点,而避免了它们的缺点。利用了对称加密算法速度快,而非对称加密算法安全的优点;同时巧妙的避免了对称加密算法的不安全性,以及需要同步密钥的缺点,也避免了非对称加密算法的速度慢的缺点。实在是巧妙了。

这里有两个问题:
(1)如何保证非对称加密算法公钥不被篡改?
解决方法:将公钥放在数字证书中。只要证书是可信的,公钥就是可信的。
(2)公钥加密计算量太大,如何减少耗用的时间?
解决方法:每一次对话(session),客户端和服务器端都生成一个"对话密钥"(session key),用它来加密信息。由于"对话密钥"是对称加密算法,所以运算速度非常快,而服务器公钥只用于加密"对话密钥"本身,这样就减少了加密运算的消耗时间。(也就是网页内容的加密使用的是对称加密算法)

因此,SSL/TLS协议的基本过程是这样的:

(1) 客户端向服务器端索要并验证非对称加密算法的公钥。
(2) 双方协商生成对称加密算法的"对话密钥"。
(3) 双方采用对称加密算法和它的"对话密钥"进行加密通信。

上面过程的前两步,又称为"握手阶段"(handshake)。

5. 握手阶段的详细过程
"握手阶段"涉及四次通信,我们一个个来看。需要注意的是,"握手阶段"的所有通信都是明文的。

1) 客户端发出请求(ClientHello)
首先,客户端(通常是浏览器)先向服务器发出加密通信的请求,这被叫做ClientHello请求。
在这一步,客户端主要向服务器提供以下信息。

(1) 浏览器支持的SSL/TLS协议版本,比如TLS 1.0版。
(2) 一个浏览器客户端生成的随机数,稍后用于生成对称加密算法的"对话密钥"。
(3) 浏览器支持的各种加密方法,对称的,非对称的,HASH算法。比如RSA非对称加密算法,DES对称加密算法,SHA-1 hash算法。
(4) 浏览器支持的压缩方法。

这里需要注意的是,客户端发送的信息之中不包括服务器的域名。也就是说,理论上服务器只能包含一个网站,否则会分不清应该向客户端提供哪一个网站的数字证书。这就是为什么通常一台服务器只能有一张数字证书的原因。

对于虚拟主机的用户来说,这当然很不方便。2006年,TLS协议加入了一个Server Name Indication扩展,允许客户端向服务器提供它所请求的域名。

2) 服务器回应(SeverHello)
服务器收到客户端请求后,向客户端发出回应,这叫做SeverHello。服务器的回应包含以下内容。

(1) 确认使用的加密通信协议版本,比如TLS 1.0版本。如果浏览器与服务器支持的版本不一致,服务器关闭加密通信。
(2) 一个服务器生成的随机数,稍后用于生成对称加密算法的"对话密钥"。
(3) 确认使用的各种加密方法,比如确认算法使用:RSA非对称加密算法,DES对称加密算法,SHA-1 hash算法
(4) 服务器证书。

除了上面这些信息,如果服务器需要确认客户端的身份,就会再包含一项请求,要求客户端提供"客户端证书"。比如,金融机构往往只允许认证客户连入自己的网络,就会向正式客户提供USB密钥,里面就包含了一张客户端证书。

3) 客户端回应

客户端收到服务器回应以后,首先验证服务器证书。如果证书不是可信机构颁布、或者证书中的域名与实际域名不一致、或者证书已经过期,就会向访问者显示一个警告,由其选择是否还要继续通信。

如果证书没有问题,客户端就会从证书中取出服务器的非对称加密算法的公钥。然后,向服务器发送下面三项信息。

(1) 一个随机数。该随机数用服务器发来的公钥进行的使用非对称加密算法加密,防止被窃听。
(2) 编码改变通知,表示随后的信息都将用双方商定的加密方法和密钥发送(比如确认使用:RSA非对称,DES对称,SHA-1 hash算法)。
(3) 客户端握手结束通知,表示客户端的握手阶段已经结束。这一项同时也是前面发送的所有内容的hash值,用来供服务器校验。

上面第一项的随机数,是整个握手阶段出现的第三个随机数,又称"pre-master key"。有了它以后,客户端和服务器就同时有了三个随机数,接着双方就用事先商定的对称加密算法,各自生成本次会话所用的同一把"会话密钥"。也就是说浏览器和服务器各自使用同一个对称加密算法,对三个相同的随机数进行加密,获得了,用来加密网页内容的 对称加密算法的秘钥。(注意:这里浏览器的三个随机数都是明文的,但是服务端获得的"pre-master key"是密文的,所以服务器需要使用非对称加密算法的私钥,来先解密获得"pre-master key"的明文,在来生成对称加密算法的秘钥。这样的目的是为了防止:"pre-master key"被窃听,因为发送明文会被窃听,但是发生的是非对称加密算法的加密过后的密文,因为窃听者不知道私钥,所以即使窃听了,也无法解密出其对应的明文。从而保证了最后生成的:用于加密网页内容的对称加密算法的秘钥的安全性!!!)

至于为什么一定要用三个随机数,来生成"会话密钥"?

"不管是客户端还是服务器,都需要随机数,这样生成的密钥才不会每次都一样。由于SSL协议中证书是静态的,因此十分有必要引入一种随机因素来保证协商出来的密钥的随机性。

对于RSA密钥交换算法来说,pre-master-key本身就是一个随机数,再加上hello消息中的随机,三个随机数通过一个密钥导出器最终导出一个对称密钥。

pre master的存在在于SSL协议不信任每个主机都能产生完全随机的随机数,如果随机数不随机,那么pre master secret就有可能被猜出来,那么仅适用pre master secret作为密钥就不合适了,因此必须引入新的随机因素,那么客户端和服务器加上pre master secret三个随机数一同生成的密钥就不容易被猜出了,一个伪随机可能完全不随机,可是是三个伪随机就十分接近随机了,每增加一个自由度,随机性增加的 可不是一。"

这里:其实如果 pre-master-key 和 浏览器生成的随机数都可能被猜出来,那么最后生成的对称加密算法的秘钥就是不安全的。因为三个随机数都可能被窃听到了。

此外,如果前一步,服务器要求客户端证书,客户端会在这一步发送证书及相关信息。

4) 服务器的最后回应

服务器收到客户端的第三个随机数pre-master key之后,计算生成本次会话所用的对称加密算法的"会话密钥"。然后,向客户端最后发送下面信息。

(1)编码改变通知,表示随后的信息都将用双方商定的对称加密算法和密钥进行加密。

(2)服务器握手结束通知,表示服务器的握手阶段已经结束。这一项同时也是前面发送的所有内容的hash值,用来供客户端校验。

至此,整个握手阶段全部结束。接下来,客户端与服务器进入加密通信,就完全是使用普通的HTTP协议,只不过用"会话密钥"加密内容。

总结

1)HTTPS 结合使用了 非对称加密算法,对称加密算法,hash算法,分别利用他们的优势,避免他们的缺点。利用非对称加密算法获得对称加密算法的秘钥,保证他的安全性;然后实际的网页内容的加密使用的是对称加密算法,利用了对称加密算法速度快的优势,hash算法主要是防止篡改的发生,是一种校验机制,最后数字证书,保证了服务器在将非对称加密算法的公钥传给浏览器时的安全性(不会被中间人篡改),同时也标志了服务器的身份

2)HTTPS的四大金刚:

非对称加密算法(对称加密算法的秘钥) + 对称加密算法(加密内容) + 数字证书(防止篡改非对称加密算法的公钥) + HASH算法(防止篡改消息)== HTTPS

3)HTTPS的本质是什么?

HTTPS的本质就是在HTTP连接发起之前,先使用SSL/TLS协议,协调客户端和服务端,在两端各自生产一个对称加密算法的秘钥,

然后使用普通的HTTP协议传输 经过对称加密算法加密的网页内容。因为对称加密算法的秘钥是安全的,所以对称加密算法加密的网页内容也是安全的。

14.backtrace 是在用户态实现的吗?能否讲讲实现它的大致思路?

显示函数调用关系(backtrace/callstack)是调试器必备的功能之一,比如在gdb里,用bt命令就可以查看backtrace。在程序崩溃的时候,函数调用关系有助于快速定位问题的根源,了解它的实现原理,可以扩充自己的知识面,在没有调试器的情况下,也能实现自己backtrace。更重要的是,分析backtrace的实现原理很有意思。现在我们一起来研究一下:

glibc提供了一个backtrace函数,这个函数可以帮助我们获取当前函数的backtrace,先看看它的使用方法,后面我们再仿照它写一个。

#include 
#include 
#include  

#define MAX_LEVEL 4 

static void test2()
{
    int i = 0;
    void* buffer[MAX_LEVEL] = {0}; 

    int size = backtrace(buffer, MAX_LEVEL); 

    for(i = 0; i < size; i++)
    {
        printf("called by %p/n",    buffer[i]);
    } 

    return;
} 

static void test1()
{
    int a=0x11111111;
    int b=0x11111112; 

    test2();
    a = b; 

    return;
} 

static void test()
{
    int a=0x10000000;
    int b=0x10000002; 

    test1();
    a = b; 

    return;
} 

int main(int argc, char* argv[])
{
    test(); 

    return 0;
}

编译运行它:
gcc -g -Wall bt_std.c -o bt_std
./bt_std

屏幕打印:
called by 0×8048440
called by 0×804848a
called by 0×80484ab
called by 0×80484c9

上面打印的是调用者的地址,对程序员来说不太直观,glibc还提供了另外一个函数backtrace_symbols,它可以把这些地址转换成源代码的位置(通常是函数名)。不过这个函数并不怎么好用,特别是在没有调试信息的情况下,几乎得不什么有用的信息。这里我们使用另外一个工具addr2line来实现地址到源代码位置的转换:

运行:
./bt_std |awk ‘{print “addr2line “$3″ -e bt_std”}’>t.sh;. t.sh;rm -f t.sh

屏幕打印:
/home/work/mine/sysprog/think-in-compway/backtrace/bt_std.c:12
/home/work/mine/sysprog/think-in-compway/backtrace/bt_std.c:28
/home/work/mine/sysprog/think-in-compway/backtrace/bt_std.c:39
/home/work/mine/sysprog/think-in-compway/backtrace/bt_std.c:48

backtrace是如何实现的呢? 在x86的机器上,函数调用时,栈中数据的结构如下:


参数N
参数… 函数参数入栈的顺序与具体的调用方式有关
参数 3
参数 2
参数 1


EIP 完成本次调用后,下一条指令的地址
EBP 保存调用者的EBP,然后EBP指向此时的栈顶。
----------------新的EBP指向这里---------------
临时变量1
临时变量2
临时变量3
临时变量…
临时变量5


(说明:下面低是地址,上面是高地址,栈向下增长的)

调用时,先把被调函数的参数压入栈中,C语言的压栈方式是:先压入最后一个参数,再压入倒数第二参数,按此顺序入栈,最后才压入第一个参数。

然后压入EIP和EBP,此时EIP指向完成本次调用后下一条指令的地址 ,这个地址可以近似的认为是函数调用者的地址。EBP是调用者和被调函数之间的分界线,分界线之上是调用者的临时变量、被调函数的参数、函数返回地址(EIP),和上一层函数的EBP,分界线之下是被调函数的临时变量。

最后进入被调函数,并为它分配临时变量的空间。gcc不同版本的处理是不一样的,对于老版本的gcc(如gcc3.4),第一个临时变量放在最高的地址,第二个其次,依次顺序分布。而对于新版本的gcc(如gcc4.3),临时变量的位置是反的,即最后一个临时变量在最高的地址,倒数第二个其次,依次顺序分布。

为了实现backtrace,我们需要:

1.获取当前函数的EBP。
2.通过EBP获得调用者的EIP。
3.通过EBP获得上一级的EBP。
4.重复这个过程,直到结束。

通过嵌入汇编代码,我们可以获得当前函数的EBP,不过这里我们不用汇编,而且通过临时变量的地址来获得当前函数的EBP。我们知道,对于gcc3.4生成的代码,当前函数的第一个临时变量的下一个位置就是EBP。而对于gcc4.3生成的代码,当前函数的最后一个临时变量的下一个位置就是EBP。

有了这些背景知识,我们来实现自己的backtrace:

#ifdef NEW_GCC
#define OFFSET 4
#else
#define OFFSET 0
#endif/*NEW_GCC*/ 

int backtrace(void** buffer, int size)
{
    int  n = 0xfefefefe;
    int* p = &n;
    int  i = 0; 

    int ebp = p[1 + OFFSET];
    int eip = p[2 + OFFSET]; 

    for(i = 0; i < size; i++)
    {
        buffer[i] = (void*)eip;
        p = (int*)ebp;
        ebp = p[0];
        eip = p[1];
    } 

    return size;
}

对于老版本的gcc,OFFSET定义为0,此时p+1就是EBP,而p[1]就是上一级的EBP,p[2]是调用者的EIP。本函数总共有5个int的临时变量,所以对于新版本gcc, OFFSET定义为5,此时p+5就是EBP。在一个循环中,重复取上一层的EBP和EIP,最终得到所有调用者的EIP,从而实现了backtrace。

现在我们用完整的程序来测试一下(bt.c):

写个简单的Makefile:

#include  

#define MAX_LEVEL 4
#ifdef NEW_GCC
#define OFFSET 4
#else
#define OFFSET 0
#endif/*NEW_GCC*/ 

int backtrace(void** buffer, int size)
{
    int     n = 0xfefefefe;
    int* p = &n;
    int     i = 0; 

    int ebp = p[1 + OFFSET];
    int eip = p[2 + OFFSET]; 

    for(i = 0; i < size; i++)
    {
        buffer[i] = (void*)eip;
        p = (int*)ebp;
        ebp = p[0];
        eip = p[1];
    } 

    return size;
} 

static void test2()
{
    int i = 0;
    void* buffer[MAX_LEVEL] = {0}; 

    backtrace(buffer, MAX_LEVEL); 

    for(i = 0; i < MAX_LEVEL; i++)
    {
        printf("called by %p/n",    buffer[i]);
    } 

    return;
} 

static void test1()
{
    int a=0x11111111;
    int b=0x11111112; 

    test2();
    a = b; 

    return;
} 

static void test()
{
    int a=0x10000000;
    int b=0x10000002; 

    test1();
    a = b; 

    return;
} 

int main(int argc, char* argv[])
{
    test(); 

    return 0;
}

CFLAGS=-g -Wall
all:
    gcc34 $(CFLAGS) bt.c -o bt34
    gcc $(CFLAGS) -DNEW_GCC  bt.c -o bt
    gcc $(CFLAGS) bt_std.c -o bt_std 

clean:
    rm -f bt bt34 bt_std

编译然后运行:
make
./bt|awk ‘{print “addr2line “$3″ -e bt”}’>t.sh;. t.sh;

屏幕打印:
/home/work/mine/sysprog/think-in-compway/backtrace/bt.c:37
/home/work/mine/sysprog/think-in-compway/backtrace/bt.c:51
/home/work/mine/sysprog/think-in-compway/backtrace/bt.c:62
/home/work/mine/sysprog/think-in-compway/backtrace/bt.c:71

对于可执行文件,这种方法工作正常。对于共享库,addr2line无法根据这个地址找到对应的源代码位置了。原因是:addr2line只能通过地址偏移量来查找,而打印出的地址是绝对地址。由于共享库加载到内存的位置是不确定的,为了计算地址偏移量,我们还需要进程maps文件的帮助:

通过进程的maps文件(/proc/进程号/maps),我们可以找到共享库的加载位置,如:

00c5d000-00c5e000 r-xp 00000000 08:05 2129013 /home/work/mine/sysprog/think-in-compway/backtrace/libbt_so.so
00c5e000-00c5f000 rw-p 00000000 08:05 2129013 /home/work/mine/sysprog/think-in-compway/backtrace/libbt_so.so

libbt_so.so的代码段加载到0×00c5d000-0×00c5e000,而backtrace打印出的地址是:
called by 0xc5d4eb
called by 0xc5d535
called by 0xc5d556
called by 0×80484ca

这里可以用打印出的地址减去加载的地址来计算偏移量。如,用 0xc5d4eb减去加载地址0×00c5d000,得到偏移量0×4eb,然后把0×4eb传给addr2line:
addr2line 0×4eb -f -s -e ./libbt_so.so

15.malloc 的指针 double free 产生的异常与访问 freed 指针有可能产生的异常有什么区别?为什么访问 freed 指针不一定产生异常?

Double Free其实就是同一个指针free两次。虽然一般把它叫做double free。其实只要是free一个指向堆内存的指针都有可能产生可以利用的漏洞。
double free的原理其实和堆溢出的原理差不多,都是通过unlink这个双向链表删除的宏来利用的。只是double free需要由自己来伪造整个chunk并且欺骗操作系统。实现对漏洞的有效利用,攻击者利用成功可导致权限提升。

一、内存问题归类
1、野指针。 指针对象指向了无效的地址,这个地址被其它对象持有了,已经属于其它对象; 或者还有一种可能, 这块内存已经被 系统回收了。 总而言之, 指针指向的内存块不能用了,要么是被别人用了,要么是被系统回收了

2、Method cache corrupted. This may be a message to an invalid object, or a memory error somewhere else. 这其实也是内存问题的一种。 这个对象你明明申请了空间,现在却说你这个对象是个无效的对象,这说明你这个对象在创建的时候,占用的内存块有问题,在内存上发生了冲突。 举例: 你调用一个系统api,将一个结构体里的buffer数据,转为另一个结构体。 你在初始化目标结构体的时候,给结构体里的data申请了1024个字节,可是系统api中,有参数指定了要拷贝4000个字节的源结构体的data到目标结构体。 这个时候, 目标结构体就会多去占用2976个字节的空间,可是你却只申请了1024个字节,这样,当你程序在运行的时候,创建对象的时候,如果又去指向那多占的2976个内存空间, 就会出现对象无效。

3、 incorrect checksum for freed object - object was probably modified after being freed. 对象被释放后,又进行了修改。

4、重复释放。 一个对象的内存已经被释放了一次,又再一次去释放。 总而言之;都是内存管理不当。

16.RunLoop 是一个不停歇在运行的死循环吗?为什么?

1.RunLoop的基本作用

1.保持程序的持续运行
2.处理app中的各种事件(比如触摸事件、定时器事件、selector事件
3.节省CPU资源,提高程序性能,有事情就做事情,没事情就休息

2.RunLoop与线程

1.关系:一个Runloop对应着一条唯一的线程
2.创建:主线程Runloop已经创建好了,子线程的runloop需要手动创建 创建方式:

NSRunLoop *currentRunloop = [NSRunLoop currentRunLoop];

子线程创建的runloop还需要手动的开启:

[currentRunloop run];

3.生命周期:Runloop在第一次获取时创建,在线程结束时销毁

3.RunLoop运行模式对NSTimer定时器的影响

1.首先创建一个定时器

 NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(task) userInfo:nil repeats:YES];

这里的task为一个普通的打印操作,这时候往控制器的view里拖一个Text View,

2.把定时器对象添加到runloop中,并且制定runloop的运行模式为默认:只有当runloop的运行模式为NSDefaultRunLoopMode的时候定时器才工作,也就是说这时候如果滑动Text View,定时器就不工作了

[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];

3.如果想在滚动Text View的时候,定时器也工作,可以:

[[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];

但是如果这样做的话,当我们停止滚动的时候定时器又不工作了

4.有时候我们需要在默认情况下以及在滚动的时候都让定时器工作,这时候我们就可以:

[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

由此可见:NSRunLoopCommonModes = NSDefaultRunLoopMode & UITrackingRunLoopMode

拓展:
①scheduledTimerWithTimeInterval方法:创建定时器并默认添加到当前线程的Runloop中指定默认运行模式

②timerWithTimeInterval:创建定时器,如果该定时器要工作还需要添加到runloop中并指定相应的运行模式

通过以上代码我们不难看出,NSTimer的定时器是受运行模式影响的,,而开发中我们有时候彻底去除这种影响,很显然,NSTimer定时器不能做到这点,这时,我们可以使用GCD的定时器。

5.GCD定时器

GCD定时器是不受运行模式的影响的,因此,以后尽量使用此定时器,,该定时器的具体参数如下所示:

-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    //01 创建定时器对象
    /*
     第一个参数:是一个宏,表示要创建的是一个定时器
     第二个参数和第三个参数:默认总是传0 描述信息
     第四个参数:队列  决定代码块(dispatch_source_set_event_handler)在哪个线程中调用(主队列-主线程)
     */
    dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(0, 0));

    //02 设置定时器
    /*
     第一个参数:定时器对象
     第二个参数:定时器开始计时的时间(开始时间)
     第三个参数:设置间隔时间 GCD的时间单位:纳秒
     第四个参数:精准度
     */
    //这句代码是设置开始两秒之后再调用
    dispatch_time_t t = dispatch_time(DISPATCH_TIME_NOW, 2.0 * NSEC_PER_SEC);
    dispatch_source_set_timer(timer, t, 2.0 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);

    //03 GCD定时器时间到了之后要执行的任务
    dispatch_source_set_event_handler(timer, ^{
        NSLog(@"GCD----%@",[NSThread currentThread]);
    });

    //04 默认是在挂起状态,需要手动恢复执行
    dispatch_resume(timer);

    //如果没有一个强指针指向,一创建就回被释放。
    self.timer = timer;

}
6. CFRunLoopModeRef
  • a.基本说明:
    CFRunloopModeRef代表着Runloop的运行模式
    一个Runloop中可以有多个mode,一个mode里面又可以有多个source\observer\timer等等
    每次runloop启动的时候,只能指定一个mode,这个mode被称为该Runloop的当前mode
    如果需要切换mode,只能先退出当前Runloop,再重新指定一个mode进入
    这样做主要是为了分割不同组的定时器等,让他们相互之间不受影响
  • b.Model的分类:
    kCFRunLoopDefaultMode:App的默认Mode,通常主线程是在这个Mode下运行
    UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动
    UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用
    GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到
    kCFRunLoopCommonModes: 这是一个占位用的Mode,不是一种真正的Mode
7.CFRunloopSourceRef

分类(根据函数调用栈来区分):
1.Source0:非基于Port的 :凡是用户主动触发事件都是Source0事件
2.Source1:基于Port的:凡是系统事件都是Source1事件

8. CFRunLoopObserverRef

作用:监听运行循环的状态

9. selecter事件与RunLoop之间的关系

默认情况下,selecter事件是被添加到当前的runloop中执行的,并且指定了运行模式为默认,由此可见,performSelecter事件是受运行模式的影响的,仔细查看以下代码,看看有什么问题:

    [NSThread detachNewThreadSelector:@selector(task) toTarget:self withObject:nil];

    -(void)task{
    [self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"Snip20160713_9"] afterDelay:3.0];
    NSLog(@"+++++");
}

一个显而易见的问题就是我们在子线程中来设置显示图片,然而抛开这个问题不管,图片依旧不会被设置上去,因为在task中缺少一个运行循环,我们需要手动开启一个子运行循环才可以。

继续查看一下代码,图片会被设置到imageView上面吗?

    [NSThread detachNewThreadSelector:@selector(task) toTarget:self withObject:nil];

    -(void)task{
    NSRunLoop *runloop = [NSRunLoop currentRunLoop];
    [runloop run];

    [self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"Snip20160713_9"] afterDelay:3.0];
    NSLog(@"+++++");
}

答案是否定的,因为我们虽然开启了子运行循环,但是当我们开启这个循环的时候,当前循环里既没有source事件(包括timer事件),也没有selecter事件,于是循环立刻就退出了。正确的书写方式如下:

    [NSThread detachNewThreadSelector:@selector(task) toTarget:self withObject:nil];

    -(void)task{
    [self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"Snip20160713_9"] afterDelay:3.0];
      NSRunLoop *runloop = [NSRunLoop currentRunLoop];
    [runloop run];
    NSLog(@"+++++");
}

注意:runLoop是一个死循环,因此++++是不会打印的

10.使用RunLoop实现常驻线程

众所周知,我们手动开启的子线程在执行完任务之后就会销毁,而有时候我们需要一个子线程在执行完当前任务后,不要销毁,等我们需要的时候再来执行其它任务,这就用到了常驻线程。

  • 假设我们又这样一个需求,但我们点击按钮1的时候会开启一条子线程来执行run1任务,当我们点击按钮2的时候,再让刚才的线程来执行run2任务,具体实现代码如下:
- (IBAction)btn1:(id)sender {
    //01 创建线程,执行任务
    NSThread *thread = [[NSThread alloc]initWithTarget:self selector:@selector(run1) object:nil];

    //02 执行任务
    [thread start];

    self.thread = thread;
}

为了不让刚开启的线程销毁,我们需要给它添加一个运行循环,保证它不释放:

-(void)run1
{
    NSLog(@"run1---%@",[NSThread currentThread]);
    //001 获得当前线程对应的runloop对象
    NSRunLoop *currentRunloop = [NSRunLoop currentRunLoop];

    //002 为runloop添加input soucre或者是timer souce或selecter事件(最好就是一个基于端口的事件,这样就不会去执行不必要的方法)
    [currentRunloop addPort:[NSPort port] forMode:NSDefaultRunLoopMode];

//以下为其它保证程序运行的方案,不推荐使用。
    //[self performSelector:@selector(run3) withObject:nil afterDelay:3.0];
    //[NSTimer scheduledTimerWithTimeInterval:3.0 target:self selector:@selector(timerTest) userInfo:nil repeats:YES];

    //003 启动runloop
    //runUntilDate |run 内部都指定了运行模式为默认
    [currentRunloop run];

}

按钮2:

- (IBAction)goOnBtnClick:(id)sender {
    //让之前创建的线程继续执行
    [self performSelector:@selector(run2) onThread:self.thread withObject:nil waitUntilDone:YES];
}

-(void)run2
{
    NSLog(@"run2---%@",[NSThread currentThread]);
}
11.RunLoop的自动释放池

runloop的自动释放池什么时候创建释放?

  1. 当runloop进入的时候会创建一个自动释放池。
    2)当runloop退出的时候会把之前的自动释放池销毁。
    3)当runloop即将进入休眠的时候会把之前的自动释放池先销毁,然后创建一个新的自动释放池。


    iOS进阶面试题(二)_第1张图片
    image.png

17.看过 runtime 的源码吗?源码中常有的 fastpath、slowpath 是什么?

fastpath和slowpath的区别在于,fastpath要求zone的unmapped file page必须大于zone规定的min_unmapped_pages,slab reclaimable大于min_slab_pages,回收内存的的页数为2^order个页数和是32个之间的最大值,而kswapd回收内存需要回收到所有的zone都满足free page大于high watermark值或者zone的high watermark值个页面。同时fastpath不会进行回写,也不会回收mapped的page。fastpath主要靠zone_reclaim来完成快速的内存回收。kswap则主要从balance_pgdat()来完成

direct reclaim和kswapd的差别在于只能回收32个页面,同时kswapd进行回写页面需要满足特定条件(当前有很多的页面需要等待writeback),而direct reclaim判断当回收过程中扫描的总页数大于48个时就会启动flush线程来进行脏页的回写操作。

我们在调性能时候都会尽量的提高extra_free_kbytes的值来防止过多的出现direct reclaim,因为direct reclaim会进行脏页的回写,这里再IO性能不是很好的时候会造成系统严重卡顿。

18.runtime 中 SideTables(不是 SideTable)存在的意义是什么?

1.数据结构分析(* SideTables、RefcountMap、weak_table_t*)

为了管理所有对象的引用计数和weak指针,苹果创建了一个全局的SideTables,虽然名字后面有个"s"不过他其实是一个全局的Hash表,里面的内容装的都是SideTable结构体而已。它使用对象的内存地址当它的key。管理引用计数和weak指针就靠它了。
    因为对象引用计数相关操作应该是原子性的。不然如果多个线程同时去写一个对象的引用计数,那就会造成数据错乱,失去了内存管理的意义。同时又因为内存中对象的数量是非常非常庞大的需要非常频繁的操作SideTables,所以能对整个Hash表加锁。苹果采用了分离锁技术。

iOS进阶面试题(二)_第2张图片
image.png

举例分析
假设有80个学生需要咱们安排住宿,同时还要保证学生们的财产安全。应该怎么安排?
显然不会给80个学生分别安排80间宿舍,然后给每个宿舍的大门上加一把锁。那样太浪费资源了锁也挺贵的,太多的宿舍维护起来也很费力气。
我们一般的做法是把80个学生分配到10间宿舍里,每个宿舍住8个人。假设宿舍号分别是101、102 、... 110。然后再给他们分配床位,01号床、02号床等。然后给每个宿舍配一把锁来保护宿舍内同学的财产安全。为什么不只给整个宿舍楼上一把锁,每次有人进出的时候都把整个宿舍楼锁上?显然这样会造成宿舍楼大门口阻塞。
OK假如现在有人要找102号宿舍的2号床的人聊天。这个人会怎么做?

1、找到宿舍楼(SideTables)的宿管,跟他说自己要找10202(内存地址当做key)。
2、宿管带着他SideTables[10202]找到了102宿舍SideTable,然后把102的门一锁lock,在他访问102期间不再允许其他访客访问102了。(这样只是阻塞了102的8个兄弟的访问,而不会影响整栋宿舍楼的访问)
3、然后在宿舍里大喊一声:"2号床的兄弟在哪里?"table.refcnts.find(02)你就可以找到2号床的兄弟了。
4、等这个访客离开的时候会把房门的锁打开unlock,这样其他需要访问102的人就可以继续进来访问了。

SideTables == 宿舍楼
SideTable == 宿舍
RefcountMap里存放着具体的床位

苹果之所以需要创造SideTables的Hash冲突是为了把对象放到宿舍里管理,把锁的粒度缩小到一个宿舍SideTable。RefcountMap的工作是在找到宿舍以后帮助大家找到正确的床位的兄弟。

2. SideTable

当我们通过SideTables[key]来得到SideTable的时候
1.一把自旋锁。spinlock_t  slock;

自旋锁比较适用于锁使用者保持锁时间比较短的情况。正是由于自旋锁使用者一般保持锁时间非常短,因此选择自旋而不是睡眠是非常必要的,自旋锁的效率远高于互斥锁。信号量和读写信号量适合于保持时间较长的情况,它们会导致调用者睡眠,因此只能在进程上下文使用,而自旋锁适合于保持时间非常短的情况,它可以在任何上下文使用。

它的作用是在操作引用技术的时候对SideTable加锁,避免数据错误。

2.引用计数器 RefcountMap  refcnts;
对象具体的引用计数数量是记录在这里的。
    这里注意RefcountMap其实是个C++的Map。为什么Hash以后还需要个Map?其实苹果采用的是分块化的方法。
    举个例子
    假设现在内存中有16个对象。
0x0000、0x0001、...... 0x000e、0x000f
    咱们创建一个SideTables[8]来存放这16个对象,那么查找的时候发生Hash冲突的概率就是八分之一。
    假设SideTables[0x0000]和SideTables[0x0x000f]冲突,映射到相同的结果。

SideTables[0x0000] == SideTables[0x0x000f] ==> 都指向同一个SideTable

  • RefcountMap的工作是在找到宿舍以后帮助大家找到正确的床位的兄弟。
  1. 维护weak指针的结构体 weak_table_t   weak_table;
    这里是一个两层结构。
  • 第一层结构体中包含两个元素。
        第一个元素weak_entry_t *weak_entries;是一个数组,上面的RefcountMap是要通过find(key)来找到精确的元素的。weak_entries则是通过循环遍历来找到对应的entry。
        (上面管理引用计数器苹果使用的是Map,这里管理weak指针苹果使用的是数组,有兴趣的朋友可以思考一下为什么苹果会分别采用这两种不同的结构)
        第二个元素num_entries是用来维护保证数组始终有一个合适的size。比如数组中元素的数量超过3/4的时候将数组的大小乘以2。

  • 第二层weak_entry_t的结构包含3个部分
    1.referent:
    被指对象的地址。前面循环遍历查找的时候就是判断目标地址是否和他相等。
    2.referrers
    可变数组,里面保存着所有指向这个对象的弱引用的地址。当这个对象被释放的时候,referrers里的所有指针都会被设置成nil。
    3.inline_referrers
    只有4个元素的数组,默认情况下用它来存储弱引用的指针。当大于4个的时候使用referrers来存储指针。

19.为什么 ARC 环境下不允许我们调用 [super dealloc]?

1.dealloc 的使用

a. 什么情况下会调用呢?
当对象的引用计数为0,系统会自动调用dealloc方法,回收内存。

//调用方法
-(void)dealloc{
   // [super dealloc];    //ARC环境下不需要调用。因为系统会 自动调用该方法帮助释放父类对象。
}

b.调用的顺序
一般说调用的顺序是,当子类的对象释放完时,然后再释放父类的所拥有的实例。这一点与调用初始化方法,正好相反

2.dealloc 误区

我们在开发过程中,用到dealloc,却因不会意识得到对象的引用计数是不是为0,dealloc到底走了没走,因而导致内存暴增,还会遇到很多奇怪的问题。我们需要知道dealloc不被调用的几种情况?
1.controller中使用了计时器 NSTimer 使用后没有销毁 导致循环引用

self.playerTimer = [NSTimerscheduledTimerWithTimeInterval:1target:selfselector:@selector(playProgressAction)userInfo:nilrepeats:YES];

使用后记得销毁
   [_playerTimerinvalidate]
    _playerTimer =nil;

2.协议delegate 应该使用weak修饰,否则会引起循环引用 不能释放内存
@property (nonatomic,weak)iddelegate;

3.使用到block的地方,block回调中不能直接使用self 否则可能引起循环引用。

__weaktypeof(self) weakSelf =self;
    _audioStream.onCompletion=^(){
        [weakSelf nextButtonAction];
    };

  1. NSNotificationCenter 记得注销
-(void)dealloc
{
    [[NSNotificationCenter defaultCenter] removeObserver:self name:@"delectOrGoDownProject" object:nil];
    [[NSNotificationCenter defaultCenter] removeObserver:self name:@"changerInfoItem" object:nil];
}

20.Objective-C 是如何保证系统升级后的 ABI 稳定性的?

自由发挥

你可能感兴趣的:(iOS进阶面试题(二))