iOS面试点总结

1,350 阅读1小时+

1, NSOperation与GCD的区别 NSThread

有关多线程

线程: 一个线程中任务的执行是串行的 同一时间内,一个线程只能执行一个任务 线程是进程中的一条执行路径

多线程: 一个进程中可以开启多条线程,多条线程可以并行(同时)执行不同的任务

多线程原理: 同一时间,CPU只能处理1条线程,只有一条线程在工作(执行) 多线程并发(同时)执行,其实是CPU快速在多条线程之间调度(切换) 多核CPU是真正意义上的多线程 CPU在多个线程之间调度,会消耗大量的CPU资源 每条线程被调度执行的频次会降低(线程执行效率会降低)

优点: 能适当提高程序的执行效率

缺点: 创建线程是有开销的,iOS下主要成本包括:内核数据结构(大约1KB)、栈空间(子线程512KB、主线程1MB) 也可以使用-setStackSize设置,但必须是4K的倍数,而且最小为16K,创建线程大约需要90毫秒的创建时间 开启大量的线程,会降低程序的性能 线程越多,CPU在调度线程上的开销越大 程序设计更加复杂:比如线程之间的通信、多线程的数据共享

多线程的安全隐患

资源共享: 一块资源可能会被多个线程共享,比如多个线程访问同一个对象、对一个变量、同一个文件

解决方法:

互斥锁
@synchronized(锁对象){ //需要锁定的代码 }
不推荐使用,消耗性能
锁定1份代码只能用1把锁,用多把是无效的
线程同步技术
原子和非原子属性
atomic 默认 原子属性 为setter方法加锁
线程安全,需要消耗大量的资源
nonatomic 非原子属性 不会为setter方法加锁
非线程安全,适合内存小的移动设备
iOS有三种多线程编程的技术

(一)NSThread

(二)Cocoa NSOperation

(三)GCD(全称:Grand Central Dispatch)

(四) pthread C 通用的多线程API,跨平台,程序员手动管理线程生命周期,使用难度大,iOS中几乎不用

这三种编程方式从上到下,抽象度层次是从低到高的,抽象度越高的使用越简单,也是Apple最推荐使用的。

三种方式的优缺点介绍:

 1)NSThread:优点:NSThread 比其他两个轻量级缺点:需要自己管理线程的生命周期,线程同步。线程同步对数据的加锁会有一定的系统开销
 2)Cocoa NSOperation优点:不需要关心线程管理,数据同步的事情,可以把精力放在自己需要执行的操作上。Cocoa operation 相关的类是 NSOperation ,NSOperationQueue。NSOperation是个抽象类,使用它必须用它的子类,可以实现它或者使用它定义好的两个子类:NSInvocationOperation 和 NSBlockOperation。
 创建NSOperation子类的对象,把对象添加到NSOperationQueue队列里执行。
 3)GCDGrand Central Dispatch (GCD)是Apple开发的一个多核编程的解决方法。在iOS4.0开始之后才能使用。GCD是一个替代诸如NSThread, NSOperationQueue, NSInvocationOperation等技术的很高效和强大的技术

NSOperation是苹果提供给我们的一套多线程解决方案。实际上NSOperation是基于GCD更高一层的封装,但是比GCD更简单易用、代码可读性也更高。 NSOperation需要配合NSOperationQueue来实现多线程。因为默认情况下,NSOperation单独使用时系统同步执行操作,并没有开辟新线程的能力,只有配合NSOperationQueue才能实现异步执行。 因为NSOperation是基于GCD的,那么使用起来也和GCD差不多,其中,NSOperation相当于GCD中的任务,而NSOperationQueue则相当于GCD中的队列。NSOperation实现多线程的使用步骤分为三步: 1 .创建任务:先将需要执行的操作封装到一个NSOperation对象中。 2 .创建队列:创建NSOperationQueue对象。 3 .将任务加入到队列中:然后将NSOperation对象添加到NSOperationQueue中。 之后呢,系统就会自动将NSOperationQueue中的NSOperation取出来,在新线程中执行操作。 NSOperation是个抽象类,并不能封装任务。我们只有使用它的子类来封装任务。我们有三种方式来封装任务。 使用子类NSInvocationOperation 使用子类NSBlockOperation 定义继承自NSOperation的子类,通过实现内部相应的方法来封装任务。 NSOperation和NSOperationQueue最吸引人的地方是它能添加操作之间的依赖关系。比如说有A、B两个操作,其中A执行完操作,B才能执行操作,那么就需要让B依赖于A 注意: (1.)优先级只能应用于相同queue中的operations。 (2.)操作的优先级高低不等于操作在队列中排列的顺序。换句话说,优先级高的操作不代表一定排在队列的前面。后入队的操作有可能因为优先级高而先被执行。PS:操作在队列中的顺序取决于队列的addOperation:方法。(证明代码如下) (3.)优先级高只代表先被执行。不代表操作先被执行完成。执行完成的早晚还取决于操作耗时长短。 (4.)优先级不能替代依赖,优先级也绝不等于依赖。优先级只是对已经准备好的操作确定其执行顺序。 (5.)操作的执行优先满足依赖关系,然后再满足优先级。即先根据依赖执行操作,然后再从所有准备好的操作中取出优先级最高的那一个执行。 <1>GCD GCD是iOS4.0推出的,主要针对多核cpu做了优化,是C语言的技术 GCD是将任务(block)添加到队列(串行/并行/全局/主队列),并且以同步/异步的方式执行任务的函数,任务的取出遵循队列的FIFO原则:先进先出,后进后出 GCD提供了一些NSOperation不具备的功能 一次性执行:可以保证某一段代码在程序运行的过程中只被执行一次;一次性执行是线程安全的,在多线程环境下也是只执行一次;应用场景:设计单例模式 延迟执行:既实现等待多长时间后在哪个队列中执行什么代码块 调度组:监听一组异步任务执行结束之后,我们能够得到统一的通知;注意:在其调度组内的任务执行完毕后执行后面的”刷新主界面”方法与”玩完”之间的执行没有先后顺序

  • GCD如何取消任务 第一种:dispatch_block_cancel iOS8之后可以调用dispatch_block_cancel来取消(需要注意必须用dispatch_block_create创建dispatch_block_t) 第二种:定义外部变量,用于标记block是否需要取消 该方法是模拟NSOperation,在执行block前先检查isCancelled = YES ?在block中及时的检测标记变量,当发现需要取消时,终止后续操作(如直接返回return)。 <2>NSOperation NSOperation是iOS2.0推出的,iOS4.0之后重写了NSOperation NSOperation将操作(异步的任务)添加到队列(并发队列),就会执行指定操作的方法 NSOperation里提供的方便的操作 最大并发数, 队列的暂定/继续 取消队列中所有的操作 指定操作之间的依赖关系(GCD可以用同步实现,但是比较麻烦)

同步和异步决定了要不要开启新的线程(同步不开,异步开) 同步:在当前线程中执行任务,不具备开启新线程的能力 异步:在新的线程中执行任务,具备开启新线程的能力

  • 串行和并发决定了任务的执行方式 并发:多个任务并发(同时)执行 串行:一个任务执行完毕后,再执行下一个任务
  • 当任务是异步的时候,队列决定了开启多少条线程 串行队列:只开一条 并发队列:可以开启多条
  • 主队列特点:主队列中的任务,只有主线程空闲的时候才会调度任务执行 主队列又叫全局串行队列,程序启动的时候就创建了主队列,在使用的时候不需要创建,直接GET.主队列中的任务是要在主线程执行的.
  • 主队列,异步任务 不开线程,同步执行
  • 主队列,同步执行 程序执行不出来(死锁) 死锁的原因,当程序执行到下面这段代码的时候 主队列:如果主线程正在执行代码,就不调度任务 同步执行:如果第一个任务没有执行,就继续等待第一个任务执行完成,再执行下一个任务此时互相等待,程序无法往下执行(死锁)
dispatch_sync(dispatch_get_main_queue(),^{

NSLog(@"%@ -- %d",[NSThreadcurrentThread],i);

});
  • 主队列和串行队列的区别 串行队列:必须等待一个任务执行完成,再调度另一个任务 主队列:以先进先出调度任务,如果主线程上有代码在执行,主队列不会调度任务 <3>综合比较其各自使用范围如下 性能:①:GCD更接近底层,而NSOperationQueue则更高级抽象,所以GCD在追求性能的底层操作来说,是速度最快的。这取决于使用Instruments进行代码性能分析,如有必要的话 ②:从异步操作之间的事务性,顺序行,依赖关系。GCD需要自己写更多的代码来实现,而NSOperationQueue已经内建了这些支持 ③:如果异步操作的过程需要更多的被交互和UI呈现出来,NSOperationQueue会 是一个更好的选择。底层代码中,任务之间不太互相依赖,而需要更高的并发能力,GCD则更有优势

2,TCP,UDP和socket,Http

一,http与https有什么区别?http有哪些方式 1、https协议需要到ca申请证书,一般免费证书较少,因而需要一定费用。 2、http是超文本传输协议,信息是明文传输,https则是具有安全性的ssl加密传输协议。 3、http和https使用的是完全不同的连接方式,用的端口也不一样,前者是80,后者是443。 4、http的连接很简单,是无状态的;HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,比http协议安全 http请求中的8种请求方法

1、opions 返回服务器针对特定资源所支持的HTML请求方法 或web服务器发送*测试服务器功能(允许客户端查看服务器性能)

2、Get 向特定资源发出请求(请求指定页面信息,并返回实体主体)

3、Post 向指定资源提交数据进行处理请求(提交表单、上传文件),又可能导致新的资源的建立或原有资源的修改

4、Put 向指定资源位置上上传其最新内容(从客户端向服务器传送的数据取代指定文档的内容)

5、Head 与服务器索与get请求一致的相应,响应体不会返回,获取包含在小消息头中的原信息(与get请求类似,返回的响应中没有具体内容,用于获取报头)

6、Delete 请求服务器删除request-URL所标示的资源*(请求服务器删除页面)

7、Trace 回显服务器收到的请求,用于测试和诊断

8、Connect HTTP/1.1协议中能够将连接改为管道方式的代理服务器 Get请求和Post请求的区别 (1)使用Get请求时,参数在URL中显示,而使用Post请求,则不会显示出来; (2)Post传输的数据量大,可以达到2M,而Get方法由于受到URL长度的限制,只能传递大约1024字节. (3)Get请求请求需注意缓存问题,Post请求不需担心这个问题; (4)Post请求必须设置Content-Type值为application/x-form-www-urlencoded; (5)发送请求时,因为Get请求的参数都在url里,所以send函数发送的参数为null,而Post请求在使用send方法时,却需赋予其参数; (6)GET方式请求的数据会被浏览器缓存起来,因此其他人就可以从浏览器的历史记录中读取到这些数据,例如账号和密码等。在某种情况下,GET方式会带来严重的安全问题。而POST方式相对来说就可以避免这些问题。 GET请求的参数在URL中,在请求的第一行Request-Line中,而POST请求的参数在请求主体Message-Body中

http协议工作原理
HTTP工作过程
一次HTTP操作称为一个事务,其工作整个过程如下:
1 ) 、地址解析,
 如用客户端浏览器请求这个页面:http://localhost.com:8080/index.htm
 从中分解出协议名、主机名、端口、对象路径等部分,对于我们的这个地址,解析得到的结果如下:
 协议名:http
 主机名:localhost.com
 端口:8080
 对象路径:/index.htm
 在这一步,需要域名系统DNS解析域名localhost.com,得主机的IP地址。

 2)、封装HTTP请求数据包
 把以上部分结合本机自己的信息,封装成一个HTTP请求数据包

3)封装成TCP包,建立TCP连接(TCP的三次握手)
在HTTP工作开始之前,客户机(Web浏览器)首先要通过网络与服务器建立连接,该连接是通过TCP来完成的,该协议与IP协议共同构建Internet,即著名的TCP/IP协议族,因此Internet又被称作是TCP/IP网络。HTTP是比TCP更高层次的应用层协议,根据规则,只有低层协议建立之后才能,才能进行更层协议的连接,因此,首先要建立TCP连接,一般TCP连接的端口号是80。这里是8080端口
4)客户机发送请求命令
 建立连接后,客户机发送一个请求给服务器,请求方式的格式为:统一资源标识符(URL)、协议版本号,后边是MIME信息包括请求修饰符、客户机信息和可内容。
 5)服务器响应
服务器接到请求后,给予相应的响应信息,其格式为一个状态行,包括信息的协议版本号、一个成功或错误的代码,后边是MIME信息包括服务器信息、实体信息和可能的内容。
 实体消息是服务器向浏览器发送头信息后,它会发送一个空白行来表示头信息的发送到此为结束,接着,它就以Content-Type应答头信息所描述的格式发送用户所请求的实际数据
6)服务器关闭TCP连接
一般情况下,一旦Web服务器向浏览器发送了请求数据,它就要关闭TCP连接,然后如果浏览器或者服务器在其头信息加入了这行代码
Connection:keep-alive
 TCP连接在发送后将仍然保持打开状态,于是,浏览器可以继续通过相同的连接发送请求。保持连接节省了为每个请求建立新连接所需的时间,还节约了网 络带宽。
如何用Charles抓HTTPS的包?其中原理和流程是什么
我们直接描述 HTTPS 的通信流程:

对于报文的加密:有对称加密和非对称加密方式,HTTPS 采用混合加密机制,在交换密钥环节,使用非对称加密,在之后通信交换报文阶段则使用对称加密方式。
对于验证通信双方身份:使用 CA 证书来校验身份。
SSL 协议具有完整性校验
HTTPS 大概流程如下,详细的流程图可以参考下文末链接;
客户端将自己支持的加密算法发送给服务器,请求服务器证书;
服务器选取一组加密算法,并将证书返回给客户端;
客户端校验证书合法性,生成随机对称密钥,用公钥加密后发送给服务器;
服务器用私钥解密出对称密钥,返回一个响应,HTTPS连接建立完成;
随后双方通过这个对称密钥进行安全的数据通信。
Charles 抓包原理

Charles 通过将自己设置成系统的网络访问代理服务器,使得所有的网络访问请求都通过它来完成,从而实现了网络封包的截取和分析(这里类似中间人攻击)。配合 Charles 的 SSL 功能,Charles 还可以分析 HTTPS 协议。
Charles 抓取 HTTPS 原理及流程
前面说过,HTTPS 可以有效防止中间人攻击,那 Charles 是如何抓取 HTTPS 包的呢
?

Charles 作为一个“中间人代理”,当浏览器和服务器通信时,Charles接收服务器的证书,但动态生成一张证书发送给浏览器,也就是说Charles作为中间代理在浏览器和服务器之间通信,所以通信的数据可以被Charles拦截并解密。由于Charles更改了证书,浏览器校验不通过会给出安全警告,必须安装Charles的证书后才能进行正常访问。

这里有个大前提,就是客户端信任了 Charles 自己制作的证书,然后导致 Charles 拿到 CA 证书和对称加密的公开密钥;这里我们简单总结下:

首先 Charles 假冒了客户端,拿到服务器的 CA 证书

然后 Charles 假冒了服务器,给客户端发送了一张自己制作的证书,客户端信任该证书
Charles 再次假冒服务器,拿到客户端的对称密钥

Charles 再次假冒客户端,将对称密钥加密发送给服务器,让服务器认为这次通信是没问题的,服务器发送成功响应
最后 Charles 假冒服务器将成功响应发给客户端
建立连接,客户端与 Charles 建立连接,Charles 与服务器建立连接(中间人攻击)

三次握手分别发送了什么 第一次握手:建立连接时,客户端发送syn包(syn=j)到服务器,并进入SYN_SENT状态,等待服务器确认;SYN:同步序列编号(Synchronize Sequence Numbers)。

第二次握手:服务器收到syn包,必须确认客户的SYN(ack=j+1),同时自己也发送一个SYN包(syn=k),即SYN+ACK包,此时服务器进入SYN_RECV状态;

第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED(TCP连接成功)状态,完成三次握手。

