开发

IOS基础:性能优化

2020-10-22  本文已影响0人  时光啊混蛋_97boy

原创:知识点总结性文章
创作不易,请珍惜,之后会持续更新,不断完善
个人比较喜欢做笔记和写总结,毕竟好记性不如烂笔头哈哈,这些文章记录了我的IOS成长历程,希望能与大家一起进步
温馨提示:由于简书不支持目录跳转,大家可通过command + F 输入目录标题后迅速寻找到你所需要的内容

目录

一、常见优化方案

1、初级

2、中级

// in your .h or inside a class extension
@property (nonatomic, strong) NSDateFormatter *formatter;

// inside the implementation (.m)
// When you need, just use self.formatter
- (NSDateFormatter *)formatter {
    if(! _formatter) {
        _formatter = [[NSDateFormatter alloc] init];
        _formatter.dateFormat = @"EEE MMM dd HH:mm:ss Z yyyy";// twitter date format
    }
    return_formatter;
}
self.view.backgroundColor = [UIColor colorWithPatternImage:[UIImage imageNamed:@"background"]];

3、高级

NSArray *urls = <# An array of file URLs #>;
for(NSURL *url in urls) {
    @autoreleasepool {
        NSError *error;
        NSString *fileContents = [NSString stringWithContentsOfURL:url
                                        encoding:NSUTF8StringEncoding error:&error];
        /* Process the string, creating and autoreleasing more objects. */
    }
}

二、屏幕显示图像的原理及优化方案

cpu的作用

gpu的作用

核心:减少cpu、gpu的资源消耗

1、屏幕显示图像的原理

渲染方式比较
a、GPU渲染流程

GPU(Graphics Processing Unit):又名图形处理器,是显卡的 “核心”。主要负责图像运算工作,具有高并行能力,通过计算将图像显示在屏幕像素中。
工作原理:将 “3D坐标” 转换成 “2D坐标” ,再将 “2D坐标” 转换为 “实际有颜色的像素” 。

GPU渲染流程

工作流水线:顶点着色器 => 形状装配 => 几何着色器 => 光栅化 => 片段着色器 => 测试与混合
顶点着色器(Vertex Shader):确定形状的点
形状装配(Shape Assembly):确定形状的线
几何着色器(Geometry Shader):确定三角形的个数,使之变成几何图形
光栅化(Rasterization):将图转化为一个个实际屏幕像素
片段着色器(Fragment Shader):对屏幕像素点着色
测试与混合(Tests and Blending):检查图层深度和透明度,并进行图层混合

b、IOS原生渲染
❶ 常使用的iOS渲染框架

UIKit:日常开发最常用的UI框架,可以通过设置UIKit组件的布局以及相关属性来绘制界面。其实本身UIView并不拥有屏幕成像的能力,而是View上的CALayer属性拥有展示能力。(UIView继承自UIResponder,其主要负责用户操作的事件响应,iOS事件响应传递就是经过视图树遍历实现的。)
SwiftUI:苹果新推出的一款全新的“声明式UI”框架,使用Swift编写。一套代码,即可完成iOSiPadOSmacOSwatchOS的开发与适配。
Core Animation:核心动画,一个复合引擎。尽可能快速的组合屏幕上不同的可视内容。分解成独立的图层(CALayer),存储在图层树中。
Core Graphics:基于Quartz高级绘图引擎,主要用于运行时绘制图像。
Core Image:运行前图像绘制,对已存在的图像进行高效处理。
OpenGL ES:OpenGL for Embedded Systems,是 OpenGL的子集。可通过C/C++编程操控GPU
Metal:渲染性能比OpenGL ES高。为了解决OpenGL ES不能充分发挥苹果芯片优势的问题。

❷ 原生渲染的流程
iOS原生渲染的整体流程

第一步:更新视图树、图层树。(分别对应View的层级结构、View上的Layer层级结构)

第二步CPU开始计算下一帧要显示的内容(包括视图创建、布局计算、视图绘制、图像解码)。当 runloopkCFRunLoopBeforeWaitingkCFRunLoopExit 状态时,会通知注册的监听,然后对图层打包,打完包后,将打包数据发送给一个独立负责渲染的进程 Render Server。 前面 CPU 所处理的这些事情统称为 Commit Transaction

第三步:数据到达Render Server后会被反序列化,得到图层树,按照图层树的图层顺序、RGBA 值、图层frame来过滤图层中被遮挡的部分,过滤后将图层树转成渲染树,渲染树的信息会转给 OpenGL ES/Metal

原生渲染的流程

第四步Render Server 会调用 GPUGPU 开始进行前面提到的顶点着色器、形状装配、几何着色器、光栅化、片段着色器、测试与混合六个阶段。完成这六个阶段的工作后,就会将 CPUGPU 计算后的数据显示在屏幕的每个像素点上。

c、大前端渲染(WebView、类React Native)
❶ WebView

