本文内容分为两大部分: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在不同时间点所执行的函数调用栈。

ios性能优化插件 ios性能优化实战pdf_时间段

ios性能优化插件 ios性能优化实战pdf_ios性能优化插件_02

ios性能优化插件 ios性能优化实战pdf_ios性能优化插件_03

ios性能优化插件 ios性能优化实战pdf_d3_04

  从上面几张图可知,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列的含义描述请看图中的红色文字。

ios性能优化插件 ios性能优化实战pdf_ios性能优化插件_05

  因为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。

ios性能优化插件 ios性能优化实战pdf_时间段_06

  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栏目即可查看这段时间范围内程序的执行情况。

ios性能优化插件 ios性能优化实战pdf_调用栈_07

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秒,为什么耗时这么多?

ios性能优化插件 ios性能优化实战pdf_ios性能优化插件_08

  如果我们通过time profile来分析这段时间的cpu运行情况,就会发现viewDidLoad方法只在cpu上出现了15次(下图左下角标红框里面的15ms),并不能看出为什么self.image = [UIImage imageNamed:@"cat.jpg"];执行耗时2.3秒。这时就需要借助thread state trace工具了。

ios性能优化插件 ios性能优化实战pdf_d3_09

  还是同样的时间段,只不过我们这时查看的是主线程(Main Thread)的运行情况,可以发现,主线程在这段时间内被block了2.3s,这就是耗时的原因。

ios性能优化插件 ios性能优化实战pdf_时间段_10

性能优化常见套路

性能优化通常有以下几个方向:

  1. 不做或者少做无用功;
  2. 相对用户而言比较重要的UI、数据提前创建、提前加载、提前渲染;
  3. 相对用户而言不重要的延迟加载或者不加载;
  4. 不需要在主线程中执行的代码尽量放到子线程中做。