释放四次 1)客户端进程发出连接释放报文,并且停止发送数据。释放数据报文首部,FIN=1,其序列号为seq=u(等于前面已经传送过来的数据的最后一个字节的序号加1),此时,客户端进入FIN-WAIT-1(终止等待1)状态。 TCP规定,FIN报文段即使不携带数据,也要消耗一个序号。 2)服务器收到连接释放报文,发出确认报文,ACK=1,ack=u+1,并且带上自己的序列号seq=v,此时,服务端就进入了CLOSE-WAIT(关闭等待)状态。TCP服务器通知高层的应用进程,客户端向服务器的方向就释放了,这时候处于半关闭状态,即客户端已经没有数据要发送了,但是服务器若发送数据,客户端依然要接受。这个状态还要持续一段时间,也就是整个CLOSE-WAIT状态持续的时间。 3)客户端收到服务器的确认请求后,此时,客户端就进入FIN-WAIT-2(终止等待2)状态,等待服务器发送连接释放报文(在这之前还需要接受服务器发送的最后的数据)。 4)服务器将最后的数据发送完毕后,就向客户端发送连接释放报文,FIN=1,ack=u+1,由于在半关闭状态,服务器很可能又发送了一些数据,假定此时的序列号为seq=w,此时,服务器就进入了LAST-ACK(最后确认)状态,等待客户端的确认。 5)客户端收到服务器的连接释放报文后,必须发出确认,ACK=1,ack=w+1,而自己的序列号是seq=u+1,此时,客户端就进入了TIME-WAIT(时间等待)状态。注意此时TCP连接还没有释放,必须经过2∗∗MSL(最长报文段寿命)的时间后,当客户端撤销相应的TCB后,才进入CLOSED状态。 6)服务器只要收到了客户端发出的确认,立即进入CLOSED状态。同样,撤销TCB后,就结束了这次的TCP连接。可以看到,服务器结束TCP连接的时间要比客户端早一些 常见面试题

【问题1】为什么连接的时候是三次握手,关闭的时候却是四次握手?

答:因为当Server端收到Client端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。但是关闭连接时,当Server端收到FIN报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,告诉Client端,"你发的FIN报文我收到了"。只有等到我Server端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送。故需要四步握手。

【问题2】为什么TIME_WAIT状态需要经过2MSL(最大报文段生存时间)才能返回到CLOSE状态?

答:虽然按道理,四个报文都发送完毕,我们可以直接进入CLOSE状态了,但是我们必须假象网络是不可靠的,有可以最后一个ACK丢失。所以TIME_WAIT状态就是用来重发可能丢失的ACK报文。在Client发送出最后的ACK回复,但该ACK可能丢失。Server如果没有收到ACK,将不断重复发送FIN片段。所以Client不能立即关闭,它必须确认Server接收到了该ACK。Client会在发送出ACK之后进入到TIME_WAIT状态。Client会设置一个计时器,等待2MSL的时间。如果在该时间内再次收到FIN,那么Client会重发ACK并再次等待2MSL。所谓的2MSL是两倍的MSL(Maximum Segment Lifetime)。MSL指一个片段在网络中最大的存活时间,2MSL就是一个发送和一个回复所需的最大时间。如果直到2MSL,Client都没有再次收到FIN,那么Client推断ACK已经被成功接收,则结束TCP连接。

TCP,UDP ,Socket的区别

http连接:http协议即超文本传送协议,是web联网的基础,也是手机联网常用协议之一,http协议是建立在tcp协议之上的一种应用。

http连接最显著的特点是客户端发送的每次请求都需要服务器回送响应,在请求结束后,会主动释放连接。从建立连接到关闭连接的过程称为“一次连接”。       1)在HTTP 1.0中,客户端的每次请求都要求建立一次单独的连接,在处理完本次请求后,就自动释放连接。        2)在HTTP 1.1中则可以在一次连接中处理多个请求,并且多个请求可以重叠进行,不需要等待一个请求结束后再发送下一个请求。

由于http在每次请求结束后都会主动释放连接,因此http连接是“短连接”。要保持客户端程序的在线状态,需要不断地向服务器发起连接请求。通常的 做法是即时不需要获得任何数据,客户端也保持每隔一段固定的时间向服务器发送一次“保持连接”的请求,服务器在收到该请求后对客户端进行回复,表明知道客 户端“在线”。若服务器长时间无法收到客户端的请求,则认为客户端“下线”,若客户端长时间无法收到服务器的回复,则认为网络已经断开。

Socket 套接字(socket)是通信的基石,是支持TCP/IP协议的网络通信的基本操作单元。

应用层通过传输层进行数据通信时,TCP会遇到同时为多个应用程序进程提供并发服务的问题。多个TCP连接或多个应用程序进程可能需要通过同一个 TCP协议端口传输数据。为了区别不同的应用程序进程和连接,许多计算机操作系统为应用程序与TCP/IP协议交互提供了套接字(Socket)接口。应用层可以和传输层通过Socket接口,区分来自不同应用程序进程或网络连接的通信,实现数据传输的并发服务。

Socket连接与TCP连接

创建Socket连接时,可以指定使用的传输层协议,Socket可以支持不同的传输层协议(TCP或UDP),当使用TCP协议进行连接时,该Socket连接就是一个TCP连接。

Socket连接与HTTP连接

由于通常情况下 Socket 连接就是TCP连接,因此 Socket 连接一旦建立,通信双方即可开始相互发送数据内容,直到双方连接断开。但在实际网络应用中,客户端到服务器之间的通信往往需要穿越多个中间节点,例如路由器、网关、防火墙等,大部分防火墙默认会关闭长时间处于非活跃状态的连接而导致 Socket连接断连,因此需要通过轮询告诉网络,该连接处于活跃状态。而HTTP连接使用的是“请求—响应”的方式,不仅在请求时需要先建立连接,而且需要客户端向服务器发出请求后,服务器端才能回复数据。很多情况下,需要服务器端主动向客户端推送数据,保持客户端与服务器数据的实时与同步。此时若双方建立的是Socket连接,服务器就可以直接将数据传送给 客户端;若双方建立的是HTTP连接,则服务器需要等到客户端发送一次请求后才能将数据传回给客户端,因此,客户端定时向服务器端发送连接请求,不仅可以保持在线,同时也是在“询问”服务器是否有新的数据,如果有就将数据传给客户端。

TCP与UDP的区别

TCP:面向连接、传输可靠(保证数据正确性,保证数据顺序)、用于传输大量数据(流模式)、速度慢,建立连接需要开销较多(时间,系统资源)。

UDP:面向非连接、传输不可靠、用于传输少量数据(数据包模式)、速度快。

网络深度优化的点
  • NSCache缓存、Last-Modified、ETag
  • 失败重发、缓存请求有网发送
  • DNS解析
  • 数据压缩:protobuf,WebP
  • 弱网:2G、3G、4G、wifi下设置不同的超时时间
  • TCP对头阻塞:GOOGLE提出QUIC协议,相当于在UDP协议之上再定义一套可靠传输协议

Runloop,线程,自动释放池三者的关系,Runloop的作用是什么

App启动后,苹果在主线程 RunLoop 里注册了两个 Observer,其回调都是 _wrapRunLoopWithAutoreleasePoolHandler()。 第一个 Observer 监视的事件是 Entry(即将进入Loop),其回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池。其 order 是-2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。 第二个 Observer 监视了两个事件: BeforeWaiting(准备进入休眠) 时调用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 释放旧的池并创建新池;Exit(即将退出Loop) 时调用 _objc_autoreleasePoolPop() 来释放自动释放池。这个 Observer 的 order 是 2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。 在主线程执行的代码,通常是写在诸如事件回调、Timer回调内的。这些回调会被 RunLoop 创建好的 AutoreleasePool 环绕着,所以不会出现内存泄漏,开发者也不必显示创建 Pool 了。 自动释放池的实现: 一个线程的自动释放池是一个指针堆栈 每一个指针或者指向被释放的对象,或者是自动释放池的POOL_BOUNDARY,POOL_BOUNDARY 是自动释放池的边界。 一个池子的 token 是指向池子 POOL_BOUNDARY 的指针。当池子被出栈的时候,每一个高于标准的对象都会被释放掉。 堆栈被分成一个页面的双向链表。页面按照需要添加或者删除。 本地线程存放着指向当前页的指针,在这里存放着新创建的自动释放的对象。

自动释放池是由 AutoreleasePoolPage 以双向链表的方式实现的,当对象调用 autorelease 方法时,会将对象加入 AutoreleasePoolPage 的栈中,调用 AutoreleasePoolPage::pop 方法会向栈中的对象发送 release 消息. 每一个自动释放池都是由一系列的 AutoreleasePoolPage 组成的,并且每一个 AutoreleasePoolPage 的大小都是 4096 字节(16 进制 0x1000) AutoreleasepoolPage 通过压栈的方式来存储每个需要自动释放的对象

  • autorelease后对象就释放了吗https://www.jianshu.com/p/f6e8fdd475e1 1.AutoreleasePool并没有单独的结构,而是由若干个AutoreleasePoolPage以双向链表的形式组合而成(分别对应结构中的parent指针和child指针)。2.AutoreleasePool是按线程一一对应的(结构中的thread指针指向当前线程)。3.AutoreleasePoolPage每个对象会开辟4096字节内存(也就是虚拟内存一页的大小),除了上面的实例变量所占空间,剩下的空间全部用来储存autorelease对象的地址。4.上面的id *next指针作为游标指向栈顶最新注册进来的autorelease对象的下一个位置。5.一个AutoreleasePoolPage的空间被占满时,会新建一个AutoreleasePoolPage对象,连接链表(用结构中的parent指针和child指针分别指向旧的AutoreleasePoolPage和新的AutoreleasePoolPage),后来的autorelease对象在新的page加入 当next 指针在栈顶的低一度内存时,当再注册一个对象进来时,那么系统就会创建一个新的AutoreleasePoolPage来保存对象,新的parent指针指向旧的AutoreleasePoolPage,形成链表的结构一样,保证释放和新建AutoreleasePool能有序进行。每向一个对象发送 autorelease 消息,就是把对象加入到next 指针处,next 指针向高内存偏移一个位置(位置大小取决于新添加进来的对象内存的大小) 这是自动释放对象add进来的大概原理。

好了,自动释放对象加进来的大概原理我们清楚了,那么反过来,对象释放的原理也差不多了。。。 慢着,释放时究竟我要释放到那个位置呢?凭借什么去标记呢?内涵在于每当objc_autoreleasePoolPush被执行时,runtime会向当前AutoreleasePoolPage加入一个标记,称为“哨兵指针”,(我很喜欢这个名字)这个哨兵指针内容是nil AutoreleasePoolPop主要做一下事情:1.AutoreleasePoolPop要传入的参数就是这个哨兵指针,首先找到这个哨兵指针所在的page。2.在next指针和哨兵指针之间的对象都发送一次 release消息,并把next指针移动上一次设立哨兵指针的位置,(由于page结构类似于链表,所有这一步可以靠parent指针跨越多个page的对象作处理)以上就是Autorelease内部的实现原理。 autorelease的对象是在每个事件循环结束后,自动释放池才会对所有自动释放的对象的引用计数减一,若引用计数变成了0,则释放对象,回收内存。因此,若想要早一点释放掉auto release对象,那么我们可以在对象外加一个自动释放池。比如,在循环处理数据时,临时变量要快速释放,就应该采用这种方式: [[UIImageView alloc] init]还有一些其他的 init 方法,返回的都是 autorelease 对象。而 autorelease 不能保证什么时候释放,所以不一定在引用计数为 0 就立即释放,只能保证在 autoreleasepool 结尾的时候释放。

for (int i = 0; i < 1000; i++) {
    UIImage* image = [UIImage imageNamed:@"some_image"];
    // 对 image 进行一些处理,比如存文件什么的
}

执行这段代码就会看到内存越增越大,容易导致崩溃。而在每一次循环结束的时候,UIImage 引用都为0了,不过系统不会把它立即释放掉;循环次数多了内存就爆掉了。 为了解决这个问题,可以改成这样:

for (int i = 0; i < 1000; i++) {
    @autoreleasepool {
        UIImage* image = [UIImage imageNamed:@"some_image"];
        // 对 image 进行一些处理,比如存文件什么的
    }
}

这样在每次循环结束的时候都会立即释放 UIImage,也不会对内存造成压力了。

总结

  • 线程是独立调度和分派的基本单位,RunLoop和自动释放池为线程服务;
  • RunLoop是一个事件循环,让线程休眠和线程保活成为了可能,线程休眠可以节省CPU资源;
  • 自动释放池一定存在于线程之中,解决了资源的释放问题。

3,https原理,对称加密,非对称加密

一,对称加密 所谓对称加密,就是它们在编码时使用的密钥e和解码时一样d(e=d),我们就将其统称为密钥k。 对称加解密的过程如下: 发送端和接收端首先要共享相同的密钥k(即通信前双方都需要知道对应的密钥)才能进行通信。发送端用共享密钥k对明文p进行加密,得到密文c,并将得到的密文发送给接收端,接收端收到密文后,并用其相同的共享密钥k对密文进行解密,得出明文p。 一般加密和解密的算法是公开的,需要保持隐秘的是密钥k,流行的对称加密算法有:DES,Triple-DES,RC2和RC4 二,非对称加密 所谓非对称加密技术是指加密的密钥e和解密的密钥d是不同的(e!=d),并且加密的密钥e是公开的,叫做公钥,而解密的密钥d是保密的,叫私钥。 非对称加解密的过程如下: 加密一方找到接收方的公钥e(如何找到呢?大部分的公钥查找工作实际上都是通过数字证书来实现的),然后用公钥e对明文p进行加密后得到密文c,并将得到的密文发送给接收方,接收方收到密文后,用自己保留的私钥d进行解密,得到明文p,需要注意的是:用公钥加密的密文,只有拥有私钥的一方才能解密,这样就可以解决加密的各方可以统一使用一个公钥即可。 常用的非对称加密算法有:RSA 非对称加密的优点是: 1,不存在密钥分发的问题,解码方可以自己生成密钥对,一个做私钥存起来,另外一个作为公钥进行发布。 2,解决了密钥管理的复杂度问题,多个加密方都可以使用一个已知的公钥进行加密,但只有拥有私钥的一方才能解密。 非对称加密不足的地方是加解密的速度没有对称加密快。 综上,分析了对称加密和非对称加密各自的优缺点后,有没有一种办法是可以利用两者的优点但避开对应的缺点呢?答应是有的,实际上用得最多的是混合加密系统,比如在两个节点间通过便捷的公开密码加密技术建立起安全通信,然后再用安全的通信产生并发送临时的随机对称密钥,通过更快的对称加密技术对剩余的数据进行加密。 SSL协议通信过程 (1) 浏览器发送一个连接请求给服务器;服务器将自己的证书(包含服务器公钥S_PuKey)、对称加密算法种类及其他相关信息返回客户端; (2) 客户端浏览器检查服务器传送到CA证书是否由自己信赖的CA中心签发。若是,执行4步;否则,给客户一个警告信息:询问是否继续访问。 (3) 客户端浏览器比较证书里的信息,如证书有效期、服务器域名和公钥S_PK,与服务器传回的信息是否一致,如果一致,则浏览器完成对服务器的身份认证。 (4) 服务器要求客户端发送客户端证书(包含客户端公钥C_PuKey)、支持的对称加密方案及其他相关信息。收到后,服务器进行相同的身份认证,若没有通过验证,则拒绝连接; (5) 服务器根据客户端浏览器发送到密码种类,选择一种加密程度最高的方案,用客户端公钥C_PuKey加密后通知到浏览器; (6) 客户端通过私钥C_PrKey解密后,得知服务器选择的加密方案,并选择一个通话密钥key,接着用服务器公钥S_PuKey加密后发送给服务器; (7) 服务器接收到的浏览器传送到消息,用私钥S_PrKey解密,获得通话密钥key。 (8) 接下来的数据传输都使用该对称密钥key进行加密。 上面所述的是双向认证SSL协议的具体通讯过程,服务器和用户双方必须都有证书。由此可见,SSL协议是通过非对称密钥机制保证双方身份认证,并完成建立连接,在实际数据通信时通过对称密钥机制保障数据安全性

