前言
在项目中,直接面向用户的客户端往往是一个项目的门面。因此,在项目开发建设的过程中,为了交付用户体验较佳的客户端App,保障产品交付质量。往往需要我们开发者关注客户端软件的性能指标问题。因此,我们要对应用的性能优化
专题有所研究!!
我们通常关注的性能指标有:
页面卡顿
耗电、发热
网络优化
应用启动
安装包瘦身
等
我们在开发建设项目过程中,可以粗略划分为几个阶段:开发阶段
、测试阶段
、维护阶段
:
- 在
开发阶段
,我们要掌握性能调试、性能监测的手段,从而保障,在当前稳定版本的客户端软件,有一个比较合理的性能保障; - 在
测试阶段
,测试团队等若干同事往往会给我们提出一些用户体验上的反馈和建议,因此,我们需要掌握性能调试的手段,从而改造出比较符合团队要求的产品; - 在上线
维护阶段
,针对已经上线的应用,我们的开发团队要有线上性能监控
的能力,从而及时收集不满足性能指标要求的业务交互场景和步骤,捕获具体问题进行分析,从而以此为依据作为有效迭代优化我们客户端的有力助力。
为此,我们本次将会用几篇文章,围绕一些常见的性能指标,去关注 如何调试、如何监测、如何改进处理问题:
- Instruments
- 其它性能指标的关注
一、概述
本文主要是针对 开发阶段
这个线下场景,围绕常见的几个性能指标要点:页面卡顿
、离屏渲染
、耗电优化
、App启动优化
、安装包瘦身
,展开来陈述相关的底层原理和一些治理手段的。关于相关的同一主题的要点,我们会在其它文章,用新的篇幅进行讨论。
二、 iOS中的页面卡顿
2.1 前知识
在展开针对页面卡顿问题如何治理之前,我们需要对页面如何成像有所认识!
关于iOS的成像原理
,我们之前在探索iOS动画渲染原理
的时候有了一定的研究并输出了几篇文章,若是你也感兴趣,欢迎指教:
相关阅读(共计14篇文章)
iOS相关专题
- 01-iOS底层原理|iOS的各个渲染框架以及iOS图层渲染原理
- 02-iOS底层原理|iOS动画渲染原理
- 03-iOS底层原理|iOS OffScreen Rendering 离屏渲染原理
- 04-iOS底层原理|页面卡顿优化-因CPU、GPU资源消耗导致卡顿的原因和解决方案
webApp相关专题
跨平台开发方案相关专题
阶段性总结:Native、WebApp、跨平台开发三种方案性能比较
Android、HarmonyOS页面渲染专题
小程序页面渲染专题
本文简单针对本次要关注的** 页面卡顿
**问题,简单回顾一下 iOS中屏幕成像原理:
我们所看到的成像,都是通过CPU
和GPU
共同协作才能完成的
一般是经过CPU
的计算和处理好的数据,交给GPU
进行渲染,然后放到帧的缓存区
,再被视频控制器读取
,才能显示到我们的屏幕上
在iOS中的帧缓存
属于双缓冲机制
,有前帧缓存和后帧缓存;GPU
会分情况进行选取用哪块缓存,这样执行效率会更高一些。
2.2 了解一下 CPU和GPU:
在屏幕成像的过程中,CPU
和GPU
起着至关重要的作用
CPU(Central Processing Unit,中央处理器)
CPU的主要任务是进行对象的创建和销毁、对象属性的调整、布局计算、文本的计算和排版、图片的格式转换和解码、图像的绘制(Core Graphics)
GPU(Graphics Processing Unit,图形处理器)
GPU的主要任务是对纹理的渲染
2.3 屏幕成像原理
在iOS中的屏幕成像是由许多帧共同组成的。每一帧都会由屏幕先发出一个垂直同步信号
,然后再发出很多行水平同步信号
,每一行水平同步信号
表示处理完一行的数据,直到屏幕发完所有的水平同步信号
,表示这一帧的数据全部处理完成了,再会进行下一轮的垂直同步信号
的发出,表示即将处理下一帧的数据
2.4 卡顿产生的原因
在图像处理过程中,CPU
处理计算数据会消耗一定时间,然后再交由GPU
,而GPU
进行渲染也会花费时间,所以CPU
和GPU
都完成已经消耗了一定的时间;
而屏幕的垂直同步信号
发出的时间如果正好是CPU、GPU
处理完的时间,那么就会完好的先该帧图像显示出来;如果CPU、GPU
处理的时间过长并且没有完全处理完,而这时垂直同步信号
已经发出了,那么就会读取上一帧的数据进行展示,这种现象叫做掉帧;
而该帧没有处理完的数据就只能等下一个垂直同步信号
再进行读取显示了,这中间也会花费一定时间进行等待,这也是掉帧;
掉帧的现象就会造成卡顿,所以我们要想解决卡顿,就要尽量减少CPU
和GPU
的资源消耗
人眼感受不到卡顿的刷帧率平均是60FPS
,表示每秒要刷60帧;通过计算相当于每隔16ms就会有一次VSync信号
,也就是说我们要在16ms内
完成CPU
和GPU
对数据的计算和渲染才行
2.5 卡顿优化
我们从前面的介绍中,不难得知,卡顿的本质 即是 掉帧
。而掉帧
的根本原因 是CPU、GPU计算资源 针对 下一帧 屏幕 显示 工作的支持 不充分,换言之 即可能是 CPU
、GPU
计算资源被不合理使用的现象导致的。因此,我们也需要围绕这两个计算资源着手优化工作。
2.5.1 关于CPU的卡顿优化
- 1.尽量用轻量级的对象
-
- 比如用不到事件处理的地方,可以考虑使用
CALayer
取代UIView
- 比如用不到事件处理的地方,可以考虑使用
-
- 还有能用基本数据类型就不用对象类型等等
- 2.不要频繁地调用UIView的相关属性
-
- 比如
frame、bounds、transform等属性
,尽量减少不必要的修改
- 比如
-
- 尽量提前计算好布局,在有需要时一次性调整对应的属性,不要多次修改属性
-
- 尽量减少使用
Autolayout
,Autolayout
会比直接设置frame
消耗更多的CPU资源
- 尽量减少使用
-
- 其他需要设置的属性最后是能确定时再赋值,不要多次更改
- 3.图片的size最好刚好跟UIImageView的size保持一致
-
- 如果图片本身的大小和我们给予的大小有出入,CPU会去进行伸缩的处理,也是会消耗资源
- 4.控制一下线程的最大并发数量
-
- 不要过多的创建线程,线程的创建和消耗也是会消耗资源的
-
- 尽量保持较少数量的线程,设置好最大并发数
-
- 如果需要长期开启线程来执行任务,可以考虑让线程常驻,并再不需要后再进行统一销毁
-
-
- 线程优化补充: iOS如何高效的使用多线程
-
- 5.尽量把耗时的操作放到子线程
-
- 比如对文本的处理(尺寸计算、绘制),都可以放到异步去做处理,例如下面代码:
// 文字计算
[@"text" boundingRectWithSize:CGSizeMake(100, MAXFLOAT) options:NSStringDrawingUsesLineFragmentOrigin attributes:nil context:nil];
// 文字绘制
[@"text" drawWithRect:CGRectMake(0, 0, 100, 100) options:NSStringDrawingUsesLineFragmentOrigin attributes:nil context:nil];
还有对图片的处理,对图片的解码和绘制都是会消耗性能的
我们经常使用的给UIImage
赋值的方法,其本质是会去进行图片的解码和绘制的,所以我们可以将解码绘制的过程放在子线程来处理,详细代码如下
// imageNamed:底层会进行对图片的解码和绘制
UIImageView *imageView = [[UIImageView alloc] init];
imageView.image = [UIImage imageNamed:@"timg"];
// 换成如下方法
UIImageView *imageView = [[UIImageView alloc] init];
self.imageView = imageView;
dispatch_async(dispatch_get_global_queue(0, 0), ^{
// 获取CGImage
CGImageRef cgImage = [UIImage imageNamed:@"timg"].CGImage;
// alphaInfo
CGImageAlphaInfo alphaInfo = CGImageGetAlphaInfo(cgImage) & kCGBitmapAlphaInfoMask;
BOOL hasAlpha = NO;
if (alphaInfo == kCGImageAlphaPremultipliedLast ||
alphaInfo == kCGImageAlphaPremultipliedFirst ||
alphaInfo == kCGImageAlphaLast ||
alphaInfo == kCGImageAlphaFirst) {
hasAlpha = YES;
}
// bitmapInfo
CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host;
bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;
// size
size_t width = CGImageGetWidth(cgImage);
size_t height = CGImageGetHeight(cgImage);
// context
CGContextRef context = CGBitmapContextCreate(NULL, width, height, 8, 0, CGColorSpaceCreateDeviceRGB(), bitmapInfo);
// draw
CGContextDrawImage(context, CGRectMake(0, 0, width, height), cgImage);
// get CGImage
cgImage = CGBitmapContextCreateImage(context);
// into UIImage
UIImage *newImage = [UIImage imageWithCGImage:cgImage];
// release
CGContextRelease(context);
CGImageRelease(cgImage);
// back to the main thread
dispatch_async(dispatch_get_main_queue(), ^{
self.imageView.image = newImage;
});
});
2.5.2 关于GPU的卡顿优化
1.尽量减少视图数量和层次
比如一个UIView
视图我们需要创建三个图层,减少到两个或者一个更利于GPU
的渲染性能
2.避免短时间内大量图片的显示
我们在处理多张图片时,尽量避免短时间内大量图片的显示,尽可能将多张图片合成一张进行显示
3.GPU纹理尺寸的控制
GPU
能处理的最大纹理尺寸是4096x4096
,一旦超过这个尺寸,就会占用CPU
资源进行处理,所以纹理尽量不要超过这个尺寸
4.减少透明度
减少透明的视图(alpha<1),不透明的就设置opaque为YES
像多个透明的视图,如果有重叠部分,那么重叠部分需要重新计算展示的颜色是什么的,会消耗GPU
资源
5.注意离屏渲染
在OpenGL
中,GPU
有2种渲染方式
- On-Screen Rendering:当前屏幕渲染,在当前用于显示的屏幕缓冲区进行渲染操作
- Off-Screen Rendering:离屏渲染,在当前屏幕缓冲区以外新开辟一个缓冲区进行渲染操作
离屏渲染消耗性能的原因
- 本身
GPU
渲染就会消耗性能 - 需要创建新的缓冲区,又会消耗性能
- 离屏渲染的整个过程,需要多次切换上下文环境,先是从当前屏幕(On-Screen)切换到离屏(Off-Screen);等到离屏渲染结束以后,将离屏缓冲区的渲染结果显示到屏幕上,又需要将上下文环境从离屏切换到当前屏幕
哪些操作会触发离屏渲染?
-
光栅化
layer.shouldRasterize = YES
-
遮罩
layer.mask
-
圆角,同时设置
layer.masksToBounds = YES、layer.cornerRadius大于0
,只满足其中之一不会触发离屏渲染- 考虑通过
CoreGraphics
绘制裁剪圆角,或者叫美工提供圆角图片
- 考虑通过
-
阴影
layer.shadowXXX
- 如果设置了
layer.shadowPath
就不会产生离屏渲染
- 如果设置了
2.5.3 AsyncDisplayKit
我之前专门针对页面卡顿
原理 和 相关 的优化手段写了一篇 文章 。还推荐了保持页面流畅的辅助库:AsyncDisplayKit
。具体的事件页面卡顿优化时也可以参考
三、 iOS中的耗电优化
iOS设备耗电快的本质(不讨论手机📱坏了漏电、电池坏了的情况),其实就是设备 的 硬件部件 在高频工作导致的,因此我们要针对一些常见的功能服务的不合理使用,进而进行合理的治理即可:
我们平时造成电量消耗的主要来源有哪些呢?
一般常见的造成耗电的功能服务有:
CPU、GPU处理,Processing
网络,Networking
定位,Location
处理图像,Graphics
3.1 耗电优化
1.尽可能降低CPU、GPU功耗
- 详情参照上面关于卡顿优化的相关处理
2.少用定时器
- 定时器的使用也会造成一定的电量消耗,因为要一直在程序中监听执行
3.优化I/O操作(文件的读写)
- 尽量
不要频繁
写入小数据,最好批量一次性
写入 - 读写大量重要数据时,考虑用
dispatch_io
,其提供了基于GCD的异步操作文件I/O的API
。用dispatch_io
系统会优化磁盘访问 - 数据量比较大的,建议使用数据库(比如SQLite、CoreData),数据库内部对读写已经做了相应的优化处理了
4.网络优化
减少、压缩网络数据
,可以采用JSON
和protobuf
这样格式相对较小的传输格式- 如果
多次请求的结果是相同的,尽量使用缓存
,可以利用NSCache
来进行缓存 - 使用
断点续传
,否则网络不稳定时可能多次传输相同的内容 - 做好
网络状态的监控,网络不可用时,不要尝试
执行网络请求 - 让用户可以取消长时间运行或者速度很慢的网络操作,
设置合适的超时时间
批量传输
,比如,下载视频流时,不要传输很小的数据包,直接下载整个文件或者一大块一大块地下载。如果下载广告,一次性多下载一些,然后再慢慢展示。如果下载电子邮件,一次下载多封,不要一封一封地下载
5.定位优化
- 如果只是需要快速确定用户位置,最好用
CLLocationManager
的requestLocation方法
。定位完成后,会自动让定位硬件断电 - 如果不是导航应用,尽量不要实时更新位置,定位完毕就关掉定位服务
- 尽量降低定位精度,比如尽量不要使用精度最高的
kCLLocationAccuracyBest
- 需要后台定位时,尽量设置
pausesLocationUpdatesAutomatically为YES
,如果用户不太可能移动的时候系统会自动暂停位置更新 - 尽量不要使用
startMonitoringSignificantLocationChanges
,优先考虑startMonitoringForRegion:
6.硬件检测优化
- 用户移动、摇晃、倾斜设备时,会产生动作(motion)事件,这些事件由加速度计、陀螺仪、磁力计等硬件检测。
- 在不需要检测的场合,应该及时关闭这些硬件
四、 APP的启动优化
4.1APP的启动
在iOS中,我们讨论的APP的启动可以分为2种
冷启动(Cold Launch)
:从零开始启动APP热启动(Warm Launch)
:APP已经在内存中,在后台存活着,再次点击图标启动APP
我们开发者讨论的对APP启动的优化,都是针对冷启动进行的优化
4.2启动监测
我们可以通过Xcode打印分析启动过程:
通过Xcode
添加环境变量可以打印出APP的启动时间分析
- 1.找到路径
Edit scheme -> Run -> Arguments -> Environment Variables
- 2.添加
DYLD_PRINT_STATISTICS
,设置为1 - 3.然后运行程序,可以看到控制台的打印如下
- 如果需要更详细的信息,那就添加
DYLD_PRINT_STATISTICS_DETAILS
设置为1 - 然后查看控制台的打印如下:
上述操作也仅仅是作为一个参考,如果启动时间小于400ms
,那就属于正常范围,如果超出该值,就需要考虑一定的启动优化了
4.3APP的启动过程
APP的冷启动可以概括为3大阶段:
dyld动态库加载
Runtime初始化
进入main函数
,开始整个应用的声明周期
4.4 dyld动态库加载
dyld(dynamic link editor)
,Apple的动态链接器,可以用来装载Mach-O文件(可执行文件、动态库等)
启动APP时,dyld
所做的事情如下:
- 启动APP时,
dyld
会先装载APP的可执行文件,同时会递归加载所有依赖的动态库 - 当
dyld
把可执行文件、动态库都装载完毕后,会通知Runtime
进行下一步的处理
4.5 Runtime初始化
启动APP时,Runtime
所做的事情如下:
Runtime
会调用map_images
进行可执行文件内容的解析和处理- 在
load_images
中调用call_load_methods
,调用所有Class
和Category
的+load方法
- 进行各种
objc
结构的初始化(注册Objc类 、初始化类对象等等) - 调用
C++
静态初始化器和__attribute__((constructor))
修饰的函数 到此为止,可执行文件和动态库中所有的符号(Class,Protocol,Selector,IMP,…)
都已经按格式成功加载到内存中,被Runtime
所管理
4.6 进入main函数
总结一下,整个启动过程可以概述为:
- APP的启动由
dyld
主导,将可执行文件加载到内存,顺便加载所有依赖的动态库 - 并由
Runtime
负责加载成objc
定义的结构 - 然后所有初始化工作结束后,
dyld
就会调用main函数
- 接下来就是
UIApplicationMain函数
,AppDelegate
的application:didFinishLaunchingWithOptions:方法
4.7APP的启动优化方案
按照不同阶段:
4.7.1 dyld动态库加载阶段
- 减少动态库、合并一些动态库(定期清理不必要的动态库)
- 减少
Objc类、分类
的数量、减少Selector
数量(定期清理不必要的类、分类) - 减少
C++
虚函数数量(C++
一旦有虚函数,就会多维护一张虚表) Swift
尽量使用struct
4.7.2 runtime加载阶段
- 用
+initialize方法
和dispatch_once
取代所有的__attribute__((constructor))
、C++
静态构造器、ObjC
的+load
4.7.3 执行main函数阶段
- 在不影响用户体验的前提下,尽可能将一些操作延迟,不要全部都放在
finishLaunching
方法中 - 按需加载
五、安装包瘦身
我们开发的安装包(IPA)主要由可执行文件
、资源组成
在我们日常开发中,项目业务会越来越多,慢慢就会积攒下一些不必要的代码和资源
,我们可以对其进行一定的瘦身优化
5.1 资源(图片、音频、视频等)
我们在使用项目里的资源时,尽量采取无损压缩
的,会适当减少包的大小
当项目里的资源太多了,我们可以通过一些工具来清除无用的或者重复的资源
5.2可执行文件瘦身
我们可以对编译器做一定的优化
Strip Linked Product、Make Strings Read-Only、Symbols Hidden by Default
这几个配置都改为YES
去掉异常支持,Enable C++ Exceptions、Enable Objective-C Exceptions
设置为NO, Other C Flags
添加-fno-exceptions
5.3 我们可以利用一些工具对没有使用的代码进行清除
- 例如
AppCode
,检测未使用的代码:菜单栏 -> Code -> Inspect Code
- 编写
LLVM
插件检测出重复代码、未被调用的代码 - 通过生成
Link Map
文件,可以查看可执行文件的具体组成和大小分析
- 将
Link Map File
的路径改成我们桌面路径,然后将Write Link Map File
改成Yes
然后会生成这么一个文件
由于其文件内容过于庞大,不利于我们分析,可借助第三方工具解析LinkMap
文件:LinkMap
总结
本文 简单介绍了 项目 开发的几个阶段,会用到的 性能关注
手段问题,但本文仅仅针对 线下开发、线下测试阶段 能用到的 性能调试工具
Instrument 做了简单介绍 。
接下来会用几篇文章,围绕几个常见的性能问题,展开对 性能调试工具
Instrument 的 基本使用介绍。
相关系列文章
Instruments
- 01-iOS 性能优化|性能调试工具Instruments简单介绍
- 02-iOS 性能优化|性能调试工具Instruments-CoreAnimation使用
- 03-iOS 性能优化|性能调试工具Instruments-Leaks工具使用
- 04-iOS 性能优化|性能调试工具Instruments-Allocations工具使用 其它性能指标的关注
- 05-iOS 性能优化|常见的几个性能指标要点:页面卡顿、离屏渲染、耗电优化、App启动优化、安装包瘦身
- 06-iOS 性能优化|性能指标监测
文章推荐
综合篇
- 微信读书iOS性能优化
- 微信读书 iOS 质量保证及性能监控
- iOS 性能优化之业务性能监控
- NeteaseAPM iOS SDK技术实现分享
- 魔窗研发副总裁沈哲:移动端SDK的优化之路
- 搜狗输入法 iOS 版开发与优化实践PPT
- 蘑菇街 App 的稳定性与性能实践PPT
卡顿优化
启动优化
- 今日头条iOS客户端启动速度优化
- WWDC 2016 Session笔记:App启动时间优化
- 百度输入法-iOS 启动速度优化
- Facebook iOS App如何优化启动时间
- obj中国-Mach-O 可执行文件
体积优化
网络优化
- 美团点评移动网络优化实践
- 开源版HttpDNS方案详解
- 携程App的网络性能优化实践
- 2016年携程App网络服务通道治理和性能优化实践
- 蘑菇街App Chromium网络栈实践
- iOS 中如何模拟弱网环境