05-iOS 性能优化|常见的几个性能指标要点:页面卡顿、离屏渲染、耗电优化、App启动优化、安装包瘦身

8,509 阅读20分钟

前言

在项目中,直接面向用户的客户端往往是一个项目的门面。因此,在项目开发建设的过程中,为了交付用户体验较佳的客户端App,保障产品交付质量。往往需要我们开发者关注客户端软件的性能指标问题。因此,我们要对应用的性能优化专题有所研究!!
我们通常关注的性能指标有:

  • 页面卡顿
  • 耗电、发热
  • 网络优化
  • 应用启动
  • 安装包瘦身

我们在开发建设项目过程中,可以粗略划分为几个阶段:开发阶段测试阶段维护阶段:

  • 开发阶段,我们要掌握性能调试性能监测的手段,从而保障,在当前稳定版本的客户端软件,有一个比较合理的性能保障;
  • 测试阶段,测试团队等若干同事往往会给我们提出一些用户体验上的反馈和建议,因此,我们需要掌握性能调试的手段,从而改造出比较符合团队要求的产品;
  • 在上线维护阶段,针对已经上线的应用,我们的开发团队要有线上性能监控的能力,从而及时收集不满足性能指标要求的业务交互场景和步骤,捕获具体问题进行分析,从而以此为依据作为有效迭代优化我们客户端的有力助力。

为此,我们本次将会用几篇文章,围绕一些常见的性能指标,去关注 如何调试、如何监测、如何改进处理问题:

一、概述

本文主要是针对 开发阶段 这个线下场景,围绕常见的几个性能指标要点:页面卡顿离屏渲染耗电优化App启动优化安装包瘦身,展开来陈述相关的底层原理和一些治理手段的。关于相关的同一主题的要点,我们会在其它文章,用新的篇幅进行讨论。

二、 iOS中的页面卡顿

2.1 前知识

在展开针对页面卡顿问题如何治理之前,我们需要对页面如何成像有所认识!
关于iOS的成像原理,我们之前在探索iOS动画渲染原理的时候有了一定的研究并输出了几篇文章,若是你也感兴趣,欢迎指教:
相关阅读(共计14篇文章)


iOS相关专题


webApp相关专题


跨平台开发方案相关专题


阶段性总结:Native、WebApp、跨平台开发三种方案性能比较


Android、HarmonyOS页面渲染专题


小程序页面渲染专题

本文简单针对本次要关注的** 页面卡顿 **问题,简单回顾一下 iOS中屏幕成像原理:

我们所看到的成像,都是通过CPUGPU共同协作才能完成的

一般是经过CPU的计算和处理好的数据,交给GPU进行渲染,然后放到帧的缓存区,再被视频控制器读取,才能显示到我们的屏幕上

-w532

在iOS中的帧缓存属于双缓冲机制,有前帧缓存和后帧缓存;GPU会分情况进行选取用哪块缓存,这样执行效率会更高一些。

2.2 了解一下 CPU和GPU:

在屏幕成像的过程中,CPUGPU起着至关重要的作用

CPU(Central Processing Unit,中央处理器)

CPU的主要任务是进行对象的创建和销毁、对象属性的调整、布局计算、文本的计算和排版、图片的格式转换和解码、图像的绘制(Core Graphics)

GPU(Graphics Processing Unit,图形处理器)

GPU的主要任务是对纹理的渲染

2.3 屏幕成像原理

在iOS中的屏幕成像是由许多帧共同组成的。每一帧都会由屏幕先发出一个垂直同步信号,然后再发出很多行水平同步信号,每一行水平同步信号表示处理完一行的数据,直到屏幕发完所有的水平同步信号,表示这一帧的数据全部处理完成了,再会进行下一轮的垂直同步信号的发出,表示即将处理下一帧的数据

-w577

2.4 卡顿产生的原因

在图像处理过程中,CPU处理计算数据会消耗一定时间,然后再交由GPU,而GPU进行渲染也会花费时间,所以CPUGPU都完成已经消耗了一定的时间;

而屏幕的垂直同步信号发出的时间如果正好是CPU、GPU处理完的时间,那么就会完好的先该帧图像显示出来;如果CPU、GPU处理的时间过长并且没有完全处理完,而这时垂直同步信号已经发出了,那么就会读取上一帧的数据进行展示,这种现象叫做掉帧;

而该帧没有处理完的数据就只能等下一个垂直同步信号再进行读取显示了,这中间也会花费一定时间进行等待,这也是掉帧;

掉帧的现象就会造成卡顿,所以我们要想解决卡顿,就要尽量减少CPUGPU的资源消耗

人眼感受不到卡顿的刷帧率平均是60FPS,表示每秒要刷60帧;通过计算相当于每隔16ms就会有一次VSync信号,也就是说我们要在16ms内完成CPUGPU对数据的计算和渲染才行

2.5 卡顿优化

我们从前面的介绍中,不难得知,卡顿的本质 即是 掉帧。而掉帧的根本原因 是CPU、GPU计算资源 针对 下一帧 屏幕 显示 工作的支持 不充分,换言之 即可能是 CPUGPU 计算资源被不合理使用的现象导致的。因此,我们也需要围绕这两个计算资源着手优化工作。