https安全连接 blog.csdn.net/u014465934/… 当Bill把他的证书发给我的时候, 我就用同样的Hash 算法, 再次生成消息摘要,然后用CA的公钥对数字签名解密, 得到CA创建的消息摘要, 两者一比,就知道有没有人篡改了! 如果没人篡改, 我就可以安全的拿到Bill的公钥喽,有了公钥, 后序的加密工作就可以开始了。 附注:我们利用上面数字证书保证传输安全的前提就是,我们默认CA的公钥是安全的。因为CA的公钥安全,所以得出的消息摘要是安全的,这就可以与Hash算法得出的消息摘要比较是否相同,从而确保消息的安全的。 HTTPS 数字证书的实例:HTTPS协议。这个协议主要用于网页加密。 1.首先,客户端向服务器发出加密请求。 2.服务器用自己的私钥加密网页以后,连同本身的数字证书,一起发送给客户端 3.客户端(浏览器)的"证书管理器",有"受信任的根证书颁发机构"列表。客户端会根据这张列表,查看解开数字证书的公钥是否在列表之内 4.如果数字证书记载的网址,与你正在浏览的网址不一致,就说明这张证书可能被冒用,浏览器会发出警告。 5.如果这张数字证书不是由受信任的机构颁发的,浏览器会发出另一种警告。 6.如果数字证书是可靠的,客户端就可以使用证书中的服务器公钥,对信息进行加密,然后与服务器交换加密信息

有关Http的区别

http1.0和http1.1区别:

1)http1.0需要使用keep-alive参数告知服务器建立长连接,http1.1默认支持

2)http1.1支持发送header信息,不带body,返回100后才发body,节省带宽

3)http1.0不支持host域,http1.1才支持

http1.1和http2.0区别:

1)多路复用:http2.0支持同一个连接并发处理多个请求(http1.0可以建立多个TCP连接处理,但是会造成额外开销),改善网络堵塞

2)在应用层和传输层之间增多了一个二进制分帧层实现低延迟和高吞吐量

3)http2.0支持对header的数据压缩

4)请求服务器的时候,服务器会额外给客户端需要的其他资源一起给客户端,免得再次发送请求

NSURLSession和NSURLConnection区别

1)ios7后NSURLSession是用来替代NSURLConnection的

2)NSURLConnection下载文件是先下载到内存,再写入沙盒的,耗费内存大,而NSURLSessionDownLoadTask默认下载到temp目录,下载完就会删除,所以会手动复制到cache目录里

3)NSURLConnection默认请求就发送请求,不需要start,cancel可以取消;而NSURLSession支持取消、暂停、继续

4)配置信息: NSURLSession有一个NSURLSessionConfiguration类的参数可以设置配置信息,其决定了cookie,安全和高速缓存策略,最大主机连接数,资源管理,网络超时等配置。NSURLConnection不能进行这个配置

4 dealloc底层做了什么事情

苹果的内存管理是通过引用计数实现的,当一个对象的引用计数为0时就去释放这个对象,此时dealloc触发。在触发dealloc之前,肯定是先对一个对象进行了release操作,我们先来看下release源码 内部只是简单的调用了c++类的rootRelease方法,直接来到最终的调用方法 首先判断该对象是否正在析构,如果是,则表明该对象过度释放,程序崩溃报错,一般在mrc时代容易出现过度释放的问题。如果不是正在析构,则标记该对象的deallocting为true。接下来通过objc_msgSend方法去调用dealloc方法,按理来说通过objc_msgSend调用方法,如果在子类中找到了sel,那么父类的dealloc不会调用,在我实际测试中,dealloc的调用顺序是子类->父类->根类(NSObject), 可以看到执行过程比较简单,_objc_rootDealloc -> rootDealloc -> objc_object::rootDealloc,在objc_object::rootDealloc中判断对象的几个条件:

1),isa.nonpointer,32位系统和64位系统isa的结构不同,为0表示isa直接指向对象的class,为1表示isa不是直接指向class 2)是否有注册weak引用表 3)是否有association关联属性 4)是否有c++析构 5)是否有引用计数表。 在object_dispose中调用objc_destructInstance主要做了以下操作: object_cxxDestruct,这里主要是用于释放对象的实例变量 _object_remove_assocations,移除掉所有关联属性,即通过objc_setAssociatedObject添加的关联属性 clearDeallocating,先清空weak变量表且将所有weak引用指向nil,然后清空引用计数表。 最后通过free函数清除对象内存

5 互斥锁和自旋锁的本质区别

互斥锁:线程会从sleep(加锁)——>running(解锁),过程中有上下文的切换,cpu的抢占,信号的发送等开销。 自旋锁:线程一直是running(加锁——>解锁),死循环检测锁的标志位,机制不复杂。 区别: 互斥锁的起始原始开销要高于自旋锁,但是基本是一劳永逸,临界区持锁时间的大小并不会对互斥锁的开销造成影响,而自旋锁是死循环检测,加锁全程消耗cpu,起始开销虽然低于互斥锁,但是随着持锁时间,加锁的开销是线性增长

自旋锁(Spin lock) 自旋锁与互斥锁有点类似,只是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是 否该自旋锁的保持者已经释放了锁,"自旋"一词就是因此而得名。其作用是为了解决某项资源的互斥使用。因为自旋锁不会引起调用者睡眠,所以自旋锁的效率远 高于互斥锁。虽然它的效率比互斥锁高,但是它也有些不足之处: 1、自旋锁一直占用CPU,他在未获得锁的情况下,一直运行--自旋,所以占用着CPU,如果不能在很短的时 间内获得锁,这无疑会使CPU效率降低。 2、在用自旋锁时有可能造成死锁,当递归调用时有可能造成死锁,调用有些其他函数也可能造成死锁,如 copy_to_user()、copy_from_user()、kmalloc()等。 因此我们要慎重使用自旋锁,自旋锁只有在内核可抢占式或SMP的情况下才真正需要,在单CPU且不可抢占式的内核下,自旋锁的操作为空操作。自旋锁适用于锁使用者保持锁时间比较短的情况下。 自旋锁的用法如下: 首先定义:spinlock_t x; 然后初始化:spin_lock_init(spinlock_t *x); //自旋锁在真正使用前必须先初始化

6 继承自NSObject对象的内存布局流程

编译器在给结构体开辟空间时,首先找到结构体中最宽的基本数据类型,然后寻找内存地址能是该基本数据类型的整倍的位置,作为结构体的首地址。将这个最宽的基本数据类型的大小作为对齐模数。 为结构体的一个成员开辟空间之前,编译器首先检查预开辟空间的首地址相对于结构体首地址的偏移是否是本成员的整数倍,若是,则存放本成员,反之,则在本成员和上一个成员之间填充一定的字节,以达到整数倍的要求,也就是将预开辟空间的首地址后移几个字节 我们可以总结内存对齐为两个原则: 原则 1. 前面的地址必须是后面的地址正数倍,不是就补齐。 原则 2. 整个Struct的地址必须是最大字节的整数倍 一个NSObject对象占用多少内存? 答:一个指针变量所占用的大小(64bit占8个字节,32bit占4个字节)

7 KVO的底层实现原理

1.1 给 Person 的实例对象 person 添加observer,监听 person 的 age 属性 1.2 当 person 的 age 属性发生变化时,会调用 observer 的回调方法(也就是NSObject(NSKeyValueObserving)分类的方法) 2.1 执行 KVO 的person1对象,它的 isa 所指向的类对象不再是 Person 类对象,而是 NSKVONotifying_Person 类对象 _NSSet***ValueAndNotify 的内部实现为:

调用willChangeValueForKey:

调用原来的setter实现 调用didChangeValueForKey:

  • didChangeValueForKey:内部会调用observer的observeValueForKeyPath:ofObject:change:context:方法 4.1 iOS用什么方式实现对一个对象的KVO?(KVO的本质是什么?)

利用Runtime API动态生成一个子类,并且让instance对象的isa指向这个全新的子类 当修改instance对象的属性时,会调用Foundation的_NSSetXXXValueAndNotify函数 4.2 如何手动触发KVO?

  • 手动调用willChangeValueForKey:和didChangeValueForKey: 4.3 直接修改成员变量会触发KVO么?
  • 不会触发KVO

8 常用算法

链接 www.jianshu.com/p/384b0ff5d… 1.冒泡排序 冒泡算法是一种基础的排序算法,这种算法会重复的比较数组中相邻的两个元素,如果一个元素比另一个元素大/小,那么就交换这两个元素的位置。重复一直比较到最后一个元素. 1.最差时间复杂度:O(n^2); 2.平均时间复杂度:O(n^2);

for (int i = 0; i < array.count; i++) {
        for (int j = 0; j<array.count-1-i; j++) {
            if ([array[j]integerValue] > [array[j+1] integerValue]) {
                [array exchangeObjectAtIndex:j withObjectAtIndex:j+1];
            }
        }
    }

2.选择排序 1.最差时间复杂度:O(n^2); 2.平均时间复杂度:O(n^2);

for (int i = 0; i < array.count; i++) {
        for (int j = i+1; j<array.count; j++) {
            if ([array[i]integerValue] > [array[j] integerValue]) {
                [array exchangeObjectAtIndex:i withObjectAtIndex:j];
            }
        }
    }

3.快速排序 4.插入排序 直接插入排序的基本思想是从第二个元素开始,依次和前面的元素比较,如果比前面的元素小则将元素依次向后移位,给需要插入的元素腾出空间。与选择排序类似的是当前索引左边的所有元素都是有序的,但是它们最终的位置不确定,因为后面可能还会出现更小或更大的元素。所以为了给更小或更大的元素腾出空间,它们随时都可能被移动。如果到达了数组的右端时,数组顺序就完成了 5.堆排序 堆排序(Heap Sort) 就是利用堆(假设利用大堆顶)进行排序的方法。它的基本思想是,将待排序的序列构成一个大顶堆

举例子

N阶楼梯,每次可以走一步或者两步,N阶楼梯有多少种不同的走法 shukuiyan.iteye.com/blog/106182…

思路一:设有x次走一阶,y次走两阶,则一定满足x+2*y=n,且x、y均为整数,那么对于任何一个满足的x的可能走法共有 C(x+(n-x)/2,x)种走法,即从数x+(n-x)/2中取x种组合,值为(x+(n-x)/2)的阶乘除以x的阶乘与(n-x)/2的阶乘的乘积。

思路二:走到第n阶时可能是从第n-1阶走一步到的,也可能是从n-2阶走两阶到的,设F(n)为走到n阶的种数,则F(n)=F(n-1)+F(n-2)。当n=1时,F(1)=1,n=2时,F(2)=2,这是一个动态规划问题。其实就是一个斐波那契数列.

9 NSObject继承链

10 KVO、Notification、delegate 各自的优缺点,效率还有使用场景

1.委托delegation;2.通知中心Notification Center;3.键值观察key value observing,KVOdelegate的优势:1.很严格的语法,所有能响应的时间必须在协议中有清晰的定义2.因为有严格的语法,所以编译器能帮你检查是否实现了所有应该实现的方法,不容易遗忘和出错3.使用delegate的时候,逻辑很清楚,控制流程可跟踪和识别4.在一个controller中可以定义多个协议,每个协议有不同的delegate5.没有第三方要求保持/监视通信过程,所以假如出了问题,那我们可以比较方便的定位错误代码。6.能够接受调用的协议方法的返回值,意味着delegate能够提供反馈信息给controllerdelegate的缺点:需要写的代码比较多 notification的优势:1.不需要写多少代码,实现比较简单2.一个对象发出的通知,多个对象能进行反应,一对多的方式实现很简单缺点:1.编译期不会接茬通知是否能被正确处理2.释放注册的对象时候,需要在通知中心取消注册3.调试的时候,程序的工作以及控制流程难跟踪4.需要第三方来管理controller和观察者的联系5.controller和观察者需要提前知道通知名称、UserInfo dictionary keys。如果这些没有在工作区间定义,那么会出现不同步的情况6.通知发出后,发出通知的对象不能从观察者获得任何反馈。KVOKVO是一个对象能观察另一个对象属性的值,前两种模式更适合一个controller和其他的对象进行通信,而KVO适合任何对象监听另一个对象的改变,这是一个对象与另外一个对象保持同步的一种方法。KVO只能对属性做出反应,不会用来对方法或者动作做出反应。优点:1.提供一个简单地方法来实现两个对象的同步2.能对非我们创建的对象做出反应3.能够提供观察的属性的最新值和先前值4.用keypaths 来观察属性,因此也可以观察嵌套对象缺点:1.观察的属性必须使用string来定义,因此编译器不会出现警告和检查2.对属性的重构将导致观察不可用3.复杂的“if”语句要求对象正在观察多个值,这是因为所有的观察都通过一个方法来指向KVO有显著的使用场景,当你希望监视一个属性的时候,我们选用KVO而notification和delegate有比较相似的用处,当处理属性层的消息的事件时候,使用KVO,其他的尽量使用delegate,除非代码需要处理的东西确实很简单,那么用通知很方便

KVC

说到了kvo就不得不说一下kvc了

KVC如何通过key找value

setValue:forKey:搜索方式 1、首先搜索setKey:方法。

2、第一步没有找到的话,如果类方法accessInstanceVariablesDirectly返回为YES。那么按照_key,_isKey,key,isKey的顺序搜索成员名。(NSObject类方法)

3、如果没有找到成员变量,调用setKey:forUnderfinedKey:

valueForKey:搜索方式

1、首先按照getKey,key,isKey的顺序查找getter方法,找到直接调用。如果是BOOL,int等内建值类型,会做NSNumber类型转化。

2、如果类方法accessInstanceVariablesDirectly返回为YES。那么按照_key,_isKey,key,isKey的顺序搜索成员名。

3、再没找到的话,调用valueForUnderfinedKey。

一旦使用 KVC 你的编译器无法检查出错误,即不会对设置的键、键路径进行错误检查,且执行效率要低于合成存取器方法和自定的 setter 和 getter 方法。因为使用 KVC 键值编码,它必须先解析字符串,然后在设置或者访问对象的实例变量

11 block原理

block本质是指向一个结构体的一个指针运行时机制 比较高级的特性 纯C语言,平时写的OC代码 转换成C语言运行时的代码指令:clang -rewrite-objc main.m(可以打印验证),默认情况下,任何block都是在栈里面的,随时可能被回收,只要对其做一次copy操作block的内存就会放在堆里面不会释放,只有copy才能产生一个新的内存地址所有地址会发生改变

struct __main_block_impl_0 {
 struct __block_impl impl;
 struct __main_block_desc_0* Desc;
 __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
 impl.isa = &_NSConcreteStackBlock;
 impl.Flags = flags;
 impl.FuncPtr = fp;
 Desc = desc;
 }
};
注意:
如何修改block内部变量的值:
1)可以通过__block来copy到堆区修改
2)block内部可以直接修改静态变量,静态全局变量和全局变量的值,不能直接修改局部变量的值
静态变量,静态全局变量和全局变量存放在常量区,不存在对自动变量(OC对象,或者是结构体实例)进行截获,所以能够直接修改

block类型 struct objc_class _NSConcreteGlobalBlock; 数据区 全局的静态 block,不会访问外部局部变量。 struct objc_class _NSConcreteStackBlock;栈区 保存在栈中的 block,当函数返回时会被销毁 struct objc_class _NSConcreteMallocBlock;堆区 保存在堆中的 block,当引用计数为 0 时会被销毁

  1. 对_NSConcreteGlobalBlock的Block执行copy操作没有什么实际意义,因为它是全局可以访问的;
  2. 对_NSConcreteMallocBlock的Block执行copy操作会使得这个Block的引用计数加1。 将一个Block赋值给一个非__weak修饰的变量会执行拷贝外,还有以下情况会执行:

显式调用Block的copy方法时; 赋值给一个具有copy修饰的Block属性时; 在ARC下,向函数或者方法传递Block时(MRC下需要手动copy); 1,在block内部如何修改外部变量,内存地址怎么变化 Block不允许修改外部变量的值,这里所说的外部变量的值,指的是栈中指针的内存地址。__block 所起到的作用就是只要观察到该变量被 block 所持有,就将“外部变量”在栈中的内存地址放到了堆中。进而在block内部也可以修改外部变量的值。 a 在定义前是栈区,但只要进入了 block 区域,就变成了堆区。这才是 __block 关键字的真正作用。 __block 关键字修饰后,int类型也从4字节变成了32字节,这是 Foundation 框架 malloc 出来的。这也同样能证实上面的结论。(PS:居然比 NSObject alloc 出来的 16 字节要多一倍)。 理解到这是因为堆栈地址的变更,而非所谓的“写操作生效”,这一点至关重要 block会对对象类型的指针进行copy,copy到堆中,但并不会改变该指针所指向的堆中的地址 __block 所起到的作用就是只要观察到该变量被 block 所持有,就将“外部变量”在栈中的内存地址放到了堆中。进而在block内部也可以修改外部变量的值。

block特性

block只有引用了栈里的临时变量, 才会被创建在stack区. 没有引用临时变量的block是放在global区, 是不会被释放的. stack区的块只要赋值给strong类型的变量, 就会自动copy到堆里. 所以要不要写copy都没关系

在iOSmrc模式下,使用copy的目的是将block创建默认放在栈区拷贝一份到堆区,因为栈区中的变量管理是由它自己管理的,随时可能被销毁,一旦被销毁后续再次调用空对象就可能会造成程序崩溃问题, block放在了堆中,block有个指针指向了栈中的block代码块,在arc模式下,系统会默认使用copy就行修饰

ARC模式来说,我觉得使用strong或者copy都是可以的,下面用事例说明:

@property (nonatomic,copy)void(^demoBolck)();
@property (nonatomic,strong)void(^demoBolck1)();

int b=8;

void (^demoBolck)() = ^{

        NSLog(@"demoBolck");

    };

    NSLog(@"demoBolck %@",demoBolck);    //<__NSGlobalBlock__: 0x1085af0e0>  无论ARC还是MRC下,因不访问外部局部(包括无外部变量或者只有全局变量),NSGlobalBlock表示在全局区

void (^demoBolck4)() = ^{

        NSLog(@"demoBolck4  %d",b);

    };

   NSLog(@"demoBolck4 %@",demoBolck4);    //<__NSGlobalBlock__: 0x10150b120>  全局区
    __block int a = 6;   //block内部引用a,并修改其值,需要用block修饰,不然可以不用。不过是引用行属性,需要

    void (^demoBolck2)() = ^{
        NSLog(@"demoBolck2 %d",a++);
    };

    demoBolck2();

    NSLog(@"demoBolck2 %@,%d",demoBolck2,a);   //<__NSMallocBlock__: 0x600000056c50> ARC下堆区  <__NSStackBlock__: 0x7fff5d0ada28>MRC下在栈区

    NSLog(@"demoBolck2.Copy %@",[demoBolck2 copy]);    //<__NSMallocBlock__: 0x600000056c50>copy操作不管MRC或者ARC都在堆区,只是在MRC下进行copy会改变地址

    self.demoBolck = demoBolck2;

    NSLog(@"self.demoBolck %@",self.demoBolck);
    self.demoBolck1 = demoBolck2;
    self.demoBolck1();     //demoBolck2  7   能执行无问题

    NSLog(@"self.demoBolck1 %@",self.demoBolck1);     //<__NSMallocBlock__: 0x600000056c50>  strong修饰并没有问题

__block和__weak区别:

__block: 不管是ARC还是MRC模式下都可以使用,可以修饰对象,还可以修饰基本数据类型。

__weak: 只能在ARC模式下使用,也只能修饰对象(NSString),不能修饰基本数据类型(int)。

__block: 对象可以在block中被重新赋值,

__weak: 不可以。

有关swift的闭包

1,逃逸闭包 会不会影响内存泄漏,, 会 这个闭包没有立即执行而是被handlers保存了,这种就是闭包就是逃逸的,必须加@escaping标注。 在逃逸闭包中使用不当容易造成内存泄漏 2,非逃逸闭包会不会循环引用问题 不会 在Swift 3.x 中,传递闭包到函数中的时候,系统会默认为非逃逸闭包类型 (Nonescaping Closures),有非逃逸闭包类型必然就有逃逸闭包(Escaping Closures), 非逃逸闭包 非逃逸闭包的生命周期比较简单:

  1. 把闭包作为参数传递给函数。
  2. 函数中运行该闭包。
  3. 退出函数。 显而易见是非逃逸闭包被限制在函数内,当函数退出的时候,该闭包引用计数不会增加,也就是说其引用计数在进入函数和退出函数时保持不变。 逃逸闭包 逃逸闭包恰恰与非逃逸闭包相反,其生命周期长于相关函数,当函数退出的时候,逃逸闭包的引用仍然被其他对象持有,不会在相关函数结束后释放。 Swift 3.x中, 闭包参数默认是非逃逸类型,如果需要其逃逸类型的闭包,记得使用关键字 @escaping
class ClassA {
  // 接受非逃逸闭包作为参数
  func someMethod(closure: () -> Void) {
    // 想干什么?
  }
}

class ClassB {
  let classA = ClassA()
  var someProperty = "Hello"

  func testClosure() {
    classA.someMethod {
      // self 被捕获
      someProperty = "闭包内..."
    }
  }
}

当传递闭包参数给函数someMethod时,要注意ClassB中的属性someProperty,虽然闭包会捕获self,但是由于默认闭包参数是非逃逸型,这里可以省略 self, 反正编译器已经知道这里不会有循环引用的潜在风险

func someMethod(closure: @escaping () -> Void) {
  // 特别行动
}

由于添加了关键词@escaping,这里闭包函数的生命周期不可预知,上面省略的self 就有必要明确地添加来提醒self已经被捕捉,循环引用的风险就在眼前。 下面是使用逃逸闭包的2个场景:

  1. 异步调用: 如果需要调度队列中异步调用闭包, 这个队列会持有闭包的引用,至于什么时候调用闭包,或闭包什么时候运行结束都是不可预知的。
  2. 存储: 需要存储闭包作为属性,全局变量或其他类型做稍后使用。 为什么区分闭包的逃逸性与非逃逸性如此重要? 简单来说,是为了管理内存。一个闭包会强引用它捕获的所有对象————如果你在闭包中访问了当前对象中的任意属性或实例方法,闭包会持有当前对象,因为这些方法和属性都隐性地携带了一个 self 参数。 这种方式很容易导致循环引用,这解释了为什么编译器会要求你在闭包中显式地写出对 self 的引用。这迫使你考虑潜在的循环引用,并使用捕获列表手动处理。 然而,使用非逃逸的闭包不会产生循环引用————编译器可以保证在函数返回时闭包会释放它捕获的所有对象。因此,编译器只要求在逃逸闭包中明确对 self 的强引用。显然,使用非逃逸闭包是一个更加愉悦的方案。 使用非逃逸闭包的另一个好处是编译器可以应用更多强有力的性能优化。例如,当明确了一个闭包的生命周期的话,就可以省去一些保留(retain)和释放(release)的调用。此外,如果闭包是一个非逃逸闭包,它的上下文的内存可以保存在栈上而不是堆上————虽然我不确定当前的编译器是否执行了这个优化(一篇公布于 2016 年 3 月的错误报告显示当时并没有执行)。 即时的参数位是什么意思? 让我们看一些示例。最简单的情况就像 map:这个函数接受一个立即执行的闭包参数。正如我们所看到的,这个闭包是一个非逃逸的(我从 map 的真实签名中省略了一些无关、不重要的细节):
func map<T>(_ transform: (Iterator.Element) -> T) -> [T]

函数类型的变量总是逃逸的 与此相比。即使没有明确的标注,指向/保存函数类型(闭包)的变量或属性,都是自动逃逸的(实际上,如果你显式添加一个 @escaping 也会报错)。这其实很合理,因为赋值给一个变量隐性地允许该值逃逸到变量的作用域中,而非逃逸闭包不允许这种行为。这可能会让人困惑,但一个未做任何标注的闭包在参数列表中与其他任何情况都不同。 可选型的闭包总是逃逸的 更令人惊讶的是,即便闭包被用作参数,但是当闭包被包裹在其他类型(例如元组、枚举的 case 以及可选型)中的时候,闭包仍旧是逃逸的。由于在这种情况下闭包不再是即时的参数,它会自动变成逃逸闭包。因此,在 Swift 3.0 中,当你编写一个接受函数类型参数的函数时,该参数不能同时是可选型和非逃逸的。思考下面这个精心设计的例子:函数 transform 接受一个整数 n 以及一个可选型的变换函数 f。正常情况下它返回 f(n),而 f 为空值时返回 n。

/// Applies `f` to `n` and returns the result.
/// Returns `n` unchanged if `f` is nil.
func transform(_ n: Int, with f: ((Int) -> Int)?) -> Int {
    guard let f = f else { return n }
    return f(n)
}

这里函数 f 是逃逸的,因为 ((Int) -> Int)? 是 Optional<(Int) -> Int> 的缩写,即函数类型不在一个即时参数位上。 将可选参数替换为默认实现 Swift 团队已经意识到了这个问题,并且会在将来的版本中解决它。在那之前,对这个问题有一定了解是非常重要的。目前没有办法让一个可选型的闭包变成非逃逸的,但是在许多情况下,你可以通过为闭包提供一个默认值的方式来避免使用可选型参数。在我们的例子中,默认值是一个特定的函数,返回一个不可变的参数:

/// Uses a default implementation for `f` if omitted
func transform(_ n: Int, with f: (Int) -> Int = { $0 }) -> Int {
    return f(n)
}

使用重载提供一个可选型和一个非逃逸的变体 类型别名总是逃逸的 最后要注意的是,在 Swift 3.0 中,你不能向 typealiases 中添加逃逸或者非逃逸的标注。如果你在函数声明中对一个函数类型的参数使用了类型别名(typealias),这个参数总会被视为逃逸的。这个 bug 已经在主分支上修复了,应该会出现在下一个 release 版本中

12 用@property声明的NSString(或NSArray,NSDictionary)经常使用copy关键字,为什么?如果改用strong关键字,可能造成什么问题?

  • 1,因为父类指针可以指向子类对象,使用 copy 的目的是为了让本对象的属性不受外界影响,使用 copy 无论给我传入是一个可变对象还是不可对象,我本身持有的就是一个不可变的副本.
  • 2,如果我们使用是 strong ,那么这个属性就有可能指向一个可变对象,如果这个可变对象在外部被修改了,那么会影响该属性. copy 此特质所表达的所属关系与 strong 类似。然而设置方法并不保留新值,而是将其“拷贝” (copy)。 当属性类型为 NSString 时,经常用此特质来保护其封装性,因为传递给设置方法的新值有可能指向一个 NSMutableString 类的实例。这个类是 NSString 的子类,表示一种可修改其值的字符串,此时若是不拷贝字符串,那么设置完属性之后,字符串的值就可能会在对象不知情的情况下遭人更改。所以,这时就要拷贝一份“不可变” (immutable)的字符串,确保对象中的字符串值不会无意间变动。只要实现属性所用的对象是“可变的” (mutable),就应该在设置新属性值时拷贝一份。

总结:

只有对不可变的(NSString、NSArray、NSDictionary、NSSet)进行copy 是浅复制,其他都是深复制。或者带mutable的都是深复制。

a. 对不可变属性,需要使用copy来修饰,进行深拷贝赋值。
1. NSString、NSArray,NSDictionary建议使用copy修饰
2. block建议使用copy修饰
b. 对自定义对象使用copy方法时,需要先重写copyWithZone方法。
c. 对可变属性,不建议使用copy修饰

13 内存管理的几条原则是什么?按照默认法则.哪些方法生成的对象需要手动释放?在和property结合的时候怎样有效的避免内存泄露?

1> 只要调用了alloc、copy、new、mutableCopy方法产生了一个新对象,都必须在最后调用一次release或者autorelease 2> 只要调用了retain,都必须在最后调用一次release或者autorelease 3> @property如果用了copy或者retian,就需要对不再使用的属性做一次release操作 4> 当引用计数为0的时候,再去访问此对象:A地址仍然是同一个A地址,会先去访问存放在A地址上的对象,但是对象已经被释放了,会报错EXC_BAD_ACCESS,野指针错误,assign一样 5> 通过类方法拿到的对象是非自己生成的 [NSArray array] —> 系统生成对象,然后加到自动释放池,再返回给调用者 8.OC中创建线程的方法是什么?如果指定在主线程中执行代码?如何延时执行代码? 1> 创建线程的方法 Ø NSThread Ø NSOperationQueue和NSOperation Ø GCD 2> 主线程中执行代码 Ø [self performSelectorOnMainThread: withObject: waitUntilDone:]; Ø [self performSelector: onThread:[NSThread mainThread] withObject: waitUntilDone:]; Ø dispatch_async(dispatch_get_main_queue(), ^{ }); 3> 延时执行 Ø [self performSelector: withObject: afterDelay:]; Ø [NSTimer scheduledTimerWithTimeInterval: target: selector: userInfo: repeats:]; 这里注意一点:如果定时器是放在子线程操作时,一定要开启runloop,因为子线程默认是没有开启runloop的

14 tableView卡顿原因

图像显示原理

  • 关于CPU和GPU都是通过总线连接起来的,在CPU当中输出的往往是一个位图,再经由总线在合适的时机传递个GPU
  • GPU拿到这个位图之后,会对这个位图的图层进行渲染,包括纹理的合成等
  • 之后会把这个结果放到帧缓冲区中,然后视频控制器会按照VSync信号逐行读取帧缓冲区的数据,经过可能的数模转换传递给显示器,达到最终的显示效果

那么接下来让我们看一下CPU和GPU分别做了哪些事情

  • 首先当我们创建一个UIView控件的时候,其中负责显示的CALayer
  • CALayer中有一个contents属性,就是我们最终要绘制到屏幕上的一个位图,比如说我们创建了一个UILabel,那么在contents里面就放了一个关于Hello world的文字位图
  • 然后系统会在一个合适的时机回调给我们一个drawRect:的方法,这个方法中我们可以去绘制一些自定义的内容
  • 绘制好了之后,最终会由Core Animation这个框架提交给GPU部分的OpenGL渲染管线,进行最终的位图的渲染,包括纹理合成等,然后显示在屏幕上
那么CPU和GPU具体做了哪些工作承担呢

1.CPU

具体分为四个阶段

  • Layout:这里主要涉及到一些UI布局,文本计算等,例如一个label的size
  • Display:绘制阶段,例如drawRect方法就在这一步骤中
  • Prepare:图片的编解码等操作在此步骤中
  • Commit:提交位图

2.GPU渲染管线

  • 顶点着色
  • 图元装配
  • 光栅化
  • 片段着色
  • 片段处理
UI卡顿、掉帧的原因

在显示器中是固定的频率,比如iOS中是每秒60帧(60FPS),即每帧16.7ms 从上图中可以看出,每两个VSync信号之间有时间间隔(16.7ms),在这个时间内,CPU主线程计算布局,解码图片,创建视图,绘制文本,计算完成后将内容交给GPU,GPU变换,合成,渲染(详细可学习 OpenGL相关课程),放入帧缓冲区

假如16.7ms内,CPU和GPU没有来得及生产出一帧缓冲,那么这一帧会被丢弃,显示器就会保持不变,继续显示上一帧内容,这就将导致导致画面卡顿

卡顿、掉帧优化方案切入点

1.CPU

CPU在准备下一帧的所做的工作非常多导致耗时,基于减轻CPU工作时长和压力来达到一个优化效果

1、部分对象的创建、调整和销毁可以放到子线程去做

2、预排版( 布局计算、文本计算),这些计算也可以放到子线程去做,这样主线程也可以有更多的时间去响应用户的交互

3、预渲染(文本等异步绘制、图片编解码等)

2.GPU

1、纹理渲染:假如说我们触发了离屏渲染,例如我们设置圆角时对maskToBounds的设置,包括一些阴影、蒙层等都会触发GPU层面的离屏渲染,对于这种情况下,GPU对于纹理渲染的工作量就会非常的大,我们可以基于此对GPU进行优化,就是尽量减少离屏渲染,我们也可以通过CPU的异步绘制来减轻GPU的压力