对于WebView渲染,其主要工作在WebKit中完成。WebKit本身的渲染基于macOSLay Rendering架构,iOS本身渲染也是基于这套架构。因此,本身从渲染的实现方式来说,性能应该和原生差别不大。但为什么我们能明显感觉到使用WebView渲染要比原生渲染的慢呢?

第一,首次加载。会额外多出网络请求和脚本解析工作。 即使是本地网页加载,WebView也要比原生多出脚本解析的工作。 WebView要额外解析HTML+CSS+JavaScript代码。

第二,语言解释执行性能来看。JavaScript的语言解析执行性能要比原生弱。 特别是遇到复杂的逻辑与大量的计算时,WebView 的解释执行性能要比原生慢不少。

第三WebView的渲染进程是独立的,每一帧的更新都要通过IPC调用GPU进程,会造成频繁的IPC进程通信,从而造成性能消耗。并且,两个进程无法共享纹理资源,GPU无法直接使用context光栅化,而必须要等待WebView通过IPCcontext传给GPU再光栅化。因此GPU自身的性能发挥也会受影响。

❷ 类React Native(使用JavaScriptCore引擎做为虚拟机方案)

代表:React NativeWeex、小程序等。以 ReactNative 举例:

React Native的渲染层直接走的是iOS原生渲染,只不过是多了Json+JavaScript脚本解析工作。

JavaScriptCoreiOS 原生与 JS 之间的桥梁,其原本是 WebKit 中解释执行 JavaScript 代码的引擎。

通过JavaScriptCore引擎将“JS”与“原生控件”产生相对应的关联。进而,达成通过JS来操控iOS原生控件的目标。(简单来说,这个json就是一个脚本语言到本地语言的映射表,KEY是脚本语言认识的符号,VALUE是本地语言认识的符号。)

但与WebView 一样,RN也需要面临JS语言解释性能的问题。因此,从渲染效率角度来说,WebView < 类ReactNative < 原生。 (因为json的复杂度比html+css低)

d、Flutter渲染
❶ Flutter的架构
Flutter的架构

可以看到,Flutter重写了UI框架,从UI控件到渲染全部自己重新实现了,不依赖 iOSAndroid 平台的原生控件,依赖Engine(C++)层的Skia图形库与系统图形绘制相关接口,因此,在不同的平台上有了相同的体验。

❷ Flutter的渲染流程
Flutter的渲染流程

简单来说,Flutter的界面由Widget组成,所有Widget会组成Widget Tree。界面更新时,会更新Widget Tree,再更新Element Tree,最后更新RenderObjectTree

Flutter渲染在 Framework层会有 BuildWidget TreeElement TreeRenderObject TreeLayoutPaintComposited Layer 等几个阶段。

FlutterC++ 层,使用 Skia 库,将 Layer 进行组合,生成纹理,使用 OpenGL的接口向GPU 提交渲染内容进行光栅化与合成。

提交到 GPU 进程后,合成计算,显示屏幕的过程和iOS 原生渲染基本是类似的,因此性能上是差不多的。

2、卡顿产生的原因

双缓冲机制GPU 会预先渲染好一帧放入一个缓冲区内,让视频控制器读取,当下一帧渲染好后,GPU会直接把视频控制器的指针指向第二个缓冲器。如此一来效率会有很大的提升。

GPU 通常有一个机制叫做垂直同步(简写也是 V-Sync):当开启垂直同步后,GPU会等待显示器的 VSync信号发出后,才进行新的一帧渲染和缓冲区更新。这样能解决画面撕裂现象,也增加了画面流畅度,但需要消费更多的计算资源,也会带来部分延迟。

卡顿产生的原因

VSync信号到来后,系统图形服务会通过 CADisplayLink 等机制通知AppApp 主线程开始在CPU中计算显示内容,比如视图的创建、布局计算、图片解码、文本绘制等。随后 CPU会将计算好的内容提交到GPU 去,由GPU 进行变换、合成、渲染。随后GPU会把渲染结果提交到帧缓冲区去,等待下一次 VSync 信号到来时显示到屏幕上。由于垂直同步的机制,如果在一个 VSync 时间内,CPU 或者 GPU 没有完成内容提交,则那一帧就会被丢弃,等待下一次机会再显示,而这时显示屏会保留之前的内容不变。这就是界面卡顿的原因。

从上面的图中可以看到,CPUGPU 不论哪个阻碍了显示流程,都会造成掉帧现象。所以开发时,也需要分别对 CPUGPU 压力进行评估和优化。

3、CPU 资源消耗原因和解决方案

a、对象创建

- 如果对象不涉及 UI 操作,则尽量放到后台线程去创建,但可惜的是包含有CALayer 的控件,都只能在主线程创建和操作。

- 通过 Storyboard创建视图对象时,其资源消耗会比直接通过代码创建对象要大非常多

b、对象调整

- 当视图层次调整时,UIViewCALayer 之间会出现很多方法调用与通知,所以在优化性能时,应该尽量避免调整视图层次、添加和移除视图。

c、对象销毁

