iOS 知识点回顾(三)

2020-01-15 16:20:04 浏览数 (1)

温故而知新

目录

一. 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程序会拷贝到这里。

iOS程序的内存布局

四. 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

Tagged Pointer优化NSNumber示例

五. copy和mutableCopy

copy和mutableCopy

六. OC对象的内存管理

  • 在iOS中,使用引用计数来管理OC对象的内存。
  • 一个新创建的OC对象引用计数默认是1,当引用计数减为0,OC对象就会销毁,释放其占用的内存空间。
  • 调用retain会让OC对象的引用计数 1,调用release会让OC对象的引用计数-1。
  • 当调用alloc、new、copy、mutableCopy方法返回了一个对象,在不需要这个对象时,要调用release或者autorelease来释放它;想拥有某个对象,就让它的引用计数 1;不想再拥有某个对象,就让它的引用计数-1。

引用计数的存储

dealloc

七. AutoreleasePool自动释放池

AutoreleasePool(自动释放池) 是OC中的一种内存自动回收机制,在释放池中的调用了autorelease方法的对象都会被压在该池的顶部(以栈的形式管理对象)。当自动释放池被销毁的时候,在该池中的对象会自动调用release方法来释放资源,销毁对象。以此来达到自动管理内存的目的。

自动释放池的结构

__AtAutoreleasePool 实际是一个结构体,在内部首先执行objc_autoreleasePoolPush(),然后在调用objc_autoreleasePoolPop(atautoreleasepoolobj)。

代码语言:javascript复制
struct __AtAutoreleasePool {
  __AtAutoreleasePool() {
//构造函数,在创建结构体时调用
atautoreleasepoolobj = objc_autoreleasePoolPush();
}
  ~__AtAutoreleasePool() {
//析构函数,在结构体销毁的时候调用
objc_autoreleasePoolPop(atautoreleasepoolobj);
}
  void * atautoreleasepoolobj;
};

AutoreleasePoolPage的结构

  • 每个AutoreleasePoolPage对象占用4096字节内存,除了用来存放它内部的成员变量,剩下的空间用来存放autorelease对象的地址。
  • 所有的AutoreleasePoolPage对象通过双向链表的形式连接在一起。

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,优先级最低,保证其释放池子发生在其他所有回调之后。

Runloop和Autorelease

八. 图片的解压缩到渲染过程

    1. 假设我们使用 imageWithContentsOfFile: 方法从磁盘中加载一张图片,这个时候的图片并没有解压缩;
    1. 然后将生成的 UIImage 赋值给 UIImageView ;
    1. 接着一个隐式的 CATransaction 捕获到了 UIImageView 图层树的变化;
    1. 在主线程的下一个 runloop 到来时,Core Animation 提交了这个隐式的 transaction ,这个过程可能会对图片进行 copy 操作,而受图片是否字节对齐等因素的影响,这个 copy 操作可能会涉及以下部分或全部步骤:
  • 分配内存缓冲区用于管理文件 IO 和解压缩操作;
  • 将文件数据从磁盘读到内存中;
  • 将压缩的图片数据解码成未压缩的位图形式,这是一个非常耗时的 CPU 操作;
  • 最后 Core Animation 中CALayer使用未压缩的位图数据渲染 UIImageView 的图层。
  • CPU计算好图片的Frame,对图片解压之后.就会交给GPU来做图片渲染
    1. 渲染流程
  • 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启动优化

APP启动优化


0 人点赞