2、视图混合: 比如说我们视图层级比较复杂,视图之间层层叠加,那么GPU就要做每一个视图的合成,合成每一个像素点的像素值,如果我们可以减少视图的层级,也是可以减轻GPU的压力,我们也可以通过CPU的异步绘制机制来达到一个提交的位图本身就是一个层级比较少的位图

UIView的绘制原理

流程图

  • 当我们调用[UIView setNeedsDisplay]这个方法时,其实并没有立即进行绘制工作,系统会立刻调用CALayer的同名方法,并且会在当前layer上打上一个标记,然后会在当前runloop将要结束的时候调用[CALayer display]这个方法,然后进入我们视图的真正绘制过程
  • 而在[CALayer display]这个方法的内部实现中会判断这个layer的delegate是否响应displayLayer:这个方法,如果不响应这个方法,就会进入到系统绘制流程中;如果响应这个方法,那么就会为我们提供异步绘制的入口
上面就是UIView的绘制原理,接下来我们看一下系统绘制流程是怎样的

流程图

  • 在CALayer内部会先创建backing store,我可以理解为CGContext,我们一般在drawRect:方法中通过上下文堆栈当中取出栈顶的context,也就是上下文
  • 然后这个layer会判断是否有代理,如果没有代理,那么就会调用[CALayer drawInCotext:];如果有代理,会调用代理的drawLayer:inContext:方法,然后做当前视图的绘制工作(这一步是发生在系统内部的),然后在一个合适的时机给与我们这个十分熟悉的[UIView drawRect:]方法的回调,[UIView drawRect:]这个方法默认是什么都不做,,系统给我们开这个口子是为了让我们可以再做一些其他的绘制工作
  • 然后无论是哪个分支,最终都会由CALayer上传对应的backing store(可以理解为位图)给GPU,然后就结束了系统默认的绘制流程
tableView卡顿优化

1.cell没注册ID,没重用,每次都取新的

2.创建时就布局,避免cell的重新布局

3.提前计算并缓存cell的属性及内容

  • 当我们创建cell的数据源方法时,编译器并不是先创建cell 再定cell的高度
  • 而是先根据内容一次确定每一个cell的高度,高度确定后,再创建要显示的cell,滚动时,每当cell进入凭虚都会计算高度,提前估算高度告诉编译器,编译器知道高度后,紧接着就会创建cell,这时再调用高度的具体计算方法,这样可以方式浪费时间去计算显示以外的cell

4.减少cell中控件的数量,不适用的可以先隐藏

5.不要使用ClearColor,无背景色,透明度也不要设置为0,渲染耗时比较长

6.使用局部更新

  • 如果只是更新某组的话,使用reloadSection进行局部更新

7.加载网络数据,下载图片,使用异步加载,并缓存

8.少使用addView 给cell动态添加view

9.按需加载cell,cell滚动很快时,只加载范围内的cell

10.不要实现无用的代理方法,tableView只遵守两个协议

11.缓存行高:estimatedHeightForRow不能和HeightForRow里面的layoutIfNeed同时存在,这两者同时存在才会出现“窜动”的bug。所以我的建议是:只要是固定行高就写预估行高来减少行高调用次数提升性能。如果是动态行高就不要写预估方法了,用一个行高的缓存字典来减少代码的调用次数即可

有关栅格化怎么理解的?离屏渲染是什么原理的?
  1. 栅格化

shouldRasterize(光栅化)是比较特别的一种

光栅化概念:将图转化为一个个栅格组成的图象。

光栅化特点:每个元素对应帧缓冲区中的一像素。

shouldRasterize = YES在其他属性触发离屏渲染的同时,会将光栅化后的内容缓存起来,如果对应的layer及其sublayers没有发生改变,在下一帧的时候可以直接复用。shouldRasterize = YES,这将隐式的创建一个位图,各种阴影遮罩等效果也会保存到位图中并缓存起来,从而减少渲染的频度(不是矢量图)。

2.离屏渲染

屏幕显示图像的原理:高中物理应该学过显示器是如何显示图像的:需要显示的图像经过CRT电子枪以极快的速度一行一行的扫描,扫描出来就呈现了一帧画面,随后电子枪又会回到初始位置循环扫描,形成了我们看到的图片或视频。为了让显示器的显示跟视频控制器同步,当电子枪新扫描一行的时候,准备扫描的时发送一个水平同步信号(HSync信号),显示器的刷新频率就是HSync信号产生的频率。然后CPU计算好frame等属性,将计算好的内容交给GPU去渲染,GPU渲染好之后就会放入帧缓冲区。然后视频控制器会按照HSync信号逐行读取帧缓冲区的数据,经过可能的数模转换传递给显示器,就显示出来了。这里只是简作描述,专业描述请自行查询。

GPU屏幕渲染有两种方式:

(1)On-Screen Rendering (当前屏幕渲染) 指的是GPU的渲染操作是在当前用于显示的屏幕缓冲区进行。

(2)Off-Screen Rendering (离屏渲染)指的是在GPU在当前屏幕缓冲区以外开辟一个缓冲区进行渲染操作。当前屏幕渲染不需要额外创建新的缓存,也不需要开启新的上下文,相对于离屏渲染性能更好。但是受当前屏幕渲染的局限因素限制(只有自身上下文、屏幕缓存有限等),当前屏幕渲染有些情况下的渲染解决不了的,就使用到离屏渲染。相比于当前屏幕渲染,离屏渲染的代价是很高的,主要体现在两个方面:

(1)创建新缓冲区要想进行离屏渲染,首先要创建一个新的缓冲区。

(2)上下文切换离屏渲染的整个过程,需要多次切换上下文环境:先是从当前屏幕(On-Screen)切换到离屏(Off-Screen),等到离屏渲染结束以后,将离屏缓冲区的渲染结果显示到屏幕上有需要将上下文环境从离屏切换到当前屏幕。而上下文环境的切换是要付出很大代价的。

下面的情况或操作会引发离屏渲染:

  • 1.为图层设置遮罩(layer.mask)
  • 2.将图层的layer.masksToBounds / view.clipsToBounds属性设置为true
  • 3.将图层layer.allowsGroupOpacity属性设置为YES和layer.opacity小于1.0
  • 4.为图层设置阴影(layer.shadow *)。
  • 5.为图层设置layer.shouldRasterize=true(光栅化)
  • 6.具有layer.cornerRadius,layer.edgeAntialiasingMask,layer.allowsEdgeAntialiasing的图层
  • 7.文本(任何种类,包括UILabel,CATextLayer,Core Text等)。
  • 8.使用CGContext在drawRect :方法中绘制大部分情况下会导致离屏渲染,甚至仅仅是一个空的实现。
  • 9.edge antialiasing(抗锯齿)
  • 10.渐变

卡顿监控的实现一般有两种方案:

(1)主线程卡顿监控。通过子线程监测主线程的 runLoop,判断两个状态区域之间的耗时是否达到一定阈值。具体原理和实现,这篇文章介绍得比较详细。
(2)FPS监控。要保持流畅的UI交互,App 刷新率应该当努力保持在 60fps。监控实现原理比较简单,通过记录两次刷新时间间隔,就可以计算出当前的 FPS。

主线程卡顿监控实现思路:使用CFRunLoopObserverRef监控NSRunLoop的状态,以实时获得这些状态值(kCFRunLoopBeforeSources、kCFRunLoopBeforeWaiting、kCFRunLoopAfterWaiting等)的变化。开启一个子线程,实时计算主线程NSRunLoop两个状态区域之间的耗时是否到达某个阀值,从而判断是否卡顿。

15 UIView和CALayer区别

UIView和CALayer的联系

UIView有个属性 layer,苹果官方给的注释是:

@property(nonatomic,readonly,strong)   CALayer  *layer
// returns view's layer. Will always return a non-nil value. view is layer's delegate

CALayer是图层,和界面展示相关。UIView很多属性和方法和CALayer里的属性和方法是一致的。UIView可以看做是CALayer的管理者,除了负责视图展示之外,还可以处理一些事件,比如手势交互等。但我们对UIView做的一些有关视图显示和动画的操作,本质上还是对CALayer进行的操作。 比如我们访问UIView跟绘图和跟坐标有关的属性,例如frame,bounds等等,实际上内部都是在访问它所包含的CALayer的相关属性。

1.View可以接受并处理事件,而 Layer 不可以

2.每个 UIView 内部都有一个 CALayer 在背后提供内容的绘制和显示,并且 UIView 的尺寸样式都由内部的 Layer 所提供。CALayer的坐标系统比UIView多了一个anchorPoint属性

3.在 View显示的时候,UIView 做为 Layer 的 CALayerDelegate,View 的显示内容由内部的 CALayer 的 display

4.CALayer 是默认修改属性支持隐式动画的,在给 UIView 的 Layer 做动画的时候,View 作为 Layer 的代理,Layer 通过 actionForLayer:forKey:向 View请求相应的 action(动画行为)

16 数据持久化

1.NSUserDefault(偏好设置) 2.Plist文件 3.NSKeyedArchiver 归档(NSCoding协议) 4.数据库(FMDB、SQLite、CoreData、第三方类库、WCDB) 5.钥匙串(keychain)

本地数据存储安全问题

plist文件(序列化)

可以被序列化的类型只有如下几种:
NSArray;  //数组
NSMutableArray;  //可变数组
NSDictionary;  //字典
NSMutableDictionary;  //可变字典
NSData;  //二进制数据
NSMutableData;  //可变二进制数据
NSString;  //字符串
NSMutableString;  //可变字符串
NSNumber;  //基本数据
NSDate;  //日期

Preference通常用来保存应用程序的配置信息的,一般不要在偏好设置中保存其他数据。

使用偏好设置对数据进行保存,它保存的时间是不确定的,会在将来某一时间自动将数据保存到 Preferences 文件夹下,如果需要即刻将数据存储,使用 [defaults synchronize]。

Preference(偏好设置) 和plist文件(序列化)都是保存在 plist 文件中,但是plist文件(序列化)操作读取时需要把整个plist文件都进行读取,而Preference(偏好设置) 可以直接通过 key-value单个读取。

要使用归档,其归档对象必须实现NSCoding协议。

NSCoding协议声明的两个方法都必须实现。

encodeWithCoder:用来说明如何将对象编码到归档中。

initWithCoder:用来说明如何进行解档来获取一个新对象。

sqlite
创建表:
create table 表名称(字段1,字段2,……,字段n,[表级约束])[TYPE=表类型];
插入记录:
insert into 表名(字段1,……,字段n) values (值1,……,值n);
删除记录:
delete from 表名 where 条件表达式;
修改记录:
update 表名 set 字段名1=值1,……,字段名n=值n where 条件表达式;
查看记录:
select 字段1,……,字段n from 表名 where 条件表达式;
CoreData常用类的作用描述
PersistentObjectStore:存储持久对象的数据库(例如SQLite,注意CoreData也支持其他类型的数据存储,例如xml、二进制数据等)。
ManagedObjectModel:对象模型,对应Xcode中创建的模型文件。
PersistentStoreCoordinator:对象模型和实体类之间的转换协调器,用于管理不同存储对象的上下文。
ManagedObjectContext:对象管理上下文,负责实体对象和数据库之间的交互。
有关安全问题

1.NSUserDefaults不安全

NSUserDefaults其实是plist文件中键值存储,并且最大的问题是存在与沙盒中,这就对安全性埋下了隐患。如果攻击者破解app,拿到了沙盒中的数据,就会造成数据泄漏,后果不堪设想。 当然,一般也不会有把密码直接使用NSUserDefaults存储的,都会进行加密、或者是多重加密后再进行NSUserDefaults存储。这么做其实是可行的,前提是加密算法不能泄漏

2.Plist不安全

什么是Plist文件?

属性列表(Plist,Property List)是一种结构化的二进制格式文件,包含了内嵌键值对的可执行bundle的基本配置信息。Plist文件主要用于存储App的用户设置及配置信息,例如,游戏类App经常会在Plist文件中存储游戏等级和分数信息。一般来说,App会将存储用户数据的Plist文件保存在“[App home目录]/documents/”目录下。Plist文件可以是XML格式或二进制格式。

Plist文件有哪些安全风险?

Plist文件主要用于存储用户设置及App的配置信息,但App可能使用Plist文件存储明文的用户名、密码或其它一些个人敏感信息。而保存在Plist文件中的二进制格式文件数据则可以使用Plist文件编辑器(如plutil)进行查看或修改,即使在一个没有越狱的设备上,plist文件也可以通过工具iExplorer获取。对于以编码、未加密或弱加密形式存储的敏感信息就可能会导致敏感信息泄露了

3.相对比较安全的Keychain Keychain是iOS所提供的一种安全存储参数的方式,最常用来存储账号,密码,用户信息,银行卡资料等信息,Keychain会以加密的方式存储在设备中

APP加密处理

1.本地数据加密

对NSUserDefaults,sqlite存储文件数据加密,保护帐号和关键信息

2.URL编码加密

对程序中出现的URL进行编码加密,防止URL被静态分析

3.网络传输数据加密

对客户端传输数据提供加密方案,有效防止通过网络接口的拦截获取数据, 使用MD5加密。

4.方法体,方法名高级混淆

对应用程序的方法名和方法体进行混淆,保证源码被逆向后无法解析代码。 使用hopper disassembler 反编译iPA之后不能得到相应的方法调用信息

5.程序结构混排加密

对应用程序逻辑结构进行打乱混排,保证源码可读性降到最低

6.借助第三方APP加固

例如:网易云易盾

17 instance(对象),class(类对象),meta-class(元类对象)

1)OC的类是基于C的结构体实现的

2)一个NSObject对象是一个结构体,成员变量只存放一个isa指针,指针在64bit系统占用8个字节;32bit系统占用4个字节

3)可以通过runtime的getInstanceSize获取类占用多少存储空间

instance(对象),class(类对象),meta-class(元类对象) 1)instance对象在内存中存储的信息包括 isa指针和其它成员变量

2)每个类在内存中有且仅有一个class对象(如NSObject,此类存放的地址都是一样的,如对象无关)

1、class对象在内存中存储的信息主要包括isa指针,superclass指针,属性,对象方法,协议信息等

3)和类对象结构一样,内存中主要存储isa指针,superclass指针,类方法

18 app瘦身、启动时间优化

瘦身:

1)通过工具删除无用、重复的图片资源和不用的类(fui)

2)压缩图片

3)检查cocoapods没用上的依赖

4)静态库去掉armv7s,armv7兼容7s(真机用到)

main()之前调用:

1)解析info.plist,加载闪屏,权限检查

2)加载可执行文件(app所有.o文件)

3)加载动态库dylb(包括系统framework、runtime的libobjc、libSystem、GCD、Block)

4)加载类扩展和+load函数

启动时间优化:

1)移除不用的动态库、不用的类(产品废弃的代码)

2)减少动态库和Objc类的数目

3)压缩资源图片

4)优化applicationWillFinishLaunching 如:启动过程并发的http请求、耗时的计算,数据处理

5)优化rootviewController加载,加载部分需要的视图和本地数据,然后联网后再更新数据

app性能优化

1 instruments

在iOS上进行性能分析的时候,首先考虑借助instruments这个利器分析出问题出在哪,不要凭空想象,不然你可能把精力花在了1%的问题上,最后发现其实啥都没优化,比如要查看程序哪些部分最耗时,可以使用Time Profiler,要查看内存是否泄漏了,可以使用Leaks等。关于instruments网上有很多资料,作为一个合格iOS开发者,熟悉这个工具还是很有必要的。

2 不要阻塞主线程

在iOS里关于UIKit的操作都是放在主线程,因此如果主线程被阻塞住了,你的UI可能无法及时响应事件,给人一种卡顿的感觉。大多数阻塞主线程的情况是在主线程做IO操作,比如文件的读写,包含数据库、图片、json文本或者log日志等,尽量将这些操作放放到子线程(如果数据库有一次有较多的操作,记得采用事务来处理,性能相差还是挺大的),或者在后台建立对应的dispatch queue来做这些操作,比如一个低级别的serial queue来负责log文件的记录等等。程序中如果你的代码逻辑是按照同步的逻辑来写的,尽量修改逻辑代码吧。。。

