iOS底层-性能优化
CPU和GPU
- 在屏幕成像的过程中,CPU和GPU起着至关重要的作用
- CPU(Central Processing Unit,中央处理器)
- 对象的创建和销毁、对象属性的调整、布局计算、文本的计算和排版、图片的格式转换和解码、图像的绘制(Core Graphics)
- GPU(Graphics Processing Unit,图形处理器)
- 纹理的渲染
- 手机屏幕上显示的文字和图片都是CPU和GPU配合处理显示出来的
- CPU负责计算,比如:文字的大小、颜色、位置,图片的解码等
- CPU将计算好的数据提交给GPU,GPU将计算好的数据进行渲染
- 只有通过GPU渲染过的才能显示到屏幕上
- CPU直接计算出来的数据是无法显示在屏幕上的
- 屏幕想要显示数据必须有固定的格式
- GPU专门是处理这个的,将CPU计算好的数据处理成屏幕上可以显示的格式
- 这个过程叫做渲染
- GPU将渲染后的数据放在了帧缓存中(一个缓冲区)
- 视屏控制器从帧缓存中读取数据,然后直接将这些数据显示在屏幕上
- 在iOS中是双缓冲机制,有前帧缓存、后帧缓存
- 意思就是iOS有2块帧缓存,可以相互协调
- 这样效率比较高
屏幕成像的原理
- 屏幕上的画面都是帧一帧的显示的,就像电影交卷一样
- 要显示一帧画面时,先发出垂直同步信号,这就意味着要显示一页的数据
- 然后发出一条水平同步信号,填充一行
- 接着再次发出一条水平同步信号,填充下一行,直到填满整个帧
- 然后在发出一条垂直同步信号,显示下一帧。。。
- 每一帧连起来,就是画面了
卡顿产生的原因
-
下图
- 垂直信号的发送频率是固定的,按一定的周期发送
- 每次垂直信号来时水平同步信号就会将GPU计算(渲染)出来的数据填充到当前帧
- 这就要保证GPU要在下一次垂直信号发出之前将数据处理(渲染)好
- 如果当前帧的垂直信号来了,但是GPU仍然没有处理好当前帧数据,那么当前帧就不能被填充,即刷新界面,那么当前屏幕显示的还是上一帧的画面,GUP处理完了,只能等到下一帧垂直信号来时,才能刷帧,然而当前帧的信号就会丢失了,这就是丢帧。从而也导致了屏幕卡顿
- 因此,卡顿主要的原因是因为:CPU、GPU消耗的时间太长,大于垂直同步信号的周期
- 总结UI卡顿的原因
- iOS设备的硬件时钟会发出Vsync(垂直同步信号)
- 然后App的CPU会去计算屏幕要显示的内容,之后将计算好的内容提交到GPU去渲染。
- 随后,GPU将渲染结果提交到帧缓冲区,等到下一个VSync到来时将缓冲区的帧显示到屏幕上。
- 也就是说,一帧的显示是由CPU和GPU共同决定的。
- 一般来说,页面滑动流畅是60fps,也就是1s有60帧更新,即每隔16.7ms就要产生一帧画面,而如果CPU和GPU加起来的处理时间超过了16.7ms,就会造成掉帧甚至卡顿。
- 1s = 1000 ms ,每帧需要 1000/60= 16.7ms
- 因此,每隔16.7ms就要产生一帧画面
- 卡顿解决的主要思路
- 尽可能减少CPU、GPU资源消耗
- 按照60FPS的刷帧率,每隔16.7ms就会有一次VSync信号
卡顿优化 - CPU
- 尽量用轻量级的对象,比如用不到事件处理的地方,可以考虑使用CALayer取代UIView
- CALayer是UIView的属性,CALayer是负责显示内容的,UIView是负责事件处理的
- 如果只需要显示内容,不需要事件处理,那么直接用CALayer即可
- 不要频繁地调用UIView的相关属性,比如frame、bounds、transform等属性,尽量减少不必要的修改
- 尽量提前计算好布局,在有需要时一次性调整对应的属性,不要多次修改属性
- Autolayout会比直接设置frame消耗更多的CPU资源
- 图片的size最好刚好跟UIImageView的size保持一致
- 控制一下线程的最大并发数量
- 尽量把耗时的操作放到子线程
-
文本处理(尺寸计算、绘制)
- (void)text{ // 文字计算 [@"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:@"timg"];
- 这个方法其实是不能直接显示图片的,这个是压缩后图片的二进制数据
- 要想显示还需要解码成屏幕所需要的格式,而且这个解码还是在主线程进行的,这个图片比较大的话也会造成卡顿
- 解决办法
- 提前对图片进行解码
- 解码过程放在子线程,主线程渲染显示。
- 网上的很多框架就是这么干的
-
举例:
- (void)image { UIImageView *imageView = [[UIImageView alloc] init]; imageView.frame = CGRectMake(100, 100, 100, 56); [self.view addSubview:imageView]; self.imageView = imageView; //图片的解码 dispatch_async(dispatch_get_global_queue(0, 0), ^{ // 获取CGImage // [UIImage imageWithData:[NSData dataWithContentsOfURL:<#(nonnull NSURL *)#>]] //网络图片 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; }); }); }
-
卡顿优化 - GPU
- 尽量避免短时间内大量图片的显示,尽可能将多张图片合成一张进行显示
- GPU能处理的最大纹理尺寸是4096x4096,一旦超过这个尺寸,就会占用CPU资源进行处理,所以纹理尽量不要超过这个尺寸
- 尽量减少视图数量和层次
- 减少透明的视图(alpha<1),不透明的就设置opaque为YES
- 尽量避免出现离屏渲染
离屏渲染
- 在OpenGL中,GPU有2种渲染方式
- On-Screen Rendering:当前屏幕渲染,在当前用于显示的屏幕缓冲区进行渲染操作
- Off-Screen Rendering:离屏渲染,在当前屏幕缓冲区以外新开辟一个缓冲区进行渲染操作
- 离屏渲染消耗性能的原因
- 需要创建新的缓冲区
- 离屏渲染的整个过程,需要多次切换上下文环境,先是从当前屏幕(On-Screen)切换到离屏(Off-Screen);等到离屏渲染结束以后,将离屏缓冲区的渲染结果显示到屏幕上,又需要将上下文环境从离屏切换到当前屏幕
- 另外由于离屏渲染会增加GPU的工作量,可能会导致CPU+GPU的处理时间超出16.7ms,导致掉帧卡顿。
- 哪些操作会触发离屏渲染?
- 光栅化:
layer.shouldRasterize = YES
- 遮罩:
layer.mask
- 圆角,同时设置
layer.masksToBounds = YES、layer.cornerRadius
大于0- 考虑通过CoreGraphics绘制裁剪圆角,或者叫美工提供圆角图片
- 阴影:
layer.shadowXXX
- 如果设置了
layer.shadowPath
就不会产生离屏渲染
- 如果设置了
- 光栅化:
卡顿检测
- 平时所说的“卡顿”主要是因为在主线程执行了比较耗时的操作
- 可以添加Observer到主线程RunLoop中,通过监听RunLoop状态切换的耗时,以达到监控卡顿的目的
- demo示例:https://github.com/UIControl/LXDAppFluecyMonitor
耗电优化
- 耗电的主要来源
- CPU处理,Processing
- 网络,Networking
- 定位,Location
- 图像,Graphics
- 耗电优化
- 尽可能降低CPU、GPU功耗
- 少用定时器
- 优化I/O操作
- 尽量不要频繁写入小数据,最好批量一次性写入
- 读写大量重要数据时,考虑用dispatch_io,其提供了基于GCD的异步操作文件I/O的API。用dispatch_io系统会优化磁盘访问
- 数据量比较大的,建议使用数据库(比如SQLite、CoreData)
- 网络优化
- 减少、压缩网络数据
- 如果多次请求的结果是相同的,尽量使用缓存
- 使用断点续传,否则网络不稳定时可能多次传输相同的内容
- 网络不可用时,不要尝试执行网络请求
- 让用户可以取消长时间运行或者速度很慢的网络操作,设置合适的超时时间
- 批量传输,比如,下载视频流时,不要传输很小的数据包,直接下载整个文件或者一大块一大块地下载。如果下载广告,一次性多下载一些,然后再慢慢展示。如果下载电子邮件,一次下载多封,不要一封一封地下载
- 定位优化
- 如果只是需要快速确定用户位置,最好用CLLocationManager的requestLocation方法。定位完成后,会自动让定位硬件断电
- 如果不是导航应用,尽量不要实时更新位置,定位完毕就关掉定位服务
- 尽量降低定位精度,比如尽量不要使用精度最高的kCLLocationAccuracyBest
- 需要后台定位时,尽量设置pausesLocationUpdatesAutomatically为YES,如果用户不太可能移动的时候系统会自动暂停位置更新
- 尽量不要使用startMonitoringSignificantLocationChanges,优先考虑startMonitoringForRegion:
- 硬件检测优化
- 用户移动、摇晃、倾斜设备时,会产生动作(motion)事件,这些事件由加速度计、陀螺仪、磁力计等硬件检测。在不需要检测的场合,应该及时关闭这些硬件
启动优化
- APP的启动可以分为2种
- 冷启动(Cold Launch):从零开始启动APP
- 热启动(Warm Launch):APP已经在内存中,在后台存活着,再次点击图标启动APP
- APP启动时间的优化,主要是针对冷启动进行优化
- WWDC 2016 中首次出现了 App 启动优化的话题,其中提到:
- App 启动最佳速度是400ms以内,因为从点击 App 图标启动,然后 Launch Screen 出现再消失的时间就是400ms;
- App 启动最慢不得大于20s,否则进程会被系统杀死;(启动时间最好以 App 所支持的最低配置设备为准。)
- WWDC 2016 中首次出现了 App 启动优化的话题,其中提到:
- 通过添加环境变量可以打印出APP的启动时间分析Xcode自带(Edit scheme -> Run -> Arguments->Environment Variables)
-
添加字段:DYLD_PRINT_STATISTICS设置为1
Total pre-main time: 1.7 seconds (100.0%) //pre-main指在main函数之前所消耗的时间,总时间 dylib loading time: 157.53 milliseconds (8.8%) rebase/binding time: 135.50 milliseconds (7.6%) ObjC setup time: 610.08 milliseconds (34.4%) initializer time: 869.34 milliseconds (49.0%) slowest intializers : //比较慢的加载 libSystem.B.dylib : 21.53 milliseconds (1.2%) libMainThreadChecker.dylib : 239.20 milliseconds (13.4%) libglInterpose.dylib : 338.49 milliseconds (19.0%) libMTLInterpose.dylib : 79.28 milliseconds (4.4%) VRSHOW : 187.95 milliseconds (10.6%)
-
如果需要更详细的信息,那就添加字段将DYLD_PRINT_STATISTICS_DETAILS设置为1
-
- pre-main 阶段
- pre-main 阶段指的是从用户唤起 App 到 main() 函数执行之前的过程。
APP启动阶段
- APP的冷启动可以概括为3大阶段
- dyld
- runtime
- main
- dyld
- dyld(dynamic link editor),Apple的动态链接器(ios/MAC都有),可以用来装载Mach-O文件(可执行文件、动态库等)
- 其本质也是 Mach-O 文件,一个专门用来加载 dylib 文件的库。
- dyld 位于 /usr/lib/dyld,可以在 mac 和越狱机中找到。
- dyld 会将 App 依赖的动态库和 App 文件加载到内存后执行。
- 启动APP时,dyld所做的事情有
- 装载APP的可执行文件(Mach-O),同时会递归加载所有依赖的动态库
- 当dyld把可执行文件、动态库都装载完毕后,会通知Runtime进行下一步的处理
- dyld(dynamic link editor),Apple的动态链接器(ios/MAC都有),可以用来装载Mach-O文件(可执行文件、动态库等)
- runtime
- 启动APP时,runtime所做的事情有
- 解析可执行文件Mach-O:调用map_images进行可执行文件(Mach-O)内容的解析和处理
- 装载类:在load_images中调用call_load_methods,调用所有Class和Category的+load方法
- 初始化: 进行各种objc结构的初始化(注册Objc类 、初始化类对象等等)
- 调用C++静态初始化器和__attribute__((constructor))修饰的函数
- 到此为止,可执行文件和动态库中所有的符号(Class,Protocol,Selector,IMP,…)都已经按格式成功加载到内存中,被runtime 所管理
- 启动APP时,runtime所做的事情有
- main
- APP的启动由dyld主导,将可执行文件加载到内存,顺便加载所有依赖的动态库
- 并由runtime负责加载成objc定义的结构
- 所有初始化工作结束后,dyld就会调用main函数
- 接下来就是
UIApplicationMain
函数,AppDelegate
的application:didFinishLaunchingWithOptions:
方法
- APP的启动优化
- 按照不同的阶段
- dyld
- 减少动态库、合并一些动态库(定期清理不必要的动态库)
- 减少Objc类、分类的数量、减少Selector数量(定期清理不必要的类、分类)
- 减少C++虚函数数量
- Swift尽量使用struct
- runtime
- 用+initialize方法和dispatch_once取代所有的__attribute__((constructor))、C++静态构造器、ObjC的+load
- main
- 在不影响用户体验的前提下,尽可能将一些操作延迟,不要全部都放在finishLaunching方法中
- 按需加载
- dyld
- 按照不同的阶段
知识点
Mach-O
- Mach-O(Mach Object File Format)是一种用于记录可执行文件、对象代码、共享库、动态加载代码和内存转储的文件格式。App 编译生成的二进制可执行文件就是 Mach-O 格式的,iOS 工程所有的类编译后会生成对应的目标文件 .o 文件,而这个可执行文件就是这些 .o 文件的集合。
-
在 Xcode 的控制台输入以下命令,可以打印出运行时所有加载进应用程序的 Mach-O 文件。
image list -o -f
- Mach-O 文件主要由三部分组成:
- Mach header:描述 Mach-O 的 CPU 架构、文件类型以及加载命令等;
- Load commands:描述了文件中数据的具体组织结构,不同的数据类型使用不同的加载命令;
-
Data:Data 中的每个段(segment)的数据都保存在这里,每个段都有一个或多个 Section,它们存放了具体的数据与代码,主要包含这三种类型:
__TEXT 包含 Mach header,被执行的代码和只读常量(如C 字符串)。只读可执行(r-x)。 __DATA 包含全局变量,静态变量等。可读写(rw-)。 __LINKEDIT 包含了加载程序的元数据,比如函数的名称和地址。只读(r–-)。
dyld shared cache
- dyld shared cache 就是动态库共享缓存。当需要加载的动态库非常多时,相互依赖的符号也更多了,为了节省解析处理符号的时间,OS X 和 iOS 上的动态链接器使用了共享缓存。OS X 的共享缓存位于 /private/var/db/dyld/,iOS 的则在 /System/Library/Caches/com.apple.dyld/。
- 当加载一个 Mach-O 文件时,dyld 首先会检查是否存在于共享缓存,存在就直接取出使用。每一个进程都会把这个共享缓存映射到了自己的地址空间中。这种方法大大优化了 OS X 和 iOS 上程序的启动时间。
images
- images 在这里不是指图片,而是镜像。每个 App 都是以 images 为单位进行加载的。images 类型包括:
- executable:应用的二进制可执行文件;
- dylib:动态链接库;
- bundle:资源文件,属于不能被链接的 dylib,只能在运行时通过 dlopen() 加载。
安装包优化
- 安装包(IPA)主要由可执行文件、资源组成
资源(图片、音频、视频等)优化
- 采取无损压缩(ImageOptim、PPDuck)
- 去除没有用到的资源: https://github.com/tinymind/LSUnusedResources
可执行文件瘦身
- 编译器优化
Strip Linked Product、Make Strings Read-Only、Symbols Hidden by Default
设置为YES (当前的Xcode已经打开了)- 去掉异常支持,
Enable C++ Exceptions、Enable Objective-C Exceptions
设置为NO,Other C Flags
添加-fno-exceptions
- 利用
AppCode(https://www.jetbrains.com/objc/)
检测未使用的代码:菜单栏 -> Code -> Inspect Code- AppCode也是一个开发iOS的IDE,下载安装包安装,该软件收费,但是可以试用
- 通过该IDE打开项目,然后:菜单栏 -> Code -> Inspect Code,能够检查出当前项目那些类没有使用
- 编写LLVM插件检测出重复代码、未被调用的代码
- LinkMap
-
生成LinkMap文件,可以查看可执行文件的具体组成
-
可借助第三方工具解析LinkMap文件: https://github.com/huanxsd/LinkMap
- 这是一个mac项目,然后将第一步生成的LinkMap文件导入即可
- 可以查看每个类占用的大小
-