对象的销毁虽然消耗资源不多,但累积起来也是不容忽视的。通常当容器类持有大量对象时,其销毁时的资源消耗就非常明显。同样的,如果对象可以放到后台线程去释放,那就挪到后台线程去。

d、布局计算
e、Autolayout

Autolayout 是苹果本身提倡的技术,在大部分情况下也能很好的提升开发效率,但是 Autolayout对于复杂视图来说常常会产生严重的性能问题。随着视图数量的增长,Autolayout带来的 CPU 消耗会呈指数级上升。可以使用AsyncDisplayKit框架。

f、文本计算
g、文本渲染

屏幕上能看到的所有文本内容控件,包括 UIWebView,在底层都是通过 CoreText 排版、绘制为 Bitmap 显示的。常见的文本控件 (UILabelUITextView 等),其排版和绘制都是在主线程进行的,当显示大量文本时,CPU 的压力会非常大。

对此解决方案只有一个,那就是自定义文本控件,用 TextKit 或最底层的 CoreText 对文本异步绘制。尽管这实现起来非常麻烦,但其带来的优势也非常大,CoreText 对象创建好后,能直接获取文本的宽高等信息,避免了多次计算(调整 UILabel 大小时算一遍、UILabel 绘制时内部再算一遍);CoreText 对象占用内存较少,可以缓存下来以备稍后多次渲染。

h、图片的解码

pngjpeg这种都是压缩格式,解码就是解压缩的过程,图片解码需要大量计算,耗时长。当你用 UIImageCGImageSource 的那几个方法创建图片时,图片数据并不会立刻解码。图片设置到 UIImageView 或者 CALayer.contents 中去,并且 CALayer 被提交到 GPU 前,CGImage 中的数据才会得到解码。这一步是发生在主线程的,并且不可避免。如果想要绕开这个机制,常见的做法是在后台线程先把图片绘制到 CGBitmapContext 中,然后从Bitmap 直接创建图片。目前常见的网络图片库都自带这个功能。

g、图像的绘制

图像的绘制通常是指用那些以 CG 开头的方法把图像绘制到画布中,然后从画布创建图片并显示这样一个过程。这个最常见的地方就是 [UIView drawRect:] 里面了。由于CoreGraphic 方法通常都是线程安全的,所以图像的绘制可以很容易的放到后台线程进行。

4、GPU 资源消耗原因和解决方案

相对于CPU 来说,GPU 能干的事情比较单一:接收提交的纹理(Texture)和顶点描述(三角形),应用变换(transform)、混合并渲染,然后输出到屏幕上。

a、纹理的渲染

所有的 Bitmap,包括图片、文本、栅格化的内容,最终都要由内存提交到显存,绑定为 GPU Texture。不论是提交到显存的过程,还是 GPU 调整和渲染 Texture 的过程,都要消耗不少 GPU 资源。当在较短时间显示大量图片时(比如 TableView 存在非常多的图片并且快速滑动时),CPU 占用率很低,GPU 占用非常高,界面仍然会掉帧。避免这种情况的方法只能是尽量减少在短时间内大量图片的显示,尽可能将多张图片合成为一张进行显示。

b、视图的混合 (Composing)

当多个视图(或者说 CALayer)重叠在一起显示时,GPU 会首先把他们混合到一起。如果视图结构过于复杂,混合的过程也会消耗很多 GPU 资源。为了减轻这种情况的 GPU 消耗,应用应当尽量减少视图数量和层次,并在不透明的视图里标明 opaque 属性以避免无用的 Alpha 通道合成。当然,这也可以用上面的方法,把多个视图预先渲染为一张图片来显示。

c、图形的生成

CALayerborder、圆角、阴影、遮罩(mask),CASharpLayer 的矢量图形显示,通常会触发离屏渲染(offscreen rendering),而离屏渲染通常发生在 GPU 中。当一个列表视图中出现大量圆角的 CALayer,并且快速滑动时,可以观察到 GPU 资源已经占满,而 CPU 资源消耗很少。这时界面仍然能正常滑动,但平均帧数会降到很低。对于只需要圆角的某些场合,也可以用一张已经绘制好的圆角图片覆盖到原本视图上面来模拟相同的视觉效果。最彻底的解决办法,就是把需要显示的图形在后台线程绘制为图片,避免使用圆角、阴影、遮罩等属性。

触发离屏渲染需要3个条件

触发离屏渲染需要3个条件:
1、contents :设置图片即意味着添加了内容contents
2、背景色 或 border:为什么说是或而不是和,因为他们是2个图层,超过一个图层的渲染就会触发离屏渲染。

❶ 设置圆角触发离屏渲染的情况

情况一:添加内容和设置背景色。

- (void)viewDidLoad
{
    [super viewDidLoad];
    UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(100, 100, 200, 200)];
    imageView.backgroundColor = [UIColor redColor];
    imageView.image = [UIImage imageNamed:@"海贼王"];
    imageView.layer.cornerRadius = 50;//圆角
    imageView.layer.masksToBounds = YES;//裁减
    [self.view addSubview:imageView];
}