3 使用cache

一般为了提升用户体验,都会在应用中使用缓存,比如对于图片资源可以使用SDWebImage这个开源库,里面就实现了一个图片缓存的功能。参考SDWebImage的代码自己也可以实现缓存功能:

4 减少程序启动过程中的任务

当用户点击app的图标之后,程序应该尽可能快的进入到主页面,尽可能减少用户的等待时间,比如我们的应用程序在启动的时候会去做3d模型的渲染操作,完成之后在进入首页面展示,但其实我们可以先进入到主页面,将渲染3d的任务放到子线程去完成,缩短用户需要等待的时间。

5 使用合适的数据结构

根据不同的业务场景来选择合适的数据结构,可能在数据量比较少的时候看不出什么区别,但是假如你存储的数据量比较大且数据结构比较复杂的话,这有可能会影响到你的程序性能。一般用的比较多的数据结构就是array,但我们知道它的查找复杂度是O(n),因此假如需要快速的查找某个元素,可以使用map。可以参考下 Apple Collections Programming Topics 。

6 内存

一般开发都使用的ARC,不太需要开发者去关注内存的创建和释放这块,但假如你使用的是MRC,并且跟其它语言混杂在一起(比如c++和lua)等的时候,如何确保内存正确释放就是你需要考虑的问题了。有时候一些内存泄漏instruments可能无法准确的分析出来,那么就需要自己去排查了,可以使用method swizzling方法来辅助我们排查内存泄漏的问题,确保程序的正确运行。

7 懒加载view 不要在cell里面嵌套太多的view,这会很影响滑动的流畅感,而且更多的view也需要花费更多的CPU跟内存。假如由于view太多而导致了滑动不流畅,那就不要在一次就把所有的view都创建出来,把部分view放到需要显示cell的时候再去创建。

8 lua优化

由于项目的业务是以及部分框架是用lua语言实现的,因此也顺便说一下lua这块遇到的问题。lua号称是最快的脚本语言,一般性能上不会有什么问题,如果lua代码要优化的话,网上也有很多这块优化的注意点,这次我主要说个可能影响性能的点---lua的垃圾回收。垃圾回收是一个比较耗时的操作,假如垃圾回收的操作太过于频繁势必会影响到这个程序的运行

9 耗电优化

  • 尽可能降低CPU、GPU功耗
  • 少用定时器
  • 优化I/O操作
  • 尽量不要频繁写入小数据,最好批量一次性写入
  • 读写大量重要数据时,考虑用dispatch_io,其提供了基于GCD的异步操作文件I/O的API。用dispatch_io系统会优化磁盘访问
  • 数据量比较大的,建议使用数据库(比如SQLite、CoreData)

10 网络优化

  • 减少、压缩网络数据
  • 如果多次请求的结果是相同的,尽量使用缓存
  • 使用断点续传,否则网络不稳定时可能多次传输相同的内容
  • 网络不可用时,不要尝试执行网络请求
  • 让用户可以取消长时间运行或者速度很慢的网络操作,设置合适的超时时间
  • 批量传输,比如,下载视频流时,不要传输很小的数据包,直接下载整个文件或者一大块一大块地下载。如果下载广告,一次性多下载一些,然后再慢慢展示。如果下载电子邮件,一次下载多封,不要一封一封地下载

11 定位优化

  • 如果只是需要快速确定用户位置,最好用CLLocationManager的requestLocation方法。定位完成后,会自动让定位硬件断电
  • 如果不是导航应用,尽量不要实时更新位置,定位完毕就关掉定位服务
  • 尽量降低定位精度,比如尽量不要使用精度最高的kCLLocationAccuracyBest
  • 需要后台定位时,尽量设置pausesLocationUpdatesAutomatically为YES, 如果用户不太可能移动的时候系统会自动暂停位置更新
  • 尽量不要使用startMonitoringSignificantLocationChanges,优先考虑startMonitoringForRegion:

12 硬件检测优化 用户移动、摇晃、倾斜设备时,会产生动作(motion)事件,这些事件由加速度计、陀螺仪、磁力计等硬件检测。在不需要检测的场合,应该及时关闭这些硬件

webview内存优化

1)在页面加载完毕后,通过userdefaults关闭缓存

2)离开webview的控制器时,加载nil的URL,并置空webview

3)收到内存警告时,清理网络的缓存 [[NSURLCache sharedURLCache] removeAllCachedResponses];

19 iOS的系统架构

答: iOS的系统架构分为( 核心操作系统层 theCore OS layer )、( 核心服务层theCore Services layer )、( 媒体层 theMedia layer )和( Cocoa 界面服务层 the Cocoa Touch layer )四个层次。

20 控件主要响应3种事件

答:1). 基于触摸的事件 ; 2). 基于值的事件 ; 3).基于编辑的事件。

21 分类,继承,扩展的区别

一 oc 可以多重继承吗?可以实现多个接口吗?重写一个类的方式用继承好还是分类好呢?为什么?

Objective-c的类不可以有多继承,OC里面都是单继承,多继承可以用protocol委托代理来模拟实现 可以实现多个接口,可以通过实现多个接口完成OC的多重继承

想要实现多继承效果

1.1 实现消息转发功能 在给程序添加消息转发功能以前,必须覆盖两个方法,即methodSignatureForSelector: 和 forwardInvocation:。methodSignatureForSelector:的作用在于为另一个类实现的消息创建一个有效的方法签名。forwardInvocation:将选择器转发给一个真正实现了该消息的对象. 1.2类里对象调用 1.3 代理

分类 扩展,继承的区别

1.分类

iOS中,当原有类的方法不够用时,这时候分类就出现了。category是在现有类的基础上添加新的方法,利用objective-c 的动态运行时分配机制,可以为现有类添加新方法。可以在分类中添加方法和 成员变量,但是添加的成员变量不会自动生成setter和getter方法,需要在实现部分给出实现。所以要自己写出setter,getter方法,用objc_setAssociatedObject和objc_getAssociatedObject关联

如果分类中有和原有类同名的方法, 会优先调用分类中的方法, 就是说会忽略原有类的方法。所以同名方法调用的优先级为 分类 > 本类 > 父类。因此在开发中尽量不要覆盖原有类;

如果多个分类中都有和原有类中同名的方法, 那么调用该方法的时候执行谁由编译器决定;编译器会执行最后一个参与编译的分类中的方法。

总结:

普通方法的优先级: 分类> 子类 > 父类, 优先级高的同名方法覆盖优先级低的 +load方法的优先级: 父类> 子类> 分类 +load方法是在main() 函数之前调用,所有的类文件都会加载,包括分类 +load方法不会被覆盖 同一主类的不同分类中的普通同名方法调用, 取决于编译的顺序, 后编译的文件中的同名方法会覆盖前面所有的,包括主类. +load方法的顺序也取决于编译顺序, 但是不会覆盖 分类中的方法名和主类方法名一样会报警告, 不会报错 声明和实现可以写在不同的分类中, 依然能找到实现 当第一次用到类的时候, 如果重写了+ initialize方法,会去调用 当调用子类的+ initialize方法时候, 先调用父类的,如果父类有分类, 那么分类的+ initialize会覆盖掉父类的, 和普通方法差不多 父类的+ initialize不一定会调用, 因为有可能父类的分类重写了它

分类覆盖的原理:

我们发现原来指针并没有改变,至始至终指向开头的位置。并且经过memmove和memcpy方法之后,分类的方法,属性,协议列表被放在了类对象中原本存储 的方法,属性,协议列表前面。 那么为什么要将分类方法的列表追加到本来的对象方法前面呢,这样做的目的是为了保证分类方法优先调用,我们知道当分类重写本类的方法时,会覆盖本类的方法。 其实经过上面的分析我们知道本质上并不是覆盖,而是优先调用。本类的方法依然在内存中的。我们可以通过打印所有类的所有方法名来查看.

分类总结

问:Category中有load方法吗?load方法是什么时候调用的?load 方法能继承吗?

答:Category中有load方法,load方法在程序启动装载类信息的时候就会调用。load方法可以继承。调用子类的load方法之前,会先调用父类的load方法

问:load、initialize的区别,以及它们在category重写的时候的调用的次序。

答:区别在于调用方式和调用时刻

调用方式:load是根据函数地址直接调用,initialize是通过objc_msgSend调用

调用时刻:load是runtime加载类、分类的时候调用(只会调用1次),initialize是类第一次接收到消息的时候调用,每一个类只会initialize一次(父类的initialize方法可能会被调用多次) 调用顺序:先调用类的load方法,先编译那个类,就先调用load。在调用load之前会先调用父类的load方法。分类中load方法不会覆盖本类的load方法,先编译的分类优先调用load方法。initialize先初始化父类,之后再初始化子类。如果子类没有实现+initialize,会调用父类的+initialize(所以父类的+initialize可能会被调用多次),如果分类实现了+initialize,就覆盖类本身的+initialize调用。

2.扩展

iOS中的extension就是匿名的分类,只有头文件没有实现文件。只能扩展方法,不能添加成员变量。扩展的方法只能在原类中实现。例如你扩展NSString,那么你只能在NSString的.m实现(这是不可能的),所以尽量少用扩展。用 分类就可以了。

3.继承

学习objective-c语言没有人是不知道继承,继承在面向对象语言是非常重要的。在iOS中继承是单继承,既只能有一个父类。在继承中,子类可以使用父类的方法和变量,当子类想对本类或者父类的变量进行初始化,那么需要重写init()方法 。父类也可以访问子类的方法和成员变量

Category是类别,也叫类目,用Category重写类的方法,它仅仅只对本Category有效,并不会影响到其他类和原有类的关系,如果是要在不修改原有类的基础上增加其他原有类没有的方法,就要用类目,继承是可以重写父类的方法,只是子类继承父类的方法来使用

类别的作用?继承和类别在实现中有何区别?

category 可以在不获悉,不改变原来代码的情况下往里面添加新的方法,只能添加,不能删除修改,并且如果类别和原来类中的方法产生名称冲突,则类别将覆盖原来的方法,因为类别具有更高的优先级。

类别主要有3个作用:

1).将类的实现分散到多个不同文件或多个不同框架中。

2).创建对私有方法的前向引用。

3).向对象添加非正式协议。

继承可以增加,修改或者删除方法,并且可以增加属性。

接下来我们探究下分类不能添加属性的实质原因:

我们知道在一个类中用@property声明属性,编译器会自动帮我们生成_成员变量和setter/getter,但分类的指针结构体中,根本没有属性列表。所以在分类中用@property声明属性,既无法生成_成员变量也无法生成setter/getter。

因此结论是:我们可以用@property声明属性,编译和运行都会通过,只要不使用程序也不会崩溃。但如果调用了_成员变量和setter/getter方法,报错就在所难免了。

类别与类扩展的区别: ①类别中原则上只能增加方法(能添加属性的的原因只是通过runtime解决无setter/getter的问题而已);

②类扩展不仅可以增加方法,还可以增加实例变量(或者属性),只是该实例变量默认是@private类型的( 用范围只能在自身类,而不是子类或其他地方);

③类扩展中声明的方法没被实现,编译器会报警,但是类别中的方法没被实现编译器是不会有任何警告的。这是因为类扩展是在编译阶段被添加到类中,而类别是在运行时添加到类中。

④类扩展不能像类别那样拥有独立的实现部分(@implementation部分),也 就是说,类扩展所声明的方法必须依托对应类的实现部分来实现。

⑤定义在 .m 文件中的类扩展方法为私有的,定义在 .h 文件(头文件)中的类扩展方法为公有的。类扩展是在 .m 文件中声明私有方法的非常好的方式。

22 frame和bounds的区别,bounds是否能改变frame?

1.frame是每个view必备的属性,代表的是当前视图的位置和大小,没有设置他,当前视图是看不到的。

位置需要有参照物才能确定,数学中我们用坐标系来确定坐标系中的某个点的位置

在iOS坐标系中以左上角为坐标原点,往右为X正方向,往下是Y正方向 frame中的位置是以父视图的坐标系为标准来确定当前视图的位置

同样的默认情况下,本视图的左上角就是子视图的坐标原点

更改frame中位置,则当前视图的位置会发生改变 更改frame的大小,则当前视图以当前视图的左上角为基准的进行大小的修改

2.bounds是每个View都有的属性,这个属性我们一般不进行设置,他同样代表位置和大小; 每个视图都有自己的坐标系,且这个坐标系默认以自身的左上角为坐标原点,所有子视图以这个坐标系的原点为基准点。 bounds的位置代表的是子视图看待当前视图左上角的位置;bounds的大小代表当前视图的大小;

更改bounds中的位置对于当前视图没有影响,相当于更改了当前视图的坐标系,对于子视图来说当前视图的左上角已经不再是(0,0), 而是改变后的坐标,坐标系改了,那么所有子视图的位置也会跟着改变 更改bounds的大小,bounds的大小代表当前视图的长和宽,修改长

23 id声明的对象有什么特性?

id声明的对象可以是任意类型的OC对象;具有运行时的特点,在程序运行时才确定对象的类型。

24 原子(atomic)跟非原子(non-atomic)属性有什么区别

1). atomic提供多线程安全。是防止在写未完成的时候被另外一个线程读取,造成数据错误

2). non-atomic:在自己管理内存的环境中,解析的访问器保留并自动释放返回的值,如果指定了 nonatomic ,那么访问器只是简单地返回这个值

25 简述线程与进程的区别和联系?

进程和线程的区别和联系

1.进程有自己独立的地址空间;而线程共享进程的地址空间;

2.一个程序至少有一个进程,一个进程至少有一个线程;

3.线程是处理器调度的基本单位,但进程不是;

4.二者均可并发执行

26 原生和H5的交互

第三方 WebViewJavaScriptBridge
利用JavaScriptCore实现交互
JavaScriptCore中类及协议:
JSContext:给JavaScript提供运行的上下文环境
JSValue:JavaScript和Objective-C数据和方法的桥梁
JSManagedValue:管理数据和方法的类
JSVirtualMachine:处理线程相关,使用较少
JSExport:这是一个协议,如果采用协议的方法交互,自己定义的协议必须遵守此协议

OC中提供了JavaScriptCore 这个库,使得OC与js的交互变得更加方便。
使用方法:
1 加入JavaScriptCore 这个framework
2 引入头文件<JavaScriptCore/JavaScriptCore.h>
3 在VC里面加入一个JSContext属性
 @property (strong, nonatomic) JSContext *context;
 JSContext是什么那? 我们看一下api里面的解释

JSContext是一个JS的执行环境,所有的JS执行都发生在一个context里面, 所有的JS value都绑定到context里面
说到WKWebView, 首先要说下WKWebView的优势 iOS8后出现的

1 更多的支持HTML5的特性
2 官方宣称的高达60fps的滚动刷新率以及内置手势
3 将UIWebViewDelegate与UIWebView拆分成了14类与3个协议,以前很多不方便实现 的功能得以实现
4 Safari相同的JavaScript引擎
5 占用更少的内存

27 有关swift struct和class 怎么取舍呢?

究竟什么时该用struct, 什么时候用class呢?
A: 需要值语义的时候用struct, 需要引用语义的时候用class
A: 最普遍的情况是构建数据模型时用struct, 因为struct支持写时复制, 在性能上比class更好
值语义与引用语义的区别
一个比较容易理解的方式是, 这两者存储数据的方式不同,
* 对于值语义, 数据是直接保存在变量中,
* 对于引用语义, 变量存储的是数据的引用地址.
在拷贝数据时, 对于值语义, 拷贝的是数据本身, 对于引用语义, 拷贝的是数据的引用地址
对于class, 拷贝数据时是引用地址的拷贝, 他们操作的一直是同一块内存空间, 所以数据是一样的.

28 数据结构

NSCache和NSDictionary有什么区别,yymodel数据存储采用什么方式?为啥不用NSDictionary
1)NSCache是苹果官方提供的缓存类,具体使用和NSMutableDictionary类似,在AFN和SDWebImage框架中被使用来管理缓存
2)苹果官方解释NSCache在系统内存很低时,会自动释放对象(但模拟器演示不会释放)
    建议:接收到内存警告时主动调用removeAllObject方法释放对象
