一. 性能指标

APP的性能指标主要是包括CPU、GPU、内存、电池耗电、网络加载几个大的方面,网络加载在下文会提及,电池耗电主要是由于CPU、GPU、网络等因素决定,所以不作为基础的指标。

ios性能分析工具 ios 性能_Instrument

1. CPU占有率

IOS APP为单进程的应用,不涉及到跨进程通讯(不包括Extention)。

1.1 线程使用

线程的使用及通讯会带来CPU的开销,大量的线程启用自然时候使得CPU使用率上升,不同线程之间的通讯需要添加锁来确保线程安全,又加大了线程的使用周期。

使用线程时需要注意:

  • 不要在并行队列中使用过多的线程锁操作,如果必要则需要降低加锁代码的执行时耗,尽量精简化,也可以直接采用串行队列来实现同步。
  • 特地场景使用各自的队列,例如SDImage的存储(串行)、解码(串行)、下载(并行)。

1.2 执行方法耗时

常见较为耗时的场景如下。

  • 对象创建:对象的创建会分配内存、调整属性,个别类de 对象创建则更为耗时,如NSDateFormatter、NSCalendar...。
  • 布局计算:视图布局的计算会由于不同逻辑的运行时耗而带来不同程度的CPU开销。
  • 图像绘制:图像的绘制通常是指用那些以 CG 开头的方法把图像绘制到画布中,然后从画布创建图片并显示这样一个过程。
  • 图片解码:图片设置到 UIImageView 或者 CALayer.contents 中去,并且 CALayer 被提交到 GPU 前,CGImage 中的数据才会得到解码(iOS的5种图片缩略技术以及性能探讨)。

1.3 I/O操作  

I/O操作是指文件的读取、写入、更新。磁盘IO的执行速度要远低于CPU和内存的速度。文件的读写主要性能开销是I/O,同时也会有小占比的CPU与内存的消耗。

在APP运行过程中,由于I/O操作速度较慢,方法的调用时耗自然也就更大,通常会使用多线程来进行文件的读写操作,防止主线程的堵塞。文件大小与文件数量关系着线程资源的开销,最终决定CPU的性能开销。

1.4 CPU使用分析

Xcode自带的CPU检测工具:

第三方开源的CPU检测组件:

  • 滴滴的DoraemonKit,一款面向泛前端产品研发全生命周期的效率平台。

2. GPU渲染-FPS

FPS :Frames Per Second 的简称缩写,意思是每秒传输帧数,可以理解为我们常说的“刷新率”(单位为Hz)。FPS是测量用于保存、显示动态视频的信息数量。每秒钟帧数愈多,所显示的画面就会愈流畅,FPS值越低就越卡顿,所以这个值在一定程度上可以衡量应用在图像绘制渲染处理时的性能。iOS系统中正常的屏幕刷新率为60Hz(60次每秒)。页面渲染优化相关内容会在下文根据具体场景列举说明。

Xcode自带的FPS检测工具:

第三方开源的FPS检测组件:

  • 滴滴的DoraemonKit,一款面向泛前端产品研发全生命周期的效率平台。

 3. 内存

这里讲的内存主要是内存缓存,不对内存管理做过多的叙述,有兴趣可以看一下我之前写的博客-IOS内存管理

每一台Iphone机子都拥有固定的物理内存空间,也就是我们常说的运行内存2个G、4个G这种硬件配置。系统的运行会有一部分的内存开销,其他的则由运行的APP共同分配。

和安卓不同的是,IOS系统并没有限制固定的内存分配规则,所以运行一个APP,有时候可以达到几百甚至超过1GB的内存使用,不过这样无限制的消耗内存会导致内存警告,最终导致进程被杀掉。

内存的使用场景:

  • 临时/局部,临时申请的内存空间,使用完即释放,如二级页面的数据源缓存。
  • 静态/全局,静态内存,static、const、extern声明的常量与对象(单例对象、全局数组)。

内存的缓存策略:MemoryCache

  • 常规缓存,NSDictionary、NSArray、NSSet、NSPointerArray / NSMapTable / NSHashTable(支持弱引用)。
  • 缓存+淘汰策略,LRU、LFU、NSCache(LFU优先于LRU)。

Xcode自带的内存检测工具:

第三方开源的内存监控组件:

  • Facebook的FBMemoryProfiler,分析iOS内存使用和检测循环引用,仅检测OC。
  • 腾讯的OOMDetector,OOM 监控、大内存分配监控、内存泄漏检测,支持监控C++对象和malloc内存块以及VM内存。

二. 场景应用

1. 启动

IOS冷启动流程分为Pre-main与main,也就是main函数入口的之前与之后的两部分。网上这方面的资料也很多,这里就大概过一下,需要了解细节的小伙伴推荐字节官方博客:抖音-iOS启动优化之原理篇抖音-iOS启动优化之实战篇抖音-基于二进制文件重排的解决方案

