美文网首页开发
IOS基础:性能优化

IOS基础:性能优化

作者: 时光啊混蛋_97boy | 来源:发表于2020-10-22 14:42 被阅读0次

    原创:知识点总结性文章
    创作不易,请珍惜,之后会持续更新,不断完善
    个人比较喜欢做笔记和写总结,毕竟好记性不如烂笔头哈哈,这些文章记录了我的IOS成长历程,希望能与大家一起进步
    温馨提示:由于简书不支持目录跳转,大家可通过command + F 输入目录标题后迅速寻找到你所需要的内容

    目录

    • 一、常见优化方案
      • 初级
      • 中级
      • 高级
    • 二、屏幕显示图像的原理及优化方案
      • 1、屏幕显示图像的原理
      • 2、卡顿产生的原因
      • 3、CPU 资源消耗原因和解决方案
      • 4、GPU 资源消耗原因和解决方案
    • 三、检测优化效果
      • 1、如何评测界面的流畅度
      • 2、如何检测内存是否泄漏及使用 / 分配情况
      • 3、如何检测分析代码的执行时间
      • 4、如何进行APP耗电量检测
      • 5、如何进行流量检测
      • 6、其他instruments工具
    • 四、App启动优化
      • 1、iOS应用启动流程
      • 2、App总启动时间 = pre-main耗时 + main耗时
      • 3、阿里数据iOS端启动速度优化实践
    • 五、音视频的优化方案
      • 1、图片文件优化
      • 2、音频文件优化
    • Demo
    • 参考文献

    一、常见优化方案

    1、初级

    • ARC管理内存

    • 可以把项目中大的资源文件或者包放在服务器上,等APP下载以后再下载下来。举个例子,字体库拿出来放在公司服务器上,APP拿到下载链接,让用户下载完成APP以后再在手机里面下载字体库

    • 把图片放在Xcode自带的图片管理工具里面Images.asssets,这样的好处就是打包的资源包中的图片有被系统压缩

    • 避免庞大的xibstoryBoard,尽量使用纯代码开发

    • 不要频繁的刷新页面,能刷新1行cell最好只刷新一行,尽量不要使用reloadData

    • UITableViewCellsUICollectionViewCellsUITableViewHeaderFooterViews,地图视图 ( MKPinAnnotationView )设置reuseIdentifier,可以模仿UITableviewUICollectionView实现自定义重用机制

    • 如果opaque设为YES,渲染系统就认为这个view是完全不透明的,这使得渲染系统优化一些渲染过程和提高性能

    • 不要阻塞主线程

    • Image Views中调整图片大小,如果要在UIImageView中显示一个来自bundle的图片,你应保证图片的大小和UIImageView的大小相同。在运行中缩放图片是很耗费资源的,特别是UIImageView嵌套在UIScrollView中的情况下。如果图片是从远端服务加载的你不能控制图片大小,你可以在下载完成后,最好是用backgroundthread,缩放一次,然后在UIImageView中使用缩放后的图片

    • ArraysDictionariesSets选择适合的数据结构,NSArray使用index来查找很快(插入和删除很慢),NSDictionary使用键来查找很快,NSSet是无序的,用键查找很快,插入/删除也很快。

    • 打开gzip压缩, NSURLConnection中默认支持了gzip压缩,当然AFNetworking这些基于它的框架亦然

    • 少用运算获得圆角,不论view.maskToBounds还是layer.clipToBounds都会有很大的资源开销,必须要用圆角的话,不如把图片本身做成圆角或者可通过Core Graphics画出圆角矩形(UIBezierPath画圆)

    • 在版本迭代过程中,如果业务发生变化,导致相应的代码也发生变化,一般情况下我们需要把对应的旧代码和旧资源删除掉(旧资源会增加App体积,旧代码会增加执行文件的大小,进而增加Objc类数量或者selector数量造成启动APP缓慢)

    • 尽可能地复用UI,在添加某个功能时,先去查查我们的代码中是否已经实现了该功能,减少重复。

    2、中级

    • 延迟加载,不要一次性创建所有的subview,而是需要时才创建,消耗更少内存,但是会稍显卡顿。懒加载适用于一些可能不会加载的页面,比如弹框、空数据页面之类的,使用得当可以避免内存暴涨,使用不好,比如在必定会弹出的页面中使用懒加载可能会在增加页面响应时间,所以使用懒加载一定要注意使用场景,避免产生副作用

    • 缓存所需要的,也就是那些不大可能改变但是需要经常读取的东西,远端服务器的响应,图片,甚至计算结果,比如UITableView的行高

    • 用事先渲染好的图片更快一些,因为如此一来就免去了创建一个图片再画东西上去然后显示在屏幕上

    • 处理内存警告,一个有图片缓存的app可以移除不在屏幕上显示的图片,即可以被重现创建的object来释放内存

    • 重用大开销对象,一些objects的初始化很慢,比如NSDateFormatterNSCalendar,然而,你又不可避免地需要使用它们。性能测试表明,NSDateormatter的性能瓶颈是由于NSData格式到NSString格式的转化,所以把NSDateFormatter创建单例意义不大,推荐的做法是:把最常用到的日期格式做缓存。

    // in your .h or inside a class extension
    @property (nonatomic, strong) NSDateFormatter *formatter;
    
    // inside the implementation (.m)
    // When you need, just use self.formatter
    - (NSDateFormatter *)formatter {
        if(! _formatter) {
            _formatter = [[NSDateFormatter alloc] init];
            _formatter.dateFormat = @"EEE MMM dd HH:mm:ss Z yyyy";// twitter date format
        }
        return_formatter;
    }
    
    • 避免反复处理数据,在服务器端和客户端使用相同的数据结构很重要

    • 解析JSON会比XML更快一些,JSON也通常更小更便于传输,而XML的好处,比如使用SAX来解析XML就像解析本地文件一样,你不需像解析json一样等到整个文档下载完成才开始解析。当你处理很大的数据的时候就会极大地减低内存消耗和增加性能

    • 如果你使用全画幅的背景图使用UIImageView,如果你用小图平铺来创建背景,你就需要用UIColorcolorWithPatternImage来做了,它会更快地渲染也不会花费很多内存

    self.view.backgroundColor = [UIColor colorWithPatternImage:[UIImage imageNamed:@"background"]];
    
    • 选择正确的数据存储选项,NSUerDefaultsplistNSCodingSQLite数据库、 Core Data

    • Timer的时间间隔不宜太短,满足需求即可

    3、高级

    • 加速启动时间

    • 优化算法,减少循环次数

    • 线程适量,不宜过多,不要阻塞主线程

    • 定位和蓝牙按需取用,定位之后要关闭或降低定位频率

    • 自动释放池因为有系统帮我们监管平时不需要管它,但是如果我们一个页面创建了太多的类或者对象,如果等页面销毁的时候由系统统一释放难免会出现一个峰值影响整体性能,这时候我们就可以考虑使用autoreleasepool了,避免峰值的出现!假如你创建很多临时对象,你会发现内存一直在减少,直到这些对象被release的时候,所以需要手动在@autoreleasepool里创建临时的对象来避免这个行为,这段代码在每次遍历后释放所有autorelease对象:

    NSArray *urls = <# An array of file URLs #>;
    for(NSURL *url in urls) {
        @autoreleasepool {
            NSError *error;
            NSString *fileContents = [NSString stringWithContentsOfURL:url
                                            encoding:NSUTF8StringEncoding error:&error];
            /* Process the string, creating and autoreleasing more objects. */
        }
    }
    
    • 如果你要加载一个大图片而且是一次性使用,那么就没必要缓存这个图片,用imageWithContentsOfFile足矣,这样不会浪费内存来缓存它。然而,在图片反复重用的情况下imageNamed是一个好得多的选择

    • 使用 SQLite 数据库,需要注意:1、尽最不要使用 LIKE模糊匹配查询, 使用=查询 , 把非文本的条件放在前面。2、索引就像是书中的目录,插入和删除数据必然造成索引重排 ,所以创建索引要慎重。3、建立索引、限制返回记录数和 where条件子句等可以提高查找性能。

    • 使用文件,需要注意:1、文件访问优化: 避免多次写入很少的数据, 最好是当数据积攒到 一定数量时一次写入。 2、频 繁的 IO操作会影响性能, 所以 最好将文件读写访间从主线程中剥离出来,由一个子线程负责。3、文件的写入应该采用增量方式,每次只写入变化的部分。

    • 使用plist,需要注意:1、plist文件就是很好的结构化文件,其结构是层次模型的树 形结构,层 次的深浅会影响读取/写入的速度(减少),调整文件结构可以减少文件大小。2、通过序列化.plist文件来减少文件大小(NSPropertyListSerialization)。


    二、屏幕显示图像的原理及优化方案

    cpu的作用

    • 对象管理(销毁与创建);
    • 对象的维护(属性调整、布局计算、文本计算和排版、图片格式转换和解码)
    • 图像的绘制(CG)

    gpu的作用

    • 纹理渲染
    • 视图合成

    核心:减少cpu、gpu的资源消耗

    1、屏幕显示图像的原理

    渲染方式比较
    a、GPU渲染流程

    GPU(Graphics Processing Unit):又名图形处理器,是显卡的 “核心”。主要负责图像运算工作,具有高并行能力,通过计算将图像显示在屏幕像素中。
    工作原理:将 “3D坐标” 转换成 “2D坐标” ,再将 “2D坐标” 转换为 “实际有颜色的像素” 。

    GPU渲染流程

    工作流水线:顶点着色器 => 形状装配 => 几何着色器 => 光栅化 => 片段着色器 => 测试与混合
    顶点着色器(Vertex Shader):确定形状的点
    形状装配(Shape Assembly):确定形状的线
    几何着色器(Geometry Shader):确定三角形的个数,使之变成几何图形
    光栅化(Rasterization):将图转化为一个个实际屏幕像素
    片段着色器(Fragment Shader):对屏幕像素点着色
    测试与混合(Tests and Blending):检查图层深度和透明度,并进行图层混合

    b、IOS原生渲染
    ❶ 常使用的iOS渲染框架

    UIKit:日常开发最常用的UI框架,可以通过设置UIKit组件的布局以及相关属性来绘制界面。其实本身UIView并不拥有屏幕成像的能力,而是View上的CALayer属性拥有展示能力。(UIView继承自UIResponder,其主要负责用户操作的事件响应,iOS事件响应传递就是经过视图树遍历实现的。)
    SwiftUI:苹果新推出的一款全新的“声明式UI”框架,使用Swift编写。一套代码,即可完成iOSiPadOSmacOSwatchOS的开发与适配。
    Core Animation:核心动画,一个复合引擎。尽可能快速的组合屏幕上不同的可视内容。分解成独立的图层(CALayer),存储在图层树中。
    Core Graphics:基于Quartz高级绘图引擎,主要用于运行时绘制图像。
    Core Image:运行前图像绘制,对已存在的图像进行高效处理。
    OpenGL ES:OpenGL for Embedded Systems,是 OpenGL的子集。可通过C/C++编程操控GPU
    Metal:渲染性能比OpenGL ES高。为了解决OpenGL ES不能充分发挥苹果芯片优势的问题。

    ❷ 原生渲染的流程
    iOS原生渲染的整体流程

    第一步:更新视图树、图层树。(分别对应View的层级结构、View上的Layer层级结构)

    第二步CPU开始计算下一帧要显示的内容(包括视图创建、布局计算、视图绘制、图像解码)。当 runloopkCFRunLoopBeforeWaitingkCFRunLoopExit 状态时,会通知注册的监听,然后对图层打包,打完包后,将打包数据发送给一个独立负责渲染的进程 Render Server。 前面 CPU 所处理的这些事情统称为 Commit Transaction

    第三步:数据到达Render Server后会被反序列化,得到图层树,按照图层树的图层顺序、RGBA 值、图层frame来过滤图层中被遮挡的部分,过滤后将图层树转成渲染树,渲染树的信息会转给 OpenGL ES/Metal

    原生渲染的流程

    第四步Render Server 会调用 GPUGPU 开始进行前面提到的顶点着色器、形状装配、几何着色器、光栅化、片段着色器、测试与混合六个阶段。完成这六个阶段的工作后,就会将 CPUGPU 计算后的数据显示在屏幕的每个像素点上。

    c、大前端渲染(WebView、类React Native)
    ❶ WebView

    对于WebView渲染,其主要工作在WebKit中完成。WebKit本身的渲染基于macOSLay Rendering架构,iOS本身渲染也是基于这套架构。因此,本身从渲染的实现方式来说,性能应该和原生差别不大。但为什么我们能明显感觉到使用WebView渲染要比原生渲染的慢呢?

    第一,首次加载。会额外多出网络请求和脚本解析工作。 即使是本地网页加载,WebView也要比原生多出脚本解析的工作。 WebView要额外解析HTML+CSS+JavaScript代码。

    第二,语言解释执行性能来看。JavaScript的语言解析执行性能要比原生弱。 特别是遇到复杂的逻辑与大量的计算时,WebView 的解释执行性能要比原生慢不少。

    第三WebView的渲染进程是独立的,每一帧的更新都要通过IPC调用GPU进程,会造成频繁的IPC进程通信,从而造成性能消耗。并且,两个进程无法共享纹理资源,GPU无法直接使用context光栅化,而必须要等待WebView通过IPCcontext传给GPU再光栅化。因此GPU自身的性能发挥也会受影响。

    ❷ 类React Native(使用JavaScriptCore引擎做为虚拟机方案)

    代表:React NativeWeex、小程序等。以 ReactNative 举例:

    React Native的渲染层直接走的是iOS原生渲染,只不过是多了Json+JavaScript脚本解析工作。

    JavaScriptCoreiOS 原生与 JS 之间的桥梁,其原本是 WebKit 中解释执行 JavaScript 代码的引擎。

    通过JavaScriptCore引擎将“JS”与“原生控件”产生相对应的关联。进而,达成通过JS来操控iOS原生控件的目标。(简单来说,这个json就是一个脚本语言到本地语言的映射表,KEY是脚本语言认识的符号,VALUE是本地语言认识的符号。)

    但与WebView 一样,RN也需要面临JS语言解释性能的问题。因此,从渲染效率角度来说,WebView < 类ReactNative < 原生。 (因为json的复杂度比html+css低)

    d、Flutter渲染
    ❶ Flutter的架构
    Flutter的架构

    可以看到,Flutter重写了UI框架,从UI控件到渲染全部自己重新实现了,不依赖 iOSAndroid 平台的原生控件,依赖Engine(C++)层的Skia图形库与系统图形绘制相关接口,因此,在不同的平台上有了相同的体验。

    ❷ Flutter的渲染流程
    Flutter的渲染流程

    简单来说,Flutter的界面由Widget组成,所有Widget会组成Widget Tree。界面更新时,会更新Widget Tree,再更新Element Tree,最后更新RenderObjectTree

    Flutter渲染在 Framework层会有 BuildWidget TreeElement TreeRenderObject TreeLayoutPaintComposited Layer 等几个阶段。

    FlutterC++ 层,使用 Skia 库,将 Layer 进行组合,生成纹理,使用 OpenGL的接口向GPU 提交渲染内容进行光栅化与合成。

    提交到 GPU 进程后,合成计算,显示屏幕的过程和iOS 原生渲染基本是类似的,因此性能上是差不多的。

    2、卡顿产生的原因

    双缓冲机制GPU 会预先渲染好一帧放入一个缓冲区内,让视频控制器读取,当下一帧渲染好后,GPU会直接把视频控制器的指针指向第二个缓冲器。如此一来效率会有很大的提升。

    GPU 通常有一个机制叫做垂直同步(简写也是 V-Sync):当开启垂直同步后,GPU会等待显示器的 VSync信号发出后,才进行新的一帧渲染和缓冲区更新。这样能解决画面撕裂现象,也增加了画面流畅度,但需要消费更多的计算资源,也会带来部分延迟。

    卡顿产生的原因

    VSync信号到来后,系统图形服务会通过 CADisplayLink 等机制通知AppApp 主线程开始在CPU中计算显示内容,比如视图的创建、布局计算、图片解码、文本绘制等。随后 CPU会将计算好的内容提交到GPU 去,由GPU 进行变换、合成、渲染。随后GPU会把渲染结果提交到帧缓冲区去,等待下一次 VSync 信号到来时显示到屏幕上。由于垂直同步的机制,如果在一个 VSync 时间内,CPU 或者 GPU 没有完成内容提交,则那一帧就会被丢弃,等待下一次机会再显示,而这时显示屏会保留之前的内容不变。这就是界面卡顿的原因。

    从上面的图中可以看到,CPUGPU 不论哪个阻碍了显示流程,都会造成掉帧现象。所以开发时,也需要分别对 CPUGPU 压力进行评估和优化。

    3、CPU 资源消耗原因和解决方案

    a、对象创建
    • 对象的创建会分配内存、调整属性、甚至还有读取文件等操作,比较消耗 CPU资源。尽量用轻量的对象代替重量的对象,可以对性能有所优化。比如 CALayer 比 UIView要轻量许多,那么不需要响应触摸事件的控件,用 CALayer显示会更加合适。

    - 如果对象不涉及 UI 操作,则尽量放到后台线程去创建,但可惜的是包含有CALayer 的控件,都只能在主线程创建和操作。

    - 通过 Storyboard创建视图对象时,其资源消耗会比直接通过代码创建对象要大非常多

    b、对象调整
    • CALayer 内部并没有属性,当调用属性方法时,它内部是通过运行时 resolveInstanceMethod 为对象临时添加一个方法,并把对应属性值保存到内部的一个 Dictionary 里,同时还会通知 delegate、创建动画等等,非常消耗资源。UIView 的关于显示相关的属性(比如frame/bounds/transform)等实际上都是 CALayer属性映射来的,所以对 UIView 的这些属性进行调整时,消耗的资源要远大于一般的属性。对此你在应用中,应该尽量减少不必要的属性修改。

    - 当视图层次调整时,UIViewCALayer 之间会出现很多方法调用与通知,所以在优化性能时,应该尽量避免调整视图层次、添加和移除视图。

    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、文本渲染

    屏幕上能看到的所有文本内容控件,包括 UIWebView,在底层都是通过 CoreText 排版、绘制为 Bitmap 显示的。常见的文本控件 (UILabelUITextView 等),其排版和绘制都是在主线程进行的,当显示大量文本时,CPU 的压力会非常大。

    对此解决方案只有一个,那就是自定义文本控件,用 TextKit 或最底层的 CoreText 对文本异步绘制。尽管这实现起来非常麻烦,但其带来的优势也非常大,CoreText 对象创建好后,能直接获取文本的宽高等信息,避免了多次计算(调整 UILabel 大小时算一遍、UILabel 绘制时内部再算一遍);CoreText 对象占用内存较少,可以缓存下来以备稍后多次渲染。

    h、图片的解码

    pngjpeg这种都是压缩格式,解码就是解压缩的过程,图片解码需要大量计算,耗时长。当你用 UIImageCGImageSource 的那几个方法创建图片时,图片数据并不会立刻解码。图片设置到 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、图形的生成

    CALayerborder、圆角、阴影、遮罩(mask),CASharpLayer 的矢量图形显示,通常会触发离屏渲染(offscreen rendering),而离屏渲染通常发生在 GPU 中。当一个列表视图中出现大量圆角的 CALayer,并且快速滑动时,可以观察到 GPU 资源已经占满,而 CPU 资源消耗很少。这时界面仍然能正常滑动,但平均帧数会降到很低。对于只需要圆角的某些场合,也可以用一张已经绘制好的圆角图片覆盖到原本视图上面来模拟相同的视觉效果。最彻底的解决办法,就是把需要显示的图形在后台线程绘制为图片,避免使用圆角、阴影、遮罩等属性。

    触发离屏渲染需要3个条件

    触发离屏渲染需要3个条件:
    1、contents :设置图片即意味着添加了内容contents
    2、背景色 或 border:为什么说是或而不是和,因为他们是2个图层,超过一个图层的渲染就会触发离屏渲染。

    ❶ 设置圆角触发离屏渲染的情况

    情况一:添加内容和设置背景色。

    - (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个属性呢?既然是搭配使用,又是万年重复的代码,一个属性不好吗?

    imageView.layer.cornerRadius = 5;//设置圆角
    imageView.layer.masksToBounds = YES;//裁减
    

    因为设置layer.cornerRadius只会设置border的圆角,不会设置content的圆角,除非同时设置了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颜色
    

    运行效果如下:

    添加内容和设置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];
    

    运行效果如下:

    子视图
    ❷设置圆角不会触发离屏渲染的情况

    情况一:不添加内容只设置背景色。

    imageView.backgroundColor = [UIColor redColor];
    // imageView.image = [UIImage imageNamed:@"海贼王"];
    

    设置了背景颜色,仅有一个图层,既然视图只有一个图层,还需要裁减吗,答案是不需要,即layer.masksToBounds = YES;裁剪语句无影响。

    不添加内容只设置背景色

    情况二:设置了图片,不设置背景色和border

    // imageView.backgroundColor = [UIColor redColor];
    imageView.image = [UIImage imageNamed:@"海贼王"];
    

    运行效果如下:

    设置了图片,不设置背景色和border

    情况三:没有设置图片,但设置了背景色和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颜色
    

    效果如下:

    没有设置图片,但设置了背景色和border
    ❸ 绘制圆角时出现离屏渲染的解决方案

    当前屏幕渲染实现圆角。好处是直接在当前屏幕渲染绘制可以提高性能。实现方式是为UIImage类扩展一个实例方法:

    #import "UIImage+CornerRadius.h"
    
    @implementation UIImage (CornerRadius)
    
    //当前屏幕渲染, 扩展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

    InstumentsGPU Driver 预设,能够实时查看到 CPUGPU 的资源消耗。在这个预设内,你能查看到几乎所有与显示有关的数据,比如 Texture 数量、CA 提交的频率、GPU消耗等,在定位界面卡顿的问题时,这是最好的工具。

    c、CADisplayLink

    CADisplayLink 监控,结合子线程和信号量,两次事件触发时间间隔超过一个VSync的时长,就上报调用栈。

    d、runloop中添加监听

    runloop中添加监听,如果kCFRunLoopBeforeSourceskCFRunLoopBeforeWaiting中间的耗时超过VSync的时间,那么就是卡帧了,然后这个时候拿到线程调用栈,看看哪个部分耗时长即可。

    2、如何检测内存是否泄漏及使用 / 分配情况

    Allocations:监测内存使用 / 分配情况,需要注意到,Allocations是检测程序运行过程中的内存分配情况的,也需要同时运行着程序。
    Leaks—动态内存泄露检测:需要一边运行程序,一边检测。一般用静态分析检查过的代码,内存泄露都比较少。
    Analyze—静态分析工具:静态分析不需要运行程序,就能检查到存在内存泄露的地方。

    常见的三种泄露情形

    • 创建了一个对象,但是并没有使用。
    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工具

    第一步:进入手机"设置"->"电池",可以直观的看出来手机应用的耗电情况。

    手机"设置"->"电池"

    第二步:使用Xcode打开你的工程,然后插上手机,使用真机running,点击Energy Impact

    Energy Impact

    Energy Impact工具里的参数解释:

    • 蓝色表示--合理
    • 黄色--表示程序比较耗电
    • 红色--表示仅仅轻度
    • 图表中Utilization栏中是表示瞬间耗电情况
    • 图表中Average栏中,表示平均耗电情况
    • 图表中Energy Impactcoat(蓝色)表示运行项目代码需要电量,overhead(红色)表示开销,包括CPU的唤起,蓝牙&WiFi,和其他系统资源的调用等,灰色表示有电量消耗,白色表示没有电量消耗
    d、使用Instrument的Energy Log工具

    第一步:打开手机设置,点击“开发者”。

    打开手机设置,点击“开发者”

    第二步:点击Logging

    点击Logging

    第三步:勾选Energy,并点击startRecording

    勾选Energy,并点击startRecording

    第四步:运行需要测试的APP(确保手机消耗的是手机自身的电池),运行3-5分钟,在进入手机设置点击stopRecording

    在进入手机设置点击stopRecording

    第五步:使用Xcode,把手机和Xcode相连,并打开instruments中的Energy Log,点击工具栏中import Logged Data from Device

    点击工具栏中import Logged Data from Device

    第六步:得到了电池损耗日志,对于Energy Usage Level的值(0--20),值越大表示越耗电,而CPU Activity表示CPU各种活动。

    电池损耗日志

    5、如何进行流量检测

    本地统计流量,主要有两种实现方案,均存在局限性,需要结合使用。
    方案一:Hook
    针对URLConnectionCFNetworkNSURLSession三种网络做HookHook的具体技术可以是method swizzle,也可以是Proxy

    具体来说就是将流量监测代码插入系统方法实现,method swizzling可以达到这个效果。例如,对于网络请求开始,我们通过监测-[NSURLConnection start]方法就可以统计这一次请求的数据大小。

    通过这种方式,可以监控指定类的指定方法,我们可以取得方法调用的时机, 但是程序中除了方法调用还存在方法回调,这是不适合用这种方式监控的情况。

    例如NSURLConnection的构造方法和start方法可以通过Method Swizzling监控到, 但是回调消息的接收者delegate的类名不固定,可能是任意一个页面实例, 如果还要使用Method Swizzling的方法来监控,会面对未知个数的页面的delegate方法,不是一个好办法。

    解决方法是构造一个回调消息的转发者作为代理,在转发者中收集数据,再转发给用户。

    方案二:NSURLProtocol
    可以使用 NSURLProtocol对网络请求的拦截,进而得到流量、响应时间等信息,但是NSURLProtocol有自己的局限,比如NSURLProtocol只能拦截NSURLSessionNSURLConnection以及UIWebView,但是对于CFNetwork则无能为力。

    NSURLProtocol拦截是监控UIWebView请求最普遍的解决方案。具体可以参考美团技术团队的实现。

    优化方案

    网络优化分为提速、节流、安全,选择合理网络协议处理专门业务(比如聊天的APP需要用socket)。

    提速

    • 增加缓存,比如图片缓存,H5缓存,列表页数据放入数据库缓存
    • 降低请求次数,多个借口合并,这里需要服务端配合
    • 压缩传输内容,减少不必要数据传输

    节流

    • 压缩思路同上
    • 在用户角度上,在视频等大流量场景要判断是否为wifi网络,并提示用户

    安全

    • 使用https协议
    • 数据加密,防止中间人窃听
    • 加入签名,防止中间人篡改
    • 加入https证书校验,防止抓包

    6、其他instruments工具

    Core Data:监测读取、缓存未命中、保存等操作,能直观显示是否保存次数远超实际需要。
    Cocoa Layout:观察约束变化,找出布局代码的问题所在。
    Network:跟踪 TCP / IP 和 UDP / IP 连接。
    Automations:创建和编辑测试脚本来自动化 iOS 应用的用户界面测试。


    四、App启动优化

    这部分涉及到很多底层原理,阅读起来比较困难,大家觉得无聊的可略过。

    1、iOS应用启动流程

    1)解析Info.plist

    • 加载相关信息,例如启动故事板
    • 沙箱建立、权限检查

    2)Mach-O加载
    先补充Mach-O的文件类型这个知识点:

    • Executable:应用的主要二进制可执行文件
    • Dylib:动态链接库
    • Bundle:不能被链接,只能在运行时使用dlopen加载
    • Image:包含ExecutableDylibBundle
    • Framework:包含Dylib、资源文件和头文件的文件夹

    接着继续分析Mach-O的加载:

    • 如果是二进制文件,寻找适合当前CPU类别的部分
    • 加载所有依赖的Mach-O文件(递归调用Mach-O加载的方法)
    • 定位内部、外部指针引用,例如字符串、函数等
    • 执行声明为__attribute__((constructor))C函数
    • 加载类扩展(Category)中的方法
    • C++静态对象加载、调用ObjC+load函数

    3)程序执行

    • 调用main()
    • 调用UIApplicationMain()
    • 调用applicationWillFinishLaunchin

    2、App总启动时间 = pre-main耗时 + main耗时

    a、pre-main阶段
    • 加载应用的可执行文件
    • 加载动态链接库加载器dyld(dynamic loader)
    • dyld递归加载应用所有依赖的dylibdynamic library 动态链接库)
    pre-main阶段
    b、这里对dyld进行介绍

    ❶ 什么是dyld?
    动态链接库的加载过程主要由dyld来完成,dyld是苹果的动态链接器

    系统先读取App的可执行文件(Mach-O文件),从里面获得dyld的路径,然后加载dylddyld去初始化运行环境,开启缓存策略,加载程序相关依赖库(其中也包含我们的可执行文件),并对这些库进行链接,最后调用每个依赖库的初始化方法,在这一步,runtime被初始化。dyld此时会把App类用到的所有动态库给加载起来,其中有个核心动态库libSystem,每个App都需要它,我们的Runtime就在里面,那么当加载到此动态库时,Runtime就会向dyld注册几个回调函数:

    _dyld_objc_notify_register(&map_images, load_images, unmap_image);
    
    dyld

    dyld每次往内存中添加新的二进制文件(此时称为image)之后,都会执行这些回调函数,比较重要的回调函数是map_imagesload_imagesmap_images方法里面就会往类的方法列表添加这个类的所有方法(方法是一个结构体,包含了方法名SEL,还有方法实现IMP),除此之外还有很多类的相关操作都在这里面,分类中的方法、协议、属性也是在这个时候添加到对应的类里去的;而load_images方法里主要是调用了一个load方法,所以我们可以发现OC类中load方法的调用时机比main函数都早。

    当所有依赖库的初始化后,轮到最后一位(程序可执行文件)进行初始化,在这时runtime会对项目中所有类进行类结构初始化,然后调用所有的load方法。

    最后dyld返回main函数地址,main函数被调用,我们便来到了熟悉的程序入口。

    验证下loadinitializeMain函数的加载顺序:

    + (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函数调用之前调用的

    ❷ dyld共享库缓存
    当你构建一个真正的程序时,将会链接各种各样的库。它们又会依赖其他一些framework和动态库。需要加载的动态库会非常多。而对于相互依赖的符号就更多了。可能将会有上千个符号需要解析处理,这将花费很长的时间。

    对于每一种架构,操作系统都有一个单独的文件,文件中包含了绝大多数的动态库,这些库都已经链接为一个文件,并且已经处理好了它们之间的符号关系。当加载一个 Mach-O文件 (一个可执行文件或者一个库) 时,动态链接器首先会检查共享缓存看看是否存在其中,如果存在,那么就直接从共享缓存中拿出来使用。每一个进程都把这个共享缓存映射到了自己的地址空间中。这个方法大大优化了 OS XiOS 上程序的启动时间。

    ❸ dyld加载过程
    dyld所需要加载的是动态库列表一个递归依赖的集合。
    来看一下QQReader依赖的共享动态库,输入命令:otool -L QQReaderUI

    QQReader依赖的共享动态库
    c、pre-main阶段流程

    ❶ 冷启动 - 首次启动:即后台线程中未有当前打开的应用,所有的资源都需要加载并初始化。dyld -> runtime -> main

    QQReader的冷启动时间测量结果

    ❷ 热启动 - 后台激活:即后台线程中保留有当前应用,应用的资源在内存中有保存。通过环境变量DYLD_PRINT_STATISTICS查看启动时间,在 Xcode 中Edit scheme -> Run -> Auguments将环境变量DYLD_PRINT_STATISTICS 设为1。

    添加DYLD_PRINT_STATISTICS选项 QQReader的热启动时间测量结果
    • main()函数之前总共使用了564.97ms
    • 564.97ms中,加载动态库用了109.23ms,指针重定位使用了37.42ms,ObjC类初始化使用了92.56ms,各种初始化使用了325.67ms。
    • 在初始化耗费的325.67ms中,用时最多的几个初始化是libSystem.B.dyliblibBacktraceRecording.dyliblibglInterpose.dylib以及libMTLInterpose.dylib
    d、pre-main阶段优化方案

    ❶ Load dylibs:依赖的dylib越少越好

    • 尽量不使用内嵌(embedded)dylib,加载内嵌dylib性能开销较大
    • 合并已有的dylib和使用静态库(static archives),减少dylib的使用个数
    • 懒加载dylib,但是要注意dlopen()可能造成一些问题,且实际上懒加载做的工作会更多

    WMLinkMapAnalyzer分析下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对工程进行扫描,删除无用代码(未使用的参数、值,未被调用的静态变量、类和方法)**

    删除无用代码

    ❸ Objc setup
    ❹ Initializers

    • 少在类的+load方法里做事情,尽量把这些事情推迟到+initiailize, 因为load是在启动的时候调用,而initialize是在类首次被使用的时候调用
    • 减少构造器函数个数,在构造器函数里少做些事情
    • 减少C++静态全局变量的个数
    e、main阶段

    main阶段:main方法执行之后到AppDelegate类中的didFinishLaunchingWithOptions方法执行结束前这段时间,主要是构建第一个界面,并完成渲染展示。

    • dyld调用main()
    • 调用UIApplicationMain()
    • 调用applicationWillFinishLaunching
    • 调用didFinishLaunchingWithOptions
    main阶段
    f、启动耗时的测量

    测量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);
    
    g、main阶段优化方案:

    这一阶段的优化主要是减少didFinishLaunchingWithOptions方法里的工作,在didFinishLaunchingWithOptions方法里,我们会创建应用的window,指定其rootViewController,调用windowmakeKeyAndVisible方法让其可见。由于业务需要,设置系统UI风格,检查是否需要显示引导页、是否需要登录、是否有新版本等,这里的代码容易变得比较庞大,启动耗时难以控制。

    具体优化方案如下:

    • 梳理各个第三方库,找到可以延迟加载的库,做延迟加载处理,比如放到首页控制器的viewDidAppear方法里。
    • 梳理业务逻辑,把可以延迟执行的逻辑,做延迟执行处理。比如检查新版本、注册推送通知等逻辑。
    • 避免复杂/多余的计算。
    • 避免在首页控制器的viewDidLoadviewWillAppear做太多事情,这2个方法执行完,首页控制器才能显示,部分可以延迟创建的视图应做延迟创建/懒加载处理。
    • 采用性能更好的API。
    • 首页控制器用纯代码方式来构建。

    实际例子:
    QQReaderdidFinishLaunchingWithOptions有将近30多个启动模块,其中耗时最多的前6个模块耗时占比将近86%,对这主要的6个模块进行逐个分析,比如字体加载模块、打点上报模块等采用懒加载的方式进行优化。

    3、阿里数据iOS端启动速度优化实践

    a、pre-main阶段的优化
    • 排查无用的dylib,移除不再使用的libicucore.tbd
    • 删除无用文件&库,合并功能类似的类和扩展(Category
    • 移除不再使用的库UMSocialPSTCollectionViewMCSwipeTableViewCell
    • 移除功能重复的库Mantle
    • 通过 LSUnusedResources 工具,扫描出项目中不再使用的图片资源,将其移除
    • 梳理各个类的+load方法,将多个类中+load方法做的事延迟到+initiailize里去做。
    • 移除不需要用到的类

    使用了一个叫做fui(Find Unused Imports)的开源项目,它能很好的分析出不再使用的类,准确率非常高,唯一的问题是它处理不了动态库和静态库里提供的类,也处理不了C++的类模板。

    使用方法是在Terminalcd到项目所在的目录,然后执行fui find,然后等上那么几分钟(是的你没有看错,真的需要好几分钟甚至需要更长的时间),就可以得到一个列表了。由于这个工具还不是100%靠谱,可根据这个列表,在Xcode中手动检查并删除不再用到的类。

    原来启动时间:

    原来启动时间

    pre-main阶段优化之后的启动时间:

    pre-main阶段优化之后的启动时间
    b、main()阶段的优化
    • 去掉其中100msdispatch_after...检查代码发现之前会故意让启动图多显示100ms,不知道是什么逻辑...
    • 将多个三方库延迟加载。包括TBCrashReporterTBAccsSDKUTTRemoteDebuggerATSDK等。
    • 将若干系统UI配置、业务逻辑延迟执行。包括注册推送、检查新版本、更新Orange配置等。
    • 避免多余的计算。之前会前后两次获取是否要显示广告图,每次获取都需要反序列化Orange中的配置信息,再比较配置中的开始/结束时间,大约耗时20ms。目前的解决方案是第一次计算后,用一个BOOL属性缓存起来,下次直接取用。
    • 延迟加载&懒加载部分视图。快捷密码验证页是启动图消失后用户看到的第一个页面,这个页面由于涉及到图片的解码、多个视图的创建&布局,viewDidLoad阶段会耗时100ms左右。目前的解决方案是把其中密码输入框视图延迟到viewDidAppear里加载,对密码错误提示视图做成懒加载,耗时降低到30ms左右。

    通过instrumentsTime Profiler分析,优化后启动速度有明显提升,didFinishLaunchingWithOptions耗时在75ms左右。其中目前耗时最多的是快捷密码验证页(PAPasscodeViewController)的创建&布局,其次是DTLaunchViewControlle里对是否要显示广告页的判断代码。可以看到PAPasscodeViewControllerviewDidAppear耗时了78ms,但已经没有太大关系,此时用户已经看到了页面,准备去验证指纹/密码了。

    main()阶段的优化

    # 五、音视频的优化方案

    资源文件是放置在应用程序本地与应用程序一起编译、 打包和发布的非程序代码文件,如应用 中用到的声音、 视频、图片和文本文件,本地资源文件编译后,会放置于应用程序包文件中( 即<应用名>.app文件)。

    1、图片文件优化

    图片文件优化

    苹果推荐使用PNG格式,设定编译参数Compress PNG Files

    Finder中查看该文件的属性,它是一个320 X 480px、 大小为 317 KBPNG图片,在编译之后的目录中找到lmageFile.app包文件。打开包文件,查看目录中background.png文件的属性,可以发现该文件是205 KBPNG图片了。说明Xcode工具可以在编译时优化PNG图片,但是即便经过优化和压缩的PNG图片文件,也比JPEG图片文件大得多。

    如果是分布在网络云服务器中的资源文件,应用在加载这些 图片时,会从网络上下载到本地,这时候JPEG就很有优势了。在本地资源的情况下,我们应该优先使用 PNG格式文件,如果资源来源于网络,最好采用JPEG 格式文件。

    +imageNamed:方法会在内存中建立缓存,这些缓存直到应用停止才清除 。 如果是贯穿 整个应用的图片(如图标、 logo等),推荐使用 +imageNamed:创建,如果是仅使用 一 次的图片,推荐使用构造函数 - initWithContentsOfFile:创建。

    2、音频文件优化

    WAV文件:由于文件较大,不太适合移动设备这些存储容量小的设备
    MP3:有损压缩格式,适合于移动设备这些存储容量小的设备
    CAFF:苹果开发的专门用于macOS和iOS系统的无压缩音频格式,它被设计用来替换老的WAV格式
    AIFF:压缩格式是AIFF-C (或AIFC),将数据以4 : I压缩率进行压缩,应用于macOS和iOS系统

    背景音乐会在应用中反复播放,它会 一 直驻留在内存中并耗费 CPU,所以更合适比较小的文件,需要进行压缩。

    压缩文件主要有 AIFCMP3这两种格式,一般我们首选 AIFC,因为这是苹果推荐的格式。原始文件格式不一定是AIFC,这种情况下我们需要使用afconvert工具。将其转换为AIFC格式,终端中执行如下命令 :

    //-f AIFC参数用于转换为AIFC格式
    //-d ima4参数指定解码方式
    //Fx08822_cast.wav是要转换的源文件
    //转换成功后, 会在相同目录下生成Fx08822_cast.aifc文件
    $ afconvert -f AIFC-d ima4 Fx08822_cast.wav
    

    MP3本身是有损压缩,如果再经过afconvert转换,音频的质量会受到影响。

    音乐特效优化,如发射子弹、敌人被打死或按钮点击等发出的声音,这些声音都是比较短的,追求震撼的 3D效果,可以采用苹果专用的无压缩CAFF格式文件,可以使用 afconvert工具将其转换为 CAFF格式:

    //-f caff参数用千转换为CAFF格式
    //-d LEI16参数指定解码方式
    //Fx08822_cast.wav是要转换的源文件
    //音频的采样频率为 22 050Hz
    $ afconvert -f caff -d LEI16 Fxo8822_cat.wav
    

    综上,音频文件在使用本地资源的情况下,应用背景音乐时AIFC格式是首选,应用于音乐特效时CAFF格式 是首选 。 如果资源来源于网络,最好采用MP3格式文件。


    Demo

    Demo在我的Github上,欢迎下载。
    PerformanceOptimizationDemo

    参考文献

    关于性能优化
    iOS 保持界面流畅的技巧
    浅谈 GPU 及 “App渲染流程”
    iOS耗电量检测与优化
    iOS性能分析和优化工具Instruments
    移动端监控体系之技术原理剖析
    iOS启动优化
    阿里数据iOS端启动速度优化的一些经验

    相关文章

      网友评论

        本文标题:IOS基础:性能优化

        本文链接:https://www.haomeiwen.com/subject/dooumktx.html