2.5.1 关于CPU的卡顿优化

  • 1.尽量用轻量级的对象
    • 比如用不到事件处理的地方,可以考虑使用CALayer取代UIView
    • 还有能用基本数据类型就不用对象类型等等
  • 2.不要频繁地调用UIView的相关属性
    • 比如frame、bounds、transform等属性,尽量减少不必要的修改
    • 尽量提前计算好布局,在有需要时一次性调整对应的属性,不要多次修改属性
    • 尽量减少使用AutolayoutAutolayout会比直接设置frame消耗更多的CPU资源
    • 其他需要设置的属性最后是能确定时再赋值,不要多次更改
  • 3.图片的size最好刚好跟UIImageView的size保持一致
    • 如果图片本身的大小和我们给予的大小有出入,CPU会去进行伸缩的处理,也是会消耗资源
  • 4.控制一下线程的最大并发数量
    • 不要过多的创建线程,线程的创建和消耗也是会消耗资源的
    • 尽量保持较少数量的线程,设置好最大并发数
    • 如果需要长期开启线程来执行任务,可以考虑让线程常驻,并再不需要后再进行统一销毁
  • 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设备耗电快的本质(不讨论手机📱坏了漏电、电池坏了的情况),其实就是设备 的 硬件部件 在高频工作导致的,因此我们要针对一些常见的功能服务的不合理使用,进而进行合理的治理即可: 我们平时造成电量消耗的主要来源有哪些呢?
一般常见的造成耗电的功能服务有:

image.png

  • 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.网络优化

  • 减少、压缩网络数据,可以采用JSONprotobuf这样格式相对较小的传输格式
  • 如果多次请求的结果是相同的,尽量使用缓存,可以利用NSCache来进行缓存
  • 使用断点续传,否则网络不稳定时可能多次传输相同的内容
  • 做好网络状态的监控,网络不可用时,不要尝试执行网络请求
  • 让用户可以取消长时间运行或者速度很慢的网络操作,设置合适的超时时间
  • 批量传输,比如,下载视频流时,不要传输很小的数据包,直接下载整个文件或者一大块一大块地下载。如果下载广告,一次性多下载一些,然后再慢慢展示。如果下载电子邮件,一次下载多封,不要一封一封地下载

5.定位优化

  • 如果只是需要快速确定用户位置,最好用CLLocationManagerrequestLocation方法。定位完成后,会自动让定位硬件断电
  • 如果不是导航应用,尽量不要实时更新位置,定位完毕就关掉定位服务
  • 尽量降低定位精度,比如尽量不要使用精度最高的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
  • -w933
  • 3.然后运行程序,可以看到控制台的打印如下
  • -w793
  • 如果需要更详细的信息,那就添加DYLD_PRINT_STATISTICS_DETAILS设置为1
  • 然后查看控制台的打印如下:
  • -w853

上述操作也仅仅是作为一个参考,如果启动时间小于400ms,那就属于正常范围,如果超出该值,就需要考虑一定的启动优化了

4.3APP的启动过程

APP的冷启动可以概括为3大阶段:

  • dyld动态库加载
  • Runtime初始化
  • 进入main函数,开始整个应用的声明周期

-w897

4.4 dyld动态库加载

dyld(dynamic link editor),Apple的动态链接器,可以用来装载Mach-O文件(可执行文件、动态库等)

启动APP时,dyld所做的事情如下:

  1. 启动APP时,dyld会先装载APP的可执行文件,同时会递归加载所有依赖的动态库
  2. dyld把可执行文件、动态库都装载完毕后,会通知Runtime进行下一步的处理

4.5 Runtime初始化

启动APP时,Runtime所做的事情如下:

  • Runtime会调用map_images进行可执行文件内容的解析和处理
  • load_images中调用call_load_methods,调用所有ClassCategory+load方法
  • 进行各种objc结构的初始化(注册Objc类 、初始化类对象等等)
  • 调用C++静态初始化器和__attribute__((constructor))修饰的函数 到此为止,可执行文件和动态库中所有的符号(Class,Protocol,Selector,IMP,…)都已经按格式成功加载到内存中,被Runtime所管理

4.6 进入main函数

总结一下,整个启动过程可以概述为:

  1. APP的启动由dyld主导,将可执行文件加载到内存,顺便加载所有依赖的动态库
  2. 并由Runtime负责加载成objc定义的结构
  3. 然后所有初始化工作结束后,dyld就会调用main函数
  4. 接下来就是UIApplicationMain函数AppDelegateapplication:didFinishLaunchingWithOptions:方法

4.7APP的启动优化方案

按照不同阶段:

4.7.1 dyld动态库加载阶段

  1. 减少动态库、合并一些动态库(定期清理不必要的动态库)
  2. 减少Objc类、分类的数量、减少Selector数量(定期清理不必要的类、分类)
  3. 减少C++虚函数数量(C++一旦有虚函数,就会多维护一张虚表)
  4. Swift尽量使用struct

4.7.2 runtime加载阶段

  1. +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

-w621 -w631 -w573

去掉异常支持,Enable C++ Exceptions、Enable Objective-C Exceptions设置为NO, Other C Flags添加-fno-exceptions

-w602 -w592 -w542

5.3 我们可以利用一些工具对没有使用的代码进行清除

  1. 例如AppCode,检测未使用的代码:菜单栏 -> Code -> Inspect Code
  2. 编写LLVM插件检测出重复代码、未被调用的代码
  3. 通过生成Link Map文件,可以查看可执行文件的具体组成和大小分析
  • Link Map File的路径改成我们桌面路径,然后将Write Link Map File改成Yes

-w838

然后会生成这么一个文件

-w682

由于其文件内容过于庞大,不利于我们分析,可借助第三方工具解析LinkMap文件:LinkMap

总结

本文 简单介绍了 项目 开发的几个阶段,会用到的 性能关注 手段问题,但本文仅仅针对 线下开发、线下测试阶段 能用到的 性能调试工具 Instrument 做了简单介绍 。
接下来会用几篇文章,围绕几个常见的性能问题,展开对 性能调试工具 Instrument 的 基本使用介绍。

相关系列文章

Instruments

文章推荐

综合篇

卡顿优化

启动优化

体积优化

网络优化

相关开源库