ios性能分析工具 ios 性能_Instrument_02

1.1 Pre-main

ios性能分析工具 ios 性能_UI渲染_03

1)具体流程

  • Dyld:动态链接器,在系统内核做好程序准备工作之后,交由dyld负责余下的工作。
  • Load Dylibs:加载动态库,IOS的动态库包含dylib与动态framework,静态库包含.a与静态framework。
  • Rebase:将镜像读入内存,并以page为单位进行加密验证,保证不会被篡改,性能消耗主要在IO。
  • Bind:查询符号表,设置指向镜像外部的指针,性能消耗主要在CPU计算。 
  • Objc:读取所有类,将类对象其注册到这个全局表中;读取所有分类,把分类加载到类对象中; 检查selector的唯一性。
  • initalizers:dyld开始运行程序的初始化函数,调用每个Objc类和分类的+load方法,调用C/C++ 中的构造器函数,和创建非基本类型的C++静态全局变量。  

2)优化策略

  • 排查无用的dylib,减少dylib的数目。
  • 减少ObjC类、方法(selector)、类别(category)的数量。
  • 删除无用的库 压缩图片资源,删除无用的图片(IO操作)。
  • 少在类的+load方法里做事情,尽量推迟到+initailize实现。

1.2 Main

1)具体流程

ios性能分析工具 ios 性能_性能优化_04

ios性能分析工具 ios 性能_UI渲染_05

2)优化策略

  • SDK注册较为耗时的可以使用异步并发加载,部分二级页才用到的SDK可以采用懒加载的形式。
  • 防止启动时有过多的串行接口操作,尽量精简。
  • 避免启动后出现过多的耗性能操作,例如频繁读写IO,数据解码等耗时方法的调用。

2. 页面

2.1 原生页面-渲染原理

1)View的渲染

View的展示是由Layer实现,View主要处理Touch响应链相关的事件。UIView提供了绘图API-drawRect,可以在该方法中获取图形上下文,并实现图形的绘制,调用setNeedsDisplay刷新绘制。

向View中添加子视图后,在mainRunloop的回调中实现绘制并调用layoutSubviews,subView布局发生变化时,也是在mainRunloopp的回调中实现绘制并调用layoutSubviews,所以layoutSubviews只有多个操作在不同mainRunloop的触发时间段才会有多次调用。

ios性能分析工具 ios 性能_UI渲染_06

上面提到View的本质是layer,layer则包含contents,这个contents指向的是一块缓存又名Baking Store。Objective-c提供了Core Animation的渲染内核,底层是由OpenGL实现GPU渲染,流程大致如下:

  1. 初始化用于绘制的上下文EAGLContext;
  2. 创建帧缓冲区和渲染缓冲区,设置画布的宽高;
  3. 添加附件,比如颜色附件或者深度附件;
  4. 切换到帧缓冲区,在帧缓冲中进行绘制;
  5. 切换到屏幕缓冲区,读取帧缓冲中的信息;
  6. 绘制到屏幕上,在容器dealloc时删除缓冲区。

ios性能分析工具 ios 性能_Instrument_07

  

2)GPU的离屏渲染

  • 当前屏幕渲染,指的是GPU的渲染操作在当前用于显示的屏幕缓冲区进行。
  • 离屏渲染,指的是GPU在当前屏幕缓冲区外新开辟一个缓冲区进行渲染操作。

离屏渲染主要开销包括创建新的缓冲区、屏幕缓冲区到离屏缓冲区的来回切换。

IOS中主要是由于layer的某些属性设置导致的离屏渲染,常见的有遮罩(mask)、裁切(clip)、透明(opaque)、阴影(shadow)、栅格化(rasterize)、圆角(cornerRadius),离屏渲染会让APP的交互变得不流畅(如:比较复杂的图文混排list),所以应该尽量避免使用,采用其他方式来实现。

2.2 原生页面-复杂布局

ios性能分析工具 ios 性能_性能优化_08

原生页面的复杂布局一般有两种常见的场景:

  • 微博、空间、朋友圈这些样式多样化的列表页,呈现的特点是cell的复用程度较低,单元图层元素也较为复杂。
  • 股票K线图,图像编辑、动态图表等图形绘制页面,主要特点是在一个固定的画布中,根据数据源与对应的场景需求,快速地绘制并展示图形。

2)低复用列表

ios性能分析工具 ios 性能_ios_09

  • 离屏渲染:mask/clip/opaque/shadow/rasterize/cornerRadius,这些属性都会引起离屏渲染,低刷新频率并不会出现卡帧的现象,主要出现在列表页快速滑动的时候。
  • 视图过度绘制:cell中的非交互图层使用layer代替;减少图层嵌套,精简图层的数量;使用异步渲染,开启子线程把复杂的图层元素绘制成一个bitmap,然后切回主线程展示。
  • 数据加载:通过懒加载(需要用到在获取)/预加载(提前获取备用),具体场景具体应用;使用异步线程实现数据的获取/加工(IO操作、图片转码等)。