如何检测项目中哪些图层触发了离屏渲染?打开模拟器的Color Off-screen Rendered,如果触发了离屏渲染,会有浅黄色背景出现。

如何检测项目中哪些图层触发了离屏渲染

圆角为什么要设置2个属性呢?既然是搭配使用,又是万年重复的代码,一个属性不好吗?

imageView.layer.cornerRadius = 5;//设置圆角
imageView.layer.masksToBounds = YES;//裁减

因为设置layer.cornerRadius只会设置border的圆角,不会设置content的圆角,除非同时设置了layer.masksToBounds = YES

情况二:添加内容和设置border

// imageView.backgroundColor = [UIColor redColor];
imageView.image = [UIImage imageNamed:@"海贼王"];
imageView.layer.cornerRadius = 50;//圆角
imageView.layer.masksToBounds = YES;//裁减
imageView.layer.borderWidth = 2.0;//border宽度
imageView.layer.borderColor = UIColor.blackColor.CGColor;//border颜色

运行效果如下:

添加内容和设置border

情况三:子视图中3个任何一个属性被设置都会触发。
设置背景色:

imageView.layer.cornerRadius = 50;//圆角
imageView.layer.masksToBounds = YES;//裁减
[self.view addSubview:imageView];

UIView *imageViewTwo = [[UIView alloc] initWithFrame:CGRectMake(100, 120, 200, 200)];
imageViewTwo.backgroundColor = UIColor.blueColor;
[imageView addSubview:imageViewTwo];

运行效果如下:

子视图

设置内容:

imageView.layer.cornerRadius = 50;//圆角
imageView.layer.masksToBounds = YES;//裁减
[self.view addSubview:imageView];

UIView *imageViewTwo = [[UIView alloc] initWithFrame:CGRectMake(100, 120, 200, 200)];
imageViewTwo.layer.contents = (__bridge id)([UIImage imageNamed:@"海贼王"].CGImage);
[imageView addSubview:imageViewTwo];

运行效果如下:

子视图

设置边框:

imageView.layer.cornerRadius = 50;//圆角
imageView.layer.masksToBounds = YES;//裁减
[self.view addSubview:imageView];

UIView *imageViewTwo = [[UIView alloc] initWithFrame:CGRectMake(100, 120, 200, 200)];
imageViewTwo.layer.borderWidth = 2.0;//border宽度
imageViewTwo.layer.borderColor = UIColor.blackColor.CGColor;//border颜色
[imageView addSubview:imageViewTwo];

运行效果如下:

子视图
❷设置圆角不会触发离屏渲染的情况

情况一:不添加内容只设置背景色。

imageView.backgroundColor = [UIColor redColor];
// imageView.image = [UIImage imageNamed:@"海贼王"];

设置了背景颜色,仅有一个图层,既然视图只有一个图层,还需要裁减吗,答案是不需要,即layer.masksToBounds = YES;裁剪语句无影响。

不添加内容只设置背景色

情况二:设置了图片,不设置背景色和border

// imageView.backgroundColor = [UIColor redColor];
imageView.image = [UIImage imageNamed:@"海贼王"];

运行效果如下:

设置了图片,不设置背景色和border

情况三:没有设置图片,但设置了背景色和border

imageView.backgroundColor = [UIColor redColor];
imageView.layer.cornerRadius = 50;//圆角
imageView.layer.masksToBounds = YES;//裁减
imageView.layer.borderWidth = 2.0;//border宽度
imageView.layer.borderColor = UIColor.blackColor.CGColor;//border颜色

效果如下:

没有设置图片,但设置了背景色和border
❸ 绘制圆角时出现离屏渲染的解决方案

当前屏幕渲染实现圆角。好处是直接在当前屏幕渲染绘制可以提高性能。实现方式是为UIImage类扩展一个实例方法:

#import "UIImage+CornerRadius.h"

@implementation UIImage (CornerRadius)

//当前屏幕渲染, 扩展UIimage
-(UIImage *)imageWithCornerRadius:(CGFloat)radius ofSize:(CGSize)size
{    
    //边界问题    
    if(radius < 0)    
    {        
        radius = 0;     
    }
    else if (radius > MIN(size.height, size.width))
    {
        //如果radius大于最小边,取最小边的一半
        radius = MIN(size.height, size.width)/2;
    }
    //当前image的可见绘制区域
    CGRect rect = CGRectMake(0, 0, size.width, size.height);
    
    //创建基于位图的上下文
    UIGraphicsBeginImageContextWithOptions(size, NO, UIScreen.mainScreen.scale);//scale:范围

    /*
     //在当前位图的上下文添加圆角绘制路径
     CGContextAddPath(UIGraphicsGetCurrentContext(), [UIBezierPath bezierPathWithRoundedRect:rect cornerRadius:radius].CGPath);
     //当前绘制路径和原绘制路径相交得到最终裁减绘制路径
     CGContextClip(UIGraphicsGetCurrentContext());
     */
    //等效于上面的2句代码
    [[UIBezierPath bezierPathWithRoundedRect:rect cornerRadius:radius] addClip];

    //绘制
    [self drawInRect:rect];

    //取得裁减后的image
    UIImage *image =UIGraphicsGetImageFromCurrentImageContext();

    //关闭当前位图上下文
    UIGraphicsEndImageContext();

    return image;
}