3)NSCache是线程安全的,在多线程操作中,不需要对NSCache加锁
4)NSCache的Key只是对对象进行Strong引用,不是拷贝,在清理的时候计算的是实际大小而不是引用的大小
当系统资源将要耗尽时,NSCache可以自动删减缓存。如果采用普通的字典,那么就要自己编写挂钩,在系统通知时手动删减缓存,NSCache会先行删减 时间最久为被使用的对象

NSCache 并不会拷贝键,而是会保留它。此行为用NSDictionary也可以实现,但是需要编写比较复杂的代码。NSCache对象不拷贝键的原因在于,很多时候键都是不支持拷贝操作的对象来充当的。因此NSCache对象不会自动拷贝键,所以在键不支持拷贝操作的情况下,该类比字典用起来更方便

NScache是线程安全的,NSDictionary不是。在开发者自己不编写加锁代码的前提下,多个线程可以同时访问NSCache。对缓存来说,线程安全通常是很重要的,因为开发者可能在某个线程中读取数据,此时如果发现缓存里找不着指定的键,那么就要下载该键对应的数据了
hash map表里面是怎么实现的?跟链表有什么区别?
数组:存储区间连续,占用内存严重,寻址容易,插入删除困难
链表:存储区间离散,占用内存比较宽松,寻址困难,插入删除容易
hashmap综合应用了这两种数据结构,实现了寻址容易,插入删除也容易

hashmap结构:哈希表是由数组+链表组成的,数组的默认长度为16(可以自动变长。在构造HashMap的时候也可以指定一个长度),数组里每个元素存储的是一个链表的头结点。而组成链表的结点其实就是hashmap内部定义的一个类:Entity。Entity包含三个元素:key,value和指向下一个Entity的next
实现原理:用一个数组来存储元素,但是这个数组存储的不是基本数据类型。HashMap实现巧妙的地方就在这里,数组存储的元素是一个Entry类,这个类有三个数据域,key、value(键值对),next(指向下一个Entry)
那HashMap是怎么确定插入一个值的时候怎么确定该把这个元素插入这个数组的哪个位置呢?
实际上是通过这个算法实现的:key.hashCode()%Array[].length    位置下标由key的哈希值对数组的长度取模得到

29 对于dispatch_once里做了什么事情?有什么问题吗?

dispatch_once主要是根据onceToken的值来决定怎么去执行代码。

1.当onceToken= 0时,线程执行dispatch_once的block中代码

2.当onceToken= -1时,线程跳过dispatch_once的block中代码不执行

3.当onceToken为其他值时,线程被阻塞,等待onceToken值改变当线程调用shareInstance,此时onceToken= 0,调用block中的代码,此时onceToken的值变为140734537148864。当其他线程再调用shareInstance方法时,onceToken的值已经是140734537148864了,线程阻塞。当block线程执行完block之后,onceToken变为-1.其他线程不再阻塞,跳过block。下次再调用shareInstance时,block已经为-1.直接跳过block

主要在dispatch_once不要写太多代码,最好只创建了对象操作即可,

如果销毁对象的,除了对象置nil后,还需要当onceToken赋值为0

30 UIView UIWindow, UIViewController,UINavgationController是什么关系,有关浮窗怎么去设计的,如果APP里变换UIWindow的话,你的浮窗怎么保证永远在顶层呢

1.1 UIWindow

  • UIWindow是一个特殊的UIView,UIWindow类继承自UIView;

  • 一般来说,一个App只有一个UIWindow,但特定情况下也会出现多个UIWindow,例如,键盘弹出的情况下,就会存在多个UIWindow;

  • iOS启动完毕后,创建的第一个视图控件就是UIWindow,此时需要给UIWindow对象制定根控制器rootViewController属性,系统会自动把rootViewController.view添加到window上(addSubview:方法),显示出来;

  • 只要UIWindow对象存在,即App未被销毁,则rootViewcontroller就不会被销毁。

1.2 UIView

  • 每个UIView都负责在屏幕上定义个矩形区域的显示,可以用设置backgroundColor属性来查看矩形的大小;
  • 每个UIView对象都要负责渲染视图矩形区域内的内容,并且响应该区域中发生的触碰事件;
  • 每个UIView对象中都可以添加一个或多个子UIView对象;
  • UIView对象显示在UIWindow上,用户才能看到界面的样式。
  • 通过view.window属性,可以获得某个UIView所在的UIWindow。

1.3 UIViewController

  • 每个UIViewController都有一个view属性,来显示该控制器的样式;
  • UIViewController负责管理在view上显示的数据,并协调他们和应用程序其他部分的关系;
  • UIViewController类负责创建其管理的视图及在低内存时将它们从内容中移出(didReceiveMemoryWarning方法中实现);
  • 视图控制器还为某些标准的系统行为提供自动响应。比如,在响应设备方向变化时,如果应用程序支持该方向,视图控制器可以对其管理的视图进行尺寸调整,使其适应新的方向。
  • 常见的容器型控制器有:UINavigationController,UITabbarController;
  • 容器型控制器的显示,会把多个子控制器view组合起来显示; 导航栏是viewController的一部分,而不是 navigationController 的一部分,navigationController 是负责视图压栈,出栈的控制器,而导航栏是负责显示的,与 navigationController 没有必然的联系,只不过名字部分相同而已

UINavigationController是用于构建分层应用程序的主要工具,它维护了一个UIViewController栈。这个栈中必须有一个根UIViewController,其他的UIViewController都是子UIViewController。

UINavigationItem表示UINavigationBar中的控件,比如左按钮、中间标题、右按钮。 UINavigationController会自动在当前子UIViewController的UINavigationBar左边添加一个返回按钮。按钮名称是上一个UIViewController的标题。

31 监控卡顿的日志,怎么监控的?里面堆栈信息怎么获取的?原理是什么?

调用栈

首先聊聊栈,它是每个线程独享的一种数据结构。借用维基百科上的一张图片 上图表示了一个栈,它分为若干栈帧(frame),每个栈帧对应一个函数调用,比如蓝色的部分是 DrawSquare 函数的栈帧,它在执行的过程中调用了 DrawLine 函数,栈帧用绿色表示。

可以看到栈帧由三部分组成:函数参数,返回地址,帧内的变量。

举个例子,在调用 DrawLine 函数时首先把函数的参数入栈,这是第一部分;随后将返回地址入栈,这表示当前函数执行完后回到哪里继续执行;在函数内部定义的变量则属于第三部分。

Stack Pointer(栈指针)表示当前栈的顶部,由于大部分操作系统的栈向下生长,它其实是栈地址的最小值。根据之前的解释,Frame Pointer 指向的地址中,存储了上一次 Stack Pointer 的值,也就是返回地址。

在大多数操作系统中,每个栈帧还保存了上一个栈帧的 Frame Pointer,因此只要知道当前栈帧的 Stack Pointer 和 Frame Pointer,就能知道上一个栈帧的 Stack Pointer 和 Frame Pointer,从而递归的获取栈底的帧。 显然当一个函数调用结束时,它的栈帧就不存在了。

Mach_thread 回忆之前对栈的介绍,只要知道 StackPointer 和 FramePointer 就可以完全确定一个栈的信息

那有没有办法拿到所有线程的 StackPointer 和 FramePointer 呢?

答案是肯定的,首先系统提供了 task_threads 方法,可以获取到所有的线程,注意这里的线程是最底层的 mach 线程,它和 NSThread 的关系稍后会详细阐述。

对于每一个线程,可以用 thread_get_state 方法获取它的所有信息,信息填充在_STRUCT_MCONTEXT 类型的参数中。这个方法中有两个参数随着 CPU 架构的不同而改变,因此我定义了 BS_THREAD_STATE_COUNT 和 BS_THREAD_STATE 这两个宏用于屏蔽不同 CPU 之间的区别。 在 _STRUCT_MCONTEXT 类型的结构体中,存储了当前线程的 Stack Pointer 和最顶部栈帧的 Frame Pointer,从而获取到了整个线程的调用栈。

在项目中,调用栈存储在 backtraceBuffer 数组中,其中每一个指针对应了一个栈帧,每个栈帧又对应一个函数调用,并且每个函数都有自己的符号名。

32 Quatrz 2D的绘图功能的三个核心概念是什么?并简述其作用

上下文:主要用于描述图形写入哪里

路径:是在图层上绘制的内容;

状态:用于保存配置变换的值、填充和轮廓, alpha 值等。

33 iPhone os主要提供了几种播放音频的方法?

  • System Sound Services
  • AVAudioPlayer 类
  • Audio Queue Services
  • OpenAL
  1. System Sound Services

System Sound Services 是最底层也是最简单的声音播放服务,调用 AudioServicesPlaySystemSound 这个方法就可以播放一些简单的音频文件,使用此方法只适合播放一些很小的提示或者警告音,因为它有很多限制:

  • 声音长度要小于 30 秒
  • In linear PCM 或者 IMA4 (IMA/ADPCM) 格式的
  • 打包成 .caf, .aif, 或者 .wav 的文件
  • 不能控制播放的进度
  • 调用方法后立即播放声音
  • 没有循环播放和立体声控制
  1. AVAudioPlayer 类

