原创:知识点总结性文章
创作不易,请珍惜,之后会持续更新,不断完善
个人比较喜欢做笔记和写总结,毕竟好记性不如烂笔头哈哈,这些文章记录了我的IOS成长历程,希望能与大家一起进步
温馨提示:由于简书不支持目录跳转,大家可通过command + F 输入目录标题后迅速寻找到你所需要的内容
目录
- 一、常见优化方案
- 1、初级优化建议
- 2、中级优化建议
- 3、高级优化建议
- 二、屏幕显示图像的原理及优化方案
- 1、屏幕显示图像的原理
- 2、卡顿产生的原因
- 3、CPU 资源消耗原因和解决方案
- 4、GPU 资源消耗原因和解决方案
- 5、触发离屏渲染
- 三、检测优化效果
- 1、如何评测界面的流畅度
- 2、如何检测内存是否泄漏及使用 / 分配情况
- 3、如何检测分析代码的执行时间
- 4、如何进行APP耗电量检测
- 5、如何进行流量检测
- 6、其他instruments工具
- 四、App启动优化
- 1、iOS应用启动流程
- 2、动态链接库 dyld
- 3、pre-main 阶段耗时
- 4、main 阶段耗时
- 5、启动速度优化实践
- 五、音视频的优化方案
- 1、图片文件优化
- 2、图片压缩
- Demo
- 参考文献
一、常见优化方案
1、初级优化建议
- 用
ARC
管理内存 - 不要阻塞主线程
- 避免庞大的
xib
,storyBoard
,尽量使用纯代码开发
不要频繁的刷新页面,能刷新1行cell
最好只刷新一行,尽量不要使用reloadData
。
把图片放在Xcode自带的图片管理工具里面Images.asssets
,这样的好处就是打包的资源包中的图片有被系统压缩。
可以把项目中大的资源文件或者包放在服务器上,等APP下载以后再下载下来。举个例子,字体库拿出来放在公司服务器上,APP拿到下载链接,让用户下载完成APP以后再在手机里面下载字体库。
给UITableViewCells
,UICollectionViewCells
,UITableViewHeaderFooterViews
,地图视图 ( MKPinAnnotationView
)设置reuseIdentifier
,可以模仿UITableview
和UICollectionView
实现自定义重用机制。
在Image Views
中调整图片大小,如果要在UIImageView
中显示一个来自bundle
的图片,你应保证图片的大小和UIImageView
的大小相同。在运行中缩放图片是很耗费资源的,特别是UIImageView
嵌套在UIScrollView
中的情况下。如果图片是从远端服务加载的你不能控制图片大小,你可以在下载完成后,最好是用backgroundthread
缩放一次,然后在UIImageView
中使用缩放后的图片。
Arrays
、Dictionaries
、Sets
选择适合的数据结构,NSArray
使用index
来查找很快(插入和删除很慢),NSDictionary
使用键来查找很快,NSSet
是无序的,用键查找很快,插入/删除也很快。
少用运算获得圆角,不论view.maskToBounds
还是layer.clipToBounds
都会有很大的资源开销,必须要用圆角的话,不如把图片本身做成圆角或者可通过Core Graphics
画出圆角矩形(UIBezierPath
画圆)。
在版本迭代过程中,如果业务发生变化,导致相应的代码也发生变化,一般情况下我们需要把对应的旧代码和旧资源删除掉(旧资源会增加App体积,旧代码会增加执行文件的大小,进而增加Objc
类数量或者selector
数量造成启动APP缓慢)。
尽可能地复用UI,在添加某个功能时,先去查查我们的代码中是否已经实现了该功能,减少重复。
2、中级优化建议
延迟加载,不要一次性创建所有的subview
,而是需要时才创建,消耗更少内存,但是会稍显卡顿。懒加载适用于一些可能不会加载的页面,比如弹框、空数据页面之类的,使用得当可以避免内存暴涨,使用不好,比如在必定会弹出的页面中使用懒加载可能会在增加页面响应时间,所以使用懒加载一定要注意使用场景,避免产生副作用。
缓存所需要的,也就是那些不大可能改变但是需要经常读取的东西,远端服务器的响应,图片,甚至计算结果,比如UITableView
的行高。
用事先渲染好的图片更快一些,因为如此一来就免去了创建一个图片再画东西上去然后显示在屏幕上。
处理内存警告,一个有图片缓存的app
可以移除不在屏幕上显示的图片,即可以释放可以被重新创建的object
来释放内存。
避免反复处理数据,在服务器端和客户端使用相同的数据结构很重要。
解析JSON
会比XML
更快一些,JSON
也通常更小更便于传输,而XML
的好处,比如使用SAX
来解析XML
就像解析本地文件一样,你不需像解析json
一样等到整个文档下载完成才开始解析。当你处理很大的数据的时候就会极大地减低内存消耗和增加性能。
如果你使用全画幅的背景图使用UIImageView
,如果你用小图平铺来创建背景,你就需要用UIColor
的colorWithPatternImage
来做了,它会更快地渲染也不会花费很多内存。
选择正确的数据存储选项,NSUerDefaults
、plist
、NSCoding
、SQLite
数据库、 Core Data
。
产生卡顿的原因:大量的计算、视图的层级比较复杂、耗时任务。对此,可以减少图层的层次绘制成一张图片,在子线程中执行耗时任务。
创建的异步绘制任务需要在 Runloop
处于(waiting / exiting
)的时候才开始执行回调函数【view.layer setNeedDisplay
】【layer display
】【异步绘制】。
3、高级优化建议
- 加速启动时间
- 优化算法,减少循环次数
- 线程适量,不宜过多,不要阻塞主线程
- 定位和蓝牙按需取用,定位之后要关闭或降低定位频率
自动释放池因为有系统帮我们监管平时不需要管它,但是如果我们一个页面创建了太多的类或者对象,如果等页面销毁的时候由系统统一释放难免会出现一个峰值影响整体性能,这时候我们就可以考虑使用autoreleasepool
了,避免峰值的出现!假如你创建很多临时对象,你会发现内存一直在减少,直到这些对象被release
的时候,所以需要手动在@autoreleasepool
里释放临时的对象来避免这个行为。
如果你要加载一个大图片而且是一次性使用,那么就没必要缓存这个图片,用imageWithContentsOfFile
足矣,这样不会浪费内存来缓存它。然而,在图片反复重用的情况下imageNamed
是一个好得多的选择。
使用 SQLite 数据库,需要注意:1、尽量不要使用 LIKE模糊匹配查询, 使用=查询 , 把非文本的条件放在前面。2、索引就像是书中的目录,插入和删除数据必然造成索引重排 ,所以创建索引要慎重。3、建立索引、限制返回记录数和 where条件子句等可以提高查找性能。
使用文件,需要注意:1、文件访问优化:避免多次写入很少的数据, 最好是当数据积攒到 一定数量时一次写入。 2、频 繁的IO操作会影响性能, 所以 最好将文件读写访问从主线程中剥离出来,由一个子线程负责。3、文件的写入应该采用增量方式,每次只写入变化的部分。
使用plist
,需要注意:1、plist
文件就是很好的结构化文件,其结构是层次模型的树形结构,层次的深浅会影响读取/写入的速度(减少),调整文件结构可以减少文件大小。2、通过序列化.plist
文件来减少文件大小(NSPropertyListSerialization
)。
二、屏幕显示图像的原理及优化方案
CPU的作用
- 对象管理(销毁与创建)
- 对象的维护(属性调整、布局计算、文本计算和排版、图片格式转换和解码)
- 图像的绘制(CG)
GPU的作用
- 纹理渲染
- 视图合成
1、屏幕显示图像的原理
a、GPU渲染流程
GPU(Graphics Processing Unit)
又名图形处理器,是显卡的 “核心”。主要负责图像运算工作,具有高并行能力,通过计算将图像显示在屏幕像素中。工作原理是将3D坐标转换成2D坐标,再将2D坐标转换为实际有颜色的像素。
- 工作流水线:顶点着色器 => 形状装配 => 几何着色器 => 光栅化 => 片段着色器 => 测试与混合
- 顶点着色器(Vertex Shader):确定形状的点
- 形状装配(Shape Assembly):确定形状的线
- 几何着色器(Geometry Shader):确定三角形的个数,使之变成几何图形
- 光栅化(Rasterization):将图转化为一个个实际屏幕像素
- 片段着色器(Fragment Shader):对屏幕像素点着色
- 测试与混合(Tests and Blending):检查图层深度和透明度,并进行图层混合
合成(Compositing)
Compositing
是指将多个纹理拼到一起的过程,对应UIKit
是指处理多个view
合到一起的情况。如果view
之间没有叠加,那么GPU只需要做普通渲染即可。 如果多个view
之间有叠加部分,GPU需要做blending
。
b、iOS 原生渲染框架
UIKit
日常开发最常用的UI
框架,可以通过设置UIKit
组件的布局以及相关属性来绘制界面。其实本身UIView
并不拥有屏幕成像的能力,而是View
上的CALayer
属性拥有展示能力。UIView
继承自UIResponder
,其主要负责用户操作的事件响应,iOS事件响应传递就是经过视图树遍历实现的。
SwiftUI
苹果新推出的一款全新的“声明式UI”
框架,使用Swift
编写。一套代码,即可完成iOS
、iPadOS
、macOS
、watchOS
的开发与适配。
Core Animation
核心动画,一个复合引擎。尽可能快速的组合屏幕上不同的可视内容。分解成独立的图层(CALayer
),存储在图层树中。
Core Graphics
基于Quartz
高级绘图引擎,主要用于运行时绘制图像。
Core Image
运行前图像绘制,对已存在的图像进行高效处理。
OpenGL ES
OpenGL for Embedded Systems
,是 OpenGL
的子集。可通过C/C++
编程操控GPU
。
Metal
渲染性能比OpenGL ES
高。为了解决OpenGL ES
不能充分发挥苹果芯片优势的问题。
c、IOS 原生渲染的流程
layer
内部维护着三个layer tree
,分别是presentLayer Tree
(动画树),modeLayer Tree
(模型树),Render Tree
(渲染树),在做 iOS动画的时候,我们修改动画的属性,在动画的其实是Layer
的presentLayer
的属性值,而最终展示在界面上的其实是提供View
的modelLayer
。
每一个UIView
都有一个layer
,每一个layer
都有个content
,这个content
指向的是一块缓存,叫做backing store
。当backing store
写完后,通过render server
交给GPU
去渲染,将backing store
中的bitmap
数据显示在屏幕上。其实说到底CPU
就是做绘制的操作把内容放到缓存里,GPU
负责从缓存里读取数据然后渲染到屏幕上。
第一步
更新视图树、图层树。分别对应View
的层级结构、View
上的Layer
层级结构。
第二步
CPU
开始计算下一帧要显示的内容(包括视图创建、布局计算、视图绘制、图像解码)。当 runloop
在 kCFRunLoopBeforeWaiting
和 kCFRunLoopExit
状态时,会通知注册的监听,然后对图层打包,打完包后,将打包数据发送给一个独立负责渲染的进程 Render Server
。 前面 CPU
所处理的这些事情统称为 Commit Transaction
。
第三步
数据到达Render Server
后会被反序列化,得到图层树,按照图层树的图层顺序、RGBA
值、图层frame
来过滤图层中被遮挡的部分,过滤后将图层树转成渲染树,渲染树的信息会转给 OpenGL ES/Metal
。
第四步
Render Server
会调用 GPU
,GPU
开始进行前面提到的顶点着色器、形状装配、几何着色器、光栅化、片段着色器、测试与混合六个阶段。完成这六个阶段的工作后,就会将 CPU
和 GPU
计算后的数据显示在屏幕的每个像素点上。
d、WebView 前端渲染
对于WebView
渲染,其主要工作在WebKit
中完成。WebKit
本身的渲染基于macOS
的Lay Rendering
架构,iOS
本身渲染也是基于这套架构。因此,本身从渲染的实现方式来说,性能应该和原生差别不大。但为什么我们能明显感觉到使用WebView
渲染要比原生渲染的慢呢?
首次加载会额外多出网络请求和脚本解析工作。 即使是本地网页加载,WebView
也要比原生多出脚本解析的工作。 WebView
要额外解析HTML+CSS+JavaScript
代码。
从语言解释执行性能来看。JavaScript
的语言解析执行性能要比原生弱。 特别是遇到复杂的逻辑与大量的计算时,WebView
的解释执行性能要比原生慢不少。
WebView
的渲染进程是独立的,每一帧的更新都要通过IPC
调用GPU
进程,会造成频繁的IPC
进程通信,从而造成性能消耗。并且,两个进程无法共享纹理资源,GPU
无法直接使用context
光栅化,而必须要等待WebView
通过IPC
把context
传给GPU
再光栅化。因此GPU
自身的性能发挥也会受影响。
e、React Native 渲染(使用JavaScriptCore引擎做为虚拟机方案)
React Native
的渲染层直接走的是iOS
原生渲染,只不过是多了Json+JavaScript
脚本解析工作。
JavaScriptCore
是iOS
原生与 JS
之间的桥梁,其原本是 WebKit
中解释执行 JavaScript
代码的引擎。
通过JavaScriptCore
引擎将JS与原生控件产生相对应的关联。进而,达成通过JS
来操控iOS
原生控件的目标。(简单来说,这个json
就是一个脚本语言到本地语言的映射表,KEY
是脚本语言认识的符号,VALUE
是本地语言认识的符号。)
但与WebView
一样,RN
也需要面临JS
语言解释性能的问题。因此,从渲染效率角度来说,WebView
< ReactNative
< 原生。 (因为json
的复杂度比html+css
低)
f、Flutter渲染
Flutter的架构
可以看到,Flutter
重写了UI
框架,从UI
控件到渲染全部自己重新实现了,不依赖 iOS
、Android
平台的原生控件,依赖Engine(C++)
层的Skia
图形库与系统图形绘制相关接口,因此,在不同的平台上有了相同的体验。
Flutter的渲染流程
简单来说,Flutter
的界面由Widget
组成,所有Widget
会组成Widget Tree
。界面更新时,会更新Widget Tree
,再更新Element Tree
,最后更新RenderObject Tree
。
在 Flutter
的 C++
层,使用 Skia
库,将 Layer
进行组合,生成纹理,使用 OpenGL
的接口向GPU
提交渲染内容进行光栅化与合成。
提交到 GPU
进程后,合成计算,显示屏幕的过程和iOS
原生渲染基本是类似的,因此性能上是差不多的。
2、卡顿产生的原因
双缓冲机制
GPU
会预先渲染好一帧放入一个缓冲区内,让视频控制器读取,当下一帧渲染好后,GPU
会直接把视频控制器的指针指向第二个缓冲器。如此一来效率会有很大的提升。
GPU 通常有一个机制叫做垂直同步(简写也是 V-Sync)
当开启垂直同步后,GPU
会等待显示器的 VSync
信号发出后,才进行新的一帧渲染和缓冲区更新。这样能解决画面撕裂现象,也增加了画面流畅度,但需要消费更多的计算资源,也会带来部分延迟。
在VSync
信号到来后,系统图形服务会通过 CADisplayLink
等机制通知App
。App
主线程开始在CPU
中计算显示内容,比如视图的创建、布局计算、图片解码、文本绘制等。随后 CPU
会将计算好的内容提交到GPU
去,由GPU
进行变换、合成、渲染。随后GPU
会把渲染结果提交到帧缓冲区去,等待下一次 VSync
信号到来时显示到屏幕上。由于垂直同步的机制,如果在一个 VSync
时间内,CPU
或者 GPU
没有完成内容提交,则那一帧就会被丢弃,等待下一次机会再显示,而这时显示屏会保留之前的内容不变。这就是界面卡顿的原因。
从上面的图中可以看到,CPU
和 GPU
不论哪个阻碍了显示流程,都会造成掉帧现象。所以开发时,也需要分别对 CPU
和 GPU
压力进行评估和优化。
3、CPU 资源消耗原因和解决方案
a、对象创建
对象的创建会分配内存、调整属性、甚至还有读取文件等操作,比较消耗 CPU
资源。尽量用轻量的对象代替重量的对象,可以对性能有所优化。比如 CALayer 比 UIView
要轻量许多,那么不需要响应触摸事件的控件,用 CALayer
显示会更加合适。
如果对象不涉及 UI
操作,则尽量放到后台线程去创建,但可惜的是包含有CALayer
的控件,都只能在主线程创建和操作。
通过 Storyboard
创建视图对象时,其资源消耗会比直接通过代码创建对象要大非常多。
b、对象调整
CALayer
内部并没有属性,当调用属性方法时,它内部是通过运行时 resolveInstanceMethod
为对象临时添加一个方法,并把对应属性值保存到内部的一个 Dictionary
里,同时还会通知 delegate
、创建动画等等,非常消耗资源。UIView
的关于显示相关的属性(比如frame/bounds/transform
)等实际上都是 CALayer
属性映射来的,所以对 UIView
的这些属性进行调整时,消耗的资源要远大于一般的属性。对此你在应用中,应该尽量减少不必要的属性修改。
当视图层次调整时,UIView
、CALayer
之间会出现很多方法调用与通知,所以在优化性能时,应该尽量避免调整视图层次、添加和移除视图。
c、对象销毁
对象的销毁虽然消耗资源不多,但累积起来也是不容忽视的。通常当容器类持有大量对象时,其销毁时的资源消耗就非常明显。同样的,如果对象可以放到后台线程去释放,那就挪到后台线程去。
d、布局计算
视图布局的计算是 App
中最为常见的消耗CPU
资源的地方。如果能在后台线程提前计算好视图布局、并且对视图布局进行缓存,那么这个地方基本就不会产生性能问题了。
不论通过何种技术对视图进行布局,其最终都会落到对UIView.frame/bounds/center
等属性的调整上。上面也说过,对这些属性的调整非常消耗资源,所以尽量提前计算好布局,在需要时一次性调整好对应属性,而不要多次、频繁的计算和调整这些属性。
e、Autolayout
Autolayout
是苹果本身提倡的技术,在大部分情况下也能很好的提升开发效率,但是 Autolayout
对于复杂视图来说常常会产生严重的性能问题。随着视图数量的增长,Autolayout
带来的 CPU
消耗会呈指数级上升。可以使用AsyncDisplayKit
框架。
f、文本计算
如果一个界面中包含大量文本(比如微博微信朋友圈等),文本的宽高计算会占用很大一部分资源,并且不可避免。如果你对文本显示没有特殊要求,可以参考下 UILabel
内部的实现方式:用 [NSAttributedString boundingRectWithSize:options:context:]
来计算文本宽高,用 [NSAttributedString drawWithRect:options:context:]
来绘制文本。尽管这两个方法性能不错,但仍旧需要放到后台线程进行以避免阻塞主线程。
如果你用 CoreText
绘制文本,那就可以先生成 CoreText
排版对象,然后自己计算了,并且 CoreText
对象还能保留以供稍后绘制使用。
g、文本渲染
屏幕上能看到的所有文本内容控件在底层都是通过 CoreText
排版、绘制为 Bitmap
显示的。常见的文本控件 (UILabel
、UITextView
等),其排版和绘制都是在主线程进行的,当显示大量文本时,CPU
的压力会非常大。
对此解决方案只有一个,那就是自定义文本控件,用 TextKit
或最底层的 CoreText
对文本异步绘制。尽管这实现起来非常麻烦,但其带来的优势也非常大。CoreText
对象创建好后,能直接获取文本的宽高等信息,避免了多次计算(调整 UILabel
大小时算一遍、UILabel
绘制时内部再算一遍)。CoreText
对象占用内存较少,可以缓存下来以备稍后多次渲染。
h、图片的解码
png
、jpeg
这种都是压缩格式,解码就是解压缩的过程,图片解码需要大量计算,耗时长。当你用 UIImage
或 CGImageSource
的那几个方法创建图片时,图片数据并不会立刻解码。图片设置到 UIImageView
或者 CALayer.contents
中去,并且 CALayer
被提交到 GPU
前,CGImage
中的数据才会得到解码。这一步是发生在主线程的,并且不可避免。如果想要绕开这个机制,常见的做法是在后台线程先把图片绘制到 CGBitmapContext
中,然后从Bitmap
直接创建图片。目前常见的网络图片库都自带这个功能。
g、图像的绘制
图像的绘制通常是指用那些以 CG
开头的方法把图像绘制到画布中,然后从画布创建图片并显示这样一个过程。这个最常见的地方就是 [UIView drawRect:]
里面了。由于CoreGraphic
方法通常都是线程安全的,所以图像的绘制可以很容易的放到后台线程进行。
4、GPU 资源消耗原因和解决方案
相对于CPU
来说,GPU
能干的事情比较单一:接收提交的纹理(Texture
)和顶点描述(三角形),应用变换(transform
)、混合并渲染,然后输出到屏幕上。
a、纹理的渲染
所有的 Bitmap
,包括图片、文本、栅格化的内容,最终都要由内存提交到显存,绑定为 GPU Texture
。不论是提交到显存的过程,还是 GPU
调整和渲染 Texture
的过程,都要消耗不少 GPU
资源。当在较短时间显示大量图片时(比如 TableView
存在非常多的图片并且快速滑动时),CPU
占用率很低,GPU
占用非常高,界面仍然会掉帧。避免这种情况的方法只能是尽量减少在短时间内大量图片的显示,尽可能将多张图片合成为一张进行显示。
b、视图的混合 (Composing)
当多个视图(或者说 CALayer
)重叠在一起显示时,GPU
会首先把他们混合到一起。如果视图结构过于复杂,混合的过程也会消耗很多 GPU
资源。为了减轻这种情况的 GPU
消耗,应用应当尽量减少视图数量和层次,并在不透明的视图里标明 opaque
属性以避免无用的 Alpha
通道合成。当然,这也可以用上面的方法,把多个视图预先渲染为一张图片来显示。
c、图形的生成
CALayer
的 border
、圆角、阴影、遮罩(mask
),CASharpLayer
的矢量图形显示,通常会触发离屏渲染(offscreen rendering
),而离屏渲染通常发生在 GPU
中。当一个列表视图中出现大量圆角的 CALayer
,并且快速滑动时,可以观察到 GPU
资源已经占满,而 CPU
资源消耗很少。这时界面仍然能正常滑动,但平均帧数会降到很低。对于只需要圆角的某些场合,也可以用一张已经绘制好的圆角图片覆盖到原本视图上面来模拟相同的视觉效果。最彻底的解决办法,就是把需要显示的图形在后台线程绘制为图片,避免使用圆角、阴影、遮罩等属性。
5、触发离屏渲染
a、离屏渲染原因
我们来看一下关于iOS中图形绘制框架的大致结构。UIKit
是iOS中用来管理用户图形交互的框架,但是UIKit
本身构建在CoreAnimation
框架之上,CoreAnimation
分成了两部分OpenGL ES
和Core Graphics
,OpenGL ES
是直接调用底层的GPU
进行渲染。Core Graphics
是一个基于CPU
的绘制引擎。
我们平时所说的硬件加速其实都是指OpenGL
。Core Animation/UIKit
基于GPU
之上对计算机图形合成以及绘制的实现,由于CPU
渲染能力要低于GPU
,所以当采用CPU
绘制时动画时会有明显的卡顿。其中的有些绘制会产生离屏渲染,额外增加GPU
以及CPU
的绘制渲染。
OpenGL
中,GPU
屏幕渲染有以下两种方式:一是On-Screen Rendering
即当前屏幕渲染,指的是GPU
的渲染操作是在当前用于显示的屏幕缓冲区中进行。Off-Screen Rendering
即离屏渲染,指的是GPU
在当前屏幕缓冲区以外新开辟一个缓冲区进行渲染操作。
离屏渲染的代价主要包括两方面内容:一是创建新的缓冲区。二是上下文的切换,离屏渲染的整个过程,需要多次切换上下文环境:先是从当前屏幕(On-Screen
)切换到离屏(Off-Screen
);等到离屏渲染结束以后,将离屏缓冲区的渲染结果显示到屏幕上又需要将上下文环境从离屏切换到当前屏幕,而上下文环境的切换是要付出很大代价的。
为什么需要离屏渲染?目的在于当使用圆角、阴影、遮罩的时候,图层属性的混合体被指定为在未预合成之前不能直接在屏幕中绘制,即当主屏的还没有绘制好的时候,就需要到屏幕外渲染,最后当主屏已经绘制完成的时候,再将离屏的内容转移至主屏上。下述的一些属性设置都会产生离屏渲染的问题,大大降低GPU的渲染性能。
离屏渲染的触发方式
- shouldRasterize(光栅化)
- masks(遮罩)
- shadows(阴影)
- edge antialiasing(抗锯齿)
- group opacity(不透明)
b、触发离屏渲染需要3个条件
-
contents:设置图片即意味着添加了内容
contents
- 背景色 或 border:为什么说是或而不是和,因为他们是2个图层,超过一个图层的渲染就会触发离屏渲染
b、设置圆角触发离屏渲染的情况
情况一:添加内容和设置背景色
- (void)viewDidLoad
{
[super viewDidLoad];
UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(100, 100, 200, 200)];
imageView.backgroundColor = [UIColor redColor];
imageView.image = [UIImage imageNamed:@"海贼王"];
imageView.layer.cornerRadius = 50;//圆角
imageView.layer.masksToBounds = YES;//裁减
[self.view addSubview:imageView];
}
如何检测项目中哪些图层触发了离屏渲染?打开模拟器的Color Off-screen Rendered
,如果触发了离屏渲染,会有浅黄色背景出现。
圆角为什么要设置2个属性呢?既然是搭配使用,又是万年重复的代码,一个属性不好吗?因为设置layer.cornerRadius
只会设置border
的圆角,不会设置content
的圆角,除非同时设置了layer.masksToBounds = YES
。
imageView.layer.cornerRadius = 5;//设置圆角
imageView.layer.masksToBounds = YES;//裁减
情况二:添加内容和设置border
// imageView.backgroundColor = [UIColor redColor];
imageView.image = [UIImage imageNamed:@"海贼王"];
imageView.layer.cornerRadius = 50;//圆角
imageView.layer.masksToBounds = YES;//裁减
imageView.layer.borderWidth = 2.0;//border宽度
imageView.layer.borderColor = UIColor.blackColor.CGColor;//border颜色
运行效果如下:
情况三:子视图中3个任何一个属性被设置都会触发
设置背景颜色
imageView.layer.cornerRadius = 50;//圆角
imageView.layer.masksToBounds = YES;//裁减
[self.view addSubview:imageView];
UIView *imageViewTwo = [[UIView alloc] initWithFrame:CGRectMake(100, 120, 200, 200)];
imageViewTwo.backgroundColor = UIColor.blueColor;
[imageView addSubview:imageViewTwo];
运行效果如下:
设置内容
imageView.layer.cornerRadius = 50;//圆角
imageView.layer.masksToBounds = YES;//裁减
[self.view addSubview:imageView];
UIView *imageViewTwo = [[UIView alloc] initWithFrame:CGRectMake(100, 120, 200, 200)];
imageViewTwo.layer.contents = (__bridge id)([UIImage imageNamed:@"海贼王"].CGImage);
[imageView addSubview:imageViewTwo];
运行效果如下:
设置边框
imageView.layer.cornerRadius = 50;//圆角
imageView.layer.masksToBounds = YES;//裁减
[self.view addSubview:imageView];
UIView *imageViewTwo = [[UIView alloc] initWithFrame:CGRectMake(100, 120, 200, 200)];
imageViewTwo.layer.borderWidth = 2.0;//border宽度
imageViewTwo.layer.borderColor = UIColor.blackColor.CGColor;//border颜色
[imageView addSubview:imageViewTwo];
运行效果如下:
c、设置圆角不会触发离屏渲染的情况
情况一:不添加内容只设置背景色
imageView.backgroundColor = [UIColor redColor];
// imageView.image = [UIImage imageNamed:@"海贼王"];
设置了背景颜色,仅有一个图层,既然视图只有一个图层,还需要裁减吗,答案是不需要,即layer.masksToBounds = YES;
裁剪语句无影响。
情况二:设置了图片,不设置背景色和border
// imageView.backgroundColor = [UIColor redColor];
imageView.image = [UIImage imageNamed:@"海贼王"];
运行效果如下:
情况三:没有设置图片,但设置了背景色和border
imageView.backgroundColor = [UIColor redColor];
imageView.layer.cornerRadius = 50;//圆角
imageView.layer.masksToBounds = YES;//裁减
imageView.layer.borderWidth = 2.0;//border宽度
imageView.layer.borderColor = UIColor.blackColor.CGColor;//border颜色
效果如下:
d、解决方案:扩展UIimage使当前屏幕渲染实现圆角
- (UIImage *)imageWithCornerRadius:(CGFloat)radius ofSize:(CGSize)size
{
//边界问题
if(radius < 0)
{
radius = 0;
}
else if (radius > MIN(size.height, size.width))
{
//如果radius大于最小边,取最小边的一半
radius = MIN(size.height, size.width)/2;
}
//当前image的可见绘制区域
CGRect rect = CGRectMake(0, 0, size.width, size.height);
//创建基于位图的上下文
UIGraphicsBeginImageContextWithOptions(size, NO, UIScreen.mainScreen.scale);//scale:范围
/*
//在当前位图的上下文添加圆角绘制路径
CGContextAddPath(UIGraphicsGetCurrentContext(), [UIBezierPath bezierPathWithRoundedRect:rect cornerRadius:radius].CGPath);
//当前绘制路径和原绘制路径相交得到最终裁减绘制路径
CGContextClip(UIGraphicsGetCurrentContext());
*/
//等效于上面的2句代码
[[UIBezierPath bezierPathWithRoundedRect:rect cornerRadius:radius] addClip];
//绘制
[self drawInRect:rect];
//取得裁减后的image
UIImage *image =UIGraphicsGetImageFromCurrentImageContext();
//关闭当前位图上下文
UIGraphicsEndImageContext();
return image;
}
调用方式
- (void)viewDidLoad
{
[super viewDidLoad];
UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(100, 100, 100, 200)];
imageView.image = [[UIImage imageNamed:@"海贼王"] imageWithCornerRadius:120 ofSize:imageView.frame.size];
[self.view addSubview:imageView];
}
运行效果
可见,实现了同样的效果,却避免了离屏渲染。
三、检测优化效果
1、如何评测界面的流畅度
a、FPS指示器
如果你需要一个明确的 FPS
指示器,可以尝试一下 KMCGeigerCounter。对于 CPU
的卡顿,它可以通过内置的 CADisplayLink
检测出来;对于 GPU
带来的卡顿,它用了一个 SKView
来进行监视。这个项目有两个小问题:SKView
虽然能监视到 GPU
的卡顿,但引入 SKView
本身就会对 CPU/GPU
带来额外的一点的资源消耗。
这里有个简易版的 FPS
指示器:FPSLabel 只有几十行代码,仅用到了 CADisplayLink
来监视 CPU
的卡顿问题。虽然不如上面这个工具完善,但日常使用没有太大问题。
b、GPU Driver
用 Instuments
的 GPU Driver
预设,能够实时查看到 CPU
和 GPU
的资源消耗。在这个预设内,你能查看到几乎所有与显示有关的数据,比如 Texture
数量、CA
提交的频率、GPU
消耗等,在定位界面卡顿的问题时,这是最好的工具。
c、CADisplayLink
CADisplayLink
监控,结合子线程和信号量,两次事件触发时间间隔超过一个VSync
的时长,就上报调用栈。
d、runloop中添加监听
在runloop
中添加监听,如果kCFRunLoopBeforeSources
和kCFRunLoopBeforeWaiting
中间的耗时超过VSync
的时间,那么就是卡帧了,然后这个时候拿到线程调用栈,看看哪个部分耗时长即可。
2、如何检测内存是否泄漏及使用 / 分配情况
a、内存检测工具
-
Allocations:监测内存使用 / 分配情况,需要注意到,
Allocations
是检测程序运行过程中的内存分配情况的,也需要同时运行着程序 - Leaks—动态内存泄露检测:需要一边运行程序,一边检测。一般用静态分析检查过的代码,内存泄露都比较少。
- Analyze—静态分析工具:静态分析不需要运行程序,就能检查到存在内存泄露的地方
b、常见的泄露情形
创建了一个对象,但是并没有使用。
Xcode提示信息: Value Stored to 'number' is never read 。
翻译一下:存储在'number'里的值从未被读取过。
创建了一个对象且初始化了,但是初始化的值一直没读取过。
Xcode提示信息: Value Stored to 'str' during its initialization is never read
调用了让某个对象引用计数加1的函数,但没有调用相应让其引用计数减1的函数。
Xcode提示信息: Potential leak of an object stored into 'subImageRef' 。
翻译一下:subImageRef对象的内存单元有潜在的泄露风险。
3、如何检测分析代码的执行时间
目的是检查耗时函数。在开始进行应用程序性能分析前,请一定要使用真机,因为模拟器运行在Mac
上,然而Mac
上的CPU
往往比iOS
设备要快。
应用程序一定要运行在Distribution
而不是Debug
模式下。在发布环境打包的时候,编译器会引入一系列提高性能的优化,例如去掉调试符号或者移除并重新组织代码。
另外iOS
引入了一种Watch Dog[看门狗]
机制,不同的场景下,“看门狗”
会监测应用的性能。如果超出了该场景所规定的运行时间,“看门狗”
就会强制终结这个应用的进程。开发者可以crashlog
看到对应的日志,但Xcode
在调试配置下会禁用Watch Dog
。
Time Profiler 检测分析代码的执行时间
-
Separate By Thread:线程分离,在调用路径中能看到占用
CPU
最大的线程 -
Invert Call Tree:从上到下跟踪堆栈信息,可以看到方法调用路径最深方法占用
CPU
耗时,比如A{B{C}}
勾选后显示为C->B->A
- Hide System Libraries:隐藏系统的方法
- 双击对应的方法名,就可以直接跳转到代码里对应的位置了
- 或者偷懒一点可以使用
CACurrentMediaTime()
两次的差值计算方法耗时
4、如何进行APP耗电量检测
a、影响电量的五个因素
-
CPU:
CPU
使用率超过20%就会快速耗干电池电量,高效实用CPU
,并且当用户出现模糊输入时快速做出反应 - Network:网络活动会唤起需要长时间周期性供电的无线电,可以分批次进行网络请求,来降低开销
- Location:精密&高频的定位会增加开销,需要按需使用
-
GPU:图形处理器(显卡的处理器),乱使用
GPU
会导致交互差,并且降低电池寿命 -
Background:后台状态
APP
仍会消耗电量,APP
要按需执行后台操作,并使用延迟APIs
来保证系统运算高效执行,另外,在APP
进入后台状态时,立即减少动作,并且通知系统这些动作已经完成
b、定时器
使用定时器,每隔一段时间获取一次电量,并上报。
+ (float)getBatteryLevel
{
[UIDevice currentDevice].batteryMonitoringEnabled = YES;
return [UIDevice currentDevice].batteryLevel;
}
c、Energy Impact 工具
打开Energy Impact 工具
第一步:进入手机"设置"->"电池"
,可以直观的看出来手机应用的耗电情况。
第二步:使用Xcode
打开你的工程,然后插上手机,使用真机running
,点击Energy Impact
。
Energy Impact工具里的参数解释
- 蓝色表示--合理
- 黄色--表示程序比较耗电
- 红色--表示仅仅轻度
- 图表中
Utilization
栏中是表示瞬间耗电情况 - 图表中
Average
栏中,表示平均耗电情况 - 图表中
Energy Impact
中coat
(蓝色)表示运行项目代码需要电量,overhead
(红色)表示开销,包括CPU
的唤起,蓝牙&WiFi,和其他系统资源的调用等,灰色表示有电量消耗,白色表示没有电量消耗
d、使用Instrument的Energy Log 工具
第一步:打开手机设置,点击“开发者”。
第二步:点击Logging
。
第三步:勾选Energy
,并点击startRecording
。
第四步:运行需要测试的APP(确保手机消耗的是手机自身的电池),运行3-5分钟,在进入手机设置点击stopRecording
。
第五步:使用Xcode
,把手机和Xcode
相连,并打开instruments
中的Energy Log
,点击工具栏中import Logged Data from Device
。
第六步:得到了电池损耗日志,对于Energy Usage Level
的值(0--20),值越大表示越耗电,而CPU Activity
表示CPU
各种活动。
5、如何进行流量检测
可以使用 NSURLProtocol
对网络请求拦截,进而得到流量、响应时间等信息,但是NSURLProtocol
有自己的局限,比如NSURLProtocol
只能拦截NSURLSession
以及WebView
,但是对于CFNetwork
则无能为力。NSURLProtocol
拦截是监控WebView
请求最普遍的解决方案。
优化方案
网络优化分为提速、节流、安全,选择合理网络协议处理专门业务(比如聊天的APP需要用socket
)。
提速
- 增加缓存,比如图片缓存,列表页数据放入数据库缓存
- 降低请求次数,多个接口合并,这里需要服务端配合
- 压缩传输内容,减少不必要数据传输
节流
- 压缩传输内容,减少不必要数据传输
- 站在用户角度上,在视频等大流量场景要判断是否为wifi网络,并提示用户
安全
- 使用https协议
- 数据加密,防止中间人窃听
- 加入签名,防止中间人篡改
- 加入https证书校验,防止抓包
6、其他instruments工具
Core Data:监测读取、缓存未命中、保存等操作,能直观显示是否保存次数远超实际需要
Cocoa Layout:观察约束变化,找出布局代码的问题所在。
Network:跟踪 TCP / IP 和 UDP / IP
连接
Automations:创建和编辑测试脚本来自动化 iOS 应用的用户界面测试
四、App启动优化
这部分涉及到很多底层原理,阅读起来比较困难,大家觉得无聊的可略过。
1、iOS应用启动流程
a、解析Info.plist
- 加载相关信息,例如启动故事板
- 沙箱建立、权限检查
b、Mach-O加载
先补充Mach-O的文件类型这个知识点
- Executable:应用的主要二进制可执行文件
- Dylib:动态链接库
-
Bundle:不能被链接,只能在运行时使用
dlopen
加载 -
Image:包含
Executable
、Dylib
和Bundle
-
Framework:包含
Dylib
、资源文件和头文件的文件夹
接着继续分析Mach-O的加载
- 如果是二进制文件,寻找适合当前
CPU
类别的部分 - 加载所有依赖的
Mach-O
文件(递归调用Mach-O
加载的方法) - 定位内部、外部指针引用,例如字符串、函数等
- 执行声明为
__attribute__((constructor))
的C
函数 - 加载类扩展
(Category)
中的方法 -
C++
静态对象加载、调用ObjC
的+load
函数
c、程序执行
- 调用
main()
- 调用
UIApplicationMain()
- 调用
applicationWillFinishLaunchin
2、动态链接库 dyld
a、什么是dyld
动态链接库的加载过程主要由dyld
来完成,dyld
是苹果的动态链接器。
系统先读取App
的可执行文件(Mach-O
文件),从里面获得dyld
的路径,然后加载dyld
。dyld
去初始化运行环境,开启缓存策略,加载程序相关依赖库(其中也包含我们的可执行文件),并对这些库进行链接,最后调用每个依赖库的初始化方法,在这一步,runtime
被初始化。dyld
此时会把App
类用到的所有动态库给加载起来,其中有个核心动态库libSystem
,每个App
都需要它,我们的Runtime
就在里面,那么当加载到此动态库时,Runtime
就会向dyld
注册几个回调函数:
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
当dyld
每次往内存中添加新的二进制文件(此时称为image
)之后,都会执行这些回调函数,比较重要的回调函数是map_images
和load_images
。map_images
方法里面就会往类的方法列表添加这个类的所有方法(方法是一个结构体,包含了方法名SEL
,还有方法实现IMP
),除此之外还有很多类的相关操作都在这里面,分类中的方法、协议、属性也是在这个时候添加到对应的类里去的。而load_images
方法里主要是调用了一个load
方法,所以我们可以发现OC
类中load
方法的调用时机比main
函数都早。
当所有依赖库的初始化后,轮到最后一位(程序可执行文件)进行初始化,在这时runtime
会对项目中所有类进行类结构初始化,然后调用所有的load
方法。
最后dyld
返回main
函数地址,main
函数被调用,我们便来到了熟悉的程序入口。
验证下load
、initialize
、Main
函数的加载顺序。
+ (void)load
{
printf("\n RootViewController load()");
}
+ (void)initialize
{
printf("\n RootViewController initialize()");
}
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
@autoreleasepool {
// Setup code that might create autoreleased objects goes here.
appDelegateClassName = NSStringFromClass([AppDelegate class]);
printf("\n main()");
}
return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
输出结果为:
RootViewController load()
main()
RootViewController initialize()
所以可以确定的是load
的确是在在main
函数调用之前调用的。
b、dyld共享库缓存
当你构建一个真正的程序时,将会链接各种各样的库。它们又会依赖其他一些framework
和动态库。需要加载的动态库会非常多。而对于相互依赖的符号就更多了。可能将会有上千个符号需要解析处理,这将花费很长的时间。
对于每一种架构,操作系统都有一个单独的文件,文件中包含了绝大多数的动态库,这些库都已经链接为一个文件,并且已经处理好了它们之间的符号关系。当加载一个 Mach-O
文件 (一个可执行文件或者一个库) 时,动态链接器首先会检查共享缓存看看是否存在其中,如果存在,那么就直接从共享缓存中拿出来使用。每一个进程都把这个共享缓存映射到了自己的地址空间中。这个方法大大优化了 OS X
和 iOS
上程序的启动时间。
c、dyld加载过程
dyld
所需要加载的是动态库列表一个递归依赖的集合。
3、pre-main 阶段耗时
a、作用
- 加载应用的可执行文件
- 加载动态链接库加载器
dyld(dynamic loader)
-
dyld
递归加载应用所有依赖的dylib
(dynamic library
动态链接库)
b、流程
冷启动 - 首次启动
即后台线程中未有当前打开的应用,所有的资源都需要加载并初始化。dyld -> runtime -> main
。
热启动 - 后台激活
即后台线程中保留有当前应用,应用的资源在内存中有保存。通过环境变量DYLD_PRINT_STATISTICS
监测main
函数启动之前动态库、静态库、类、协议、分类的启动时间。在 Xcode 中Edit scheme -> Run -> Auguments
将环境变量DYLD_PRINT_STATISTICS
设为1。
Total pre-main time: 22.79 milliseconds (100.0%)
dylib loading time: 40.67 milliseconds (178.4%)
rebase/binding time: 126687488.9 seconds (179105063.3%)
ObjC setup time: 7.54 milliseconds (33.0%)
initializer time: 26.82 milliseconds (117.6%)
slowest intializers :
libSystem.B.dylib : 2.64 milliseconds (11.6%)
libBacktraceRecording.dylib : 3.27 milliseconds (14.3%)
libobjc.A.dylib : 0.85 milliseconds (3.7%)
CoreFoundation : 0.77 milliseconds (3.4%)
libMainThreadChecker.dylib : 16.53 milliseconds (72.5%)
libLLVMContainer.dylib : 1.03 milliseconds (4.5%)
-
main()
函数之前总共使用了564.97ms
- 在
564.97ms
中,加载动态库用了109.23ms
,指针重定位使用了37.42ms
,ObjC类初始化使用了92.56ms
,各种初始化使用了325.67
ms(数据不来自于上方) - 在初始化耗费的
325.67
ms中,用时最多的几个初始化是libSystem.B.dylib
、libBacktraceRecording.dylib
、libglInterpose.dylib
以及libMTLInterpose.dylib
。
c、优化方案
Load dylibs:依赖的dylib越少越好
- 尽量不使用内嵌
(embedded)
的dylib
,加载内嵌dylib
性能开销较大 - 合并已有的
dylib
和使用静态库(static archives)
,减少dylib
的使用个数 - 懒加载
dylib
,但是要注意dlopen()
可能造成一些问题,且实际上懒加载做的工作会更多
linkmap 文件
这个文件可以让你了解整个APP编译后的情况,也许从中可以发现一些异常,还可以用这个文件计算静态链接库在项目里占的大小,有时候我们在项目里链了很多第三方库,导致APP体积变大很多,我们想确切知道每个库占用了多大空间,可以给我们优化提供方向。
LinkMap
里有了每个目标文件每个方法每个数据的占用大小数据,所以只要写个脚本,就可以统计出每个.o
最后的大小。属于一个.a
静态链接库的.o
加起来,就是这个库在APP里占用的空间大小。将各模块体积大小从大到小排列,然后就可以根据分析结果决定具体优化模块了。
Core1(xxxx1.o) 256.00M
Core2(xxxx2.o) 208.00M
Core3(xxxx3.o) 64.00M
Core4(xxxx4.o) 20.41M
...
Rebase/Bind
- 减少
ObjC
类(class
)、方法(selector
)、分类(category
)的数量 - 使用
Swift structs
(内部做了优化,符号数量更少)
在iOS代码中可能会为同一个类写很多分类方法,由于参与开发同学较多,可能会导致方法重复,但是实际上运行起来只能有一个分类的方法被调用,这取决于哪个分类后被加载,然而编译的二进制代码中,两个方法应该是都存在的,这不仅会增加app体积,也会增加启动时间,所以应该杜绝这样的重复问题。
有很多地方可能是名字不同,但是函数的功能相同,这个不容易被发现,需要大家在写代码的过程中注意;又或者两个函数名字比较接近,里面有很多相似的代码,这种情况下可以进行相同的代码的提取。
可以使用AppCode
对工程进行扫描,删除无用代码(未使用的参数、值,未被调用的静态变量、类和方法)。
Initializers
- 少在类的
+load
方法里做事情,尽量把这些事情推迟到+initiailize
, 因为load
是在启动的时候调用,而initialize
是在类首次被使用的时候调用 - 减少构造器函数个数,在构造器函数里少做些事情
- 减少
C++
静态全局变量的个数
4、main 阶段耗时
a、作用
main
方法执行之后到AppDelegate
类中的didFinishLaunchingWithOptions
方法执行结束前这段时间,主要是构建第一个界面,并完成渲染展示。
-
dyld
调用main()
- 调用
UIApplicationMain()
- 调用
applicationWillFinishLaunching
- 调用
didFinishLaunchingWithOptions
b、启动耗时的测量
测量main()
函数开始执行到didFinishLaunchingWithOptions
执行结束的耗时,就需要自己插入代码到工程中了。
❶ 先在main()
函数里用变量StartTime
记录当前时间
CFAbsoluteTime StartTime;
int main(int argc, char * argv[]) {
StartTime = CFAbsoluteTimeGetCurrent();
}
❷ 再在AppDelegate.m
文件中用extern
声明全局变量StartTime
extern CFAbsoluteTime StartTime;
❸ 最后在didFinishLaunchingWithOptions
里,再获取一下当前时间,与StartTime
的差值即是main()
阶段运行耗时
double launchTime = (CFAbsoluteTimeGetCurrent() - StartTime);
c、优化方案
这一阶段的优化主要是减少didFinishLaunchingWithOptions
方法里的工作,在didFinishLaunchingWithOptions
方法里,我们会创建应用的window
,指定其rootViewController
,调用window
的makeKeyAndVisible
方法让其可见。由于业务需要,设置系统UI风格,检查是否需要显示引导页、是否需要登录、是否有新版本等,这里的代码容易变得比较庞大,启动耗时难以控制。
- 梳理各个第三方库,找到可以延迟加载的库,做延迟加载处理,比如放到首页控制器的
viewDidAppear
方法里 - 梳理业务逻辑,把可以延迟执行的逻辑,做延迟执行处理。比如检查新版本、注册推送通知等逻辑
- 避免复杂/多余的计算
- 避免在首页控制器的
viewDidLoad
和viewWillAppear
做太多事情,这2个方法执行完,首页控制器才能显示,部分可以延迟创建的视图应做延迟创建/懒加载处理 - 采用性能更好的API
- 首页控制器用纯代码方式来构建
工程在didFinishLaunchingWithOptions
有将近30多个启动模块,其中耗时最多的前6个模块耗时占比将近86%,对这主要的6个模块进行逐个分析,比如字体加载模块、打点上报模块等采用懒加载的方式进行优化。
5、启动速度优化实践
a、pre-main阶段的优化
优化方案
- 排查无用的
dylib
,移除不再使用的libicucore.tbd
- 删除无用文件&库,合并功能类似的类和扩展(
Category
) - 移除不再使用的库
UMSocial
、PSTCollectionView
、MCSwipeTableViewCell
- 移除功能重复的库
Mantle
- 通过 LSUnusedResources 工具,扫描出项目中不再使用的图片资源,将其移除
- 梳理各个类的
+load
方法,将多个类中+load
方法做的事延迟到+initiailize
里去做。 - 移除不需要用到的类
检测各个文件的大小
瘦身应用包的大小,删除未使用的资源文件
移除不需要用到的类
使用了一个叫做 fui(Find Unused Imports)的开源项目,它能很好的分析出不再使用的类,准确率非常高,唯一的问题是它处理不了动态库和静态库里提供的类,也处理不了C++
的类模板。使用方法是在Terminal
中cd
到项目所在的目录,然后执行fui find
,然后等上那么几分钟(是的你没有看错,真的需要好几分钟甚至需要更长的时间),就可以得到一个列表了。由于这个工具还不是100%靠谱,可根据这个列表,在Xcode
中手动检查并删除不再用到的类。
UI卡顿,检测FPS
b、main()阶段的优化
去掉其中100ms
的dispatch_after...
检查代码发现之前会故意让启动图多显示100ms
,不知道是什么逻辑。
将多个三方库延迟加载。包括TBCrashReporter
、TBAccsSDK
、UT
、TRemoteDebugger
、ATSDK
等。
将若干系统UI配置、业务逻辑延迟执行。包括注册推送、检查新版本、更新Orange
配置等。
避免多余的计算。之前会前后两次获取是否要显示广告图,每次获取都需要反序列化Orange
中的配置信息,再比较配置中的开始/结束时间,大约耗时20ms。目前的解决方案是第一次计算后,用一个BOOL
属性缓存起来,下次直接取用。
延迟加载&懒加载部分视图。快捷密码验证页是启动图消失后用户看到的第一个页面,这个页面由于涉及到图片的解码、多个视图的创建&布局,viewDidLoad
阶段会耗时100ms
左右。目前的解决方案是把其中密码输入框视图延迟到viewDidAppear
里加载,对密码错误提示视图做成懒加载,耗时降低到30ms
左右。
通过instruments
的Time Profiler
分析,优化后启动速度有明显提升,didFinishLaunchingWithOptions
耗时在75ms
左右。其中目前耗时最多的是快捷密码验证页(PAPasscodeViewController
)的创建&布局,其次是DTLaunchViewControlle
里对是否要显示广告页的判断代码。可以看到PAPasscodeViewController
的viewDidAppear
耗时了78ms
,但已经没有太大关系,此时用户已经看到了页面,准备去验证指纹/密码了。
五、音视频的优化方案
资源文件是放置在应用程序本地与应用程序一起编译、 打包和发布的非程序代码文件,如应用中用到的声音、 视频、图片和文本文件,本地资源文件编译后,会放置于应用程序包文件中( 即<应用名>.app文件
)。
1、图片文件优化
苹果推荐使用PNG
格式,设定编译参数Compress PNG Files
。
在Finder
中查看该文件的属性,它是一个320 X 480px
、 大小为 317 KB
的PNG
图片,在编译之后的目录中找到lmageFile.app
包文件。打开包文件,查看目录中background.png
文件的属性,可以发现该文件是205 KB
的PNG
图片了。说明Xcode工具可以在编译时优化PNG
图片,但是即便经过优化和压缩的PNG
图片文件,也比JPEG
图片文件大得多。
如果是分布在网络云服务器中的资源文件,应用在加载这些 图片时,会从网络上下载到本地,这时候JPEG
就很有优势了。在本地资源的情况下,我们应该优先使用 PNG
格式文件,如果资源来源于网络,最好采用JPEG
格式文件。
+imageNamed:
方法会在内存中建立缓存,这些缓存直到应用停止才清除 。 如果是贯穿 整个应用的图片(如图标、 logo
等),推荐使用 +imageNamed:
创建,如果是仅使用 一 次的图片,推荐使用构造函数 - initWithContentsOfFile:
创建。
2、图片压缩
压缩图片质量
压缩图片质量的原因
- 节约存储空间:存储空间有限或者价格比较昂贵等原因导致没有足够的存储空间去储存图片
- 加快传输速度,提升用户体验:本身对于图片质量没有特别高的需求,压缩图片质量可以加快客户端与服务器的交互响应时间提升用户体验
- 某些特殊场景的需求:在一些场景下不得不进行图片压缩。例如使用极光进行微信分享时要求缩略图的大小不能超过32K
压缩图片的时机
- 在拍照时定义
AVCaptureSession
的sessionPreset
属性,可以控制拍摄图片的质量 - 在获取到图片对象以后,使用相关api进行压缩
压缩原理
- 在压缩过程主要通过
UIImageJPEGRepresentation
实现 - 由于
compressionQuality
和返回NSData
对象的大小并没有函数关系,所以为了更加精确接近我们需要的结果,我们采用二分法来靠近计算结果
压缩图片质量的算法实现
我们假定将图片压缩到小于等于maxSize
,大于0.9 * maxSize
大小即符合要求,我们做测试,设定最终要求的图片大小maxSize=30KB
。
- (UIImage *)imageToMaxSize:(CGFloat)maxSize {
UIImage *image = (UIImage *)self;
NSData *data = UIImageJPEGRepresentation(image, 1.0);
CGFloat sizeOfOccupy = data.length / 1024.0;
NSLog(@"Space size == before:%@", @(sizeOfOccupy));
if (sizeOfOccupy > maxSize) {
CGFloat max = 1.0f, min = 0.0f;
CGFloat compression = (max + min) / 2.0;
while (true) {
data = UIImageJPEGRepresentation(image, compression);
sizeOfOccupy = data.length / 1024.0;
NSLog(@"Space size == after:%@,scale: %f", @(sizeOfOccupy), compression);
if (sizeOfOccupy > maxSize) {
max = sizeOfOccupy;
} else if (sizeOfOccupy < 0.9 * maxSize) {
min = compression;
} else {
break;
}
compression = (max + min) / 2.0;
}
}
return [UIImage imageWithData:data];
}
运行结果如下:
我们发现这个api在图片质量被压缩到一定程度之后,随着compressionQuality
参数变小,图片的质量不再发生变化,需要我们添加退出循环压缩的第一个终止条件:当压缩之后如果NSData
的大小不再发生变化时, 终止循环。
while (true) {
NSData *_data = UIImageJPEGRepresentation(image, compression);
if ([_data isEqualToData:data]) {
break;
}
data = _data;
sizeOfOccupy = data.length / 1024.0;
NSLog(@"Space size == after:%@,scale: %f", @(sizeOfOccupy), compression);
if (sizeOfOccupy > maxSize) {
max = sizeOfOccupy;
} else if (sizeOfOccupy < 0.9 * maxSize) {
min = compression;
} else {
break;
}
compression = (max + min) / 2.0;
}
加上第一个终止条件之后,当返回的NSData
对象与上次相同时,及时终止了循环,防止出现死循环的情况。
现在我们来审视一下,这个代码是不是已经可以应对所有的情况了呢?我们的算法使用的二分法来逐渐逼近我们想要的目标值,所以在某些情况下就会出现为了逼近某个对我们来说不那么重要的最终值(或者是一个无法达到的极限值)而进行许多次循环情况,比如为了两次运算相差了0.00000000001的精确度,却还在为了接近目标值而进行循环。这种情况对于像素要求不高的操作来说没有太大实际意义,但是却浪费了很多内存。如果某个目标值需要的循环次数比较多,就有可能造成内存被耗尽,所以我们使用手动添加一个自动释放池来平稳释放掉局部变量占用内存。修改之后的代码大概这个样子,就基本上可以满足我们的需求。
while (true) {
@autoreleasepool {
NSData *_data = UIImageJPEGRepresentation(image, compression);
NSInteger distance = _data.length - data.length;
if ([_data isEqualToData:data] || labs(distance) < 0.5 * 1024) {
break;
}
data = _data;
sizeOfOccupy = data.length / 1024.0;
NSLog(@"Space size == before:%@", @(sizeOfOccupy));
if (sizeOfOccupy > maxSize) {
max = sizeOfOccupy;
} else if (sizeOfOccupy < 0.9 * maxSize) {
min = compression;
} else {
break;
}
compression = (max + min) / 2.0;
}
}
压缩图片的尺寸
压缩图片尺寸的原因
- 为了更好的使用自适应进行UI适配:有时候为了使用控件的固有属性来做图片展示适应,就需要控制图片本身的尺寸。
- 使用统一的尺寸标准:移动端设备多种多样,为了各个移动端传回的图片标注一致,需要对图片的尺寸做统一处理
- 加快传输速度,提升用户体验:业务需求对图片本身的尺寸要求不高的情况下,可以适当控制图片尺寸,来减小图片的大小,加快上传/下载速度,提升用户体验
- 某些特殊场景的需求:在一些场景下不得不进行图片压缩,例如使用极光进行微信分享时要求缩略图的大小不能超过32K等
压缩图片尺寸的时机
- 在移动端拍摄图片时定义,定义相机
AVCaptureVideoPreviewLayer
预览图层的大小来限制图片的尺寸 - 在获取到图片对象之后,使用
UIGraphics
框架中的相关方法进行重绘。下面的讨论主要针对这一类型
图片尺寸压缩原理
主要是用UIGraphics
中的两个C函数来实现:
-
UIGraphicsBeginImageContext
(或者UIGraphicsBeginImageContextWithOptions
)初始化绘图上下文 - 使用
UIImage
中的drawInRect:
在指定的区域内重新绘制图片 - 使用
UIGraphicsGetImageFromCurrentImageContext
获取重绘之后的新图片 - 使用
UIGraphicsEndImageContext
关闭上下文
在很多使用场景下,单一的压缩方式有时候不能保证达到需求,比如单一使用压缩图片质量达到指定物理大小之后发现根本无法识别图片内容等状况,这时候就需要根据实际的情况交替使用两种压缩方达到目的,比如先使用质量压缩到能够识别的尺寸,再逐渐使用压缩尺寸尝试做进一步压缩,具体需求需要具体分析。
算法实现
- (UIImage *)imageToMaxSize:(CGFloat)maxSize {
UIImage *image = (UIImage *)self;
NSData *data = UIImageJPEGRepresentation(image, 1.0);
CGFloat sizeOfOccupy = data.length / 1024.0;
if (sizeOfOccupy > maxSize) {
CGFloat max = 1.0f, min = 0.0f;
CGFloat compression = (max + min) / 2.0;
CGSize orinalSize = image.size;
CGSize size = (CGSize){orinalSize.width * compression, orinalSize.height * compression};
while (true) {
@autoreleasepool {
UIGraphicsBeginImageContext(size);
[image drawInRect:(CGRect){.origin = CGPointZero, .size = size}];
image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
NSData *_data = UIImageJPEGRepresentation(image, 1.0);
NSInteger distance = _data.length - data.length;
if (labs(distance) < 0.5 * 1024) {
break;
}
data = _data;
sizeOfOccupy = data.length / 1024.0;
NSLog(@"Space size == before:%@", @(sizeOfOccupy));
if (sizeOfOccupy > maxSize) {
max = sizeOfOccupy;
} else if (sizeOfOccupy < 0.9 * maxSize) {
min = compression;
} else {
break;
}
compression = (max + min) / 2.0;
size = (CGSize){orinalSize.width * compression, orinalSize.height * compression};
}
}
}
return [UIImage imageWithData:data];
}
Demo
Demo在我的Github上,欢迎下载。
BasicsDemo