调用方式为:

- (void)viewDidLoad
{
    [super viewDidLoad];

    UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(100, 100, 100, 200)];
    imageView.image = [[UIImage imageNamed:@"海贼王"] imageWithCornerRadius:120 ofSize:imageView.frame.size];
    [self.view addSubview:imageView];
}

运行效果为:

离屏渲染的解决方案

可见,实现了同样的效果,却避免了离屏渲染。


三、检测优化效果

1、如何评测界面的流畅度

a、FPS指示器

如果你需要一个明确的 FPS 指示器,可以尝试一下 KMCGeigerCounter。对于 CPU 的卡顿,它可以通过内置的 CADisplayLink检测出来;对于 GPU 带来的卡顿,它用了一个 SKView 来进行监视。这个项目有两个小问题:SKView 虽然能监视到 GPU 的卡顿,但引入 SKView 本身就会对 CPU/GPU 带来额外的一点的资源消耗;

这里有个简易版的 FPS 指示器:FPSLabel 只有几十行代码,仅用到了 CADisplayLink 来监视 CPU 的卡顿问题。虽然不如上面这个工具完善,但日常使用没有太大问题。

b、GPU Driver

InstumentsGPU Driver 预设,能够实时查看到 CPUGPU 的资源消耗。在这个预设内,你能查看到几乎所有与显示有关的数据,比如 Texture 数量、CA 提交的频率、GPU消耗等,在定位界面卡顿的问题时,这是最好的工具。

c、CADisplayLink

CADisplayLink 监控,结合子线程和信号量,两次事件触发时间间隔超过一个VSync的时长,就上报调用栈。

d、runloop中添加监听

runloop中添加监听,如果kCFRunLoopBeforeSourceskCFRunLoopBeforeWaiting中间的耗时超过VSync的时间,那么就是卡帧了,然后这个时候拿到线程调用栈,看看哪个部分耗时长即可。

2、如何检测内存是否泄漏及使用 / 分配情况

Allocations:监测内存使用 / 分配情况,需要注意到,Allocations是检测程序运行过程中的内存分配情况的,也需要同时运行着程序。
Leaks—动态内存泄露检测:需要一边运行程序,一边检测。一般用静态分析检查过的代码,内存泄露都比较少。
Analyze—静态分析工具:静态分析不需要运行程序,就能检查到存在内存泄露的地方。

常见的三种泄露情形

Xcode提示信息: Value Stored to 'number' is never read 。
翻译一下:存储在'number'里的值从未被读取过。
Xcode提示信息: Value Stored to 'str' during its initialization is never read
Xcode提示信息: Potential leak of an object stored into 'subImageRef' 。 
翻译一下:subImageRef对象的内存单元有潜在的泄露风险。

3、如何检测分析代码的执行时间

目的是检查耗时函数,在开始进行应用程序性能分析前,请一定要使用真机,因为模拟器运行在Mac上,然而Mac上的CPU往往比iOS设备要快。

应用程序一定要运行在Distribution 而不是Debug模式下。在发布环境打包的时候,编译器会引入一系列提高性能的优化,例如去掉调试符号或者移除并重新组织代码。

另外iOS引入一种Watch Dog[看门狗]机制,不同的场景下,“看门狗”会监测应用的性能。如果超出了该场景所规定的运行时间,“看门狗”就会强制终结这个应用的进程。开发者可以crashlog看到对应的日志,但Xcode在调试配置下会禁用Watch Dog

Time Profiler:检测分析代码的执行时间

或者偷懒一点可以使用CACurrentMediaTime()两次的差值计算方法耗时。

4、如何进行APP耗电量检测

a、影响电量的五个因素
b、定时器

使用定时器,每隔一段时间获取一次电量,并上报。

+ (float)getBatteryLevel {
    [UIDevice currentDevice].batteryMonitoringEnabled = YES;
    return [UIDevice currentDevice].batteryLevel;
}
c、Energy Impact工具

第一步:进入手机"设置"->"电池",可以直观的看出来手机应用的耗电情况。

手机"设置"->"电池"

第二步:使用Xcode打开你的工程,然后插上手机,使用真机running,点击Energy Impact

Energy Impact

Energy Impact工具里的参数解释:

d、使用Instrument的Energy Log工具

第一步:打开手机设置,点击“开发者”。

打开手机设置,点击“开发者”

第二步:点击Logging

点击Logging

第三步:勾选Energy,并点击startRecording

勾选Energy,并点击startRecording

第四步:运行需要测试的APP(确保手机消耗的是手机自身的电池),运行3-5分钟,在进入手机设置点击stopRecording

