视图-更新机制

系统有基本稳定的刷新频率,在layer内容改变的时候,把这个layer做个需要刷新的标记,即是setNeedsDisplay

然后每次刷新时,把上次刷新之后被标记的layer一次性全部提交给图形系统,所以这里还有一个东西,就是**事务(CATransaction)**。

其他类似举例:

1
2
3
4
tableView.beginUpdates()
tableView.insertRows(at: [IndexPath(row: tableData.count-2, section: 0)], with: UITableViewRowAnimation.automatic)
tableView.insertRows(at: [IndexPath(row: tableData.count-1, section: 0)], with: UITableViewRowAnimation.automatic)
tableView.endUpdates()

beginUpdatesendUpdates 方法的作用是,让这两条语句之间的对 tableView 的操作( insert/delete)不立即执行,而是先聚合起来,然后同时更新 UI

1、刷新频率

1.1、显示器的刷新率 VS 显卡渲染的帧率

1、显示器的刷新率为 50Hz,显卡渲染的帧率是 200FPS

即显示器20ms显示一帧,显卡5ms渲染一帧。

则20ms里,显卡渲染了4帧数据,但显示器只能显示了一次,该画面由4帧数据组成,造成撕裂。

所以需要压制显卡的渲染速率,使显卡的帧缓冲区切换行为与显示器的帧绘制保持同步。垂直同步(Vertical Synchronization, VSync)即是为了处理此。

好文推荐: 扫描,撕裂和垂直同步 - VSync 技术实现

1.1.1、显示数据的提供来源:帧缓冲区

问:帧完全渲染后直接显示不是更快,为什么先放到缓冲区等待下一次刷新频率?

答:如果帧完全渲染后直接显示,而不经过缓冲区,理论上听起来更直接,但实际上会带来一些问题,尤其是在现代显示系统中。1. 直接显示的问题,屏幕撕裂(Tearing)

  • 如果帧直接显示,而屏幕正在刷新(例如从上到下逐行刷新),可能会导致屏幕上半部分显示旧帧,下半部分显示新帧,这种现象称为“屏幕撕裂”。
  • 撕裂会严重影响视觉体验,尤其是在快速移动的场景中(如游戏或视频)。

通常,显示器是一台独立工作的设备,状态与显卡无关,会以恒定不变的频率从某个「池」里面读取画面,以保证稳定的图像输出。与此同时,显卡也会往这个「池」里写画面,以供显示器读取。这个「池」叫「Framebuffer(帧缓冲区)」。

一张显卡通常有 2 个帧缓冲区:主/副(Primary/Secondary)缓冲区(也称前/后缓冲区(Front/Back)),由数据选择器(Multiplexer)选择连接到显示器的缓冲区。连接到显示器的缓冲区总是主缓冲区,显示器从中读取图像内容;未连接到显示器的缓冲区总是副缓冲区,显卡向其中写入渲染好的内容。

显卡总是会尝试以最快的速度渲染内容,每完成一帧渲染即切换主/副缓冲区,与显示器的工作状态完全无关。

image-20240905163331025

当渲染完成后,副缓冲区的内容会与主缓冲区的内容进行交换,这个过程是双缓冲(Double Buffering)技术的一部分,用于避免屏幕撕裂和闪烁,同时提高图像渲染的效率。下面详细解释这个过程:

  1. 主缓冲区(Front Buffer):这是当前正在屏幕上显示的图像所在的缓冲区。用户看到的所有内容都存储在这里,显示器会不断读取这个缓冲区的内容来显示图像。
  2. 副缓冲区(Back Buffer):这是显卡用来准备下一帧图像的缓冲区。当一帧图像显示在屏幕上时,显卡可以在副缓冲区中渲染下一帧图像,而不会影响当前显示的内容。
  3. 渲染过程:显卡开始在副缓冲区中渲染新的一帧图像。这个过程可能包括执行复杂的图形计算,如光照计算、纹理映射、深度测试等。
  4. 缓冲区交换(Buffer Swap):一旦副缓冲区中的新帧渲染完成,显卡会执行一个缓冲区交换操作。这个操作会将副缓冲区的内容复制到主缓冲区,这样显示器就可以开始显示新的一帧图像,而副缓冲区则准备好接受下一帧的渲染数据。
  5. 避免屏幕撕裂:由于显示器是逐行刷新的,如果在显示器刷新过程中显卡正在渲染新的帧,就可能出现屏幕撕裂现象。双缓冲技术通过在渲染完成后才进行缓冲区交换,确保了显示器在任何时候都不会读取到半成品的帧,从而避免了屏幕撕裂。
  6. 提高效率:在双缓冲机制下,显卡可以在一帧显示的同时准备下一帧,这样可以更有效地利用显卡资源,提高渲染效率。
  7. 垂直同步(V-Sync):为了进一步提高图像质量和减少撕裂,有时会使用垂直同步技术。垂直同步会同步显卡的渲染速度和显示器的刷新率,确保缓冲区交换发生在显示器刷新周期的合适时刻。

通过这种方式,双缓冲技术能够在不牺牲渲染效率的情况下,提供流畅且无撕裂的图像显示。

1.2、垂直同步(Vertical synchronization)

cpu渲染完一帧后会立即渲染下一帧吗?还是需要等到V-Sync?

  • 开启 V-Sync 时
    • CPU 和 GPU 需要等待 V-Sync 信号才能开始下一帧的工作。(当开启 V-Sync(垂直同步) 时,CPU 和 GPU 需要等待 V-Sync 信号才能开始下一帧的工作,这看起来似乎浪费了时间,但实际上这是为了保证帧的同步显示避免资源浪费。如果 CPU 和 GPU 不等待 V-Sync 信号,而是尽快处理下一帧,可能会导致在一个V-Sync内生成了两帧,但等下只会使用一帧,从而多余的帧会被丢弃,浪费 CPU 和 GPU 的计算资源。)
    • 这是为了避免帧率超过屏幕刷新率,减少屏幕撕裂和功耗。
  • 关闭 V-Sync 时
    • CPU 和 GPU 会尽可能快地渲染帧,不受屏幕刷新率的限制。
    • 这可能会导致屏幕撕裂,但可以提高帧率(例如在游戏中)。

V-Sync 的主要作用就是保证只有在帧缓冲区中的图像被渲染之后,后备缓冲区中的内容才可以被拷贝到帧缓冲区中

其实到这里关于屏幕渲染的内容就已经差不多结束了。

iOS 的显示系统是由 VSync 信号驱动的,VSync 信号由硬件时钟生成,每秒钟发出 60 次(这个值取决设备硬件,比如 iPhone 真机上通常是 59.97)。

RunLoop是一个事件循环对象,用于管理线程的事件处理。每个线程都有一个与之对应的RunLoop对象,主线程的RunLoop在应用程序启动时自动创建并运行,而子线程的RunLoop需要手动创建和维护。RunLoop的作用是让线程能够随时处理事件而不退出,它通过循环检查输入源(如Timer、Source等)和定时源,等待接收事件,当没有事件时让线程休眠以节省资源。

Core Animation(CA)是 iOS 中的动画和图形渲染引擎,它在 RunLoop 中注册了 Observer 来监听特定的事件,以便在适当的时机进行渲染操作。这些 Observer 可以监听多种 RunLoop 的事件,包括:

  1. BeforeWaiting:RunLoop 即将进入休眠状态之前,此时没有待处理的事件,系统可能在等待新的输入事件或定时器事件。Core Animation 会在这个时候进行渲染操作,准备下一帧的显示内容。
  2. Exit:RunLoop 即将退出时,这通常意味着线程将结束运行。在这个阶段,Core Animation 可能会进行一些清理工作。

手机硬件时钟生成Vsync信号

=> 图形服务接收到 VSync 信号后,会通过 IPC 通知到 App 内。

=> app收到当VSync信号到达时,应用程序的主线程开始处理显示内容,如视图创建、布局计算等,然后将内容提交给GPU进行渲染。渲染完成后,GPU将结果提交到帧缓冲区,等待下一个VSync信号到来时显示到屏幕上。

