一. 性能指标APP 的性能指标主要是包括 CPU、GPU、内存、电池耗电、网络加载几个大的方面网络加载在下文会提及电池耗电主要是由于 CPU、GPU、网络等因素决定所以不作为基础的指标。1. CPU占有率iOS APP 为单进程的应用不涉及到跨进程通讯不包括 Extention。1.1 线程使用线程的使用及通讯会带来 CPU 的开销大量的线程启用自然时候使得 CPU 使用率上升不同线程之间的通讯需要添加锁来确保线程安全又加大了线程的使用周期。使用线程时需要注意不要在并发队列中使用过多的线程锁操作如果必要则需要降低加锁代码的执行时耗尽量精简化也可以直接采用串行队列来实现同步。不同场景使用不同的 GCD 队列例如 执行多个独立的 I/O 操作 | 数据编解码全局并发队列任务执行不分先后、单线程数据库操作自定义串行队列同步执行任务、实现文件的多读单写 | 多个并行任务的组合操作自定义并发队列执行的任务存在依赖关系。1.2 执行方法耗时常见较为耗时的场景如下。对象创建对象的创建会分配内存、调整属性个别类的对象创建则更为耗时如NSDateFormatter、NSCalendar频繁生产临时变量可以改成单例调用。布局计算视图布局的计算会由于不同逻辑的运行时耗而带来不同程度的 CPU 开销。图像绘制开启 ImageContext把图像绘制到画布中从当前的 ImageContext 获取 image并赋值给 layer.contents 显示。图片解码图片设置到 UIImageView 或者 CALayer.contents 中去并且 CALayer 被提交到 GPU 前CGImage 中的数据才会得到解码。图片过大超过 GPU 的最大纹理尺寸时图片需要先由 CPU 进行预处理这对 CPU 和 GPU 都会带来额外的资源消耗iOS图片裁剪/缩略性能探究。/* 避免频繁创建NSDateFormatter实例 */ (NSDateFormatter *)dateFormatter { static NSDateFormatter *formatter; static dispatch_once_t onceToken; dispatch_once(onceToken, ^{ formatter [[NSDateFormatter alloc] init]; formatter.dateFormat yyyy-MM-dd HH:mm:ss; }); return formatter; }1.3 I/O操作I/O 操作是指文件的读取、写入、更新。磁盘 I/O 的执行速度要远低于 CPU 和内存的速度。文件的读写主要性能开销是 I/O同时也会有小占比的 CPU 与内存的消耗。在 APP 运行过程中由于 I/O 操作速度较慢方法的调用时耗自然也就更大通常会使用多线程来进行文件的读写操作防止主线程的堵塞。文件大小与文件数量关系着线程资源的开销最终决定 CPU 的性能开销。1.4 CPU使用分析Xcode自带的 CPU 检测工具instruments-Activity Monitor分析 CPU 的使用情况Instruments之TimeProfiler。第三方开源的 CPU 检测组件滴滴的 DoraemonKit一款面向泛前端产品研发全生命周期的效率平台。2. GPU渲染-FPSFPS Frames Per Second 的简称缩写意思是每秒传输帧数可以理解为我们常说的“刷新率”单位为Hz。FPS 是测量用于保存、显示动态视频的信息数量。每秒钟帧数愈多所显示的画面就会愈流畅FPS 值越低就越卡顿所以这个值在一定程度上可以衡量应用在图像绘制渲染处理时的性能。iOS 系统中正常的屏幕刷新率为 60Hz60次每秒。页面渲染优化相关内容会在下文根据具体场景列举说明。Xcode 自带的 FPS 检测工具instruments-CoreAnimation查内存分配情况的Instruments之CoreAnimation第三方开源的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、NSCacheLFU 优先于 LRU。Xcode自带的内存检测工具instruments-Allocations查内存分配情况的Instruments之Allocationsinstruments-Leaks动态内存分析内存检测Instruments之LeaksXcode-Product-Analyze静态内存分析静态内存分析-Analyze的使用第三方开源的内存监控组件Faceboo k的 FBMemoryProfiler分析 iOS 内存使用和检测循环引用仅检测 OC。腾讯的 OOMDetectorOOM 监控、大内存分配监控、内存泄漏检测支持监控 C 对象和 malloc 内存块以及 VM 内存。二. 场景应用1. 启动iOS 冷启动流程分为 Pre-main 与 main也就是 main 函数入口的之前与之后的两部分。网上这方面的资料也很多这里就大概过一下相关的博文推荐抖音-iOS启动优化之原理篇、抖音-iOS启动优化之实战篇、抖音-基于二进制文件重排的解决方案1.1 Pre-main1具体流程Dyld动态链接器在系统内核做好程序准备工作之后交由 dyld 负责余下的工作。Load Dylibs加载动态库iOS 的动态库包含 dylib 与动态 framework。Rebase将镜像加载到内存修正镜像内部的指针偏移ASLR并以 Page 为单位进行签名验证保证不会被篡改性能消耗主要在 I/O。Bind查询符号表将指针指向镜像外部的内容符号绑定性能消耗主要在 CPU 计算。Objc注册 Objc 类将类别插入类的⽅法列表⾥检查 selector 的唯一性。initalizers调用 Objc 类与类别的 load 方法调用 C/C 中的构造器函数创建非基本类型的C静态全局变量。2优化策略合并自定义的动态库删除冗余的 dylib 与动态 framework。将动态库转为静态库Mach-O TypeStatic Library。使用二进制重排减少 Page 载入的缺页中断问题。减少 ObjC 类、方法selector、类别category的数量。减少 ObjC 的 load 方法类/协议的绑定通过启动项自注册的方式实现。减少 C/C 的 constructor 函数、C 静态全局变量。1.2 Main1具体流程2优化策略SDK 注册较为耗时的可以使用异步并发加载部分二级页才用到的 SDK 可以采用懒加载。避免启动后出现过多的耗性能操作例如频繁读写 I/O数据解码等耗时方法的调用。避免启动时多个接口请求的串行等待采用任务队列的方式替代。2. 页面2.1 原生页面-渲染原理1View的渲染View 的展示是由 Layer 实现View 主要处理 Touch 响应链相关的事件。当 View / Layer 的 frame 与图层结构发生改变时View / Layer 被标记为待处理状态系统会监听 mainRunLoop 的 BeforeWaiting / Exit 状态在监听回调中遍历所有待处理 View / Layer刷新 UI 布局。上面提到 View 的本质是 LayerLayer 则包含 contents这个 contents 指向的是一块缓存又名Baking Store。Objective-c 提供了 Core Animation 的渲染内核底层是由 OpenGL 实现 GPU 渲染流程大致如下初始化用于绘制的上下文 EAGLContext创建帧缓冲区和渲染缓冲区设置画布的宽高添加附件比如颜色附件或者深度附件切换到帧缓冲区在帧缓冲中进行绘制切换到屏幕缓冲区读取帧缓冲中的信息绘制到屏幕上在容器 dealloc 时删除缓冲区。所有的 Bitmap包括图片、文本、栅格化的内容最终都要由内存提交到显存绑定为 GPU Texture。2GPU的离屏渲染当前屏幕渲染指的是 GPU 的渲染操作在当前用于显示的屏幕缓冲区进行。离屏渲染指的是 GPU 在当前屏幕缓冲区外新开辟一个缓冲区进行渲染操作。离屏渲染主要开销包括创建新的缓冲区、屏幕缓冲区到离屏缓冲区的来回切换。iOS 中主要是由于 Layer 的某些属性设置导致的离屏渲染常见的有遮罩mask、透明opaque、阴影shadow、光栅化rasterize、圆角cornerRadius离屏渲染会让 APP 的交互变得不流畅如比较复杂的图文混排 List需要避免频繁触发离屏渲染相关博文推荐iOS离屏渲染场景及优化方案2.2 原生页面-渲染优化离屏渲染mask / opaque / shadow / rasterize / cornerRadius这些属性都会引起离屏渲染低刷新频率并不会出现卡帧的现象主要出现在列表页快速滑动的时候。视图结构减少图层嵌套精简图层数量避免多个半透明图层叠加。数据加工预排版 - 提前将数据模型转换成布局模型使用异步线程实现数据加工I/O操作、数据换算。异步渲染 把复杂的图像绘制逻辑放在子线程去执行具体实现可以参考开源库Graver、AsyncDisplayKit。图片预解码使用异步线程把图片绘制到 CGBitmapContext 中生成 Bitmap 缓存 。/* 图片预解码 */ - (void)drawImage:(UIImage *)image { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ CGImageRef imageRef image.CGImage; size_t width image.size.width; size_t height image.size.height; size_t bitsPerComponent CGImageGetBitsPerComponent(imageRef); size_t bytesPerRow CGImageGetBytesPerRow(imageRef); CGColorSpaceRef space CGImageGetColorSpace(imageRef); uint32_t bitmapInfo CGImageGetBitmapInfo(imageRef); CGContextRef contextRef CGBitmapContextCreate(NULL, width, height, bitsPerComponent, bytesPerRow, space, bitmapInfo); CGContextDrawImage(contextRef, CGRectMake(0, 0, width, height), imageRef); CGImageRef tImageRef CGBitmapContextCreateImage(contextRef); CGContextRelease(contextRef); dispatch_async(dispatch_get_main_queue(), ^{ self.layer.contents (__bridge id)tImageRef; CGImageRelease(tImageRef); }); }); }2.2 原生页面-动画效果iOS 项目中最常见的动画包括帧动画、显式动画和隐式动画。通过 UIImageView 配置帧图片的方式或者 Gif 组件实现帧动画效果UIImageView.animations 适用于帧数较少的场景省去了 Gif 解析的环节直接配置帧图片。Gif 的播放对 CPU 与内存的开销较大文件解析-缓存-定时器-解码显示可以使用FLAnimatedImage / YYImage本地、SDWebImage网络都对 Gif 渲染做了优化。就 FLAnimatedImage 的实现而言从三个方面优化了 Gif 的渲染分别是异步解析 gifData、CADisplayLink 的使用、gifData 大小制定缓存策略见下方图片。尽管对 Gif 渲染做了一定的优化但在 Gif 帧数及帧图片较大的时候Gif 仍是会带来不少的开销特别是多个 Gif 同时渲染的页面。Lottie 的出现很好地解决了这个问题一个基于移动端和 web 端的跨平台动画框架。动画的冲突也会出现明显的卡顿现象如在 Push 一个 VC 时该 VC 页面即刻唤起键盘就会出现卡顿或者是没有弹起动效的情况可以通过异步调用的方式来规避。CoreAnimation 提供 CABasicAnimation、CAKeyframeAnimation、CAAnimationGroup、CATransition 的相关能力用 CoreAnimation 的 API 替换 UIView.animate 可以减少额外的开销。2.3 web页面1白屏时间长资源本地化web 页面常见的问题就是白屏时间长需要依次加载 htmlcdn 资源文件以及页面的网络请求。可以通过加载 H5 本地资源包的方式或者 cdn 资源拦截本地映射的方式来减少白屏时长具体实现可以参考H5资源本地化策略-iOS。骨架屏尽管页面加载网络数据时会有加载圈提示但接口响应较慢会导致页面一直在转圈这时就需要引入骨架屏页面在加载完 web 资源后通过 webpage 打包生成的骨架屏预先展示出页面的大致结构 Vue页面骨架屏注入实践或者是通过设置各个 UI 组件的占位来预先展示出页面的大致结构。2图片展示上传压缩减少网路加载时耗以及大图片的渲染开销。图片占位防止图片加载时页面出现跳动的现象。2.4 网络加速1图片加载支持webpWebP 是一种同时提供了 有损压缩 与 无损压缩可逆压缩的图片文件格式派生自影像编码格式 VP8是由 Google 在购买 On2 Technologies 后发展出来以 BSD 授权条款发布。具体实现流程服务端支持图片的 webp 加载通过 Hook 文件下载 API给图片 url 添加后缀 ‘.webp’加载 webp 资源文件SDWebImage 自带 webp 解码器APP 启动时注册一下即可webp 解码成 jpg/png图片展示。2HttpDNS解析HttpDNS 解析是使用 HTTP 协议进行域名解析代替现有基于 UDP 的 DNS 协议域名解析请求直接发送到阿里云的 HTTPDNS 服务器从而绕过运营商的 Local DNS能够避免 Local DNS 造成的域名劫持问题和调度不精准问题。httpDns 解析将现有域名解析成 IP 地址通过 IP 直连的方式进行网络访问。市面上的 APP 大部分是通过的阿里云与腾讯云提供的 SDK 来实现。HTTPDNS_域名解析_域名防劫持_开发与运维-阿里云移动解析HttpDNS_移动互联网域名解析_域名防劫持 - 腾讯云具体实现流程通过 NSURLProtocol 对请求进行重定向获取域名解析后的IP信息将原有请求 URL 的域名替换成 IP重新发送请求实现 IP 直连。3使用网络缓存 请求数据压缩 接口分屏加载使用网络缓存系统提供了网路请求缓存 NSURLCache默认 diskCapacity 为10MNSURLCache详解。请求数据压缩目前大部分公司都是使用 JSON 数据格式进行网络数据传输比起 XML 更为精简减少了数据大小与数据解析的时耗JSON模型转换库评测。接口分屏加载分屏加载主要看页面接口的时耗如果页加载时耗较长可以拆分为两个接口优先获取首屏展示的数据。三. 编译打包1. 编译打包优化在项目经过长周期的迭代后Run / Archive 的时长从一开始的几分钟到十几二十几分钟一方面由于 Mac 设备更新换代另一方面则是工程架构的复杂化或者是项目设计不合理导致的臃肿。工程配置-Build Settings设置 Optimization Level、Debug Information Format、Build Active Architecture Only提高Xcode的编译速度。将不经常改动的源码打包成静态库.a / .framework省去重新编译的时间。减少 Storyboard 和 Xib 的使用xml 解析与节点处理会带来一定的性能开销。排查项目中无用的资源文件、类和第三方库去除冗余资源。资源压缩/合并使用 TinyPNG 进行图片压缩合并 Asset合并 ObjC 类一大堆工具类、API 拆分过度、功能重复的类库等避免过度封装。PCH 文件只放置相对静态且较为通用的类声明减少不必要的重新编译。2. 包大小优化原生业务比较多的 APP在经过一定迭代后ipa 包都会比较大上百兆、甚至达到了一两百兆。这时候就需要优化包大小相关的博文推荐今日头条 iOS 安装包大小优化工程配置-Build Settings设置 Asset Optimization 为 spaceLink-Time Optimization 为 Incremental。排查项目中无用的资源文件、类和第三方库去除冗余资源。较大的内置资源文件存放在云端等需要用到的时候才去下载包括图片 / 音视频文件等。资源压缩/合并使用 TinyPNG 进行图片压缩合并 Asset合并 ObjC 类一大堆工具类、API 拆分过度、功能重复的类库等避免过度封装。属性动态化用 dynamic 修饰一个属性不生成成员变量、get/set 方法。四. APP稳定性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自定义 TabBarItem 类需要添加 image 与 title 属性。2. 卡顿问题1.1 采集/度量集成 Bugly 或者 FireBase Performance Monitor实现卡顿的采集与分析。使用 Xcode 自带的I nstruments-Core Animation / Time Profiler 检测 FPS 与耗时 API。通过 DoraemonKit-debug 工具在 debug 模式采集应用的卡顿信息。1.2 常见卡顿优化耗时方法优化包含数据编解码、系统耗时 API、I/O 操作、处理大量遍历逻辑等阻塞 UI 线程的操作上文已经做了较详细的叙述这里不在具体展开。页面渲染优化具体细节可以查阅上文的 ‘页面’ 章节。