在进入手机设置点击stopRecording

第五步:使用Xcode,把手机和Xcode相连,并打开instruments中的Energy Log,点击工具栏中import Logged Data from Device

点击工具栏中import Logged Data from Device

第六步:得到了电池损耗日志,对于Energy Usage Level的值(0--20),值越大表示越耗电,而CPU Activity表示CPU各种活动。

电池损耗日志

5、如何进行流量检测

本地统计流量,主要有两种实现方案,均存在局限性,需要结合使用。
方案一:Hook
针对URLConnectionCFNetworkNSURLSession三种网络做HookHook的具体技术可以是method swizzle,也可以是Proxy

具体来说就是将流量监测代码插入系统方法实现,method swizzling可以达到这个效果。例如,对于网络请求开始,我们通过监测-[NSURLConnection start]方法就可以统计这一次请求的数据大小。

通过这种方式,可以监控指定类的指定方法,我们可以取得方法调用的时机, 但是程序中除了方法调用还存在方法回调,这是不适合用这种方式监控的情况。

例如NSURLConnection的构造方法和start方法可以通过Method Swizzling监控到, 但是回调消息的接收者delegate的类名不固定,可能是任意一个页面实例, 如果还要使用Method Swizzling的方法来监控,会面对未知个数的页面的delegate方法,不是一个好办法。

解决方法是构造一个回调消息的转发者作为代理,在转发者中收集数据,再转发给用户。

方案二:NSURLProtocol
可以使用 NSURLProtocol对网络请求的拦截,进而得到流量、响应时间等信息,但是NSURLProtocol有自己的局限,比如NSURLProtocol只能拦截NSURLSessionNSURLConnection以及UIWebView,但是对于CFNetwork则无能为力。

NSURLProtocol拦截是监控UIWebView请求最普遍的解决方案。具体可以参考美团技术团队的实现。

优化方案

网络优化分为提速、节流、安全,选择合理网络协议处理专门业务(比如聊天的APP需要用socket)。

提速

节流

安全

6、其他instruments工具

Core Data:监测读取、缓存未命中、保存等操作,能直观显示是否保存次数远超实际需要。
Cocoa Layout:观察约束变化,找出布局代码的问题所在。
Network:跟踪 TCP / IP 和 UDP / IP 连接。
Automations:创建和编辑测试脚本来自动化 iOS 应用的用户界面测试。


四、App启动优化

这部分涉及到很多底层原理,阅读起来比较困难,大家觉得无聊的可略过。

1、iOS应用启动流程

1)解析Info.plist

2)Mach-O加载
先补充Mach-O的文件类型这个知识点:

接着继续分析Mach-O的加载:

3)程序执行

2、App总启动时间 = pre-main耗时 + main耗时

a、pre-main阶段
pre-main阶段
b、这里对dyld进行介绍

❶ 什么是dyld?
动态链接库的加载过程主要由dyld来完成,dyld是苹果的动态链接器

系统先读取App的可执行文件(Mach-O文件),从里面获得dyld的路径,然后加载dylddyld去初始化运行环境,开启缓存策略,加载程序相关依赖库(其中也包含我们的可执行文件),并对这些库进行链接,最后调用每个依赖库的初始化方法,在这一步,runtime被初始化。dyld此时会把App类用到的所有动态库给加载起来,其中有个核心动态库libSystem,每个App都需要它,我们的Runtime就在里面,那么当加载到此动态库时,Runtime就会向dyld注册几个回调函数:

_dyld_objc_notify_register(&map_images, load_images, unmap_image);
dyld

dyld每次往内存中添加新的二进制文件(此时称为image)之后,都会执行这些回调函数,比较重要的回调函数是map_imagesload_imagesmap_images方法里面就会往类的方法列表添加这个类的所有方法(方法是一个结构体,包含了方法名SEL,还有方法实现IMP),除此之外还有很多类的相关操作都在这里面,分类中的方法、协议、属性也是在这个时候添加到对应的类里去的;而load_images方法里主要是调用了一个load方法,所以我们可以发现OC类中load方法的调用时机比main函数都早。

当所有依赖库的初始化后,轮到最后一位(程序可执行文件)进行初始化,在这时runtime会对项目中所有类进行类结构初始化,然后调用所有的load方法。

最后dyld返回main函数地址,main函数被调用,我们便来到了熟悉的程序入口。

验证下loadinitializeMain函数的加载顺序:

+ (void)load
{
    printf("\n RootViewController load()");
}

+ (void)initialize
{
    printf("\n RootViewController initialize()");
}

int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {
        // Setup code that might create autoreleased objects goes here.
        appDelegateClassName = NSStringFromClass([AppDelegate class]);
        printf("\n main()");
    }

    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}

输出结果为:

RootViewController load()
main()
RootViewController initialize()

所以可以确定的是load的确是在在main函数调用之前调用的