=> Runloop接收到时钟信号(App 的 Runloop 在启动后会注册对应的 CFRunLoopSource 通过 mach_port 接收传过来的时钟信号通知,随后 Source 的回调会驱动整个 App 的动画与显示。

当VSync信号到来时,系统图形服务会通过CADisplayLink等机制通知应用程序,

离屏渲染

在iOS设备上,GPU渲染通常发生在帧缓冲区中,这是GPU用来临时存储即将显示到屏幕上的像素数据的区域。帧缓冲区通常采用双缓冲机制,即存在一个前台缓冲区和一个后台缓冲区。前台缓冲区是当前显示在屏幕上的帧,而后台缓冲区是GPU正在渲染的下一帧。

离屏渲染(Off-Screen Rendering)是当GPU无法直接在帧缓冲区中渲染某些效果时所采用的一种技术。以下是一些可以在帧缓冲区处理的效果,以及一些需要离屏渲染的效果:

可以在帧缓冲区处理的效果:

  1. 简单的颜色填充和边框绘制:这些可以直接在GPU的帧缓冲区中进行,因为它们不涉及复杂的像素操作。
  2. 使用contents属性设置的图像:如果图层的内容是一个简单的图像,且没有复杂的变换或混合,这些可以直接在帧缓冲区中渲染。

需要离屏渲染的效果:

  1. 圆角(Rounded Corners):当为UIView或其子类设置cornerRadius属性时,如果视图同时具有不透明背景色或复杂的背景图像,可能会触发离屏渲染。
  2. 阴影(Shadows):设置layer的shadow属性(如shadowColor、shadowOffset、shadowRadius等)会产生阴影效果,这些效果通常需要离屏渲染。
  3. 透明度(Opacity):当视图的alpha值小于1或使用了CALayer的opacity属性时,如果有复杂混合层级,可能触发离屏渲染。
  4. 遮罩(Masking):使用CALayer的mask属性或UIView的maskView时,遮罩效果通常需要离屏渲染。
  5. 非默认混合模式:当视图或图层使用非默认的混合模式(如multiply、screen、overlay等)时,系统可能需要在离屏缓冲区中进行混合操作。
  6. 多重渲染目标(Multiple Render Passes):需要多次渲染才能完成的效果,如复杂动画、多重叠加效果等,可能需要离屏缓冲区进行中间结果的存储和合并。

为什么某些效果不行:

某些效果需要在渲染过程中进行多次像素级的处理,这在帧缓冲区的单次渲染流程中难以实现。例如,阴影效果需要在原始图层渲染后,再在其周围绘制额外的阴影像素,这涉及到对已经渲染的像素进行二次处理,因此需要在离屏缓冲区中先进行渲染,然后再与主帧缓冲区的内容合并。

优化建议:

  • 避免不必要的离屏渲染:例如,对于圆角效果,可以考虑使用系统提供的圆角属性,而不是通过离屏渲染实现。
  • 合理利用视图层级关系:在iOS中,视图层级关系会影响渲染的优先级,可以通过调整视图的层级来优化渲染性能。
  • 使用offscreen rendering进行调试:通过打开offscreen rendering的调试选项,可以观察到应用在进行离屏渲染时的具体情况,帮助定位性能瓶颈。

通过深入理解离屏渲染的原理并采取有效的优化措施,可以提升应用的性能和用户体验。在实际开发中,应尽量避免不必要的离屏渲染操作,合理利用视图层级关系和Metal API进行自定义渲染,从而打造出流畅、高效的iOS应用。

Flutter刷新机制

Flutter setState 标脏 子widget build 等是什么关系?

Widget不可改变。

StatefulWidget是有状态的Widget,其持有一个关联的State对象,用来保存状态信息,并可以通过调用setState来更新状态。StatefulWidget调用setState时候,该Widget被标记为“脏”(dirty),且它的所有子Widget也会被标记为脏。

通常情况下,被标记为“脏”的Widget会在下一次绘制帧中调用其build方法。然而,这并不意味着它的build方法一定会被调用,即如果Flutter框架能够确定一个Widget及其子树在逻辑上没有变化,即使它被标记为“脏”,Flutter也可能跳过重建过程,复用之前的Widget实例。这种情况通常发生在为一个Widget设置了GlobalKeyUniqueKey,并且Flutter能够识别出这个Widget在逻辑上没有变化,即使它被标记为“脏”,它的build方法也可能不会被调用。

HTTP与网络基础

必备知识架构-线程与网络-③网络

目录

1、传输层协议

传输层的主要协议有TCP、UDP。

TCP是一种面向连接的、可靠的、基于字节流的传输层通信协议。应用于需要可靠传输的应用中,如网页浏览(HTTP)、文件传输(FTP)、邮件传输(SMTP)、远程登录(SSH)等。

  • UDP:适用于对实时性要求高的应用,如在线游戏、VoIP(Voice over IP)、视频会议、DNS查询等。

UDP是一种无连接的传输层协议。相比TCP不用建立三次握手,请求更快。适用于对实时性要求高的应用,如视频会议、在线游戏、实时监控等。

2、应用层协议

HTTP基于tcp,且默认是短连接,如果要长链接,则设置Connection “keep-alive”

WebSocket也是基于tpc,固定是长链接。

RTSP基于TCP或UDP,通常使用TCP来保证控制消息的可靠传输。

HTTP、RTSP都是应用层、WebSocket也可算是应用层(在四层模型:TCP/IP模型中,应用层是一个综合层,它包括了七层模型:OSI模型中的会话层、表示层和应用层的功能。)。

## 九、在一个https连接的网站里,输入账号密码点击登录后,到服务器返回这个请求前,中间经历了什么 > [< 返回目录](#目录)

回答这个问题,我们必须要先了解,一次完整的HTTP请求过程,大概是什么样的。

点击登录和服务器返回这个请求前,中间发生的起始就是一个域名解析和一次完整的http请求过程。

域名解析:即请求DNS服务器,进行域名解析。DNS服务器负责将你的网络地址解析成IP地址,这个IP地址对应网上一台机器;

而一次完整的HTTP请求过程,下面介绍之。

1、一次完整的HTTP请求过程

一次完整的HTTP请求过程如下图所示:

一次完整的HTTP请求过程
详情可参考:一次完整的HTTP请求过程

HTTP与HTTPS的区别

http和https使用的是完全不同的连接方式。

区别统计:

①HTTP是不安全的(它的信息使用明文传输),而HTTPS是安全的(会进行加密);

②使用的端口也不同,http使用的是80端口,https使用的是443端口。

③HTTP无需证书,而HTTPS需要认证证书。

在网络模型中,HTTP工作于应用层,而HTTPS工作在传输层;

2、TCP三次握手

一个故事让大家明白为什么会有tcp三次握手

1
2
3
4
敌人封锁江面, 我方间谍和联络员只能通过电报机隔着江面交流. 但是那时的电报机质量不稳定, 有可能会出现失灵的情况. 所以就出现了如下对话:
间谍: 联络员, 你能收到我发的话么? 你要是能收到, 就说明我这个电报机可以发电报.
联络员: 间谍, 我收到你发的话了, 这说明我的电报机可以收. 但是我不确定我的电报机能不能发, 你能收到我发的这句话么? 你要是能收到, 就说明我的电报机是可以发电报的.
间谍: 联络员, 我收到你的话了. 我的电报机也是能发能收, 我们可以正式交流情报了.

TCP三次握手过程

第一次握手: 客户端给服务器发送一个含有同步序列号SYN 报文。

主机A通过向主机B 发送一个含有同步序列号SYN的标志位的数据段给主机B ,向主机B 请求建立连接,通过这个数据段,主机A告诉主机B 两件事:我想要和你通信;你可以用哪个序列号作为起始数据段来回应我.

第二次握手: 服务器收到 SYN 报文之后,会应答一个 SYN+ACK 报文。

主机B 收到主机A的请求后,用一个带有确认应答(ACK)和同步序列号(SYN)标志位的数据段响应主机A,也告诉主机A两件事:我已经收到你的请求了,你可以传输数据了;你要用哪佧序列号作为起始数据段来回应我

第三次握手: 客户端收到 SYN+ACK 报文之后,会回应一个 ACK 报文。

主机A收到这个数据段后,再发送一个确认应答,确认已收到主机B 的数据段:”我已收到回复,我现在要开始传输实际数据了,这样三次握手就完成了,主机A和主机B 就可以传输数据了.

服务器收到 ACK 报文之后,三次握手建立完成。

附:

TCP三次握手的作用是为了确认双方的接收与发送能力是否正常。

握手过程中传送的包里正常不包含数据,三次握手完毕后,客户端与服务器才正式开始传送数据

但其实第三次握手的时候,是可以携带数据的。也就是说,第一次、第二次握手不可以携带数据,而第三次握手是可以携带数据的。

3、TCP四次挥手

1
2
3
4
5
举个栗子:把客户端比作男孩,服务器比作女孩。通过他们的分手来说明“四次挥手”过程。
"第一次挥手":日久见人心,男孩发现女孩变成了自己讨厌的样子,忍无可忍,于是决定分手,随即写了一封信告诉女孩。
“第二次挥手”:女孩收到信之后,知道了男孩要和自己分手,立马给男孩写了一封回信:分手就分手,给我点时间,我要把你的东西整理好,全部还给你!男孩收到女孩的第一封信之后,明白了女孩知道自己要和她分手。随后等待女孩把自己的东西收拾好。
“第三次挥手”:过了几天,女孩把男孩送的东西都整理好了,于是再次写信给男孩:你的东西我整理好了,快把它们拿走,从此你我恩断义绝!
“第四次挥手”:男孩收到女孩第二封信之后,知道了女孩收拾好东西了,可以正式分手了,于是再次写信告诉女孩:我知道了,这就去拿回来!

4、为什么TCP建立连接要进行3次握手,而断开连接要进行4次?

35 张图解:被问千百遍的 TCP 三次握手和四次挥手面试题

详解 TCP 连接的“ 三次握手 ”与“ 四次挥手 ”/为什么“握手”是三次,“挥手”却要四次?

建立连接时,被动方服务器端结束CLOSED阶段进入“握手”阶段并不需要任何准备,可以直接返回SYN和ACK报文,开始建立连接。

释放连接时,被动方服务器,突然收到主动方客户端释放连接的请求时并不能立即释放连接,因为还有必要的数据需要处理,所以服务器先返回ACK确认收到报文,经过CLOSE-WAIT阶段准备好释放连接之后,才能返回FIN释放连接报文。

所以是“三次握手”,“四次挥手”。

tcp_三次握手tcp_四次挥手

四次握手与三次握手

四次握手其实也能够可靠的同步双方的初始化序号,但由于第二步和第三步可以优化成一步,所以就成了「三次握手」。

为什么需要四次挥手。

  • 关闭连接时,客户端向服务端发送 FIN 时,仅仅表示客户端不再发送数据了但是还能接收数据。
  • 服务器收到客户端的 FIN 报文时,先回一个 ACK 应答报文,而服务端可能还有数据需要处理和发送,等服务端不再发送数据时,才发送 FIN 报文给客户端来表示同意现在关闭连接。

如果第三次握手丢失了,服务端那一方迟迟收不到这个确认报文,就会触发超时重传机制,重传 SYN-ACK 报文,直到收到第三次握手,或者达到最大重传次数。

如果第四次挥手的 ACK 报文没有到达服务端,服务端就会重发 FIN 报文。

5、TCP两次握手会出现什么问题

《计算机网络》(谢希仁 译)中讲了原因:
1.采用两次握手,那么**若Client向Server发起的包A1如果在传输链路上遇到的故障,导致传输到Server的时间相当滞后,在这个时间段由于Client没有收到Server的对于包A1的确认(如果有确认,即Server有收到的话,会发送回一个B1包),那么就会重传一个包A2(应该是等超时了)**,假设服务器正常收到了A2的包,然后返回确认B2包。由于没有第三次握手,这个时候Client和Server就被认为是已经建立连接,可以传输数据了。

接着假设Client发送的第一个A1包随后在链路中传到了Server,对于Server来说这是一个新连接请求,然后Server又为这个连接申请资源,返回B1包,但是由于Client现在用的是A2包,A1包是无效的,Client对于返回的B1包也不会去理会,即后面建立的这个连接其实是没用的(相当于“僵尸”的连接),Server一直为这个连接维持着资源,造成资源的浪费。

所以采用两次握手,有可能会浪费Server的网络资源。

TCP的三次握手最主要是防止已过期/失效的连接再次传到被连接的主机。

为什么要进行第三次握手?

为了防止服务器端开启一些无用的连接增加服务器开销以及防止已失效的连接请求报文段突然又传送到了服务端,因而产生错误。

## 十、TCP与UDP > [< 返回目录](#目录)

1、TCP与UDP的区别

TCP(Transmission Control Protocol,传输控制协议)是基于连接的协议,也就是说,在正式收发数据前,必须和对方建立可靠的连接。
UDP(User Data Protocol,用户数据报协议)是与TCP相对应的协议。它是面向非连接的协议,它不与对方建立连接,而是直接就把数据包发送过去!
UDP适用于一次只传送少量数据、对可靠性要求不高的应用环境。

TCP与UDP的区别:

1
2
3
4
5
1. 基于连接与无连接;
2. 对系统资源的要求(TCP较多,UDP少);
3. UDP程序结构较简单;
4. 流模式与数据报模式 ;
5. TCP保证数据正确性,UDP可能丢包,TCP保证数据顺序,UDP不保证。

2、为什么说UDP是不可靠的?

答:UDP不是面向连接的,UDP传送数据前并不与对方建立连接,对接收到的数据也不发送确认信号,发送端不知道数据是否会正确接收,当然也不用重发,所以说UDP是无连接的、不可靠的一种数据传输协议
也正由于上面的特点,使得UDP的开销更小数据传输速率更高,因为不必进行收发数据的确认,所以UDP的实时性更好。

也正由于上面的特点,使得UDP的开销更小数据传输速率更高,因为不必进行收发数据的确认,所以UDP的实时性更好。

所以采用TCP传输协议的MSN比采用UDP的QQ传输文件慢,
但并不能说QQ的通信是不安全的,因为程序员可以手动对UDP的数据收发进行验证,比如发送方对每个数据包进行编号然后由接收方进行验证啊什么的,即使是这样,UDP因为在底层协议的封装上没有采用类似 TCP的“三次握手”而实现了TCP所无法达到的传输效率。

UDP(User Datagram Protocol)被认为是不可靠的,主要基于以下几个关键特性:


1. 无连接(Connectionless)

UDP 不建立连接,数据包(Datagram)是独立的,发送端直接将数据发往目标 IP+端口,接收端只要监听该端口就能收到数据。

影响

​ • 发送方不会知道接收方是否在线是否能收到数据

​ • 可能会出现数据丢失的情况。


2. 不保证数据到达

UDP 发送数据后,不会确认数据是否成功送达。

影响

​ • 如果网络不稳定,数据包可能会丢失。

​ • 需要上层应用自己实现重传机制(如 QUIC 协议或 RTP 协议)。


3. 不保证数据顺序

UDP 发送的数据包可能会乱序,因为数据包在网络中可能经过不同的路径,不同数据包的传输时间可能不同。

影响

​ • 接收方可能会收到无序的数据,需要应用层自己排序

3、移动端IM系统的协议选型:UDP还是TCP?

移动端IM系统的协议选型:UDP还是TCP?

TCP还是UDP?长连接如何实现?如何实现心跳机制?心跳的间隔如何确定?这些问题都是讨论移动端IM消息推送等类似话题时,几乎一定被问到的问题。

个人认为,更恰当的方式应该是:两种通信协议同时使用,各有侧重。UDP用于保持大量终端的在线与控制,应用与业务则通过TCP去实现。这个和FTP服务控制与数据分离,采取不同的连接,有异曲同工之处。
事实上,这个也是即时通讯巨头QQ所采用的方式。早期的时候,QQ还是主要使用TCP协议,而后来就转向了采用UDP的方式来保持在线,TCP的方式来上传和下载数据。现在,UDP是QQ的默认工作方式,表现良好。相信这个也被沿用到了微信上。
简单的考证:登录PC版QQ,关闭多余的QQ窗口只留下主窗口,并将其最小化。几分钟过后,查看系统网络连接,会发现QQ进程已不保有任何TCP连接,但有UDP网络活动。这时在发送聊天信息,或者打开其他窗口和功能,将发现QQ进程会启用TCP连接。

## 十一、网络数据缓存 > [< 返回目录](#目录)

iOS开发网络篇—数据缓存

RTMP VS RTSP

RTMP(Real-Time Messaging Protocol)和RTSP(Real-Time Streaming Protocol)都是用于流媒体数据传输的协议,但它们在设计和应用上有一些关键的区别。

  • RTMP是基于TCP的协议,RTSP是一种基于TCP或UDP的网络控制协议。
  1. 延迟
    • RTMP的延迟通常在3-30秒,适合于对实时性要求不是极高的应用。
    • RTSP的延迟通常在2-5秒,适合于对实时性要求较高的应用。
  2. 编解码器支持
    • RTMP通常支持H.264视频编码和AAC音频编码。
    • RTSP支持多种视频编码,包括H.265、H.264、VP9等。

在选择使用RTMP还是RTSP时,需要根据具体应用场景、对实时性的要求、客户端兼容性以及安全性需求来决定。例如,如果需要与HTML5兼容且对实时性要求不高,可能会选择基于HTTP的流媒体协议如HLS或DASH。如果需要低延迟和复杂的交互控制,则可能会选择RTSP。而RTMP可能更适合那些已经建立在Flash基础上的旧系统。

1、RTSP

RTSP(Real-Time Streaming Protocol)和TCP、UDP都是网络协议,它们在数据传输和流媒体领域中扮演着重要的角色。下面我将分别介绍它们的特点和用途:

  1. RTSP(Real-Time Streaming Protocol)
    • 定义:RTSP是一种网络控制协议,用于控制流媒体服务器上的媒体流。它允许客户端发出播放、暂停、停止等控制命令。
    • 用途:RTSP常用于视频监控系统中,通过它可以实现对视频流的控制,如请求视频流的开始、停止、快进、倒退等。
    • 特点:
      • 基于TCP或UDP,通常使用TCP来保证控制消息的可靠传输
      • 它不传输媒体本身,而是控制媒体流的传输,媒体数据通常通过RTP(Real-time Transport Protocol)传输。
      • 支持多种媒体类型,包括音频和视频。
  2. TCP(Transmission Control Protocol)
    • 定义:TCP是一种面向连接的、可靠的、基于字节流的传输层通信协议。
    • 用途:TCP广泛应用于需要可靠传输的应用中,如网页浏览(HTTP)、文件传输(FTP)、邮件传输(SMTP)等。
    • 特点:
      • 确保数据包按顺序到达,如果数据包丢失,会重新发送。
      • 建立连接需要三次握手,断开连接需要四次挥手,这增加了一定的延迟。
  3. UDP(User Datagram Protocol)
    • 定义:UDP是一种无连接的传输层协议,它在IP协议的基础上提供一种简单的数据传输服务。
    • 用途:UDP适用于对实时性要求高的应用,如视频会议、在线游戏、实时监控等。
    • 特点:
      • 不保证数据包的顺序、完整性或可靠性,如果数据包丢失,不会重发。
      • 没有建立连接的过程,发送数据前不需要握手,因此延迟较低。
      • 适用于那些可以容忍一定丢包率的应用,或者应用层自己实现数据的重传和顺序控制。

在实际应用中,RTSP通常与RTP(Real-time Transport Protocol)配合使用,RTSP用于控制媒体流,而RTP用于实际传输媒体数据。RTP可以选择在TCP或UDP上运行,但在流媒体传输中,UDP更常见,因为它的低延迟特性更适合实时媒体流的传输。而TCP则因其可靠性,适用于需要确保数据完整性的场景。

2、RTP

实时传输协议(RTP)是一种网络协议,专门设计用于在互联网上传输音频和视频数据。它通常与实时传输控制协议(RTCP)一起使用,后者用于监控数据传输的质量和提供反馈。RTP 被设计为一个基于数据包的协议,将媒体流分成数据包进行传输,每个数据包都有一个序列号,使得接收方能够以正确的顺序重新组合数据包。此外,RTP 还包括一个时间戳,允许接收器同步音频和视频流。

RTP 通常用于各种实时音频和视频传输的应用,如IP语音(VoIP)、视频会议、流媒体和广播电视等。它被广泛支持,并经常与其他协议如RTSP和SIP结合使用,以在互联网上传输音频和视频内容。

RTP 数据包的结构包括一个固定头部和可选的扩展头部,以及实际的媒体数据。

网络 | http常见错误码

  • HTTP状态码介绍,以及HTTP错误代码的故障修复?

    HTTP 状态码是一个客户发出请求时候,WEB服务器返回给客户的一个状态回应,常见HTTP状态有以下五类:

    1.信息代码:1xx,

    2.成功代码:2xx,

    3.重定向:3xx,

    4.客户端错误:4xx,

    5.服务器错误:5xx

    其中,我们需要关注的是客户端和服务器端错误代码,即4xx和5xx代码以及这些代码的常见解决方案。

    客户端错误,从400到499编码的客户端错误是由客户端引起的某些错误导致的,该客户端是Web浏览器、curl命令或postman等其他服务器测试软件发出的请求等。

    服务器错误,当服务器发生错误或服务器识别出无法处理请求时,将发送代码为500到599的服务器错误。

常见状态码:

一些常见的状态码为:

  • 200 – 服务器成功返回网页

  • 302

  • 304(未修改)

  • 404 – 请求的网页不存在

  • 503 – 服务器超时

1xx(临时响应)

表示临时响应并需要请求者继续执行操作的状态代码。

2xx (成功)

表示成功处理了请求的状态代码。

3xx (重定向)

表示要完成请求,需要进一步操作。 通常,这些状态代码用来重定向。

4xx(请求错误)

这些状态代码表示请求可能出错,妨碍了服务器的处理。

5xx(服务器错误)

这些状态代码表示服务器在尝试处理请求时发生内部错误。 这些错误可能是服务器本身的错误,而不是请求出错。

AFNetworking 302 重定向

所有的AF请求都会在post get等请求前执行下面这个方法,所以只需重写这个方法就能拦截请求

1
2
3
4
5
6
@interface AFURLSessionManager ()
// 省略一堆代码
@property (readwrite, nonatomic, copy) AFURLSessionTaskWillPerformHTTPRedirectionBlock taskWillPerformHTTPRedirection;
// 省略一堆代码

@end

使用举例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
manager = [AFHTTPSessionManager manager];
[manager setTaskWillPerformHTTPRedirectionBlock:^NSURLRequest * _Nonnull(NSURLSession * _Nonnull session, NSURLSessionTask * _Nonnull task, NSURLResponse * _Nonnull response, NSURLRequest * _Nonnull request) {
NSHTTPURLResponse *resPonse = (NSHTTPURLResponse *)response;
//1、如果要忽略重定向
/*
if (resPonse.statusCode == 302){
return nil;
}else {//正常请求
return request;
}
*/
// 2、通过重定向url去获取数据
if (resPonse.statusCode == 302){//如果响应code==302 就是重定向
NSMutableURLRequest *res = [NSMutableURLRequest requestWithURL:request.URL];
//通过抓包发现请求头不见了 所以在这里添加请求头,如果没有这个需求的可以忽略
NSMutableDictionary *headers = [NSMutableDictionary dictionaryWithDictionary:request.allHTTPHeaderFields];
NSString *valueStr = [NSString stringWithFormat:@" %@",localToken];
[headers setObject:valueStr forKey:@"Authorization"];
res.allHTTPHeaderFields = headers;

return res;
}else {//正常请求
return request;
}
}];

## 常见笔试/面试题 [< 返回目录](#目录)
1、简要说下Http通信协议的原理,与Socket协议的区别有哪些?

答:HTTP协议:简单对象访问协议,对应于应用层,HTTP协议是基于TCP连接的

tcp协议:对应于传输层

ip协议:对应于网络层

TCP/IP是传输层协议,主要解决数据如何在网络中传输;而HTTP是应用层协议,主要解决如何包装数据。

Socket是对TCP/IP协议的封装,Socket本身并不是协议,而是一个调用接口(API),通过Socket,才能使用TCP/IP协议。

http连接:http连接就是所谓的短连接,即客户端向服务器端发送一次请求,服务器端响应后连接即会断掉;

socket连接:socket连接就是所谓的长连接,理论上客户端和服务器端一旦建立起连接将不会主动断掉;但是由于各种环境因素可能会是连接断开,比如说:服务器端或客户端主机down了,网络故障,或者两者之间长时间没有数据传输,网络防火墙可能会断开该连接以释放网络资源。

2、cookie 和session 的区别:

1、cookie数据存放在客户的浏览器上,session数据放在服务器上。

2、安全性:
cookie不是很安全,别人可以分析存放在本地的COOKIE并进行COOKIE欺骗。
考虑到安全应当使用session。

3、session会在一定时间内保存在服务器上。当访问增多,会比较占用你服务器的性能
考虑到减轻服务器性能方面,应当使用COOKIE。

4、单个cookie保存的数据不能超过4K,很多浏览器都限制一个站点最多保存20个cookie。

5、所以个人建议:
将登陆信息等重要信息存放为SESSION
其他信息如果需要保留,可以放在COOKIE中

## END > < [返回目录](#目录)

内存-②循环引用Timer

# 目录
1
2
3
4
5
6
7
8
1、NSTimer和NSRunLoop的关系?
2、NSTimer使用细节
3、NSTimer的创建
4、NSTimer的循环引用
5、NSTimer使用的优化

6、NSTimer的销毁问题
(1)、子线程中NSTimer的创建和销毁问题

## 九、NSTimer > < [返回目录](#目录)

1、NSTimer的创建

NSTimer的创建通常有两种方式,尽管都是类方法,一种是timerWithXXX,另一种scheduedTimerWithXXX。

二者最大的区别就是后者除了创建一个定时器外会自动以NSDefaultRunLoopModeMode添加到当前线程RunLoop中,不添加到RunLoop中的NSTimer是无法正常工作的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@interface ViewController1 ()
@property (nonatomic, weak) NSTimer *timer1;
@property (nonatomic, weak) NSTimer *timer2;
@end


@implementation ViewController1

- (void)viewDidLoad {
[super viewDidLoad];

self.timer1 = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timeInterval1:) userInfo:nil repeats:YES];

