iOS 进阶知识总结(三)
3~5年开发经验的 iOS工程师 应该知道的内容~本文总结以下内容
- 视图渲染和离屏渲染
- 事件传递和响应链
- crash处理和性能优化
- 编译流程和启动流程
导航
- 对象
- 类对象
- 分类
- runtime
- 消息与消息转发
- KVO
- KVC
- 多线程
- 锁
- runloop
- 计时器
- 视图渲染和离屏渲染
- 事件传递和响应链
- crash处理和性能优化
- 编译流程和启动流程
- 内存管理
- 野指针处理
- autoreleasePool
- weak
- 单例、通知、block、继承和集合
- 网络基础
- AFNetWorking
- SDWebImage
渲染
屏幕撕裂的原因?
- 单一缓存模式下,帧缓冲区只有一个缓存空间
- 图片需要经过
CPU -> 内存 -> GPU -> 展示
的渲染过程 - CPU和GPU的协作过程中出现了偏差,GPU应该完整的绘制图片,但是工作慢了只绘制出图片上半部分。
- 此时CPU又把新数据存储到缓冲区,GPU继续绘制的时候下半部分就变成了新数据。
- 造成了两帧同时出现了一部分在屏幕上,看起来就撕裂了。
怎么解决屏幕撕裂?
- 解决上一帧和下一帧的覆盖问题,需要使用不同的缓冲区,通过两个图形缓冲区的交替来解决。
- 出现速度差的时候,就把下一帧存储在后备缓冲区,绘制完成后再切换帧。
- 当绘制完最后一个像素点就会发出这个垂直同步信号通知展示完成。
- 所以屏幕撕裂问题需要通过 双缓冲区 + 垂直同步信号 解决。
掉帧是怎么产生的?
- 屏幕正在展示A帧的时候,CPU和GPU会处理B帧。
- A帧展示完成该切换展示B帧的时候B帧的数据未准备好。
- 没办法切换就只能重复展示A帧,感官上就是卡了,这就是掉帧的问题
怎么解决掉帧?
- 掉帧根本原因是CPU和GPU渲染计算耗时过长
- 1、降低视图层级
- 2、提前或减少在渲染期的计算
CPU
渲染职能
- 布局计算:如果视图层级过于复杂,呈现或修改的时候需要消耗大量时间计算
- 视图懒加载:当显示的时候才会加载视图,这对内存使用和程序启动时间很有好处。但展示前的操作都不会被及时响应,当视图从
xib
加载或者涉及图片显示,懒加载都会比正常加载慢。 - 解压图片:
PNG
或者JPEG
压缩之后的图片会比位图小得多。在绘制到屏幕之前,必须把它扩展成完整的尺寸(通常等同于图片宽 x 长 x 4个字节)。为了节省内存,真正绘制的时候(第一次赋值UIImageView
或者把它绘制到Core Graphics
)才会解码图片,大图会占用一定的时间。 Core Graphics
绘制:实现了drawRect:
、drawLayer:inContext:
、CALayerDelegate
的方法,在绘制前都会产生一个巨大的性能开销以开辟绘画上下文。- 图层打包:由于
GPU
不知道图层结构,CPU
需要通过OpenGL
把渲染树中的每个可见图层转换成纹理三角板。CPU
的工作量和图层量成正比,如果层级关系太复杂就会渲染速度。
GPU
渲染职能
GPU
会根据生成的前后帧缓存数据,根据实际情况进行合成- 造成
GPU
渲染负担:离屏渲染,图层混合,延迟加载
一个UIImageView
添加到视图上以后,内部如何渲染到手机上的?
- 图片显示分为三个步骤: 加载、解码、渲染
- 通常,我们程序员的操作只是加载,至于解码和渲染是由
UIKit
内部进行的。 - 例如:
UIImageView
显示在屏幕上的时候需要赋值iamge
。UIImage
持有的数据是未解码的压缩数据,赋值的时候图像数据会被解码变成RGB
颜色数据,最终渲染到屏幕上。
说说渲染流程
- 1、确定顶点位置:
CPU
确定绘制图形的位置,从iOS
坐标系换算成屏幕坐标系。 - 2、图元装配:确定顶点间的连线关系。
- 3、光栅化:就是把展示用到的像素点摘出来。
- 4、着色器处理:
GPU
计算像素点展示的颜色,并存入缓冲区。 - 5、屏幕展示
什么是离屏渲染
- 普通渲染流程:APP - 帧缓冲区 - 展示
- 离屏渲染流程:APP - 离屏渲染缓冲区 - 帧缓冲区 - 展示
- 离屏渲染,是无法一次性处理渲染,需要分部处理并存储中间结果引起的。
- 所以判断是否出现离屏渲染的根本条件就是判断渲染是否需要分部处理~
- 需要分步处理,会产生离屏渲染
- 一次性渲染,不产生离屏渲染
离屏渲染的影响
- 需要分几步就需要开辟出几个离屏渲染缓冲区存储中间结果,造成空间浪费。
- 最后合并多个离屏渲染缓冲区才能展示结果,会影响性能。
什么操作会触发离屏渲染?
- 光栅化
layer.shouldRasterize = YES
- 遮罩
layer.mask
- 圆角
layer.maskToBounds = Yes
,Layer.cornerRadis
- 阴影
layer.shadowXXX
视图
AutoLayout
的原理,性能如何
Auto Layout
只关注视图之间的关系,通过布局引擎和已有的约束计算出各个视图的frame
- 每当约束改变时会重新计算各个视图的
frame
- 获得
frame
的过程,就是根据各个视图已有的约束条件解方程式的过程。 - 性能会随着视图数量的增加呈指数级增加
- 达到一定数量的视图时,布局所需要的时间就会大于16.67ms,超过屏幕的刷新频率时会出现卡顿。
你认为自动布局怎么实现的
- 原理是线性公式,使用了系统提供的
NSLayoutConstraint
Masonry
基于它封装
ViewController
生命周期
initWithCoder
:通过nib文件初始化时触发。awakeFromNib
:nib文件被加载的时候,会发生一个awakeFromNib
的消息到nib
文件中的每个对象。loadView
:开始加载视图控制器自带的view
。viewDidLoad
:视图控制器的view
被加载完成。viewWillAppear
:视图控制器的view
将要显示在window
上。updateViewConstrains
:视图控制器的view
开始更新AutoLayout
约束。viewWillLayoutSubviews
:视图控制器的view
将要更新内容视图的位置。viewDidLayoutSubviews
:视图控制器的view
已经更新视图的位置。viewDidAppear
:视图控制器的view
已经展示到window
上。viewWillDisappear
:视图控制器的view
将要从window
上消失。viewDidDisappear
:视图控制器的view
已经从window
上消失。
LayoutSubviews
调用时机
init
初始化不会调用LayoutSubviews
方法addsubView
时候会调用- 改变一个
view
的frame
的时候调用 - 滚动
UIScrollView
导致UIView
重新布局的时候会调用 - 手动调用
setNeedsLayout
或者layoutIfNeeded
setNeedsLayout
和layoutIfNeeded
的区别
setNeedsLayout
标记为需要重新布局- 异步调用
layoutIfNeeded
刷新布局,不立即刷新,在下一轮runloop
结束前刷新。 - 对于这一轮
runloop
之内的所有布局和UI更新
只会刷新一次,layoutSubviews
一定会被调用。
- 异步调用
layoutIfNeeded
- 如果有需要刷新的标记,立即调用
layoutSubviews
进行布局 - 如果没有标记,不会调用
layoutSubviews
- 如果想在当前
runloop
中立即刷新,调用顺序应该是
[self setNeedsLayout]; [self layoutIfNeeded];
- 如果有需要刷新的标记,立即调用
drawRect
调用时机
drawRect
在loadView
和ViewDidLoad
之后调用
UIView
和CALayer
是什么关系?
View
可以响应并处理用户事件,CALayer
不可以。- 每个
UIView
内部都有一个CALayer
提供尺寸样式(模型树),进行绘制和显示。 - 两者都有树状层级结构,
layer
内部有subLayers
,view
内部有subViews
。 CALayer
是支持隐式动画的,View
作为Layer
的代理,通过actionForLayer:forKey:
向Layer
提交相应的动画- layer 内部维护着三分
layer tree
- 动画树
presentLayer Tree
,修改动画的属性改的是动画树的属性值 - 模型树
modeLayer Tree
,最终展示在界面上的其实是提供视图的模型树 - 渲染树
render Tree
。
- 动画树
UIView
显示原理
UIView
可以显示是因为内部有一个layer
作为根图层,根图层上可以放其他子图层。UIView
中所有能够看到的内容都包含在layer
中- 当
UIView
需要显示到屏幕上会调用drawRect:
方法进行绘图,并且会将所有内容绘制在自己的layer
上 - 绘图完毕后,系统将图层展示到屏幕上,完成了UIView的显示。
UIView
显示过程
view.layer
创建一个图层类型的上下文(Layer Graphics Contex
)- 触发代理方法
drawLayer:inContext:
,传入刚才准备好的上下文 drawLayer:inContext:
内部会让view
调用drawRect:
方法绘图- 开发者在
drawRect:
方法中实现绘图代码,内容最终绘制到view.layer
- 系统将
view.layer
展示到屏幕,完成了view
的显示
UITableView
的重用机制?
UITableView
通过重用单元格来节省内存- 为每个单元格指定一个重用标识符,即指定了单元格的种类
- 当屏幕上的单元格滑出屏幕时,系统会把这个单元格添加到重用队列中,等待被重用。
- 当有新单元格从屏幕外滑入屏幕内时,从重用队列查找可重用的单元格,如果有就拿来用,如果没有就创建一个使用。
UITableView
卡顿的的原因有哪些?
- 隐式绘制
CGContext
- 绘制
Core Graphics
- 文本
CATextLayer
和UILabel
- 截图
-renderInContext:
- 可伸缩图片
- 混合图层
- 对象回收
- 离屏渲染
- 光栅化
shouldRasterize
- 阴影效果
shadowPath
- 圆角裁切
cornerRadius
- 遮罩
- 光栅化
UITableVIew
优化
- 重用机制(缓存池)
- 少用有透明度的
View
- 尽量避免使用
xib
- 尽量避免过多的层级结构
- iOS8以后出的预估高度
- 减少离屏渲染操作(圆角、阴影)
- 缓存
cell
的高度(提前计算好cell
的高度,缓存进当前的模型里面) - 异步绘制
- 滑动的时候,按需加载
- 尽量少
add、remove
子控件,最好通过hidden
控制显示
imageName
与imageWithContentsOfFile
区别?
imageWithContentsOfFile
:加载本地目录图片,不能加载image.xcassets
里面的图片资源。不缓存占用内存小,相同的图片会被重复加载到内存中。不需要重复读取的时候使用。imageNamed
:可以读取image.xcassets
的图片资源,加载到内存缓存起来,占用内存较大,相同的图片不会被重复加载。直到收到内存警告的时候才会释放不使用的UIImage
。需要重复读取同一个图片的时候用。
IBOutlet
连出来的视图属性为什么可以被设置成weak
?
- 因为
Xcode
内部把链接的控件放进了一个_topLevelObjectsToKeepAliveFromStoryboard
的私有数组中,这个数组强引用这所有topLevel
的对象,所以用weak
也无伤大雅。
UIScrollerView
实现原理
- 滚动其实是在修改原点坐标。当手指触摸后,
scrollview
拦截触摸事件创建一个计时器。 - 如果计时器到点后没有发生手指移动事件,
scrollview
发送tracking events
到被点击的subview
。 - 如果计时器到点前发生了移动事件,
scrollview
取消tracking
自己滚动。
如何实现视图的变形?
- 修改
view
的transform
。
事件响应链和事件传递
什么是响应链
- 由链接在一起的响应者(
UIResponse
及其子类)组成的链式关系。 - 最先的响应者称为第一响应者
- 最底层的响应者是
UIApplication
写出一个响应链
subView -> view -> superView -> viewController -> window -> application
什么是事件传递
- 触发事件后,事件从第一响应者到
application
的传递过程
事件传递的过程
- 当程序中发生触摸事件之后,系统会将事件添加到
UIApplication
管理的一个队列当中 application
将任务队列的首个任务向下分发application -> window -> viewController -> view
view
需要满足条件才可以处理任务,透明度>0.01、触摸在view
的区域内、userInteractionEnabled=YES
、hidden=NO
。- 满足条件的
view
遍历自身的subViews
,判断是否满足上述条件 - 如果所有
subView
都无法满足条件,那么最佳响应者就是自己。 - 如果没有任何一个
view
能处理事件,事件会被废弃。
找出触摸的View
// 返回的View是本次点击的最佳响应者
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
// 判断点是否落在某区域
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
重写hitTest:withEvent
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
// 1.判断当前控件能否接收事件
if (self.userInteractionEnabled == NO ||
self.hidden == YES ||
self.alpha <= 0.01) {
return nil;
}
// 2. 判断点在不在当前控件
if ([self pointInside:point withEvent:event] == NO) {
return nil;
}
// 3.从后往前遍历自己的子控件
NSInteger count = self.subviews.count;
for (NSInteger i = count - 1; i >= 0; i--) {
UIView *childView = self.subviews[I];
// 把当前控件上的坐标系转换成子控件上的坐标系
CGPoint childP = [self convertPoint:point toView:childView];
UIView *fitView = [childView hitTest:childP withEvent:event];
if (fitView) { // 寻找到最合适的view
return fitView;
}
}
// 循环结束 没有比自己更合适的view
return self;
}
Crash
常见Crash的原因有哪些?
- 1、找不到方法的实现
unrecognized selector sent to instance
- 2、
KVC
造成的crash - 3、
KVO
造成的crash - 4、
EXC_BAD_ACCESS
- 5、集合类相关崩溃,越界等
- 6、多线程中的崩溃,使用了被释放的对象
- 7、后台返回错误的数据结构
BAD_ACCESS
在什么情况下出现?
- 访问已经释放对象的成员变量
- 给已经释放对象发消息
- 死循环
不使用第三方,排查闪退问题?
- 1、使用
NSSetUncaughtExceptionHandler
统计闪退的信息 - 2、将统计到的信息发给后台
- 3、在后台收集信息,进行排查
static void my_uncaught_exception_handler (NSException *exception) { //获取NSException 信息 NSLog(@"***********************************************"); NSLog(@"%@",exception); NSLog(@"%@",exception.callStackReturnAddresses); NSLog(@"%@",exception.callStackSymbols); NSLog(@"***********************************************"); } - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { NSSetUncaughtExceptionHandler(&my_uncaught_exception_handler); return YES; }
性能优化
优化启动时间
- 启动时间是用户点击App图标,到第一个界面展示的时间。
- 启动时间在小于400ms是最佳的,因为从点击图标到显示
Launch Screen
,到Launch Screen
消失这段时间是400ms。 - 启动时间不可以大于20s,否则会被系统杀掉。
以main
函数作为分水岭,启动时间其实包括了两部分:
mian
函数之前的启动优化
- 减少或合并动态库(这是目前为止最耗时的了, 基本上占了95%以上的时间)
- 确认动态库是
optional or required
。如果该Framework
在支持的所有iOS系统版本都存在,那么就设为required
,否则就设为optional
,因为optional
会有些额外的检查
mian
函数之后的启动优化
- 1、合并和删减不必要的类或者分类
- 2、将不必需在
+load
方法中做的事情,延时放到+initialize
。 - 3、非必要的 SDK 和配置事件可以放在第一个界面处理
- 4、减少创建线程,线程创建和运行需要多次的上下文切换所带来的开销,平均消耗大约在 29 毫秒。这是很大的时间开销,应该避免滥用。
- 5、编译器插桩获取方法符号,生成
order file
设置到xcode
。减少页中断带来的耗时。
网络优化
IP
直连,将我们原本的请求链接www.baidu.com
修改为180.149.132.47
- 运营商在拿到请求后发现是
IP
地址会直接放行,而不会去走DNS解析 - 不走他的
DNS
解析也就不会存在DNS被劫持的问题 - 实现方法1:接使用
HTTPDNS
等sdk - 实现方法2:服务端下发
发域名-IP
对应列表,客户端缓存,通过缓存IP来进行业务访问。
包体积优化
- 1、删除陈旧代码、删除陈旧
xib/sb
,删除无用的资源文件(检测未使用图片的工具LSUnusedResources
) - 2、图片、音视频资源压缩后使用。
- 3、动图可以使用
webP
格式,加载速度比较慢,但体积小 - 4、能用动态库的尽量用动态库,一般情况静态库的体积会比动态库大
- 5、主题类的资源文件提供按需下载功能,不直接打包在应用包里面
- 6、
App Slicing
,应用程序切片,只打包下载机型所需的资源文件,不需要开发者处理 - 7、
Bitcode
,中间代码, 苹果会对可执行文件进行优化,不需要开发者处理 - 8、
ODR,On Demand Resources
,按需加载资源,需要开发者处理
电量优化
- 1.定位,尽量不要实时更新,可以适当降低精度
- 2.网络请求,能用缓存的时候尽量使用缓存,降低请求的频率,减少请求的次数,优化传输结构
- 3.CPU处理,需要复用的数据能缓存的数据尽量缓存,优化算法和数据结构
- 4.GPU处理,减少离屏渲染
短视频/直播优化
- 多播放器,像
tableview
那样维护缓存池。多播放器,才能做预加载。 - 边下边播,要实现下一个视频或者几个视频能快速的播放起来,首先应该保证正在播放的短视频能顺畅播放,边下边播任务优先级应该高于预加载任务。没有边下边播时才能执行预加载,边下边播任务进行时应当停止预加载。
- 预加载,预加载的下载应该和边下边播区分开,手势滑动到差不多出现的时候,开始播放预加载的那
1m
数据。滑动下一个的时候,就取消preload
,播放那1m
下载的数据,再继续load
。同时调起下一个preload
准备下载。
短视频缓存管理
- 缓存主要创建了三个目录管理,分别为
temp、media、trash
目录 - 缓存分为临时缓存和最终缓存,当短视频资源未下载完时是放在
temp
、当视频资源缓存完时移动到media
,这样分别存放便能方便读取和管理两种状态的缓存, - 所有需要删除的缓存文件都移入
trash
,随后再删除以此来保证较高的删除效率。所有文件命名使用视频url的md5值
保证唯一性。 - 缓存应该具有自动管理功能,默认配置下
ShortMediaCache
允许临时缓存最多保存1天,最大100Mb
,而最终缓存则允许最多保存2天最大200Mb
,如果业务需要可以自定义ShortMediaCacheConfig
配置实现。
一般是怎么用Instruments
的?
Instruments
里面工具很多,常用:Time Profiler
: 性能分析Zombies
:检查是否访问了僵尸对象,但是这个工具只能从上往下检查,不智能。Allocations
:用来检查内存。Leaks
:检查内存,看是否有内存泄露。
编译 & 启动
编译链接流程
- 0、输入文件:找到源文件
- 1、预处理:包括替换宏和导入头文件
- 2、编译阶段:词法分析、语法分析、语义分析,最终生成IR
- 2-1、预处理后会进行词法分析,词法分析会把代码切片成一个个
token
。 - 2-2、语法分析会验证语法是否正确,在词法分析的基础上把单词组合成各种语法短语,然后把节点组成抽象的语法树
- 2-3、代码生成器根据语法树自顶向下生成
LLVM IR
。OC
会在这里进行runtime
的桥接:property
的合成、ARC
处理等
- 2-1、预处理后会进行词法分析,词法分析会把代码切片成一个个
- 3、编译器后端:通过一个个
Pass
去优化,每个Pass
完成一部分功能,最终生成汇编代码- 3-1、苹果对代码做了进一步优化,并且通过
.ll
文件生成.bc
文件。 - 3-2、可以通过
.bc
或.ll
文件生成汇编代码
- 3-1、苹果对代码做了进一步优化,并且通过
- 4、生成,
.o
格式的目标文件 - 5、链接动态库和静态库
- 6、通过不同架构,生成对应的可执行文件
MachO
APP启动过程
- 1、加载可执行文件(读取
Mach-O
) - 2、加载动态库(
Dylib
) - 3、
Rebase & Bind
- 3-1、
Rebase
的作用是修正ASLR
的偏移,把当前MachO
的指针指向正确的内存 - 3-2、
Bind
的作用是重新修复外部方法指针的指向,fishhook
原理
- 3-1、
- 4、
objc_init
,加载类和分类 - 5、
Initializers
,调用load
方法,初始化C & C++
的对象等 - 6、
main()
函数 - 7、执行
AppDelegate
的代理方法(如:didFinishLaunchingWithOptions
)。 - 8、初始化
Windows
,初始化ViewController
。
dyld
做了什么
- 1、
dyld
读取Mach-O
的Header
和Load Commands
- 2、找可执行文件的依赖动态库,将依赖的动态库加载到内存中。这是一个递归的过程,依赖的动态库可能还会依赖别的动态库,所以
dyld
会递归每个动态库,直至所有的依赖库都被加载完毕。
转载自:https://juejin.cn/post/7076026684238987300