❷ dyld共享库缓存
当你构建一个真正的程序时,将会链接各种各样的库。它们又会依赖其他一些framework和动态库。需要加载的动态库会非常多。而对于相互依赖的符号就更多了。可能将会有上千个符号需要解析处理,这将花费很长的时间。

对于每一种架构,操作系统都有一个单独的文件,文件中包含了绝大多数的动态库,这些库都已经链接为一个文件,并且已经处理好了它们之间的符号关系。当加载一个 Mach-O文件 (一个可执行文件或者一个库) 时,动态链接器首先会检查共享缓存看看是否存在其中,如果存在,那么就直接从共享缓存中拿出来使用。每一个进程都把这个共享缓存映射到了自己的地址空间中。这个方法大大优化了 OS XiOS 上程序的启动时间。

❸ dyld加载过程
dyld所需要加载的是动态库列表一个递归依赖的集合。
来看一下QQReader依赖的共享动态库,输入命令:otool -L QQReaderUI

QQReader依赖的共享动态库
c、pre-main阶段流程

❶ 冷启动 - 首次启动:即后台线程中未有当前打开的应用,所有的资源都需要加载并初始化。dyld -> runtime -> main

QQReader的冷启动时间测量结果

❷ 热启动 - 后台激活:即后台线程中保留有当前应用,应用的资源在内存中有保存。通过环境变量DYLD_PRINT_STATISTICS查看启动时间,在 Xcode 中Edit scheme -> Run -> Auguments将环境变量DYLD_PRINT_STATISTICS 设为1。

添加DYLD_PRINT_STATISTICS选项 QQReader的热启动时间测量结果
d、pre-main阶段优化方案

❶ Load dylibs:依赖的dylib越少越好

WMLinkMapAnalyzer分析下linkmap文件。

这个文件可以让你了解整个APP编译后的情况,也许从中可以发现一些异常,还可以用这个文件计算静态链接库在项目里占的大小,有时候我们在项目里链了很多第三方库,导致APP体积变大很多,我们想确切知道每个库占用了多大空间,可以给我们优化提供方向。

LinkMap里有了每个目标文件每个方法每个数据的占用大小数据,所以只要写个脚本,就可以统计出每个.o最后的大小,属于一个.a静态链接库的.o加起来,就是这个库在APP里占用的空间大小

各模块体积大小,从大到小排列,然后就可以根据分析结果决定具体优化模块了:

Core1(xxxx1.o)  256.00M
Core2(xxxx2.o) 208.00M
Core3(xxxx3.o)    64.00M
Core4(xxxx4.o)    20.41M
...

❷ Rebase/Bind

在iOS代码中可能会为同一个类写很多分类方法,由于参与开发同学较多,可能会导致方法重复,但是实际上运行起来只能有一个分类的方法被调用,这取决于哪个分类后被加载,然而编译的二进制代码中,两个方法应该是都存在的,这不仅会增加app体积,也会增加启动时间,所以应该杜绝这样的重复问题;

有很多地方可能是名字不同,但是函数的功能相同,这个不容易被发现,需要大家在写代码的过程中注意;又或者两个函数名字比较接近,里面有很多相似的代码,这种情况下可以进行相同的代码的提取。

可以使用AppCode对工程进行扫描,删除无用代码(未使用的参数、值,未被调用的静态变量、类和方法)**

删除无用代码

❸ Objc setup
❹ Initializers

e、main阶段

main阶段:main方法执行之后到AppDelegate类中的didFinishLaunchingWithOptions方法执行结束前这段时间,主要是构建第一个界面,并完成渲染展示。

main阶段
f、启动耗时的测量

测量main()函数开始执行到didFinishLaunchingWithOptions执行结束的耗时,就需要自己插入代码到工程中了。

❶ 先在main()函数里用变量StartTime记录当前时间

CFAbsoluteTime StartTime;
int main(int argc, char * argv[]) {
      StartTime = CFAbsoluteTimeGetCurrent();
}

❷ 再在AppDelegate.m文件中用extern声明全局变量StartTime

extern CFAbsoluteTime StartTime;

❸ 最后在didFinishLaunchingWithOptions里,再获取一下当前时间,与StartTime的差值即是main()阶段运行耗时

double launchTime = (CFAbsoluteTimeGetCurrent() - StartTime);
g、main阶段优化方案:

这一阶段的优化主要是减少didFinishLaunchingWithOptions方法里的工作,在didFinishLaunchingWithOptions方法里,我们会创建应用的window,指定其rootViewController,调用windowmakeKeyAndVisible方法让其可见。由于业务需要,设置系统UI风格,检查是否需要显示引导页、是否需要登录、是否有新版本等,这里的代码容易变得比较庞大,启动耗时难以控制。

具体优化方案如下:

实际例子:
QQReaderdidFinishLaunchingWithOptions有将近30多个启动模块,其中耗时最多的前6个模块耗时占比将近86%,对这主要的6个模块进行逐个分析,比如字体加载模块、打点上报模块等采用懒加载的方式进行优化。

3、阿里数据iOS端启动速度优化实践

a、pre-main阶段的优化