3)频繁画布重绘

ios性能分析工具 ios 性能_UI渲染_10

  • 统一事件源触发:定时器与touch事件源的统一入口,避免事件源过于频繁的触发图层重绘。
  • 减少局部刷新:以整体数据源的变化为刷新频次,减少局部刷新的频率(与统一事件源类似)。
  • 减少图层嵌套:图形绘制的场景减少Layer以及图层嵌套,使用效率更优的CGGraphis-API。
  • 使用异步绘制:开启子线程把复杂的图层元素绘制成一个bitmap,然后切回主线程展示。
/* 异步绘制,在需要频繁重绘的视图上效果最好(比如绘图应用、TableViewCell之类)*/
- (void)drawsAsynchronously:(void(^)(CGContextRef context))drawsBlock imageBlock:(void(^)(UIImage* image))imageBlock {
    /* 开启异步线程实现图形绘制,最终刷新还是在UI线程 */
    CGSize size = self.bounds.size;
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        UIGraphicsBeginImageContext(size);
        CGContextRef context = UIGraphicsGetCurrentContext();
        drawsBlock(context);
        UIImage *resultImage = UIGraphicsGetImageFromCurrentImageContext();
        UIGraphicsEndImageContext();
        dispatch_async(dispatch_get_main_queue(), ^{
            imageBlock(resultImage);
        });
    });

    /* 或者使用系统提供的属性来实现异步绘制
     self.layer.drawsAsynchronously
     */
}

2.2 原生页面-动画效果

UI动画往往对性能的开销比较大,IOS项目中最常见的动画包括帧动画与核心动画。通过imageView配置帧图片的方式,或者git组件实现帧动画效果,

UIImageView animations适用于帧数较少的场景,省去了Gif解析的环节,直接配置帧图片。

Gif的播放对CPU与内存的开销较大(文件解析->缓存->定时器->解码显示),可以使用FLAnimatedImage / YYImage(本地)、SDWebImage(网络),都对Gif渲染做了优化。就FLAnimatedImage的实现而言,从三个方面优化了gif的渲染,分别是异步解析gifData、CADisplayLink的使用、gifData大小制定缓存策略(见下方图片)。

ios性能分析工具 ios 性能_Instrument_11

尽管对Gif渲染做了一定的优化,但在gif帧数及帧图片较大的时候,Gif仍是会带来不少的开销,特别是多个gif同时渲染的页面。Lottie的出现很好地解决了这个问题,一个基于移动端和web端的跨平台动画框架,设计师可以使用 Lottie 提供的 Bodymovin 插件将设计好的动画导出成 JSON 格式,并在移动端和Web端实现动画的渲染。

动画的冲突也会出现明显的卡顿现象,如在Push一个VC时,该VC页面即刻唤起键盘,就会出现卡顿或者是没有弹起动效的情况,可以通过异步调用的方式来规避。

核心动画包含基础动画、关键帧动画、组合动画、过度动画,可以直接调用系统提供的API实现。

2.3 web页面

1)白屏时间长 

  • 资源本地化:web页面常见的问题就是白屏时间长,需要依次加载html,cdn资源文件,以及页面的网络请求。可以通过加载H5本地资源包的方式,或者cdn资源拦截+本地映射的方式来减少白屏时长,具体实现可以参考H5资源本地化策略-IOS
  • 骨架屏:尽管页面加载网络数据时会有加载圈提示,但接口响应较慢会导致页面一直在转圈,这时就需要引入骨架屏,页面在加载完web资源后,通过webpage打包生成的骨架屏预先展示出页面的大致结构 (Vue页面骨架屏注入实践),或者是通过设置各个UI组件的占位来预先展示出页面的大致结构。

2)图片展示

  • 上传压缩:减少网路加载时耗,以及大图片的渲染开销。
  • 图片占位:防止图片加载时,页面出现跳动的现象。

 2.4 网络加速

ios性能分析工具 ios 性能_Instrument_12

1)图片加载支持webp

WebP是一种同时提供了 有损压缩 与 无损压缩(可逆压缩)的图片文件格式,派生自影像编码格式VP8,是由Google在购买 On2 Technologies 后发展出来,以BSD授权条款发布。

具体实现流程:

  1. 服务端支持图片的webp加载;
  2. 通过Hook 文件下载API,给图片url添加后缀‘.webp’;
  3. 加载webp资源文件;
  4. SDWebImage自带webp解码器,APP启动时注册一下即可;
  5. webp解码成jpg/png,图片展示。

2)HttpDNS解析