/*
错误做法:
self.timer2 = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timeInterval2:) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer2 forMode:NSDefaultRunLoopMode];
*/

//正确做法:
//特别注意:timer2创建时并没直接赋值给timer2。
//原因是timer2是weak属性,如果直接赋值给timer2会被立即释放。
//因为timerWithXXX方法创建的NSTimer默认并没有加入RunLoop,只有后面加入RunLoop以后才可以将引用指向timer2。从而导致执行到addTimer:forMode的时候,访问了野指针而发生EXC_BAD_ACCESS,崩溃。
NSTimer *tempTimer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timeInterval2:) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:tempTimer forMode:NSDefaultRunLoopMode];
self.timer2 = tempTimer;
。。。。。
}


- (void)timeInterval1:(id)object {
NSLog(@"111");
}

- (void)timeInterval2:(id)object {
NSLog(@"222");
}

@end

2、NSTimer的修饰符

runloop强制持有timer(runloop->timer),timer会强制持有其target,未处理的情况下一般都是self(timer->self),导致self无法释放。
虽然设置timer为weak属性时候,self未强制持有timer,没构成循环应用,但还是导致了self无法释放的问题,dealloc无法执行。

2、NSTimer的循环引用

关于循环引用,我们先看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@interface ViewController1 ()
@property (nonatomic, strong) NSTimer *timer;//注意这里的属性不是为weak,从而很容易引起循环引用
@end


@implementation ViewController1

- (void)viewDidLoad {
// 代码标记1 (产生timer与self之前的强引用,如下图中的L3强引用线)
NSTimer *timer = [[NSTimer alloc] initWithFireDate:[NSDate date] interval:1 target:self selector:@selector(timerFire) userInfo:nil repeats:YES];
// 代码标记2 (产生RunLoop与timer之间的强引用,如下图中的L4强引用线)
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
// 代码标记3 (产生self与timer之间的强引用,,如下图中的L2强引用线)
self.timer = timer;
}

- (void)timerFire {
NSLog(@"timer fire");
}

@end

假设代码中的视图控制器由UINavigationController管理,由于self.timer是strong类型,则强引用可以表示如下:

NSTimer循环引用例子1

由于,很容易看出来,由于timer本身在创建时候已经与self发生了强引用(不管target使用weak还是strong修饰,timer都会对target强引用。)。而赋值时候又由于timer是被设为strong的,而导致self与timer之间也发生了强引用,最终这两个强引用,就形成了循环引用。

所以,

①、首先,我们先解决循环引用,为了解决timer的循环应用问题,我们上面的timer属性应该使用weak。设置成weak后,L2就消失了。

②、但是即使使用了弱引用,上面的代码中ViewController1在pop退出的时候也无法正常释放,原因是在创建NSTimer时指定了target为self,这样一来造成了timer对ViewController1有一个强引用。从而导致,timer没释放的时候,viewController也是不会被释放的。为了让timer能够释放,我们就需要调用NSTimer的invalidate方法(注意:无论是重复执行的定时器还是一次性的定时器只要调用invalidate方法则会变得无效,只是一次性的定时器执行完操作后会自动调用invalidate方法)。所以,假设在viewController pop回去前,我们通过一个按钮来让timer调用invalidate方法,那么viewController在pop回去时候,就能够被释放了。

问题是我们一般不会有这个按钮操作,那么这时候让timer调用invalidate方法的操作,应该写在哪里呢。这时候,你可能会想到那就写在viewDidDisappear中呗。但是一旦在viewWillDisappear中写[timer invalidate]; timer = nil;那么你也得把timer的创建放在viewWillAppear中。因为我们可能执行的是push跳到下一页,再返回来的操作。然而显然将timer的创建放在viewWillAppear中这样的方式,显然会是导致当timer可能需要频繁添加。所以,我们放弃此方法,还是把timer的创建放在viewDidLoad中,然后考虑其他方法。

所以,我们最后为了让ViewController1在pop退出的时候不会因为timer的强引用,而导致无法正常释放。我们选择转移timer中的target。这样就能确保,viewController在pop退出的时候能够正常释放,从而调用viewController的dealloc方法。

附:转移timer中的target的方法通常有两种:

一种是将target分离出来独立成一个对象(在这个对象中创建NSTimer并将对象本身作为NSTimer的target),控制器通过这个对象间接使用NSTimer;

另一种方式的思路仍然是转移target,只是可以直接增加NSTimer扩展(分类),让NSTimer自身做为target,同时可以将操作selector封装到block中。

NSTimer转移target方法二
图中参考NSTimer+Block

后者相对优雅,也是目前使用较多的方案。显然Apple也认识到了这个问题,如果你可以确保代码只在iOS 10下运行就可以使用iOS 10新增的系统级block方案(上面的代码中已经贴出这种方法)。

③、如果不做②中的转移timer的target的话,那么viewController就会无法释放,造成内存泄露。
但是,我们发现通过转移timer的target后,虽然解决了UIViewController1因为被timer强引用而导致的在pop回来的时候无法释放的问题。我们的计时器,却在UIViewController1 pop退出被释放后,两个定时器仍然在运行,也就是它还没被释放。所以,我们还需要解决timer的释放。那怎么让timer释放呢?答:如果要让timer释放掉的话,需要调用NSTimer的invalidate方法(注意:无论是重复执行的定时器还是一次性的定时器只要调用invalidate方法则会变得无效,只是一次性的定时器执行完操作后会自动调用invalidate方法)。
invalidate方法有2个功能:一是将timer从runloop中移除,那么图中的L4就消失,二是timer本身也会释放它持有资源,比如它的target、userinfo、block等,因为这里的target是self,所以强引用L3也就消失。
所以,最终当viewController在pop退出的时候,其正常释放并调用了dealloc放。我们只需要在dealloc方法中,添加上timer调用invalidate的方法,即可以解决viewController被释放了,但timer没被释放的问题。

1
2
3
4
- (void)dealloc {
[self.timer invalidate];
NSLog(@"ViewController1 dealloc...");
}

所以,一个完整的timer过程,代码如下:

1

3、NSTimer和NSRunLoop的关系?

只要出现NSTimer必须要有NSRunLoop,NSTimer必须依赖NSRunLoop才能执行 。NSTimer其实也是一种资源,如果看过多线程编程指引文档的话,我们会发现所有的source如果要起作用,就得加到runloop中去。同理timer这种资源要想起作用,那肯定也需要加到runloop中才会生效喽。如果一个runloop里面不包含任何资源的话,运行该runloop时会立马退出。

NSRunLoop与timer有关方法为:

1
- (void)addTimer:(NSTimer *)timer forMode:(NSString *)mode; //在run loop上注册timer

注意事项:

我们通常在主线程中使用NSTimer,有个实际遇到的问题需要注意。当滑动界面时,系统为了更好地处理UI事件和滚动显示,主线程runloop会暂时停止处理一些其它事件,这时主线程中运行的NSTimer就会被暂停。解决办法就是改变NSTimer运行的mode(mode可以看成事件类型),不使用缺省的NSDefaultRunLoopMode,而是改用NSRunLoopCommonModes,这样主线程就会继续处理NSTimer事件了。具体代码如下:

1
2
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timer:) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

4、NSTimer使用细节:

NSTimer上的定时任务是在创建NSTimer的线程上执行的。NSTimer的销毁和创建必须在同一个线程上操作
NSTimer要被添加到当前线程的 Runloop 里面且 Runloop 被启动,定时任务(selector或者invocation)才会触发。

以下内容摘自:NSTimer定时器进阶——详细介绍,循环引用分析与解决

  1. 它需要被添加到runloop,否则不会运行,当然添加的runloop不存在也不会运行;
  2. 还要指定添加到的runloop的哪个模式,而且还可以指定添加到runloop的多个模式,模式不对也是不会运行的
  3. runloop会对timer有强引用,timer会对目标对象进行强引用(是否隐约的感觉到坑了。。。)
  4. timer的执行时间并不准确,系统繁忙的话,还会被跳过去。(具体的两种不准时,请查看原文)
  5. invalidate调用后,timer停止运行后,就一定能从runloop中消除吗,资源????invalidate方法的调用必须在timer添加到的runloop所在的线程,如果不在的话:由于调用invalidate 方法后,timer本身会释放掉它自己持有的资源比如target、userinfo、block,图中的L3会消失。但是runloop不会释放timer,即图中的L4不会消失,假设,self被pop了–>L1无效–>self引用计数为0,self释放–>L2也消失。此时就剩runloop、timer、L4,timer也就永远不会释放了,造成内存泄露。

NSTimer的强引用问题举例:

NSTimer的强引用问题举例

5、NSTimer使用的优化

问:为什么要在非主线程创建NSTimer?

  • 将 timer 添加到主线程的Runloop里面本身会增加线程负荷;
  • 如果主线程因为某些原因阻塞卡顿了,timer 定时任务触发的时间精度肯定也会受到影响;
  • 有些定时任务不是UI相关的,本来就没必要在主线程执行,给主线程增加不必要的负担。当然也可以在定时任务执行时,手动将任务指派到非主线程上,但这也是有额外开销的。

6、NSTimer的销毁问题

前面我们已经简单讲过要让NSTimer销毁释放的时候,只能通过调用其invalidate来达到销毁目的。关于invalidate的第一个作用以及它在哪个线程调用的问题,我的理解如下:

invalidate方法的第一个作用将timer从runloop中移除。这里的runLoop指的应该是当前的runLoop,而不是timer被添加到的runLoop,这个纯属个人理解,未验证,因为如果是其添加的runLoop的话,那子线程timer的销毁,就不会有人说还要和timer所在的线程一致了。所以,这里的个人理解有如下:

为了销毁timer和去除runloop与timer之间的强引用,我们调用了timer的invalidate方法。

1、对于invalidate方法的调用是写在必须在timer所添加到的runloop所在的线程(如主线程)的时候,invalidate方法会将timer从runloop中移除,并且释放它持有资源。即上面的L4和L3都消失。

2、对于invalidate方法的调用不是写在必须在timer所添加到的runloop所在的线程的时候(如子线程中添加timer,在主线程中调用该timer的invalidate),虽然timer本身会释放掉它自己持有的资源比如target、userinfo、block,图中的L3会消失。但是runloop不会释放timer,即图中的L4不会消失,假设,self被pop了–>L1无效–>self引用计数为0,self释放–>L2也消失。此时就剩runloop、timer、L4,timer也就永远不会释放了,造成内存泄露。

如果invalidate方法的调用的位置不更改的话,这时候要让L4消失的方法,

方法①手动销毁runloop。比如

1
2
3
//[[NSRunLoop currentRunLoop] run]; //将原本的方法注释掉
NSDate *date = [NSDate dateWithTimeIntervalSinceNow:5.f];
[[NSRunLoop currentRunLoop] runUntilDate:date]; //让runloop在5s后销毁

这种方式,只适用于销毁时间确定的情况。那如果销毁时间不确定怎么办?

方法②:因为当某个线程销毁时,其runloop也随之销毁,所以方法二即为通过销毁timer所在的线程,来达到销毁runloop的目的。如果是在主线程,线程一直存在,我们没法让主线程销毁。

所以,下面我们讨论的是在子线程中添加timer的时候,如果该timer的invalidate方法的调用位置不是写在对应子线程,而是写在主线程的时候,我们该怎么通过销毁子线程,来销毁runLoop。从而接触该runloop对timerd的强引用?

6.1子线程中NSTimer的创建和销毁问题

我们按上诉2中②的讨论描述的:子线程中添加timer的时候,如果该timer的invalidate方法的调用位置不是写在对应子线程,而是写在主线程的时候,写出的对应代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@property (nonatomic, weak) NSTimer *threadTimer; //子线程timer


- (void)viewDidLoad {
[super viewDidLoad];

// 开辟子线程
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(newThread) object:nil];
[thread start];
}

- (void)newThread {
@autoreleasepool {
NSThread *currentThread = [NSThread currentThread];
[currentThread setName:@"这是子线程"];

self.threadTimer = [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(threadTimerAction) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] run];
}
}

- (void)threadTimerAction {
static NSInteger counter = 0;

NSString *isMainThreadDescription = [NSThread isMainThread] ? @"YES" : @"NO";
NSLog(@"当前方法执行的线程:%@, 它是否是主线程:%@, counter = %@", [NSThread currentThread], isMainThreadDescription, @(counter++));
}

这时候,我们该怎么通过销毁子线程,来销毁runLoop。从而接触该runloop对timerd的强引用?即怎么销毁线程?

乍看当在子线程开启runloop后,timer会一直在子线程中运行,所以子线程不会销毁,runloop也就无法停止,runloop也就没法销毁,runloop与timer之间的强引用则还是被保留着,这似乎又是个死循环。但实际上,由于上述代码runloop的mode item只有Timer,所以只要销毁timer,runloop就会退出。所以,上述的代码是没问题的,不存在内存泄露问题。

附:NSTimer上的定时任务是在创建NSTimer的线程上执行的。

附:以上NSTimer的内容,有空的话还可参考NSTimer,NSRunLoop,autoreleasepool,多线程的爱恨情仇,它那边讲的,和这边自己理解的基本是一样的。只是对于有些点的介绍详细不一定一样而已。

其他有空可看iOS 中的 NSTimer

## 常见笔试/面试题 [< 返回目录](#目录)

## END [< 返回目录](#目录)

内存-③内存泄漏定位

未整合的文章:

# 目录 * [重要文章](#重要文章) * [一、Analyze—静态分析](#Analyze)
1
2
3
4
5
6
7
1、常见的三种泄露情形
(1)、创建了一个对象,但是并没有使用。Xcode提示信息:Value Stored to 'number' is never read。翻译一下:存储在'number'里的值从未被读取过。
(2)、创建了一个(指针可变的)对象,且初始化了,但是初始化的值一直没读取过。Xcode提示信息:Value Stored to 'str' during its initialization is never read
(3)、调用了让某个对象引用计数加1的函数,但没有调用相应让其引用计数减1的函数。Xcode提示信息:Potential leak of an object stored into 'subImageRef'。 翻译一下:subImageRef对象的内存单元有潜在的泄露风险。ARC中常见于CGxxxRef未release。

2、.....

1
2
3
4
5
6
7
1、ARC下dealloc的使用
ARC下,系统可以帮我们释放该对象,及其包含的对象;但是却无法释放不属于该对象的一些东西,如:
(1)、通知的观察者,或KVO的观察者;
(2)、对象强委托/引用的解除;
(3)、做一些其他的注销之类的操作,如一个ViewController在销毁之前有可能需要和server打交道。

2、controller 不能释放,不走dealloc方法的几种可能

以下内容摘自:iOS性能优化之内存管理:Analyze、Leaks、Allocations的使用和案例代码

前言

内存空间的划分: 我们知道,一个进程占用的内存空间,包含5种不同的数据区:

1
2
3
4
5
(1)BSS段:通常是存放未初始化的全局变量;
(2)数据段:通常是存放已初始化的全局变量。
(3)代码段:通常是存放程序执行代码。
(4)堆:通常是用于存放进程运行中被动态分配的内存段,OC对象(所有继承自NSObject的对象)就存放在堆里。
(5)栈:由编译器自动分配释放,存放函数的参数值,局部变量等值。

栈内存是系统来管理的,因此我们常说的内存管理,指的是堆内存的管理,也就是所有OC对象的创建和销毁的管理。

伴随着iOS5的到来,苹果推出了ARC(自动引用计数)技术,此模式下编译器会自动在合适的地方插入retain、release、autorelease语句,也就是说编译器会自动生成内存管理的代码,解放了广大程序猿的双手,也基本上避免了内存泄露问题,但是呢…

内存泄露的定义是:用动态存储分配函数动态开辟的空间,在使用完毕后未释放,结果导致一直占据该内存单元。直到程序结束。(其实说白了就是该内存空间使用完毕之后未回收)。

在iOS应用中的内存泄露,原因一般有循环引用、错用Strong/copy等。

## 重要文章 * [iOS检测内存泄漏的方法](https://blog.csdn.net/clovejq/article/details/78689759) * [iOS 内存泄漏的查找](https://www.jianshu.com/p/b72d0a442342) * [iOS 内存泄漏监测自动化](https://www.jianshu.com/p/33bda0eed3aa?utm_campaign=maleskine&utm_content=note&utm_medium=seo_notes&utm_source=recommendation)

## 一、Analyze—静态分析 > [< 返回目录](#目录)

顾名思义,静态分析不需要运行程序,就能检查到存在内存泄露的地方。

使用方法:打开Xcode,command + shift + B;或者Xcode - Product - Analyze;

1、常见的三种泄露情形:

(1)创建了一个对象,但是并没有使用。Xcode提示信息:Value Stored to ‘number’ is never read。翻译一下:存储在’number’里的值从未被读取过。

(2)创建了一个(指针可变的)对象,且初始化了,但是初始化的值一直没读取过。Xcode提示信息:Value Stored to ‘str’ during its initialization is never read

(3)调用了让某个对象引用计数加1的函数,但没有调用相应让其引用计数减1的函数。Xcode提示信息:Potential leak of an object stored into ‘subImageRef’。 翻译一下:subImageRef对象的内存单元有潜在的泄露风险。ARC中常见于CGxxxRef未release。

贴上三种常见情形的Demo代码,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
 /**
* 情 形 一:创建了一个对象,但是并没有使用。
* 提示信息:Value Stored to 'number' is never read
* 翻译一下:存储在'number'里的值从未被读取过,
*/
- (void)leakOne {
NSString *str1 = [NSString string];
NSNumber *number;
number = @(str1.length);
/*
说我们没有读取过它,那就读取一下,比如打开下面这句代码,对它发送class消息,就不再会有这个提示了。
当然最好的方法还是将有关number的代码都删掉,因为,你只对number赋值,又不使用,那干嘛创建出来呢。
这是一个比较常见和典型的错误,也很容易检查出来
*/
// [number class];
}

/**
* 情 形 二:创建了一个(指针可变的)对象,且初始化了,但是初始化的值一直没读取过。
* 提示信息:Value Stored to 'str' during its initialization is never read
*/
- (void)leakTwo {
NSString *str = [NSString string]; // 创建并初始化str,此时已经有一个内存单元保存str初始化的值
// NSString *str; // 这样就内存不泄露,因为str是可变的,只需要先声明就行。
// printf("str前 = %p\n",str);
str = @"ceshi"; // str被改变了,指向了"ceshi"所在的地址,指针改变了,但之前保存初始化值的内存空间还未释放,保存str初始化值的内存单元泄露了。
// printf("str后 = %p\n",str); // 指针改变了
[str class];

// 再举两个例子,同理

NSArray *arr = [NSArray array];
// printf("arr前 = %p\n",arr);
// NSArray *arr; // 这样就内存不泄露
arr = @[@"1",@"2"];
// printf("arr后 = %p\n",arr); // 指针改变了
[arr class];

CGRect rect = self.view.frame;
// CGRect rect = CGRectZero; // 这样就内存不泄露
rect = CGRectMake(0, 0, 0, 0);
NSLog(@"rect = %@",NSStringFromCGRect(rect));
}

/**
* 情 形 三:调用了让某个对象引用计数加1的函数,但没有调用相应让其引用计数减1的函数。
* 提示信息:Potential leak of an object stored into 'subImageRef'
* 翻译一下:subImageRef对象的内存单元有潜在的泄露风险
*/
- (void)leakThree {
CGRect rect = CGRectMake(0, 0, 50, 50);
UIImage *image;
CGImageRef subImageRef = CGImageCreateWithImageInRect(image.CGImage, rect); // subImageRef 引用计数 + 1;

UIImage* smallImage = [UIImage imageWithCGImage:subImageRef];

// 应该调用对应的函数,让subImageRef的引用计数减1,就不会泄露了
// CGImageRelease(subImageRef);

[smallImage class];
UIGraphicsEndImageContext();
}

自己遇到的实例:

情形2:静态检测内存泄露Analyze--Value stored to ‘dataArr’ during its initialization is never read
即初始化的时候开辟了一块内存,却始终没用到,导致该块内存泄漏

1
2
3
4
5
6
NSMutableArray *tempMutArr = [NSMutableArray arrayWithCapacity:0];
if ([self.clickedButtonTpye isEqualToString:KClickedButtonTypeLast]) {
tempMutArr = self.lastDataSourceArr;
}else{
tempMutArr = self.hotDataSourceArr;
}

二、Leaks—内存泄露