使用了一个叫做fui(Find Unused Imports)的开源项目,它能很好的分析出不再使用的类,准确率非常高,唯一的问题是它处理不了动态库和静态库里提供的类,也处理不了C++的类模板。

使用方法是在Terminalcd到项目所在的目录,然后执行fui find,然后等上那么几分钟(是的你没有看错,真的需要好几分钟甚至需要更长的时间),就可以得到一个列表了。由于这个工具还不是100%靠谱,可根据这个列表,在Xcode中手动检查并删除不再用到的类。

原来启动时间:

原来启动时间

pre-main阶段优化之后的启动时间:

pre-main阶段优化之后的启动时间
b、main()阶段的优化

通过instrumentsTime Profiler分析,优化后启动速度有明显提升,didFinishLaunchingWithOptions耗时在75ms左右。其中目前耗时最多的是快捷密码验证页(PAPasscodeViewController)的创建&布局,其次是DTLaunchViewControlle里对是否要显示广告页的判断代码。可以看到PAPasscodeViewControllerviewDidAppear耗时了78ms,但已经没有太大关系,此时用户已经看到了页面,准备去验证指纹/密码了。

main()阶段的优化

# 五、音视频的优化方案

资源文件是放置在应用程序本地与应用程序一起编译、 打包和发布的非程序代码文件,如应用 中用到的声音、 视频、图片和文本文件,本地资源文件编译后,会放置于应用程序包文件中( 即<应用名>.app文件)。

1、图片文件优化

图片文件优化

苹果推荐使用PNG格式,设定编译参数Compress PNG Files

Finder中查看该文件的属性,它是一个320 X 480px、 大小为 317 KBPNG图片,在编译之后的目录中找到lmageFile.app包文件。打开包文件,查看目录中background.png文件的属性,可以发现该文件是205 KBPNG图片了。说明Xcode工具可以在编译时优化PNG图片,但是即便经过优化和压缩的PNG图片文件,也比JPEG图片文件大得多。

如果是分布在网络云服务器中的资源文件,应用在加载这些 图片时,会从网络上下载到本地,这时候JPEG就很有优势了。在本地资源的情况下,我们应该优先使用 PNG格式文件,如果资源来源于网络,最好采用JPEG 格式文件。

+imageNamed:方法会在内存中建立缓存,这些缓存直到应用停止才清除 。 如果是贯穿 整个应用的图片(如图标、 logo等),推荐使用 +imageNamed:创建,如果是仅使用 一 次的图片,推荐使用构造函数 - initWithContentsOfFile:创建。

2、音频文件优化

WAV文件:由于文件较大,不太适合移动设备这些存储容量小的设备
MP3:有损压缩格式,适合于移动设备这些存储容量小的设备
CAFF:苹果开发的专门用于macOS和iOS系统的无压缩音频格式,它被设计用来替换老的WAV格式
AIFF:压缩格式是AIFF-C (或AIFC),将数据以4 : I压缩率进行压缩,应用于macOS和iOS系统

背景音乐会在应用中反复播放,它会 一 直驻留在内存中并耗费 CPU,所以更合适比较小的文件,需要进行压缩。

压缩文件主要有 AIFCMP3这两种格式,一般我们首选 AIFC,因为这是苹果推荐的格式。原始文件格式不一定是AIFC,这种情况下我们需要使用afconvert工具。将其转换为AIFC格式,终端中执行如下命令 :

//-f AIFC参数用于转换为AIFC格式
//-d ima4参数指定解码方式
//Fx08822_cast.wav是要转换的源文件
//转换成功后, 会在相同目录下生成Fx08822_cast.aifc文件
$ afconvert -f AIFC-d ima4 Fx08822_cast.wav

MP3本身是有损压缩,如果再经过afconvert转换,音频的质量会受到影响。

音乐特效优化,如发射子弹、敌人被打死或按钮点击等发出的声音,这些声音都是比较短的,追求震撼的 3D效果,可以采用苹果专用的无压缩CAFF格式文件,可以使用 afconvert工具将其转换为 CAFF格式:

//-f caff参数用千转换为CAFF格式
//-d LEI16参数指定解码方式
//Fx08822_cast.wav是要转换的源文件
//音频的采样频率为 22 050Hz
$ afconvert -f caff -d LEI16 Fxo8822_cat.wav

综上,音频文件在使用本地资源的情况下,应用背景音乐时AIFC格式是首选,应用于音乐特效时CAFF格式 是首选 。 如果资源来源于网络,最好采用MP3格式文件。


Demo

Demo在我的Github上,欢迎下载。
PerformanceOptimizationDemo

参考文献

关于性能优化
iOS 保持界面流畅的技巧
浅谈 GPU 及 “App渲染流程”
iOS耗电量检测与优化
iOS性能分析和优化工具Instruments
移动端监控体系之技术原理剖析
iOS启动优化
阿里数据iOS端启动速度优化的一些经验

上一篇 下一篇

猜你喜欢

热点阅读