原创:知识点总结性文章
创作不易,请珍惜,之后会持续更新,不断完善
个人比较喜欢做笔记和写总结,毕竟好记性不如烂笔头哈哈,这些文章记录了我的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.67ms(数据不来自于上方) - 在初始化耗费的
325.67ms中,用时最多的几个初始化是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