HttpDNS解析是使用HTTP协议进行域名解析,代替现有基于UDP的DNS协议,域名解析请求直接发送到阿里云的HTTPDNS服务器,从而绕过运营商的Local DNS,能够避免Local DNS造成的域名劫持问题和调度不精准问题。

httpDns解析将现有域名解析成IP地址,通过IP直连的方式进行网络访问。市面上的APP大部分是通过的阿里云与腾讯云提供的SDK来实现。

HTTPDNS_域名解析_域名防劫持_开发与运维-阿里云

移动解析HttpDNS_移动互联网域名解析_域名防劫持 - 腾讯云

具体实现流程:

  1. 通过NSURLProtocol对请求进行重定向;
  2. 获取域名解析后的IP信息;
  3. 将原有请求URL的域名替换成IP;
  4. 重新发送请求实现IP直连。

3)使用网络缓存 + 请求数据压缩 + 接口分屏加载

三. 编译打包

ios性能分析工具 ios 性能_ios性能分析工具_13

1. 编译打包优化

在项目经过长周期的迭代后,Run / Archive 的时长从一开始的几分钟到十几二十几分钟,一方面由于Mac设备更新换代,另一方面则是工程架构的复杂化,或者是项目设计不合理导致的臃肿。

  •  工程配置-Build Settings,设置Optimization Level、Debug Information Format、Build Active Architecture Only(iOS如何加快编译速度 )。
  • 使用cocoaPod,避免循环引用,方便版本迭代、配置静态加载。 
  • 减少动态库的数量,或者将模块代码转成静态库(.a + bundle / framework)。
  • 排查冗余资源,可以通过项目代码排查,也可以用三方排查工具,将冗余文件去除。
  • 资源压缩/合并,使用TinyPNG进行图片压缩,合并Asset,合并OC类(一大堆工具类、API拆分过度、功能重复的类库等),避免过度封装。
  • 避免过度使用PCH,PCH的引用代表着同个工程下每个OC类都可以访问其中的声明的类以及方法。这样每当PCH中的类发生改变时,重新run起来都会比较耗时。所以,PCH只放置相对静态且通用性较强的类的引用声明。

2. 包大小优化 

原生业务比较多的APP,在经过一定迭代后,ipa都会比较大,上百兆、甚至达到了一两百兆。这时候就需要考虑包大小的优化,推荐两篇字节官方博客:今日头条 iOS 安装包大小优化抖音包 iOS 安装包大小优化

  • 排查冗余资源,可以通过项目代码排查,也可以用三方排查工具,将冗余文件去除。
  • 较大的内置资源文件存放在云端,通过下发的方式代替,包括图片、音视频文件等。
  • 资源压缩/合并,使用TinyPNG进行图片压缩,合并Asset,合并OC类(一大堆工具类、API拆分过度、功能重复的类库等),避免过度封装。
  • 工程配置-Build Settings,设置Asset Optimization为space,Link-Time Optimization为Incremental。

四. APP稳定性

ios性能分析工具 ios 性能_性能优化_14

1. 闪退问题

1.1 采集/度量

  • 集成Bugly或者Fabric(及时性与精确度较高),实现crash的采集与分析。
  • 使用Xcode自带的Instruments-Zombies检测僵尸对象,主要是在应用上线前的度量。
  • 通过Xcode的Organizer-Crashes,查看用户上报的crash日志,应用发布上线之后的分析。

1.2 常见闪退优化

  • 数据容错:像数组越界、字典获取对象类型异常,常见的做法是新增Array、Dictionary的类别方法来容错,通过切面编程在原有IMP调用之前实现逻辑容错。 
  • 系统API异常:每次IOS更新大版本都需要对APP做一次系统兼容性的全面测试,修复新系统带来的兼容性问题。
  • 页面堆栈异常:页面Push/Pop切换太过频繁导致堆栈异常,只需要在BaseNavigationVC中对页面的频次做限制即可,如果没有集成base类可以通过Hook的方式来实现。
  • 方法属性缺失:改写系统UI组件结构导致其调用属性/方法异常导致的crash,只需要在相应层级的类添加属性或者方法即可(例如:替换UITabBar的内部元素UITabBarItem,QFTabBarItem类需要添加image与title属性)。

2. 卡顿问题

1.1 采集/度量

  • 集成Bugly或者FireBase Performance Monitor,实现卡顿的采集与分析。
  • 使用Xcode自带的Instruments-Core Animation / Time Profiler检测FPS与耗时API。
  • 通过DoraemonKit-debug工具,在debug模式采集应用的卡顿信息。

1.2 常见卡顿优化

  • 耗时方法优化:包含数据编解码、系统耗时API、IO操作、处理大量遍历逻辑等阻塞UI线程的操作,上文已经做了较详细的叙述,这里不在具体展开。
  • 页面渲染优化:具体细节可以查阅上文的 ‘页面’ 章节。