AVAudioPlayer 是 AVFoundation.framework 中定义的一个类,所以使用要先在工程中引入 AVFoundation.framework。我们可以把 AVAudioPlayer 看作是一个高级的播放器,它支持广泛的音频格式,主要是以下这些格式:

  • AAC
  • AMR(AdaptiveMulti-Rate, aformatforspeech)
  • ALAC(AppleLossless)
  • iLBC(internetLowBitrateCodec, anotherformatforspeech)
  • IMA4(IMA/ADPCM)
  • linearPCM(uncompressed)
  • µ-lawanda-law
  • MP3(MPEG-1audiolayer3 AVAudioPlayer 可以播放任意长度的音频文件、支持循环播放、可以同步播放多个音频文件、控制播放进度以及从音频文件的任意一点开始播放等,更高级的功能可以参考 AVAudioPlayer 的文档
  1. Audio Queue Services

如果以上两种音频播放的解决方案都无法满足你的需求,那么我想你肯定需要使用 Audio Queue Services。使用 Audio Queue Services 对音频进行播放,你可以完全实现对声音的控制。例如,你可以在声音数据从文件读到内存缓冲区后对声音进行一定处理再进行播放,从而实现对音频的快速/慢速 播放的功能。

  1. OpenAL

OpenAL 是一套跨平台的开源的音频处理接口,与图形处理的 OpenGL 类似,它为音频播放提供了一套更加优化的方案。它最适合开发游戏的音效,用法也与其他平台下相同。

了解哪些音视频的框架,简单介绍

音频框架 AudioToolbox AudioUnit Core Audio

视频框架 AVFoundation AVkit Core Audio Kit Core Media Core Video Media Player VideoToolbox PhotoKit

34 如何追踪APP的崩溃率,如何解决线上闪退?

违反iOS系统规则产生crash的三种类型:

(1) 内存报警闪退

(2) 响应超时

(3) 用户强制退出

常见的崩溃原因基本都是代码逻辑问题或资源问题,比如数组越界,访问野指针或者资源不存在,或资源大小写错误等。

线上Bug:项目使用了友盟统计,因此会有崩溃日志,通过解析dYSM可以直接定位到大部分bug崩溃之处。解决线上bug需要从主干拉一个新的分支,解决bug并测试通过后,再合并到主干,然后上线。若是多团队开发,可以将fix bug分支与其他团队最近要上线的分支集成,然后集成测试再上线。测试Bug:根据测试所反馈的bug描述,若语义不清晰,则直接找到提bug人,操作给开发人员看,最好是可以bug复现。解决bug时,若能根据描述直接定位bug出错之处,则好处理;若无法直观定位,则根据bug类型分几种处理方式,比如崩溃的bug可以通过instruments来检测、数据显示错误的bug,则需要阅读代码一步步查看逻辑哪里写错

crash的收集如果是在windows上你可以通过itools或pp助手等辅助工具查看系统产生的历史crash日志,然后再根据app来查看。如果是在Mac 系统上,只需要打开xcode->windows->devices,选择device logs进行查看,如下图,这些crash文件都可以导出来,然后再单独对这个crash文件做处理分析

35 什么是事件响应链,点击屏幕时是如何互动的事件传递

事件响应链对于IOS设备用户来说,他们操作设备的方式主要有三种: 触摸屏幕、晃动设备、通过遥控设施控制设备。

对应的事件类型有以下三种:

1、触屏事件(Touch Event)

2、运动事件(Motion Event)

3、远端控制事件(Remote-Control Event)

一. 响应者链(Responder Chain)响应者对象(Responder Object),指的是有响应和处理事件能力的对象。

响应者链就是由一系列的响应者对象构成的一个层次结构。UIResponder是所有响应对象的基类,在UIResponder类中定义了处理上述各种事件的接口。我们熟悉的UIApplication、 UIViewController、UIWindow和所有继承自UIView的UIKit类都直接或间接的继承自UIResponder,所以它们的实例都是可以构成响应者链的响应者对象。响应者链有以下特点:

1、响应者链通常是由视图(UIView)构成的;

2、一个视图的下一个响应者是它视图控制器(UIViewController)(如果有的话),然后再转给它的父视图(Super View);

3、视图控制器(如果有的话)的下一个响应者为其管理的视图的父视图;

4、单例的窗口(UIWindow)的内容视图将指向窗口本身作为它的下一个响应者

需要指出的是,Cocoa Touch应用不像Cocoa应用,它只有一个UIWindow对象,因此整个响应者链要简单一点;

5、单例的应用(UIApplication)是一个响应者链的终点,它的下一个响应者指向nil,以结束整个循环。

二. 点击屏幕时是如何互动的

iOS系统检测到手指触摸(Touch)操作时会将其打包成一个UIEvent对象,并放入当前活动Application的事件队列,单例的UIApplication会从事件队列中取出触摸事件并传递给单例的UIWindow来处理,UIWindow对象首先会使用hitTest:withEvent:方法寻找此次Touch操作初始点所在的视图(View),即需要将触摸事件传递给其处理的视图,这个过程称之为hit-test view。

UIWindow实例对象会首先在它的内容视图上调用hitTest:withEvent:,此方法会在其视图层级结构中的每个视图上调用pointInside:withEvent:(该方法用来判断点击事件发生的位置是否处于当前视图范围内,以确定用户是不是点击了当前视图),如果pointInside:withEvent:返回YES,则继续逐级调用,直到找到touch操作发生的位置,这个视图也就是要找的hit-test view。hitTest:withEvent:方法的处理流程如下:首先调用当前视图的pointInside:withEvent:方法判断触摸点是否在当前视图内;若返回NO,则hitTest:withEvent:返回nil;若返回YES,则向当前视图的所有子视图(subviews)发送hitTest:withEvent:消息,所有子视图的遍历顺序是从最顶层视图一直到到最底层视图,即从subviews数组的末尾向前遍历,直到有子视图返回非空对象或者全部子视图遍历完毕;若第一次有子视图返回非空对象,则hitTest:withEvent:方法返回此对象,处理结束;如所有子视图都返回非,则hitTest:withEvent:方法返回自身(self)。

事件的传递和响应分两个链:

传递链:由系统向离用户最近的view传递。UIKit –> active app’s event queue –> window –> root view –>……–>lowest

view响应链:由离用户最近的view向系统传递。initial view –> super view –> …..–> view controller –> window –> Application

36 iOS 怎么退到后台,收到支付后主动播放到账多少钱的?

blog.csdn.net/LVXIANGAN/a…

实现方式

1、ServiceExtension中收到推送之后,用AVSpeechSynthesisVoice相关类,直接把推送过来需要播报相关的文字转化成语音播报

2、ServiceExtension中收到推送之后,将要播报的数字,找到对应的单个音频,排序,用拼接音频的方式<通过推送过来的文字去查找相关的音频,然后拼接成一个音频>,然后使用AudioServicesCreateSystemSoundID播放

    • (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler:(void (^)(UNNotificationContent * _Nonnull))contentHandler {}

以上的功能只是针对iOS10以上的系统版本可以,那么iOS10以下的怎么办?可以这么办,不用播报到账多少钱,可以通过定制远程推送的语音,来播报”支付宝,您有一笔到账,请及时查看“之类的,支付宝好像也是这个套路。

每次push之前,先去后台查看当前需要推送的设备的系统版本是啥<这个不难实现>,然后定制推送不同的内容。iOS10以上的就推送钱数,并且不推送sound。

iOS10以下的就推送 "sound"="定制的声音文件名称"。

37 static const 和sizeof关键字的作用

static

1.作用于变量: 用static声明局部变量,使其变为静态存储方式(静态数据区),作用域不变;用static声明外部变量,其本身就是静态变量,这只会改变其连接方式,使其只在本文件内部有效,而其他文件不可连接或引用该变量。 2.作用于函数:使用static用于函数定义时,对函数的连接方式产生影响,使得函数只在本文件内部有效,对其他文件是不可见的。这样的函数又叫作静态函数。使用静态函数的好处是,不用担心与其他文件的同名函数产生干扰,另外也是对函数本身的一种保护机制。如果想要其他文件可以引用本地函数,则要在函数定义时使用关键字extern,表示该函数是外部函数,可供其他文件调用。另外在要引用别的文件中定义的外部函数的文件中,使用extern声明要用的外部函数即可。

在C语言中,关键字static有3个明显的作用:

(1)在函数体,一个被声明为静态的变量在这一函数被调用的过程中维持其值不变。

(2)在模块内(但在函数体外),一个被声明为静态的变量可以被模块内所有的函数访问,但不能被模块外其他函数访问。它是一个本地的全局变量。

(3)在模块内,一个被声明为静态的函数只可被这一模块内的其他函数调用。那就是这个函数被限制在声明它的模块的本地范围内使用。
const

1.定义常量

(1)const

修饰变量,它的含义是:const修饰的变量值是不可变的,readonly。

(2)将const改为外部连接,作用于扩大至全局,编译时会分配内存,并且可以不进行初始化,仅仅作为声明,编译器认为在程序其他地方进行了定义

extend const int ValueName = value;

NSString *const 和 const NSString * 的区别

  • 1.变量存储的指针可变,变量存储的值不可变
//A modifiable pointer to a constant NSString (its value can't be modified)
const NSString * str = @"11";
str = @"22";
  • 2.变量存储的值可变,变量存储的指针不可变
//A constant pointer (not modifiable) to an NSString (its value can be modified)
NSString *const str1 = @"33";
str1 = @"44";   //会报错

总结

这里涉及到常量指针和指针常量的概念,简单的来说:
常量指针:就是指向常量的指针,关键字 const 出现在 * 左边,表示指针所指向的地址的内容是不可修改的,但指针自身可变。
指针常量:指针自身是一个常量,关键字 const 出现在 * 右边,表示指针自身不可变,但其指向的地址的内容是可以被修改的。
在此例中:我们知道,NSString永远是immutable的,也是一个指针常量,所以NSString * const 是有效的,而const NSString * 则是无效的。而使用错误的写法,则无法阻止修改该指针指向的地址,使得本应该是常量的值能被修改,造成了隐患。这是需要注意的一个常见错误。

sizeof

1.sizeof是关键字,这一点毋庸置疑。你不能将sizeof定义为任何标识符。查看C语言标准文档里的说明

2.C语言中,sizeof是运算符(操作符),而且是唯一一个以单词形式出现的运算符,它用来计算存放某一个量需要占用多少字节,它的结合性是从右到左。查看C语言标准文档里的说明:

3.sizeof不是函数。产生这样的疑问主要是因为有时候sizeof的外在表现确实有点类似函数,比如:i = sizeof(int);这样的式子,就很容易让人误以为sizeof是一个函数呢。但如果sizeof是函数,那么sizeof i;(假设i为int变量)这种式子一定不能成立,因为函数的参数不可能不用括号括起来。事实上这个sizeof i;可以正常运行,这就说明sizeof绝对不是函数。

38 描述一下iOS MVC,MVP,MVVM的开发模式

MVC是模型、视图、控制开发模式。 对于iOS SDK,所有的View都是视图层的,它应该独立于模型层,由视图控制层来控制。

所有的用户数据都是模型层,它应该独立于视图。

所有的ViewController都是控制层,由它负责控制视图,访问模型数据。

简单描述一下mvc mvp mvvm的架构图

blog.csdn.net/xiaozhuandd… 架构 www.jianshu.com/p/87ac2f075…

MVC的实现思路是: 用户操作View,在Controller层完成业务逻辑处理,更新Model层,将数据显示在View层。 在MVC中,每个层之间都有关联,耦合比较紧,在大型项目中,维护起来比较费力。

View把控制权交给Controller层,自己不执行业务逻辑;Controller层执行业务逻辑并且操作Model层,但不会直接操作View层;View和Model层的同步消息是通过观察者模式进行,而同步操作是由View层自己请求Model层的数据,然后对视图进行更新,观察者模式可以做到多视图同时更新。

MVP的实现思路是:用户操作View,在Presenter层完成业务逻辑处理,更新Model层,通过Presenter将数据显示在View层,完全隔断Model和View之间的通信。

我们通过接口的方式来连接view和presenter层,这样导致的问题是,如果页面过于复杂,我们的接口就会很多,为了更好的处理类似的问题,需要定义一些基类接口,把一些公共的逻辑,比如网络请求,toast,提示框等放在里面。

因为MVP不依赖Model,所以可以更好的进行组件化,把它从特

MVVM和MVP的最大区别是采用了双向绑定机制,View的变动,自动反映在ViewModel上。 MVVM结构如图:

39 64bit和32bit下long 和char * 所占字节是否相同

32位编译器

char :1个字节

char*(即指针变量): 4个字节(32位的寻址空间是2^32, 即32个bit,也就是4个字节。同理64位编译器)

short int : 2个字节

int:  4个字节    范围  -2147483648~2147483647

unsigned int : 4个字节

long:  4个字节 范围 和int一样

long long:  8个字节 范围  -9223372036854775808~9223372036854775807

unsigned long long:  8个字节    最大值:1844674407370955161

float:  4个字节

double:  8个字节

64位编译器 char :1个字节

char*(即指针变量): 8个字节

short int : 2个字节

int:  4个字节 范围  -2147483648~2147483647unsigned int : 4个字节

long:  8个字节 范围  -9223372036854775808~9223372036854775807

long long:  8个字节  范围  -9223372036854775808~9223372036854775807

unsigned long long:  8个字节    最大值:1844674407370955161

float:  4个字节

double:  8个字节

注意:

64bit和32bit下  long 和char*所占字节是不同的 4个字节的最大范围是4294967295, int只是占四个字节 不能用int来存储四个字节的数,要不然会越界

int与NSInteger区别在苹果的api实现中,NSInteger是一个封装,它会识别当前操作系统的位数,自动返回最大的类型。定义的代码类似于下:

ifLP64|| TARGET_OS_EMBEDDED || TARGET_OS_IPHONE || TARGET_OS_WIN32 || NS_BUILD_32_LIKE_64

typedef long NSInteger;
typedef unsigned long NSUInteger;
else
typedef int NSInteger;
typedef unsigned int NSUInteger;
endif

NSInteger与int的区别是NSInteger会根据系统的位数(32or64)自动选择int的最大数值(int or long)

40 NSTimer循环引用问题

NSTimer使用不当就会造成内存泄漏,比如常见的使用方法:

//定义
@property (nonatomic, strong) NSTimer *timer;

//实现
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(showMsg) userInfo:nil repeats:YES];

//销毁
-(void)dealloc
{
    [self.timer invalidate];
    self.timer = nil;
}

由于NSTimer会引用住self,而 self又持有NSTimer对象,所以形成循环引用,dealloc 永远不会被执行,timer 也永远不会被释放,造成内存泄漏。

尝试解决办法:

1、把timer改成弱引用

@property (nonatomic, weak) NSTimer *timer;
虽然self对timer是弱引用,但是控制的delloc方法的执行依赖于timer的invalidate,timer的invalidate又依赖于控制器的delloc方法,这是一个鸡生蛋还是蛋生鸡的问题,依旧是循环引用;

2、使用__weak

那换个思路能不能让NSTimer弱引用target:

 __weak typeof(self) weakSelf = self;
 self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:weakSelf selector:@selector(showMsg) userInfo:nil repeats:YES];
weak关键字适用于block,当block引用了块外的变量时,会根据修饰变量的关键字来决定是强引用还是弱引用,如果变量使用weak关键字修饰,那block会对变量进行弱引用,如果没有__weak关键字,那就是强引用。
  但是NSTimer的 scheduledTimerWithTimeInterval:target方法内部不会判断修饰target的关键字,所以这里传self 和 weakSelf是没区别的,其内部会对target进行强引用,还是会产生循环引用。

3、 选择合适的时机手动释放timer

- (void) viewWillDisappear:(BOOL)animated {
    [super viewWillDisappear:animated];
    
    if (self.timer) {
        [self.timer invalidate];
        self.timer = nil;
    }
}

最终解决办法

1、自定义category用block解决

网上有一些封装的比较好的block的解决方案,思路无外乎是封装一个NSTimer的category,提供block形式的接口:

#import <Foundation/Foundation.h>

@interface NSTimer (TimerBlock)

/**
 分类解决NSTimer在使用时造成的循环引用的问题

 @param interval 间隔时间
 @param block    回调
 @param repeats  用于设置定时器是否重复触发

 @return 返回NSTimer实体
 */
+ (NSTimer *)block_TimerWithTimeInterval:(NSTimeInterval)interval block:(void(^)())block repeats:(BOOL)repeats;

@end
#import "NSTimer+TimerBlock.h"

@implementation NSTimer (TimerBlock)
+ (NSTimer *)block_TimerWithTimeInterval:(NSTimeInterval)interval block:(void (^)())block repeats:(BOOL)reqeats{
    return [self timerWithTimeInterval:interval target:self selector:@selector(blockSelector:) userInfo:[block copy] repeats:reqeats];
}

+ (void) blockSelector:(NSTimer *)timer{
    void (^block)() = timer.userInfo;
    if (block) {
        block();
    }
}
@end

上述创建方式调用者是NSTImer自己,只是NSTimer捕获了参数block。这样我们在使用timer时,由于target的改变,就不再有循环引用了。

__weak typeof(self) weakSelf = self;    //避免 block 强引用 self
self.timer = [NSTimer block_TimerWithTimeInterval:3 block:^{
//    [weakSelf dosomething];
} repeats:YES];

iOS10中,定时器的API新增了block方法,实现原理与此类似,这里采用分类为NSTimer添加了带有block参数的方法,而系统是在原始类中直接添加方法,最终的行为是一致的。

2、给self添加中间件proxy

引入一个对象proxy,proxy弱引用 self,然后 proxy 传入NSTimer。即self 强引用NSTimer,NSTimer强引用 proxy,proxy 弱引用 self,这样通过弱引用来解决了相互引用,此时不会形成环

定义一个继承自NSObject的中间代理对象FFProxy,ViewController不持有timer,而是持有FFProxy实例,让FFProxy实例来弱引用ViewController,timer强引用FFProxy实例,直接看代码:

//FFProxy.h
@interface FFProxy : NSObject
+(instancetype)proxyWithTarget:(id)target;
@end

//FFProxy.m
#import "FFProxy.h"

@interface FFProxy()
@property (nonatomic ,weak) id target;
@end

@implementation FFProxy

+(instancetype)proxyWithTarget:(id)target
{
    FFProxy *proxy = [[FFProxy alloc] init];
    proxy.target = target;
    return proxy;
}

//仅仅添加了weak类型的属性还不够,为了保证中间件能够响应外部self的事件,需要通过消息转发机制,让实际的响应target还是外部self,这一步至关重要,主要涉及到runtime的消息机制。
-(id)forwardingTargetForSelector:(SEL)aSelector
{
    return self.target;
}
@end
//ViewController.m

FFProxy *proxy = [FFProxy proxyWithTarget:self];
//将timer的target设置为proxy,proxy又弱引用了控制器,其实最终还是调用了控制器的showMsg方法。
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:proxy selector:@selector(showMsg) userInfo:nil repeats:YES];

//销毁
-(void)dealloc
{
    [self.timer invalidate];
    self.timer = nil;
}
- (id)forwardingTargetForSelector:(SEL)aSelector是什么?
消息转发,简单来说就是如果当前对象没有实现这个方法,系统会到这个方法里来找实现对象。
本文中由于当前target是FFProxy,但是FFProxy没有实现showMsg方法(当然也不需要它实现),让系统去找target实例的方法实现,也就是去找ViewController中的方法实现。

3、使用NSProxy类

使用iOS的NSProxy类,NSProxy就是专门用来做消息转发的。

//FFWeakProxy.h
@interface FFWeakProxy : NSProxy
+ (instancetype)proxyWithTarget:(id)target;
@end

//FFWeakProxy.m
@interface FFWeakProxy()
@property (nonatomic ,weak)id target;
@end
@implementation FFWeakProxy
+ (instancetype)proxyWithTarget:(id)target {
    //NSProxy实例方法为alloc
    FFWeakProxy *proxy = [FFWeakProxy alloc];
    proxy.target = target;
    return proxy;
}

/**
 这个函数让重载方有机会抛出一个函数的签名,再由后面的forwardInvocation:去执行
    为给定消息提供参数类型信息
 */
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
    return [self.target methodSignatureForSelector:sel];
}

/**
 *  NSInvocation封装了NSMethodSignature,通过invokeWithTarget方法将消息转发给其他对象。这里转发给控制器执行。
 */
- (void)forwardInvocation:(NSInvocation *)invocation {
    [invocation invokeWithTarget:self.target];
}
@end

41 iOS应用导航模式有哪些?

平铺模式,一般由scrollView和pageControl组合而成的展示方式。手机自带的天气比较典型。

标签模式,tabBar的展示方式,这个比较常见。

树状模式,tableView的多态展示方式,常见的9宫格、系统自带的邮箱等展现方式

在iOS应用中,视图控制器处于重要地位。在UIKit中,视图控制器有很多种,有些负责显示视图,有些也同时兼顾导航。我们常见的视图控制器有以下几种:

  • 1、UIViewController。用于自定义视图控制器的导航。
  • 2、UINavigationController。导航控制器,它与UITableViewController结合使用,能够构建树形结构导航模式。
  • 3、UITableBarController。标签栏控制器,用于构建树标签导航模式。
  • 4、UIPageViewController。呈现电子书导航导航风格的控制器(iOS5推出)。
  • 5、UISplitViewController。把屏幕分割成几块的视图控制器,主要为iPad屏幕设计。
  • 6、UIPopoverController。呈现“气泡”风格视图的控制器,主要为iPad屏幕设计。

从组织形式上看,iPhone主要有3种导航模式,每一种导航模式都对应于不同的视图控制器。

  • 1、平铺导航模式。内容没有层次关系展示的内容都放置在一个主屏幕上,采用分屏或分页控制器进行导航,可以左右或者上下滑动屏幕查看内容(如iPod自带的天气预报应用)。
  • 2、标签导航模式。内容被分成几个功能模块,每个功能模块之间没有什么关系。通过标签管理各个模块(如新浪微博应用)。
  • 3、树形结构导航模式。内容有层次,从上到下细分或者具有分类包含等关系(如iPod自带的邮件应用)。

换一种通俗的说法

1、平铺导航:app中的轮播图样式
2、分页导航:分页控制器需要放在一个父视图控制器中,在分页控制器下面还要有子视图控制器,每个子视图控制器对应一个页面。读书器样式
3、标签导航:tabbar
4、树形结构导航:么Nav

42 堆和栈的区别?

总结区别:

按管理方式分:

  • 对于栈来讲,是由系统编译器自动管理,不需要程序员手动管理
  • 对于堆来讲,释放工作由程序员手动管理,不及时回收容易产生内存泄露

按分配方式分

  • 堆是动态分配和回收内存的,没有静态分配的堆
  • 栈有两种分配方式:静态分配和动态分配
静态分配是系统编译器完成的,比如局部变量的分配
动态分配是有alloc函数进行分配的,但是栈的动态分配和堆是不同的,它的动态分配也由系统编译器进行释放,不需要程序员手动管理