本文内容分为两大部分:1 ios常用性能分析工具; 2 性能优化常见套路。所以如果对第1部分不感兴趣,可以直接看第2部分。
ios中性能优化常用的instrument工具
现代管理学之父彼得德鲁克曾经说过:“如果你不能衡量,那么你就不能有效增长”。类似的,如果对当前程序的代码执行耗时、特定列表的滑动fps数据都不了解,就不能高效地对代码进行优化,所以在讲解具体的性能优化策略前,先来介绍几个笔者常用的性能测试工具。
。
time profile
time profile是一款用于测量特定的一段时间内,cpu所有核心上的所有在运行的线程的函数调用栈的工具。换句话说,它只统计一段时间内被执行的函数或者方法的调用次数。举个例子,我们想要了解下面代码执行时占用cpu的具体情况。
void method3() {
}
void method2(BOOL shouldCallMethod3) {
if (shouldCallMethod3) {
method3();
}
}
void method1() {
method2(NO);
method3();
method2(NO);
}
int main(int argc, char * argv[]) {
method1();
method2(YES);
method1();
}
我们可以通过time profile工具进行分析,下面几张图展示的是time profile抓取到的main函数在执行过程中的cpu在不同时间点所执行的函数调用栈。
从上面几张图可知,time profile抓取到的结果是:在5ms时间段内,main()函数在cpu中出现了5次,method1()函数出现了4次,method2()函数出现了5次(4+1次)。而method3()函数并没有被time profile抓取到。由此可知:1 time profile并不会抓取所有的函数调用; 2 time profile只专注于cpu的使用上,并不统计方法的执行耗时;3 timep profile只是把所有抓取到的函数调用情况进行聚合,最终呈现出一段时间内比较耗时或者执行次数较多的函数调用栈,进而我们就可以知道哪些函数会比较耗时,从而进行针对性优化。
我们通过下面一个例子来了解time profile界面中常用的几个概念。
@interface My01CollectionViewController : UICollectionViewController
@end
@interface My01CollectionViewController () {
NSArray *colorArray;
}
@end
@implementation My01CollectionViewController
static NSString * const reuseIdentifier = @"Cell Identifier";
- (instancetype)init{
// 设置流水布局
UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc]init];
// 定义大小
layout.itemSize = CGSizeMake(100, 100);
// 设置最小行间距
layout.minimumLineSpacing = 2;
// 设置垂直间距
layout.minimumInteritemSpacing = 2;
// 设置滚动方向(默认垂直滚动)
layout.scrollDirection = UICollectionViewScrollDirectionVertical;
return [self initWithCollectionViewLayout:layout];
}
- (void)viewDidLoad {
[super viewDidLoad];
[self.collectionView registerClass:[UICollectionViewCell class] forCellWithReuseIdentifier:reuseIdentifier];
self.collectionView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
const NSInteger numberOfColors = 60;
NSMutableArray *tmpArray = [NSMutableArray arrayWithCapacity:numberOfColors];
for (NSInteger i = 0; i < numberOfColors; ++i) {
CGFloat redValue = (arc4random() % 255) / 255.0f;
CGFloat blueValue = (arc4random() % 255) / 255.0f;
CGFloat greenValue = (arc4random() % 255) / 255.0f;
[tmpArray addObject:[UIColor colorWithRed:redValue green:greenValue blue:blueValue alpha:1.0f]];
}
colorArray = tmpArray;
UIBarButtonItem *item = [[UIBarButtonItem alloc] initWithTitle:@"setContentOffset" style:UIBarButtonItemStylePlain target:self action:@selector(setContentOffsetaa)];
self.navigationItem.rightBarButtonItem = item;
}
- (void)setContentOffsetaa {
[CATransaction begin];
[CATransaction setAnimationTimingFunction:[CAMediaTimingFunction functionWithControlPoints:0.25 :0.1 :0.25 :1]];
[UIView animateWithDuration:4.3 animations:^{
[self.collectionView setContentOffset:CGPointMake(0, 200)];
}];
[CATransaction commit];
}
#pragma mark <UICollectionViewDataSource>
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
#warning Incomplete implementation, return the number of items
return colorArray.count;
}
- (void)collectionView:(UICollectionView *)collectionView willDisplayCell:(UICollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath {
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:reuseIdentifier forIndexPath:indexPath];
cell.backgroundColor = colorArray[indexPath.item];
return cell;
}
@end
打开time profile并运行上面的代码,然后选取包含“上面代码执行”的时间段,如下图中的选中时间段所示。下图的下半部分的Weight列、Weight左边的那一列(假设称为Time列)、Self Weight列的含义描述请看图中的红色文字。
因为time profile工具每隔1ms才会抓取cpu的所有核心上正在运行的函数调用栈,所以如果线程处于阻塞状态(非running状态),那么该线程对应的函数栈就不会出现在cpu的核心里面,进而time profile就不能抓取到该线程的执行情况,所以time profile并不能统计方法的执行耗时。
points of interest & signpost
time profile并不能统计方法的执行耗时,但points of interest和signpost这两个工具可以。
signpost
signpost的用法很简单,举例如下。
#include <os/signpost.h>
#import <sys/kdebug_signpost.h>
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
os_log_t signPostLog;
uint64_t signPostPid;
signPostLog = os_log_create("com.csx.test.signpost", "csx_monitor");
signPostPid = os_signpost_id_generate(signPostLog);
if (@available(iOS 12, *)) {
os_signpost_interval_begin(signPostLog, signPostPid, "耗时打点标识");
}
NSLog(@"开始睡眠");
[NSThread sleepForTimeInterval:1];
NSLog(@"结束睡眠");
if (@available(iOS 12, *)) {
os_signpost_interval_end(signPostLog, signPostPid, "耗时打点标识");
}
}
@end
运行结果如下图所示,我们可以直观的看出os_signpost_interval_begin函数和os_signpost_interval_end函数之间代码的执行耗时为1s。
signpost虽然signpost能统计某段代码之间的耗时,但我们并不能直观的知道signpost的执行在哪个时间范围。为什么要知道“signpost的执行在哪个时间范围”呢?因为一旦我们可以知道signpost的执行在哪个时间范围,我们就可以选中这个时间范围,然后选择time profile或者thread state trace等工具来看该时间范围内的程序执行情况,进而指导我们继续优化代码。那么,怎么直观地查看我们的耗时打点方法在哪个时间段内被执行呢?请使用points of interest工具。
points of interest
前面提到:因为time profile工具每隔1ms才会抓取cpu的所有核心上正在运行的函数调用栈,所以如果线程处于阻塞状态(非running状态),那么该线程对应的函数栈就不会出现在cpu的核心里面,进而time profile就不能抓取到该线程的执行情况。此时我们就可以使用signpost来统计那些“内部有阻塞操作”的函数的执行耗时。例子如下代码所示。
#include <os/signpost.h>
#import <sys/kdebug_signpost.h>
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
kdebug_signpost_start(10, 0, 0, 0, 0); //points of interest
NSLog(@"开始睡眠");
[NSThread sleepForTimeInterval:1];
NSLog(@"结束睡眠");
kdebug_signpost_end(10, 0, 0, 0, 0);
}
@end
上面代码的运行结果如下图所示,我们可以在Points of Interest这一栏目直观地看出耗时区间,然后选中该区间,接着切换到Time Profile栏目即可查看这段时间范围内程序的执行情况。
thread state trace
thread state trace工具的作用是展示每个线程在运行过程中状态的变化,这么说有些抽象,还是通过下面这个例子来理解该工具的作用吧。
#include <mach-o/dyld.h>
#import <sys/kdebug_signpost.h>
#define SECOND 1000000
static void add_image_callbackaa(const struct mach_header *mhp, intptr_t slide) {
usleep(0.01 * SECOND);
}
@interface D2VC ()
@property(nonatomic) UIImage *image;
@end
@implementation D2VC
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = UIColor.redColor;
//注册dyld回调
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
_dyld_register_func_for_add_image(add_image_callbackaa);
});
kdebug_signpost_start(10, 0, 0, 0, 0); //points of interest的展示
self.image = [UIImage imageNamed:@"cat.jpg"];
kdebug_signpost_end(10, 0, 0, 0, 0);
}
@end
运行上面的代码,其耗时结果如下图。self.image = [UIImage imageNamed:@"cat.jpg"];
执行耗时2.3秒,为什么耗时这么多?
如果我们通过time profile来分析这段时间的cpu运行情况,就会发现viewDidLoad方法只在cpu上出现了15次(下图左下角标红框里面的15ms),并不能看出为什么self.image = [UIImage imageNamed:@"cat.jpg"];
执行耗时2.3秒。这时就需要借助thread state trace工具了。
还是同样的时间段,只不过我们这时查看的是主线程(Main Thread)的运行情况,可以发现,主线程在这段时间内被block了2.3s,这就是耗时的原因。
性能优化常见套路
性能优化通常有以下几个方向:
- 不做或者少做无用功;
- 相对用户而言比较重要的UI、数据提前创建、提前加载、提前渲染;
- 相对用户而言不重要的延迟加载或者不加载;
- 不需要在主线程中执行的代码尽量放到子线程中做。