< 返回目录

  • MLeaksFinder:精准 iOS 内存泄露检测工具

    项目 UIViewController+MemoryLeak.m NSObject+MemoryLeak.m

    原理:MLeaksFinder 一开始从 UIViewController 入手。我们知道,当一个 UIViewController 被 pop 或 dismiss 后,该 UIViewController 包括它的 view,view 的 subviews 等等将很快被释放(除非你把它设计成单例,或者持有它的强引用,但一般很少这样做)。于是,我们只需在一个 ViewController 被 pop 或 dismiss 一小段时间后,看看该 UIViewController,它的 view,view 的 subviews 等等是否还存在。

    即我们hook viewDidDisappear:dismissViewControllerAnimated: completion: 都去执行NSObject的willDealloc方法。

    在NSObject的willDealloc方法中,隔2秒后再去尝试调用另一个方法assertNotDealloc。如果UIViewController已经被成功释放,则肯定是 nil 执行 assertNotDealloc,即assertNotDealloc不会被执行,也就不会弹出内存泄露的弹窗。反之,若UIViewController没有释放,则会弹出内存泄露的弹窗。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    // UIViewController+MemoryLeak.m
    - (void)swizzled_viewDidDisappear:(BOOL)animated {
    [self swizzled_viewDidDisappear:animated];

    if ([objc_getAssociatedObject(self, kHasBeenPoppedKey) boolValue]) {
    [self willDealloc];
    }
    }

    - (void)swizzled_dismissViewControllerAnimated:(BOOL)flag completion:(void (^)(void))completion {
    [self swizzled_dismissViewControllerAnimated:flag completion:completion];
    ......

    [dismissedViewController willDealloc];
    }

    // NSObject+MemoryLeak.m
    - (BOOL)willDealloc {
    ......

    __weak id weakSelf = self;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    __strong id strongSelf = weakSelf;
    [strongSelf assertNotDealloc];
    });

    return YES;
    }

    - (void)assertNotDealloc {
    if ([MLeakedObjectProxy isAnyObjectLeakedAtPtrs:[self parentPtrs]]) {
    return;
    }
    [MLeakedObjectProxy addLeakedObject:self];

    NSString *className = NSStringFromClass([self class]);
    NSLog(@"Possibly Memory Leak.\nIn case that %@ should not be dealloced, override -willDealloc in %@ by returning NO.\nView-ViewController stack: %@", className, className, [self viewStack]);
    }
  • 自动化内存泄漏检测 项目 https://github.com/liujiakuoyx/leak_detector/blob/main/lib/src/leak_navigator_observer.dart#L121

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    ///Such as WebSocket is delay close connect.
    const int _defaultCheckLeakDelay = 500;

    typedef ShouldAddedRoute = bool Function(Route route);

    ///NavigatorObserver
    class LeakNavigatorObserver extends NavigatorObserver {
    final ShouldAddedRoute? shouldCheck;
    final int checkLeakDelay;

    ///[callback] if 'null',the all route can added to LeakDetector.
    ///if not 'null', returns ‘true’, then this route will be added to the LeakDetector.
    LeakNavigatorObserver(
    {this.checkLeakDelay = _defaultCheckLeakDelay, this.shouldCheck});

    @override
    void didPop(Route route, Route? previousRoute) {
    _remove(route);
    }

    @override
    void didPush(Route route, Route? previousRoute) {
    _add(route);
    }

    @override
    void didRemove(Route route, Route? previousRoute) {
    _remove(route);
    }

    @override
    void didReplace({Route? newRoute, Route? oldRoute}) {
    if (newRoute != null) {
    _add(newRoute);
    }
    if (oldRoute != null) {
    _remove(oldRoute);
    }
    }
    ......
    }
  • 快手 Flutter 上的内存泄漏监控

Leaks是动态的内存泄露检查工具,需要一边运行程序,一边检测。

内存泄漏Leak1

先不切换到Call Trees,先看看Statistics(统计数据)下的情况

内存泄漏Leak2
Allocation中我们主要关注的是Persistent和Persistent Bytes,分别表示当前时间段,申请了但是还没释放的内存数量和大小。

切换到Call Trees后世这样的

内存泄漏Leak3

项目中遇到过的内存泄漏

这里我们不是去查看Call Tree,而是查看Cycles & Roots

循环引用定位1

点击标记4处为黑色,即代表该处会发生内存泄漏,双击进入代码,如图:
循环引用定位2
果然存在内存泄漏

自己遇到的其他例子:

Leak_mine_1
开始不明白为什么这个变量会内存泄漏后面才能白,其实_priceDetailModel这个本身已经用OrderPriceDetailModel赋值过一次了,而这里你又赋值了一次,导致多了一个。代码情况如下两个图:
Leak_mine_2
又在set方法中生成了一个地址
Leak_mine_3

三、ARC下的dealloc

1、ARC下dealloc的使用

ARC下,系统可以帮我们释放该对象,及其包含的对象;但是却无法释放不属于该对象的一些东西,如:

1
2
3
(1)、通知的观察者,或KVO的观察者;
(2)、对象强委托/引用的解除;
(3)、做一些其他的注销之类的操作,如一个ViewController在销毁之前有可能需要和server打交道。
(1)、通知的观察者,或KVO的观察者;

由于通知中心是系统的一个单例,你在注册通知的观察者时,实际上是在通知中心注册的,

这时,即使ARC下系统帮我们释放了对象,但是在通知中心的观察还是没有移除,那么当有

该通知时,依然会尝试调用该对象的接受通知的方法,这可能会导致一些问题.

(2)、对象强委托/引用的解除;

对于其他的对象来把你当做委托 delegate时,并且是 强引用时,即时你自身被释放,但是引用你的对象依然还在,

这时需要在引用你的对象移除该delegate

(3)、做一些其他的注销之类的操作,如一个ViewController在销毁之前有可能需要和server打交道。

一个对象,如一个ViewController在销毁之前有可能需要和server打交道;

这时我们也可以在dealloc中写

2、controller 不能释放,不走dealloc方法的几种可能

主要原因还是循环引用,引起的内存泄漏。

详情参考:controller 不能释放,不走dealloc方法的4种可能

## 四、Time Profile > [< 返回目录](#目录)

详情参考:instrument Time Profiler总结

使用Time Profile前有两点需要注意的地方:

1
2
1、一定要使用真机调试
2、应用程序一定要使用发布配置

图标为黑色头像的就是Time Profiler给我们的提示,有可能存在性能瓶颈的地方

TimeProfile1

其他

其他参考材料:

问:Xcode 运行程序,左侧memory 不显示内存。。

答:运行程序后,xcode 不显示当前使用的内存情况,问题是打开了僵尸–enable zoombie Objects,关闭即可。
即打开 product—>SCheme–>EditSCheme –>enable zoombie Objects 取消选中 ok

就可以继续显示了

