目录
一. GCD和OperationQueue
二. CADisplayLink、NSTimer使用注意
三. 内存布局
四. Tagged Pointer
五. copy和mutableCopy
六. OC对象的内存管理
七. AutoreleasePool自动释放池
八. 图片的解压缩到渲染过程
九. 应用卡顿的原因以及优化
十. APP的启动
一. GCD和NSOperationQueue
GCD 可用于多核的并行运算;
GCD 会自动利用更多的 CPU 内核(比如双核、四核);
GCD 会自动管理线程的生命周期(创建线程、调度任务、销毁线程);
程序员只需要告诉 GCD 想要执行什么任务,不需要编写任何线程管理代码。
NSOperation、NSOperationQueue 是基于 GCD 更高一层的封装,完全面向对象。但是比 GCD 更简单易用、代码可读性也更高。
- 可添加完成的代码块,在操作完成后执行。
- 添加操作之间的依赖关系,设定操作执行的优先级,方便的控制执行顺序;设置最大并发数。
- 可以很方便的取消一个操作的执行。
- 使用 KVO 观察对操作执行状态的更改:isExecuteing、isFinished、isCancelled。
同步和异步主要影响:能不能开启新的线程
同步:在当前线程中执行任务,不具备开启新线程的能力
异步:在新的线程中执行任务,具备开启新线程的能力
并发和串行主要影响:任务的执行方式
并发:多个任务并发(同时)执行
串行:一个任务执行完毕后,再执行下一个任务
二. CADisplayLink、NSTimer使用注意
- CADisplayLink、NSTimer会对target产生强引用,如果target又对它们产生强引用,那么就会引发循环引用。解决办法是使用代理对象NSProxy。
- NSTimer依赖于RunLoop,如果RunLoop的任务过于繁重,可能会导致NSTimer不准时。而GCD的定时器会更加准时。
三. 内存布局
- 栈区(heap):由系统去管理。地址从高到低分配。先进后出。会存一些局部变量,函数跳转跳转时现场保护(寄存器值保存于恢复),这些系统都会帮我们自动实现,无需我们干预。所以大量的局部变量,深递归,函数循环调用都可能耗尽栈内存而造成程序崩溃 。
- 堆区(stack):需要我们自己管理内存,alloc申请内存release释放内存。创建的对象也都放在这里。 地址是从低到高分配。堆是所有程序共享的内存,当N个这样的内存得不到释放,堆区会被挤爆,程序立马瘫痪。这就是内存泄漏。
- 全局区/静态区(staic):全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。程序结束后有系统释放。
- 常量区:常量字符串就是放在这里的,还有const常量。
- 代码区:存放App代码,App程序会拷贝到这里。
四. Tagged Pointer
- 从64bit开始,iOS引入了Tagged Pointer技术,用于优化NSNumber、NSDate、NSString等小对象的存储
- 在没有使用Tagged Pointer之前, NSNumber等对象需要动态分配内存、维护引用计数等,NSNumber指针存储的是堆中NSNumber对象的地址值
- 使用Tagged Pointer之后,NSNumber指针里面存储的数据变成了:Tag + Data,也就是将数据直接存储在了指针中
- 当指针不够存储数据时,才会使用动态分配内存的方式来存储数据
- objc_msgSend能识别Tagged Pointer,比如NSNumber的intValue方法,直接从指针提取数据,节省了以前的调用开销
- 如何判断一个指针是否为Tagged Pointer?
iOS平台,最高有效位是1(第64bit);Mac平台,最低有效位是1
五. copy和mutableCopy
六. OC对象的内存管理
- 在iOS中,使用引用计数来管理OC对象的内存。
- 一个新创建的OC对象引用计数默认是1,当引用计数减为0,OC对象就会销毁,释放其占用的内存空间。
- 调用retain会让OC对象的引用计数+1,调用release会让OC对象的引用计数-1。
- 当调用alloc、new、copy、mutableCopy方法返回了一个对象,在不需要这个对象时,要调用release或者autorelease来释放它;想拥有某个对象,就让它的引用计数+1;不想再拥有某个对象,就让它的引用计数-1。
七. AutoreleasePool自动释放池
AutoreleasePool(自动释放池) 是OC中的一种内存自动回收机制,在释放池中的调用了autorelease方法的对象都会被压在该池的顶部(以栈的形式管理对象)。当自动释放池被销毁的时候,在该池中的对象会自动调用release方法来释放资源,销毁对象。以此来达到自动管理内存的目的。
__AtAutoreleasePool 实际是一个结构体,在内部首先执行objc_autoreleasePoolPush(),然后在调用objc_autoreleasePoolPop(atautoreleasepoolobj)。
struct __AtAutoreleasePool {
__AtAutoreleasePool() {
//构造函数,在创建结构体时调用
atautoreleasepoolobj = objc_autoreleasePoolPush();
}
~__AtAutoreleasePool() {
//析构函数,在结构体销毁的时候调用
objc_autoreleasePoolPop(atautoreleasepoolobj);
}
void * atautoreleasepoolobj;
};
AutoreleasePoolPage的结构
- 每个AutoreleasePoolPage对象占用4096字节内存,除了用来存放它内部的成员变量,剩下的空间用来存放autorelease对象的地址。
- 所有的AutoreleasePoolPage对象通过双向链表的形式连接在一起。
- 调用push方法会将一个POOL_BOUNDARY入栈,并且返回其存放的内存地址
- 调用pop方法时传入一个POOL_BOUNDARY的内存地址,会从最后一个入栈的对象开始发送release消息,直到遇到这个POOL_BOUNDARY
- id *next指向了下一个能存放autorelease对象地址的区域
Autorelease何时释放?
1、手动调用AutoreleasePool的释放方法(drain方法)
2、Autorelease对象是在当前的runloop迭代结束时释放的,而它能够释放的原因是系统在每个runloop迭代中都加入了自动释放池Push和Pop
Runloop和Autorelease的关系
- App启动后,苹果在主线程 RunLoop 里注册了两个 Observer,其回调都是 _wrapRunLoopWithAutoreleasePoolHandler()。
- 第一个 Observer 监视的事件是 Entry(即将进入Loop),其回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池。其 order 是 -2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。
- 第二个 Observer 监视了两个事件: BeforeWaiting(准备进入休眠) 时调用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 释放旧的池并创建新池;Exit(即将退出Loop) 时调用 _objc_autoreleasePoolPop() 来释放自动释放池。这个 Observer 的 order 是 2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。
八. 图片的解压缩到渲染过程
- 假设我们使用 +imageWithContentsOfFile: 方法从磁盘中加载一张图片,这个时候的图片并没有解压缩;
- 然后将生成的 UIImage 赋值给 UIImageView ;
- 接着一个隐式的 CATransaction 捕获到了 UIImageView 图层树的变化;
- 在主线程的下一个 runloop 到来时,Core Animation 提交了这个隐式的 transaction ,这个过程可能会对图片进行 copy 操作,而受图片是否字节对齐等因素的影响,这个 copy 操作可能会涉及以下部分或全部步骤:
- 分配内存缓冲区用于管理文件 IO 和解压缩操作;
- 将文件数据从磁盘读到内存中;
- 将压缩的图片数据解码成未压缩的位图形式,这是一个非常耗时的 CPU 操作;
- 最后 Core Animation 中CALayer使用未压缩的位图数据渲染 UIImageView 的图层。
- CPU计算好图片的Frame,对图片解压之后.就会交给GPU来做图片渲染
- 渲染流程
- GPU获取获取图片的坐标
- 将坐标交给顶点着色器(顶点计算)
- 将图片光栅化(获取图片对应屏幕上的像素点)
- 片元着色器计算(计算每个像素点的最终显示的颜色值)
- 从帧缓存区中渲染到屏幕上
总结:图片渲染到屏幕的过程: 读取文件->计算Frame->图片解码->解码后纹理图片位图数据通过数据总线交给GPU->GPU获取图片Frame->顶点变换计算->光栅化->根据纹理坐标获取每个像素点的颜色值(如果出现透明值需要将每个像素点的颜色*透明度值)->渲染到帧缓存区->渲染到屏幕
九. 应用卡顿的原因以及优化
CPU: 计算视图frame,文本计算和排版,图片解码,需要绘制纹理图片通过数据总线交给GPU。
GPU: 纹理混合,顶点变换与计算,像素点的填充计算,渲染到帧缓冲区。
平时所说的“卡顿”主要是因为在主线程执行了比较耗时的操作,
可以添加Observer到主线程RunLoop中,通过监听RunLoop状态切换的耗时,以达到监控卡顿的目的。
-
1. 屏幕呈像原理
-
2. 卡顿产生的原因
在 VSync 信号到来后,系统图形服务会通过 CADisplayLink 等机制通知 App,App 主线程开始在 CPU 中计算显示内容,比如视图的创建、布局计算、图片解码、文本绘制等。随后 CPU 会将计算好的内容提交到 GPU 去,由 GPU 进行变换、合成、渲染。随后 GPU 会把渲染结果提交到帧缓冲区去,等待下一次 VSync 信号到来时显示到屏幕上。由于垂直同步的机制,如果在一个 VSync 时间内,CPU 或者 GPU 没有完成内容提交,则那一帧就会被丢弃,等待下一次机会再显示,而这时显示屏会保留之前的内容不变。这就是界面卡顿的原因。
从上面的图中可以看到,CPU 和 GPU 不论哪个阻碍了显示流程,都会造成掉帧现象。所以开发时,也需要分别对 CPU 和 GPU 压力进行评估和优化。
-
3. 卡顿优化
CPU
- 尽量用轻量级的对象,比如用不到事件处理的地方,可以考虑使用CALayer取代UIView
- 不要频繁地调用UIView的相关属性,比如frame、bounds、transform等属性,尽量减少不必要的修改
- 尽量提前计算好布局,在有需要时一次性调整对应的属性,不要多次修改属性
- Autolayout会比直接设置frame消耗更多的CPU资源
- 图片的size最好刚好跟UIImageView的size保持一致
- 控制一下线程的最大并发数量
- 尽量把耗时的操作放到子线程:文本处理(尺寸计算、绘制)、图片处理(解码、绘制)等
GPU
- 尽量避免短时间内大量图片的显示,尽可能将多张图片合成一张进行显示
- GPU能处理的最大纹理尺寸是4096x4096,一旦超过这个尺寸,就会占用CPU资源进行处理,所以纹理尽量不要超过这个尺寸
- 尽量减少视图数量和层次
- 减少透明的视图(alpha<1),不透明的就设置opaque为YES
- 尽量避免出现 离屏渲染
十. APP的启动
APP的冷启动可以概括为3大阶段:dyld、runtime、main
-
1. dyld
dyld(dynamic link editor),Apple的动态链接器,可以用来装载Mach-O文件(可执行文件、动态库等)。
启动APP时,dyld所做的事情有:
- 装载APP的可执行文件,同时会递归加载所有依赖的动态库.
- 当dyld把可执行文件、动态库都装载完毕后,会通知Runtime进行下一步的处理.
-
2. runtime
启动APP时,runtime所做的事情有:
- 调用map_images进行可执行文件内容的解析和处理
- 在load_images中调用call_load_methods,调用所有Class和Category的+load方法
- 进行各种objc结构的初始化(注册Objc类 、初始化类对象等等)
- 调用C++静态初始化器和attribute((constructor))修饰的函数
-
3. main
接下来就是UIApplicationMain函数,AppDelegate的application:didFinishLaunchingWithOptions:方法
-
4. APP启动优化
推荐学习资料:
如果需要跟我交流的话:
※ Github: https://github.com/wsl2ls
※ 简书:https://www.jianshu.com/u/e15d1f644bea
※ 微信公众号:iOS2679114653
※ QQ群:835303405