1、MLeaksFinder 适配新版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
+ (void)alertWithTitle:(NSString *)title
message:(NSString *)message
delegate:(id<UIAlertViewDelegate>)delegate
additionalButtonTitle:(NSString *)additionalButtonTitle {
/*
[alertView dismissWithClickedButtonIndex:0 animated:NO];
UIAlertView *alertViewTemp = [[UIAlertView alloc] initWithTitle:title
message:message
delegate:delegate
cancelButtonTitle:@"OK"
otherButtonTitles:additionalButtonTitle, nil];
[alertViewTemp show];
alertView = alertViewTemp;

NSLog(@"%@: %@", title, message);
*/

// 获取当前的根视图控制器
UIViewController *rootViewController = [UIApplication sharedApplication].keyWindow.rootViewController;
while (rootViewController.presentedViewController) {
rootViewController = rootViewController.presentedViewController;
}

UIAlertController *alertController = [UIAlertController alertControllerWithTitle:title message:message preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction *okAction = [UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:nil];
[alertController addAction:okAction];
if (additionalButtonTitle) {
UIAlertAction *additionalAction = [UIAlertAction actionWithTitle:additionalButtonTitle style:UIAlertActionStyleDefault handler:nil];
[alertController addAction:additionalAction];
}
[rootViewController presentViewController:alertController animated:YES completion:nil];

NSLog(@"%@: %@", title, message);
}

## 常见笔试/面试题 > [< 返回目录](#目录)

## END > [< 返回目录](#目录)

视图-①本质

# 目录
1
2
3
4
5
6
7
8
9
附:整个响应链及事件链
1、完善响应链查找知识点
2、基础概念等详解
2.1 响应者对象(UIResponder)
2.2、UITouch(点击对象)
2.2.1、UITouch的几个主要属性和方法
2.2.2、UITouch的生成场景
2.3、UIEvent(事件对象)
3、响应链的应用

## 一、在一个app中间有一个button,在你手触摸屏幕点击后,到这个button收到点击事件,中间发生了什么 这其实是一个事件传递和响应链的问题。(其实,按钮点击后,这里还包括runloop的唤醒等知识,不过这点我们放在下一大点讲)。

答:在我们点击按钮的时候,会产生了UITouch(点击对象)和UIEvent(事件对象),这两个对象组合成一个点击事件。而发生触摸事件后,

①消息循环(runloop)/系统就会接收到这个触摸事件,并将它放到一个由UIApplication管理的消息队列(先进先出)里。

②UIApplication会从事件队列中取出最前面的事件,并将事件分发下去以便处理。首先UIApplication将事件传递给的是UIWindow对象(即一般为应用程序的主窗口keyWindow)。

③然后,UIWindow(继承自UIView)对象会继续向它的子View对象传递,直到传递到最上层。(或者说UIWindow使用hitTest:withEvent:方法查找touch操作的所在的视图view)

其中的应用程序逐级寻找能够响应这个事件的对象,直到没有响应者响应。这一寻找的过程,被称作事件的响应链

二、事件的响应链

事件的响应链大概过程如下图所示:

事件的响应链

  • 1、在传递的过程中,下一响应者的查找是通过UIView里的- pointInside: withEvent:- hitTest: withEvent:两个方法来确定的。当从最初的只有一个响应者通过这样的方式不断的找到下一响应者后,这些响应者就组成了一个响应者链。

  • 2、当通过- hitTest: withEvent:找到第一响应者后,若第一响应者没有处理事件,则沿着响应者链向上追溯寻找响应者(即灰色箭头方向)执行touches方法。这个过程就是事件的传递过程。从这可以看出它的方向是跟响应链方向相反的。这里我们可以用UITableViewCell中点击上面的label来想象。

附:整个响应链及事件链

整个响应链(向下)及事件链(向上),大概如图所示:

响应链(向下)及事件链(向上)
在上图,当- hitTest: withEvent:方法沿着红色箭头方向寻找第一响应者后,若第一响应者没有处理事件,则沿着响应者链向上追溯寻找响应者(即灰色箭头方向)执行touches方法。
所以响应链为红色部分,事件链的顺序可以理解为图上的灰色箭头部分(个人理解)。

1、完善响应链查找知识点

我们已经知道响应者链是由多个响应者组合起来的链条。那么怎么找到这些相应者呢?

响应者的查找为通过UIView内部的下面两个方法来查找的

1
2
3
4
5
//根据点击坐标返回事件是否发生在本视图以内
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event; // default returns YES if point is in bounds

// 返回响应点击事件的对象(当点击区域在分为内时候,如果有子视图则返回子视图里最终的响应者,如果没有子视图则返回自身)
- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event; // recursively calls -pointInside:withEvent:. point is in the receiver's coordinate system

对于一个视图

①、若子视图中的- pointInside: withEvent:方法返回为NO,即判断用户点击的区域不在该子视图范围内的话,则停止对这个子视图里的子视图继续查找,- hitTest: withEvent:返回nil。

②、若子视图中的- pointInside: withEvent:方法返回为YES,即判断用户点击的区域在该子视图范围内的话,则继续往该子视图里的子视图查找,直到没有子视图,然后- hitTest: withEvent:返回这个子视图,而后之前的视图的- hitTest: withEvent:也返回这个子视图。

  • 
    
    1
    2
    3
    4
    5
    6
    7
    8
    9

    hitTest-withEvent-查找过程举例,如下图
    > <img src="3视图-①本质/hitTest-withEvent-查找过程举例.png" alt="hitTest-withEvent-查找过程举例" style="zoom:33%;" />
    > 图片中view等级
    ```objective-c
    [ViewA addSubview:ViewB];
    [ViewA addSubview:ViewC];
    [ViewB addSubview:ViewD];
    [ViewB addSubview:ViewE];

那么点击viewE后,发生的过程是怎样的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
1.A 是UIWindow的根视图,首先对A进行hitTest:withEvent:
2.判断A的userInteractionEnabled,如果为NO,A的hitTest:withEvent返回nil;
3.pointInside:withEvent:方法判断用户点击是否在A的范围内,显然返回YES
4.遍历A的子视图B和C。由于从后向前遍历,因此先查看C,再查看B。
>
4.1 查看C:
调用C的hitTest:withEvent方法:pointInside:withEvent:方法判断用户点击是否在C的范围内,不在返回NO,C对应的hitTest:withEvent: 方法return nil;
>
4.2 再查看B
调用B的hitTest:withEvent方法:pointInside:withEvent:判断用户点击是否在B的返回内,在返回YES。
>遍历B的子视图D和E,从后向前遍历,所以先查看E,再查看D。
4.2.1先查看E,调用E的hitTest:withEvent方法:pointInside:withEvent:方法 判断用户点击是否在E的范围内,在返回YES,E没有子视图,因此E对应的hitTest:withEvent方法返回E,再往前回溯,就是B的hitTest:withEvent方法返回E,因此A的hitTest:withEvent方法返回E。
4.2.2查看D,略
>
至此,点击事件的第一响应者就找到了。

2、基础概念等详解

iOS中的事件可以分为3大类型:

  1. 触摸事件
  2. 加速计事件
  3. 远程控制事件

在iOS中不是任何对象都能处理事件,能接受并这些处理事件的对象只有直接或间接继承自UIResponder的对象,我们称之为“响应者对象”。

2.1 响应者对象(UIResponder)

①、为什么只有继承自UIResponder的类才能够接收并处理事件呢?因为处理这些事件的方法是卸载UIResponder中的啊。详细的UIResponder中提供的方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
4个处理触摸事件的对象方法
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event;

和3个处理加速计事件
- (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event;
- (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event;
- (void)motionCancelled:(UIEventSubtype)motion withEvent:(UIEvent *)event;

以及1个处理远程控制事件的方法
- (void)remoteControlReceivedWithEvent:(UIEvent *)event;

附:如何实现UIView的拖拽呢?即让UIView随着手指的移动而移动。

答: 重写touchsMoved:withEvent:方法

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event{
// 想让控件随着手指移动而移动,监听手指移动
// 获取UITouch对象
UITouch *touch = [touches anyObject];
// 获取当前点的位置
CGPoint curP = [touch locationInView:self];
// 获取上一个点的位置
CGPoint preP = [touch previousLocationInView:self];
// 获取它们x轴的偏移量,每次都是相对上一次
CGFloat offsetX = curP.x - preP.x;
// 获取y轴的偏移量
CGFloat offsetY = curP.y - preP.y;
// 修改控件的形变或者frame,center,就可以控制控件的位置
// 形变也是相对上一次形变(平移)
// CGAffineTransformMakeTranslation:会把之前形变给清空,重新开始设置形变参数
// make:相对于最原始的位置形变
// CGAffineTransform t:相对这个t的形变的基础上再去形变
// 如果相对哪个形变再次形变,就传入它的形变
self.transform = CGAffineTransformTranslate(self.transform, offsetX, offsetY);
}

②、那么iOS中能接收并处理事件或者说继承自UIResponder的类有哪些呢?

1
2
iOS中能接收并处理事件或者说继承自UIResponder的类有:
UIApplication、UIWindow、UIViewController和所有继承UIView的UIKit类都直接或间接的继承自UIResponder。

从UIResponder内部提供的方法可以看出,触摸方法接收两个参数,一个UITouch对象的集合,还有一个UIEvent对象。这两个参数分别代表的是点击对象和事件对象。

2.2、UITouch(点击对象)

UITouch表示单个点击,其类文件中存在枚举类型UITouchPhase的属性,用来表示当前点击的状态。这些状态包括点击开始、移动、停止不动、结束和取消五个状态。每次点击发生的时候,点击对象都放在一个集合中传入UIResponder的回调方法中。

2.2.1、UITouch的几个主要属性和方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@property(nonatomic,readonly) NSTimeInterval      timestamp;    // 记录了触摸事件产生或变化时的时间,单位是秒
@property(nonatomic,readonly) UITouchPhase phase; // 当前触摸事件所处的状态
@property(nonatomic,readonly) NSUInteger tapCount; // 短时间内点按屏幕的次数,可以根据tapCount判断单击、双击或更多的点击
@property(nonatomic,readonly) UITouchType type NS_AVAILABLE_IOS(9_0);


@property(nullable,nonatomic,readonly,strong) UIWindow *window; //触摸产生时所处的窗口
@property(nullable,nonatomic,readonly,strong) UIView *view; //触摸产生时所处的视图
@property(nullable,nonatomic,readonly,copy) NSArray <UIGestureRecognizer *> *gestureRecognizers NS_AVAILABLE_IOS(3_2);

/**
* 获取当前点击位置的坐标点
*
* @param view 调用时传入的view参数为nil的话,返回的是触摸点在UIWindow的位置
*
* @return 返回值表示触摸在view上的位置点(这里返回的位置是针对view的坐标系的(以view的左上角为原点(0, 0)))
*/
- (CGPoint)locationInView:(nullable UIView *)view;

/// 获取前一个触摸点位置的坐标点
- (CGPoint)previousLocationInView:(nullable UIView *)view;
2.2.2、UITouch的生成场景:

前言:每根手指触摸屏幕时都会创建一个与该手指相关的UITouch对象。一根手指对应一个UITouch对象。每个UITouch对象保存着跟手指相关的信息,比如触摸的位置、时间、阶段。
当手指移动时,系统会更新同一个UITouch对象,使之能够一直保存该手指在的触摸位置。
当手指离开屏幕时,系统会销毁相应的UITouch对象

实际调用现象举例:

①、当用户用一根手指触摸屏幕时,view会调用1次touchesBegan:withEvent:方法。touches参数中装着1个UITouch对象。

②、如果这两根手指一前一后分开触摸同一个view,那么view会分别调用2次touchesBegan:withEvent:方法,并且每次调用时的touches参数中只包含一个UITouch对象。

③、如果两根手指同时触摸一个view,那么view只会调用一次touchesBegan:withEvent:方法,touches参数中装着2个UITouch对象

2.3、UIEvent(事件对象)

iOS使用UIEvent表示用户交互的事件对象,在UIEvent.h文件中,我们可以看到有一个UIEventType类型的属性,这个属性表示了当前的响应事件类型。分别有多点触控、摇一摇以及远程操作(在iOS之后新增了3DTouch事件类型)。在一个用户点击事件处理过程中,UIEvent对象是唯一的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@property(nonatomic,readonly) UIEventType     type NS_AVAILABLE_IOS(3_0);
@property(nonatomic,readonly) UIEventSubtype subtype NS_AVAILABLE_IOS(3_0);


typedef NS_ENUM(NSInteger, UIEventType) {
UIEventTypeTouches,
UIEventTypeMotion,
UIEventTypeRemoteControl,
UIEventTypePresses NS_ENUM_AVAILABLE_IOS(9_0),
};

typedef NS_ENUM(NSInteger, UIEventSubtype) {
// available in iPhone OS 3.0
UIEventSubtypeNone = 0,

// for UIEventTypeMotion, available in iPhone OS 3.0
UIEventSubtypeMotionShake = 1,

// for UIEventTypeRemoteControl, available in iOS 4.0
UIEventSubtypeRemoteControlPlay = 100,
UIEventSubtypeRemoteControlPause = 101,
UIEventSubtypeRemoteControlStop = 102,
UIEventSubtypeRemoteControlTogglePlayPause = 103,
UIEventSubtypeRemoteControlNextTrack = 104,
UIEventSubtypeRemoteControlPreviousTrack = 105,
UIEventSubtypeRemoteControlBeginSeekingBackward = 106,
UIEventSubtypeRemoteControlEndSeekingBackward = 107,
UIEventSubtypeRemoteControlBeginSeekingForward = 108,
UIEventSubtypeRemoteControlEndSeekingForward = 109,
};

介绍了以上响应者对象(UIResponder)及其相关的UITouch(点击对象)和UIEvent(事件对象)相关概念后,我们就知道了用户点击后,会产生了UITouch(点击对象)和UIEvent(事件对象)并打包发送,最后由响应者对象(UIResponder)来处理这些事件。

现在的问题是你知道它是怎么通过用户的点击位置找到处理该点击事件的响应者对象吗?

3、响应链的应用

既然已经知道了系统是怎么获取响应视图的流程了,那么我们可以通过重写查找事件处理者的方法来实现不规则形状点击。

最常见的不规则视图就是圆形视图,在demo中我设置view的宽高为200,那么重写方法事件如下:

1
2
3
4
5
6
7
8
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
const CGFloat halfWidth = 100;
CGFloat xOffset = point.x - 100;
CGFloat yOffset = point.y - 100;
CGFloat radius = sqrt(xOffset * xOffset + yOffset * yOffset);
return radius <= halfWidth;
}

最终的效果图如下:
响应链的应用1_点击不规则图形

前面说过按钮点击后,这里还包括一些runloop相关的知识,如唤醒等,所以下面我们就专门开讲一件Runloop。

## 三、CALayer和UIView **UIView与CALayer**是什么关系。

<单一职责原则>
UIView为CALayer提供内容,以及负责处理触摸等事件,参与响应链
CALayer负责显示内容contents

UIViewCALayerdelegate(CALayerDelegate)

UIView继承自UIResponder类,可以响应事件

CALayer直接继承自NSObject类,不可以响应事件

UIView主要处理事件,CALayer负责绘制

每个UIView内部都有一个CALayer在背后提供内容的绘制和显示,并且UIView的尺寸样式都由内部的Layer所提供。两者都有树状层级结构,Layer内部有SubLayersView内部有SubViews,但是LayerView多了个AnchorPoint

CALayer的结构图如下:

CALayer的结构图

在 iOS 系统中所有显示的视图都是从基类UIView继承而来的,同时UIView负责接收用户交互。 但是实际上你所看到的视图内容,包括图形等,都是由UIView的一个实例图层属性来绘制和渲染的,那就是CALayer。

CALayer类的概念与UIView非常类似,它也具有树形的层级关系,并且可以包含图片文本、背景色等。它与UIView最大的不同在于它不能响应用户交互,可以说它根本就不知道响应链的存在。

在每一个UIView实例当中,都有一个默认的支持图层,UIView负责创建并且管理这个图层。实际上这个CALayer图层才是真正用来在屏幕上显示的,UIView仅仅是对它的一层封装,实现了CALayer的delegate,提供了处理事件交互的具体功能,还有动画底层方法的高级 API。

以上摘要来自:内存恶鬼drawRect - 谈画图功能的内存优化中的CALayer和UIView介绍部分。

CALayer有三个视觉元素:背景色、内容和边框,其中,内容的本质是一个CGImage

CALayer和UIView

简述CALayer和UIView的关系

答:UIView和CALayer是相互依赖的关系。UIView依赖与calayer提供的内容,CALayer依赖uivew提供的容器来显示绘制的内容。归根到底CALayer是这一切的基础,如果没有CALayer,UIView自身也不会存在,UIView是一个特殊的CALayer实现,添加了响应事件的能力

结论:
UIView来自CALayer,高于CALayer,是CALayer高层实现与封装。UIView的所有特性来源于CALayer支持。

为什么CLLayer可以在子线程,而UIView不行?

CLLayer可以在子线程中运行,而UIView不行,这主要是因为它们在iOS系统中的职责和设计原则不同。

  1. 职责不同
    • UIView:UIView是用户界面的基础类,负责处理用户交互(如触摸事件),并且所有的UI更新和渲染都必须在主线程中进行。UIView通过其内部的CALayer来处理内容的显示,UIView本身作为CALayer的delegate,负责响应CALayer的变化并进行相应的UI更新。
    • CALayer:CALayer是Core Animation框架的一部分,主要负责内容的绘制和显示。CALayer可以独立于UIView存在,并且可以在子线程中进行绘制操作,因为它不直接处理用户交互,只负责图形的渲染。
  2. 线程安全
    • UIView:UIKit框架并不是线程安全的,官方建议所有的UI操作都在主线程进行,以避免出现线程安全问题和不可预测的UI行为。
    • CALayer:CALayer可以在子线程中进行绘制,因为它支持异步绘制。通过实现CALayer的代理方法displayLayer:,可以在子线程中完成绘制工作,然后将绘制好的图像数据传回主线程更新到CALayer中,这种方式被称为异步绘制。
  3. 性能优化
    • UIView:由于UIView的所有UI操作都需要在主线程执行,这限制了其在多核CPU上的性能扩展能力。
    • CALayer:通过在子线程中进行异步绘制,可以减轻主线程的负担,提高应用的性能和响应速度,尤其是在处理复杂的图形渲染时。

综上所述,CALayer可以在子线程中运行,因为它的设计允许异步绘制,而UIView的所有UI更新必须在主线程中进行,以保证线程安全和UI的响应性。

Flutter 的 RenderObject 和 iOS 中的 CALayer 都扮演着渲染树中节点的角色,它们都负责绘制界面的一部分,并且都可以包含子节点。

在Flutter中,三棵树(Widget树、Element树、RenderObject树)与iOS中的类对应关系如下:

  1. Widget树
    • Flutter中的Widget树类似于iOS中的UIView控件树。Widget是Flutter中用于描述用户界面的构件,它们是不可变的,并且当状态变化时,Flutter会构建一个新的Widget树。
  2. Element树
    • Element树在Flutter中扮演着将Widget树的变更映射到RenderObject树的角色。每一个Widget都有一个对应的Element,Element树是Widget树和RenderObject树之间的桥梁。在iOS中,这个概念没有直接对应的类,因为它是Flutter特有的架构。但是,如果非要找一个类比,可以说Element树的作用类似于UIView的实例化和状态管理,因为Element持有Widget和RenderObject的引用,并负责协调它们的状态。
  3. RenderObject树
    • RenderObject树是Flutter中实际负责布局和绘制的树。它与iOS中的CALayer树相似,因为CALayer负责实际的渲染工作。RenderObject树根据Widget的属性进行布局(layout)和绘制(paint),这与CALayer在iOS中的作用类似。

总的来说,Flutter的Widget树类似于iOS的UIView树,Element树是Flutter特有的,没有直接对应的iOS类,而RenderObject树类似于iOS的CALayer树。这种架构使得Flutter能够在不同的平台上以统一的方式处理渲染和布局。

img

Element 同时持有 Widget 和 RenderObject。而无论是 Widget 还是 Element,其实都不负责最后的渲染,只负责发号施令,真正去干活儿的只有 RenderObject。那你可能会问,既然都是发号施令,那为什么需要增加中间的这层 Element 树呢?直接由 Widget 命令 RenderObject 去干活儿不好吗?

答案是,可以,但这样做会极大地增加渲染带来的性能损耗。

因为 Widget 具有不可变性,但 Element 却是可变的。实际上,Element 树这一层将 Widget 树的变化(diff)做了抽象,可以只将真正需要修改的部分同步到 RenderObject 树中,最大程度降低对真实渲染视图的修改,提高渲染效率,而不是销毁整个渲染视图树重建。

这,就是 Element 树存在的意义。

Element 是可复用的,只要 Widget 前后类型一样。比如 Widget 是蓝色的,重建后变红色了,Element 是会复用的。所以多个 Widget(销毁前后)可以对应一个 Element

## 常见笔试/面试题 [< 返回目录](#目录)

问:UIButton从子类到父类依次继承自什么?

答:UIControl-> UIView-> UIResponder。

哪些视图的设置能禁止其相应事件

1、userInterface = NO;
2、hidden = YES;
3、当UIBUTTON透明度为0就不响应事件了,当UIBUTTON透明度为0就不响应事件了。

更多参考:iOS开发经验:button不能响应的原因

离屏渲染

在使用圆角、阴影和遮罩等视图功能的时候,图层属性的混合体被指定为在未预合成之前不能直接在屏幕中绘制,所有就需要在屏幕外的上下文中渲染,即离屏渲染。

更多参考:iOS离屏渲染之优化分析该文非常重要。

## END [< 返回目录](#目录)

视图-①Runloop

## 一、RunLoop的理解

让线程永不休眠。

背景:负责持续性的处理各种任务(比如Source,Timer,Observer),让线程能一直运行,且在没有任务的时候能够进入休眠,减少 CPU 的使用率,从而节省电量和资源。

1、正常一个线程一次只能执行一个任务,执行完成后线程就退出了。为了让线程能随时处理事件但并不退出,使用do-while循环实现。

1
2
3
4
5
6
7
function loop() {
initialize();
do {
var message = get_next_message();
process_message(message);
} while (message != quit);
}

2、do-while的检查是一直主动检查?线程使用RunLoop时,不需要一直主动检查输入源是否有事件到来。而是靠事件驱动。即

事件驱动(Event-Driven)

在事件驱动的编程模型中,应用程序的执行流程是由事件(如用户操作、消息、定时器超时等)来驱动的。应用程序不需要不断地检查某个条件,而是注册事件处理函数,并让操作系统在相应的事件发生时通知应用程序。这样,应用程序就可以在没有事件发生时执行其他任务或者进入休眠状态,从而节省资源。

3、do-while性能消耗?

当 RunLoop 检测到没有待处理的事件时,它会将线程置于”休眠等待”状态,而不是忙等。这时,线程不会消耗 CPU 资源。当事件到来时,如用户输入、定时器超时或 I/O 完成等,RunLoop 会被唤醒,处理相应的事件,然后再次进入休眠状态。

Runloop是绑定到线程上的(每个线程可以有一个对应的 RunLoop 对象。这些 RunLoop 对象被保存在一个全局的 Dictionary 中,其中线程作为 Key,RunLoop 作为 Value。)。每个线程可以有自己的 RunLoop,这样每个线程可以独立地处理与自己任务相关的事件,提高了事件处理的效率和局部性。

每个Runloop有多种Model。不同的 Mode 可以包含不同的事件源(Sources)和定时器(Timers)。通过切换 Mode,RunLoop 可以过滤掉一些不想要的事件,只处理当前 Mode 下相关的事件。这样可以避免在处理特定任务时被不相关的事件打扰,提高程序的响应性和效率。例如当用户滚动列表时,iOS 应用程序的 RunLoop 通常会切换到 UITrackingRunLoopMode 模式。这个模式会降低非滚动相关的事件(如未将定时器添加到 NSRunLoopCommonModes 模式则timer会暂停)处理优先级,从而确保滚动操作的流畅性 。

主线程是如何切换runloop?

当系统检测到有scrollerview滑动时,系统就会将当前进程的主线程切换到UITrackingRunLoopMode,直到滑动结束,又会切换到NSDefaultRunLoopMode。

模拟主线程runloop的mode切换。在touchbegan的时候切换到UITrackingRunLoopMode,touchend的时候又切换回NSDefaultRunLoopMode。从模拟中可以看出如果所切到的mode是timer未添加的,则timer会暂停。这也就是为什么NSTimer需要设置在NSRunLoopCommonModes模式下运行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
  dispatch_async(dispatch_get_global_queue(0, 0), ^{
self.rl = CFRunLoopGetCurrent();

//timer1 运行在 default mode
NSTimer *timer1 = [NSTimer timerWithTimeInterval:1.f repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"timer1 fired");
}];
[[NSRunLoop currentRunLoop] addTimer:timer1 forMode:NSDefaultRunLoopMode]; // NSDefaultRunLoopMode

//timer2 运行在 track Mode
NSTimer *timer2 = [NSTimer timerWithTimeInterval:1.f repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"timer2 fired");
}];
[[NSRunLoop currentRunLoop] addTimer:timer2 forMode:UITrackingRunLoopMode]; // UITrackingRunLoopMode

//指定当前运行mode
self.currentMode = NSDefaultRunLoopMode;
while (1) {
[[NSRunLoop currentRunLoop] runMode:self.currentMode beforeDate:[NSDate distantFuture]];
}
});


- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"touch began")
//touchbegan 切换成track mode
self.currentMode = UITrackingRunLoopMode;
CFRunLoopStop(self.rl);
}

- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"touch end");
//touchend 切换成kCFRunLoopDefaultMode
self.currentMode = kCFRunLoopDefaultMode;
CFRunLoopStop(self.rl);
}

runloop与卡顿的关系

在iOS开发中,卡顿通常是由于主线程被长时间占用导致的。CFRunLoop的状态变化可以反映主线程的运行情况,因此通过监听CFRunLoop的状态,我们可以检测到应用的卡顿现象。

正常情况下,CFRunLoop会经历以下几个状态:

  1. kCFRunLoopEntry:即将进入RunLoop

  2. kCFRunLoopBeforeTimers:即将处理定时器。

  3. kCFRunLoopBeforeSources:即将处理输入源。

  4. kCFRunLoopBeforeWaiting:即将进入休眠。

  5. kCFRunLoopAfterWaiting:刚从休眠中唤醒。

  6. kCFRunLoopExit:即将退出RunLoop

为什么通常选择kCFRunLoopBeforeSourceskCFRunLoopAfterWaiting这两个状态来作为卡顿的判断依据,而不使用kCFRunLoopBeforeWaitingkCFRunLoopBeforeTimers呢?

因为①卡顿通常发生在事件处理阶段,而kCFRunLoopBeforeWaiting状态标志着RunLoop即将进入休眠,而不是正在处理事件。当应用处于静止状态,即用户没有进行任何操作时,RunLoop通常处于kCFRunLoopBeforeWaiting状态,等待新的事件到来。在这种状态下,线程会进入休眠模式,以节省CPU资源。

②至于kCFRunLoopBeforeTimers状态,虽然它也是RunLoop状态之一,但它主要表示RunLoop即将处理定时器事件。如果定时器回调执行时间过长,确实可能导致卡顿,但是在实际应用中,定时器回调通常执行时间较短,且定时器回调的执行时间可以通过调整定时器的触发频率来控制,因此kCFRunLoopBeforeTimers状态不是卡顿判断的主要依据。

在没有卡顿的情况下,CFRunLoopkCFRunLoopBeforeSourceskCFRunLoopAfterWaiting这两个状态的停留时间通常是非常短的。kCFRunLoopBeforeSources状态表示RunLoop即将处理输入源,而kCFRunLoopAfterWaiting状态表示RunLoop从休眠中被唤醒。如果主线程在这两个状态之间花费的时间过长,说明线程可能被阻塞,导致应用无法响应用户操作,从而出现卡顿。

这两个状态是RunLoop循环中的关键点,它们分别代表了事件处理前后的状态。如果主线程在这两个状态之间花费的时间过长,说明线程可能被阻塞,导致应用无法响应用户操作,从而出现卡顿。因此,通过监控这两个状态,我们可以有效地检测和优化应用的性能,提高用户体验。

runloop与崩溃的关系

在 iOS 开发中,由于 RunLoop 导致的崩溃通常不是直接由 RunLoop 本身引起的,而是由于 RunLoop 中的事件处理代码存在问题。

《起死回生/回光返照》见《异常与崩溃.md

runtime

运行时(Runtime)是 Objective-C 语言的核心特性之一,它提供了一组丰富的 API,允许程序在运行时查询和修改程序的行为。这种动态性使得 Objective-C 语言具有很高的灵活性。

1、RunLoop概念

Run loop,正如其名,loop表示某种循环,和run放在一起就表示一直在运行着的循环。一个run loop就是一个事件处理的循环,用来不停的调度工作以及处理输入事件。其内部就是do-while循环,这个循环内部不断地处理各种任务(比如Source,Timer,Observer)。使用run loop的目的是让你的线程在有工作的时候忙于工作,而没工作的时候处于休眠状态。

2、RunLoop和线程的关系?

run loop和线程是紧密相连的,可以这样说run loop是为了线程而生,没有线程,它就没有存在的必要。Run loops是线程相关的基础框架的一部分,Cocoa和CoreFundation都提供了run loop对象方便配置和管理线程的run loop(以下都已Cocoa为例)。

每个线程,包括程序的主线程(main thread)都有与之相应的run loop对象。

①、主线程的run loop默认是启动的。

1
2
3
4
5
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}

重点是UIApplicationMain()函数,这个方法会为main thread设置一个NSRunLoop对象,这就解释了本文开始说的为什么我们的应用可以在无人操作的时候休息,需要让它干活的时候又能立马响应。

②、对其它线程来说,run loop默认是没有启动的。

③、在任何一个Cocoa程序的线程中,都可以通过:NSRunLoop *runloop = [NSRunLoop currentRunLoop];来获取到当前线程的run loop。

3、RunLoop相关各类关系

在 CoreFoundation 里面关于 RunLoop 有5个类:

1
2
3
4
5
CFRunLoopRef
CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopTimerRef
CFRunLoopObserverRef

其中 CFRunLoopModeRef 类并没有对外暴露,只是通过 CFRunLoopRef 的接口进行了封装。他们的关系/RunLoop相关各类关系,如下图所示:

RunLoop相关各类关系
一个 RunLoop 包含若干个 Mode,每个 Mode 又包含若干个 Source/Timer/Observer。每次调用 RunLoop 的主函数时,只能指定其中一个 Mode,这个Mode被称作 CurrentMode。如果需要切换 Mode,只能退出 Loop,再重新指定一个 Mode 进入。这样做主要是为了分隔开不同组的 Source/Timer/Observer,让其互不影响。

RunLoop的内部逻辑
RunLoop的内部逻辑

3.1 CFRunLoopSourceRef

Source有两个版本:Source0 和 Source1。

  • Source0 只包含了一个回调(函数指针),它并不能主动触发事件。使用时,你需要先调用 CFRunLoopSourceSignal(source),将这个 Source 标记为待处理,然后手动调用 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop,让其处理这个事件。
  • Source1 包含了一个 mach_port 和一个回调(函数指针),被用于通过内核和其他线程相互发送消息。这种 Source 能主动唤醒 RunLoop 的线程,其原理在下面会讲到。

苹果注册了一个 Source1 (基于 mach port 的) 用来接收系统事件,其回调函数为 __IOHIDEventSystemClientQueueCallback()。

在iOS 中,除了source1可以自己唤醒run loop之外,其他的事件都需要用户手动唤醒run loop才可以。

3.1.附上题中button点击后,关于RunLoop的过程(此处略过对响应链的描述)

大概为:当一个硬件事件(触摸/锁屏/摇晃等)发生后,
①、首先由 IOKit.framework 生成一个 IOHIDEvent 事件,Source1 接收到系统事件,RunLoop被唤醒
②、RunLoop通知Observer,处理Timer和Source 0
③、RunLoop处理Source 1,Source1 触发回调,并调用_UIApplicationHandleEventQueue() 进行应用内部的分发
_UIApplicationHandleEventQueue() 会把 IOHIDEvent 处理并包装成 UIEvent 进行处理或分发,其中包括识别 UIGesture/处理屏幕旋转/发送给 UIWindow 等。通常事件比如 UIButton 点击、touchesBegin/Move/End/Cancel 事件都是在这个回调中完成的。
④、Springboard接受touch event,并用source1 的 之后mach port 转发给App进程。
⑤、RunLoop处理完毕进入睡眠,此前会释放旧的autorelease pool并新建一个autorelease pool。

3.2 CFRunLoopTimerRef

CFRunLoopTimerRef 是基于时间的触发器,它和 NSTimer 是toll-free bridged 的,可以混用。其包含一个时间长度和一个回调(函数指针)。当其加入到 RunLoop 时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行那个回调(NSTimer 其实就是 CFRunLoopTimerRef)。

3.3 CFRunLoopObserverRef

CFRunLoopObserverRef 是观察者,每个 Observer 都包含了一个回调(函数指针),当 RunLoop 的状态发生变化时,观察者就能通过回调接受到这个变化。

上面的 Source/Timer/Observer 被统称为 mode item,一个 item 可以被同时加入多个 mode。但一个 item 被重复加入同一个 mode 时是不会有效果的。如果一个 mode 中一个 item 都没有,则 RunLoop 会直接退出,不进入循环。

4、RunLoop的应用

最常见的为定时器 NSTimer

NSTimer 其实就是 CFRunLoopTimerRef,他们之间是 toll-free bridged 的。一个 NSTimer 注册到 RunLoop 后,RunLoop 会为其重复的时间点注册好事件。例如 10:00, 10:10, 10:20 这几个时间点。RunLoop为了节省资源,并不会在非常准确的时间点回调这个Timer。Timer 有个属性叫做 Tolerance (宽容度),标示了当时间点到后,容许有多少最大误差。

如果某个时间点被错过了,例如执行了一个很长的任务,则那个时间点的回调也会跳过去,不会延后执行。就比如等公交,如果 10:10 时我忙着玩手机错过了那个点的公交,那我只能等 10:20 这一趟了。

讲到RunLoop,我们需明确一点。runloop会对timer有强引用,timer会对目标对象进行强引用

其他详细参考以下文章:

4.1、autoreleasepool 自动释放池

既然说到runloop,简单说下autoreleasepool自动释放池。runloop会默认创建autoreleasepool,在runloop睡眠前或者退出前会执行pop操作。线程池详情查看下面的内存管理中的介绍。

@autoreleasepool是自动释放池,让我们更自由的管理内存;所以我们下面说说内存管理。

4.2、runloop、autorelease pool以及线程之间的关系

END

其他参考文档:runloop

视图-②布局

一、Intrinsic Content Size,Content Hugging Priority和Content Compression Resistance Priority

看一下下面的例子,看给出的例子约束是否完整?

1
2
3
4
5
6
7
8
UILabel *label = [[UILabel alloc] init];
label.font = [UIFont systemFontOfSize:15];
label.text = @"Hello";
[self.view addSubview:label];
[label mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(self.view.mas_left).offset(16);
make.top.equalTo(self.view.mas_top).offset(16);
}];

这里只定义了两个约束,left 和 top,只够计算出frame的originX和orginY,没有width和height。那么是不是属于不完整的约束呢?其实在这里给出的约束已经是完整的了。因为对于UILabel这个控件而言 ,只要通过其font和text系统就可以计算出Label该有的长度和宽度。这里的长度和宽度就是UILabel的intrinsic content size(固有属性)。

Intrinsic Content Size, 通俗来讲,就是控件(UIButton,UILabel,UIImageView)能根据它们的内容(content)计算自己的大小(Size)

开发中用到的一些控件或视图,本身就自带大小,比如UIButton控件,设置完title后就能知道这个UIButton是文字的大小再加上两个固定的button margin。
像这种控件或视图本身就带有的高度、宽度,就叫做intrinsic content size(固定内容尺寸)。

2、浅谈 iOS AutoLayout 中 Label 的抗拉伸和抗压缩

在 Autolayout 优先级的范围是 1 ~ 1000,创建一个约束,默认的优先级是最高的 1000。

Content Hugging Priority:
该优先级表示一个控件抗被拉伸的优先级。优先级越高,越不容易被拉伸(即越容易保持原状),默认是251。

Content Compression Resistance Priority:
该优先级表示一个控件抗压缩的优先级。优先级越高,越不容易被压缩(即越容易保持原状),默认是750。

使用场景:

当一个视图上有多个 intrinsic content size 的子控件,并且子控件可能会超出父视图的区域时,此属性可控制哪些视图被内容被优先压缩,使其不超出父视图区域。

场景举例:

1
2
3
4
5
[[yellowLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.center.equalTo(self.view);
make.left.equalTo(self.view).offset(100);
make.right.equalTo(self.view).offset(-100);
}];

当yellowLable的宽度最多为screenWidth-200。

则我们想让lable对左右两边的约束性没那么高,可以设置

1
2
3
4
5
[yellowLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.center.equalTo(self.view);
make.left.equalTo(self.view).offset(100).priority(250);
make.right.equalTo(self.view).offset(-100).priority(250);
}];

给出一个比较常见的需求:

在同一行中显示标题和时间,时间必须显示完全,标题如果太长就截取可显示的部分,剩余的用…表示。

intrinsic content size

目标:我们想让绿色的时间显示全,则应该要压缩前面的titleLabel。也就是要降低titleLabel的抗压缩。

1
2
3
4
5
if (b) {
[timeLabel setContentCompressionResistancePriority:UILayoutPriorityRequired forAxis:UILayoutConstraintAxisHorizontal];
// 或
//[titleLabel setContentHuggingPriority:UILayoutPriorityFittingSizeLevel forAxis:UILayoutConstraintAxisHorizontal];
}

UILayoutPriorityRequired:1000

UILayoutPriorityDefaultHigh:750

UILayoutPriorityDefaultLow:250

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
//在同一行中显示标题和时间,时间必须显示完全,标题如果太长就截取可显示的部分,剩余的用…表示。
- (UIView *)contentViewWith:(BOOL)b {
UIView *contentView = [[UIView alloc] init];
contentView.backgroundColor = [UIColor lightGrayColor];

UILabel *titleLabel = [[UILabel alloc] init];
titleLabel.backgroundColor = [UIColor redColor];
titleLabel.text = @"Each of these constraints can have its own priority. By default, ";
titleLabel.font = [UIFont systemFontOfSize:17];
[contentView addSubview:titleLabel];
[titleLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(contentView.mas_top);
make.left.equalTo(contentView.mas_left).offset(16);
}];

UILabel *timeLabel = [[UILabel alloc] init];
timeLabel.backgroundColor = [UIColor greenColor];
timeLabel.text = @"2017/03/12 18:20:22";
timeLabel.font = [UIFont systemFontOfSize:17];
[contentView addSubview:timeLabel];
[timeLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(titleLabel.mas_top);
make.left.equalTo(titleLabel.mas_right).offset(8);
make.right.lessThanOrEqualTo(contentView.mas_right).offset(-8);
}];

if (b) {
[timeLabel setContentCompressionResistancePriority:UILayoutPriorityRequired forAxis:UILayoutConstraintAxisHorizontal];
// 或
//[titleLabel setContentHuggingPriority:UILayoutPriorityFittingSizeLevel forAxis:UILayoutConstraintAxisHorizontal];
}

return contentView;
}

二、iOS使用topLayoutGuide和bottomLayoutGuide

参考文章:iOS使用topLayoutGuide和bottomLayoutGuide

在iOS中,可以使用topLayoutGuide和bottomLayoutGuide来适配屏幕内容,它们是属于UIViewController的属性,配合masonry和SnapKit等约束工具,效果更好。

1
2
3
4
5
6
7
8
UIView *bottomPayView = [[UIView alloc] init];
bottomPayView.backgroundColor = [UIColor grayColor];
[self.view addSubview:bottomPayView];
[bottomPayView mas_makeConstraints:^(MASConstraintMaker *x) {
x.height.equalTo(@45);
x.left.right.equalTo(self.view);
x.bottom.equalTo(self.mas_bottomLayoutGuide);
}];

三、UITableView自动计算cell高度并缓存,再也不用管高度啦

UITableView自动计算cell高度并缓存,再也不用管高度啦

用xib加约束和用masonry加代码约束都是可以的。注意约束一定要自上而下加好,让系统知道怎么去计算高度。

加好约束后,然后告诉tableView自己去适应高度就可以了。有两种写法:

1
2
self.tableView.rowHeight = UITableViewAutomaticDimension;
self.tableView.estimatedRowHeight = 100;

或者直接写这个代理方法就可以了

1
2
3
4
- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath
{
return 100;
}

这个的意思就是告诉tableView,你需要自己适应高度,我不给你算啦哈哈哈。但是我们需要告诉它一个大概高度,例如上面的100,理论上这个是可以随便写的,并不影响显示结果,但是越接近真实高度越好。

可能遇到的问题和解决办法

1.高度不对
有时候有可能运行出来后看到cell的高度显示的不对。这个问题是因为约束没有满足自上而下,从而系统不知道怎么去计算。解决办法就是去修改约束,直到满足为止。一定要好好理解约束啊!

2.点击状态栏无法滚动到顶部
我们知道,如果界面中有UIScrollView的话,点击状态栏会让其滚动到顶部,就像这样:

但是如果我们用了自动计算高度的方法,又调用了tableView的reloadData方法(例如我们的数据有分页的时候,加载完下一页的数据后会去刷新tableView)。这时候就会出现问题,点击状态栏就有几率不能精确滚动到顶部了:

解决这个问题的办法是去缓存cell的高度,代码如下:

1
@property (nonatomic, strong) NSMutableDictionary *heightAtIndexPath;//缓存高度所用字典
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#pragma mark - UITableViewDelegate
-(CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath
{
NSNumber *height = [self.heightAtIndexPath objectForKey:indexPath];
if(height)
{
return height.floatValue;
}
else
{
return 100;
}
}

- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
{
NSNumber *height = @(cell.frame.size.height);
[self.heightAtIndexPath setObject:height forKey:indexPath];
}

四、问题

问题1:使用Masonry的时候进行updateConstraints没有效果

原因:使用updateConstraints更新的时候必须是makeConstraints里面设置过的约束。但如果只是这样还不行,还需要约束对象匹配才能成功。

问题详见:使用Masonry的时候进行updateConstraints没有效果

iOS11适配-Safe Area

iOS11适配-Safe Area

在iOS 11,UIViewController中的UIView的topLayoutGuide和bottomLayoutGuide被替换成了新的安全区属性。

1
2
3
4
5
@available(iOS 11.0, *)
open var safeAreaInsets: UIEdgeInsets { get }

@available(iOS 11.0, *)
open var safeAreaLayoutGuide: UILayoutGuide { get }12345

safeAreaInsets属性意味着屏幕可以被任何方向遮挡,并不只是上下,当iPhone X出现时,我们就明白了为什么我们需要对左右两边也进行缩进。

Masonry动画

视图-②生命周期

# 目录

## 五、控制器View的生命周期 [< 返回目录](#目录)

更详细的生命周期请查看:iOS程序执行顺序和UIViewController 的生命周期(整理)

题目1:控制器View的生命周期及相关函数是什么?你在开发中是如何用的?
1
2
3
4
1.在视图显示之前调用viewWillAppear;该函数可以调用多次; 
2.视图显示完毕,调用viewDidAppear;
3.在视图消失之前调用viewWillDisAppear;该函数可以调用多次(如需要);
4.在布局变化前后,调用viewWill/DidLayoutSubviews处理相关信息;

viewWillAppear——-》viewWillLayoutSubviews—–》viewDidLayoutSubviews———–》

viewDidAppear

题目2:loadView, viewDidLoad, viewDidUnLoad,分别是在什么时候被调用的.

loadView, viewDidLoad, viewDidUnLoad,分别是在什么时候被调用的.

3、layoutSubviews布局与drawRect重绘

(1)、layoutSubviews布局

layoutSubviews是对subviews重新布局;
比如,我们想更新子视图的位置的时候,可以通过调用layoutSubviews方法,即可以实现对子视图重新布局。但实际上一般我们都是不要直接手动调用layoutSubviews方法。因为有操作时候,系统会自动调用layoutSubviews。

那我们进行哪些操作会触发layoutSubviews方法呢?答如下:

1
2
3
4
5
6
7
8
9
首先注意:
①init初始化不会触发layoutSubviews,
但是使用initWithFrame进行初始化时,当rect的值不为CGRectZero时,会触发layoutSubviews。
②、直接调用setLayoutSubviews。
③、addSubview的时候一般都会触发layoutSubviews。(最常见) 注:但当本View的frame为0时,addSubView也不会调用layoutSubViews。
④、当view的frame发生改变的时候触发layoutSubviews。
⑤、滑动UIScrollView的时候触发layoutSubviews。
⑥、旋转Screen会触发父UIView上的layoutSubviews事件。
⑦、改变一个UIView大小的时候也会触发父UIView上的layoutSubviews事件。

所以我们可以看出当视图约束/frame变化时候,会触发layoutSubviews,进行重新布局。

1
2
3
4
5
6
7
8
9
10
附:
如果你还是想强制更新布局,你可以调用setNeedsLayout方法;
如果你想立即显示你的views,你需要调用layoutIfNeed方法。

①、- (void)layoutSubviews;
这个方法,默认没有做任何事情,需要子类进行重写;
②、- (void)setNeedsLayout;
标记为需要重新布局,异步调用layoutIfNeeded刷新布局,不立即刷新,但layoutSubviews一定会被调用;
③、- (void)layoutIfNeeded;
如果,有需要刷新的标记,立即调用layoutSubviews进行布局(如果没有标记,不会调用layoutSubviews)。

其他参考文章:iOS layoutSubview的方法总结/重绘drawRect

什么时候用layoutSubviews?

答:仅仅在以下情况下:自动布局达不到想要效果时你才有必要重写这个方法.可以直接设置subviews的尺寸.

(2)、drawRect重绘

重绘作用:重写该方法以实现自定义的绘制内容

1
2
3
-drawRect:(CGRect)rect方法:重写此方法,执行重绘任务
-setNeedsDisplay方法:标记为需要重绘,异步调用drawRect
-setNeedsDisplayInRect:(CGRect)invalidRect方法:标记为需要局部重绘
(3)、updateConstraints更新约束、layoutSubviews重新布局与drawRect重绘的调用顺序
1
2
3
layoutSubviews是对subviews重新布局;
drawRect重绘;
layoutSubviews方法调用先于drawRect,也就是先布局子视图,在重绘。

所以,在调用updateConstraintsIfNeeded可能会立即执行updateConstraints,然后调用layoutSubviews。因为按照Autolayout布局的步骤,应该是先更新约束然后更新布局的。

## 常见笔试/面试题 [< 返回目录](#目录)

## END [< 返回目录](#目录)

视图-③跳转

# 目录

## 一、视图的跳转 [< 返回目录](#目录)

1、获取当前显示的视图控制器ViewController

2、如何在多次presentViewController后直接返回到指定层

场景:如果多个控制器都通过 present 的方式跳转呢?比如从A跳转到B,从B跳转到C,从C跳转到D,如何由D直接返回到A呢?

答:可以通过 presentingViewController 一直找到A控制器,然后调用A控制器的 dismissViewControllerAnimated 方法。方法如下:

1
2
3
4
5
UIViewController *controller = self;
while(controller.presentingViewController != nil){
controller = controller.presentingViewController;
}
[controller dismissViewControllerAnimated:YES completion:nil];

PS:如果不是想直接返回到A控制器,比如想回到B控制器,while循环的终止条件可以通过控制器的类来判断。

3、presentedViewController 与 presentingViewController

假设从A控制器通过present的方式跳转到了B控制器,那么 A.presentedViewController 就是B控制器;
B.presentingViewController 就是A控制器。

4、如何通过视图(view)获取该视图所在的控制器(viewController)

1
2
3
4
5
6
7
8
+ (nullable UIViewController *)findBelongViewControllerForView:(UIView *)view {
UIResponder *responder = view;
while ((responder = [responder nextResponder]))
if ([responder isKindOfClass: [UIViewController class]]) {
return (UIViewController *)responder;
}
return nil;
}

## 常见笔试/面试题 [< 返回目录](#目录)

## END [< 返回目录](#目录)