iOS性能优化之内存分析

成功之前我们要做应该做的事情,成功之后我们才可以做喜欢做的事情。

  从苹果的开发者文档里可以看到内存分类如下所示,其中 Leaked memoryAbandoned memory 都属于应该释放而没释放的内存,都是内存泄露(该释放的内存没有释放)。

1.Leaked memory: Memory unreferenced by your application that cannot be used again or freed (also detectable by using the Leaks instrument).

2.Abandoned memory: Memory still referenced by your application that has no useful purpose.

3.Cached memory: Memory still referenced by your application that might be used again for better performance.

一. 内存分析方法

1. 静态分析方法(Analyze)

1.1)Analyze简介

  Clang Static Analyzer是一款静态代码扫描工具,专门用于针对C,C++和Objective-C的程序进行分析。已经被Xcode集成,可以直接使用Xcode进行静态代码扫描分析,也可以单独在命令行下使用并提供html格式的输出报吿和xml格式的结果文件方便集成到Jenkins上进行展示。

  工具:Product->Analyze

image

  Analyze主要分析以下四种问题:

  • 内存管理错误检查(Memory Error),例如内存泄漏等;
  • 逻辑错误检查(Logic Error):访问空指针或未初始化的变量等;
  • 声明错误检查(Dead Store):也叫无用逻辑存储,其指永远不会被访问的变量、永远不会执行的代码;
  • Api调用错误检查(API Misuse)

  声明错误、逻辑错误、Api调用错误基本在编译时都会有警告,Analyze的主要优势在于静态分析内存泄漏及代码逻辑错误。

  点击Analyze后,Xcode会自动进行编译分析,需要一段时间,之后会像提示警告一样,提示有多少分析的结果。所有的分析结果按照如上的类别,归类显示。点击某个错误的地方,会定位到出错的地方,然后点击向上向下的箭头,会详细展示出出错的步骤。

1.2)内存管理错误检查:Memory Error

  开发的过程中,容易给声明非空的对象赋值nil,导致出错。包括:nil 赋值给了一个期望非空值的指针、Null赋值给非空对象,返回了 nil 值,期望返回一个非空值。

#pragma mark - Memory Error
- (void)MemoryErrorTest {
    // nil赋值给了一个期望非空值的指针 nonnull
    _userInfo = nil;
}

// 返回了nil值,期望返回一个非空值
- (NSArray * _Nonnull)returnValue {
    return nil;
}

  Analyze结果:

image

  内存泄露 Memory (Core Foundation/Objective-C):

  一般来说都是由于使用的CoreFoundation后没有release造成的。在RAC下Foundation框架下的不需要进行release,CoreFoundation框架下仍然需要release。比如在开启arc的环境下,输入以下一段代码:

// 内存泄漏 - 截取部分图像
- (UIImage*)getSubImage:(unsigned long)ulUserHeader
{
    UIImage * sourceImage = [UIImage imageNamed:@"image.png"];
    CGFloat height = sourceImage.size.height;
    CGRect rect = CGRectMake(0 + ulUserHeader*height, 0, height, height);

    CGImageRef imageRef = CGImageCreateWithImageInRect([sourceImage CGImage], rect);
    UIImage* smallImage = [UIImage imageWithCGImage:imageRef];
    //CGImageRelease(imageRef);

    return smallImage;
}

  用注释注释掉CGImageRelease(imageRef)这行,虽然开起了arc,不过仍然会导致imageRef对象泄漏。

  Analyze结果:

image

  Analyze已经分析出imageRef对象有内存泄漏,这种情况在编译时是无法发现的。

1.3)逻辑错误检查:Logic Error

  初看这段代码,并没有觉得有什么不妥,根据字符串获得index的值。这个前提是字符串一定要按照这个规则提供,如果没有按照这个规则提供,则index就没有值。通过Analyze分析,就检查出来了。

#pragma mark - Logic Error
- (NSInteger)statusIndex:(NSString *)status {
    NSInteger index;
    if ([status isEqualToString:@"正常"]) {
        index = 1;
    } else if ([status isEqualToString:@"异常"]) {
        index = 2;
    } else if ([status isEqualToString:@"严重"]) {
        index = 3;
    }
    return index;
}

  Analyze结果:

image

1.4)声明错误检查:Dead Store

  很多时候我们创建了一些中间变量需要使用,但是在最终功能的实现上并没有用到这个变量。但是这些变量依然留在代码中,没有删除。这就造成了内存的不必要的开销。这对这部分变量,不需要的时候就要及时的删除。同理:创建类声明的属性,如果没有用到就要及时删除。因为创建类时,会根据类的属性的多少创建对应的内存。

#pragma mark - Dead Store
- (void)deadStoreTest {
    NSDictionary *param = @{};
    NSString *stackTrace = [param objectForKey:@"key"];
    if (!stackTrace) {
        stackTrace = @"";
    }
    // 整个流程stackTrace只是被赋值,没有被使用
}

  Analyze结果:

image

1.5)Api调用错误检查:API Misuse(Apple)

  API的错误,一般是在大段的逻辑处理中没有注意OC的使用细节。如:数组不能添加空值,数组的元素不能是空值,字典的value不能是空等等。下面这单代码:str 的初始值为空,经过一段逻辑处理后,还是有可能是空,是不能添加到数组中的。

#pragma mark - API Misuse
- (void)addData {
    NSString *str = nil;
    long number = random();
    if (number % 3 == 0) {
        str = @"number是3的整数";
    } else if (number % 3 == 1) {
        str = @"number对3取余为1";
    }
    NSMutableArray *dataArray = [NSMutableArray arrayWithCapacity:0];
    [dataArray addObject:str];
}

  Analyze结果:

image

2. 动态分析方法(Instruments)

2.1)Instruments简介

  动态分析方法(Instruments)检测程序在运行过程中的内存变化, 是Xcode自带的工具。

image

  启动Instruments:Xcode -> Product ->Profile 运行 instruments。

image

  由上Instruments工具面板可以看到很多分析工具,双击和点击choose都可以打开调试 ,项目中用的最多的也就是Allocations和Leaks了,也看自己的需要检测和使用其它的工具。

2.2)Allocations(内存分配分析)

2.2.1)Allocations简介

  内存泄漏是指内存被分配了,但程序中已经没有指向该内存的指针,导致该内存无法被释放,产生内存泄漏。内存不合理运用,苹果官方称这种情况为Abandoned Memory,也就是存在已分配内存的引用,但实际上程序中不会使用,比如图片等对象加入了缓存,但缓存中的对象一直没有被使用。

  XCode提供的Instruments中的Allocations工具可以用来帮你分析内存的分配情况(监测内存分配情况),当你的App收到内存警告、内存暴增,且持续不释放的情况,除了是内存泄漏外,还有就是对性能代码质量不过关导致,这时首先应该用Allocations进行内存分析,了解内存分配情况,哪些对象占用了太多内存。用真机调试比较准确。

  在 ARC 时代更常见的内存泄露是循环引用导致的Abandoned memory,Leaks 工具查不出这类内存泄露,我们也可以通过Allocations内存分析来发现这部分内存泄漏。

2.2.2)Allocations几种分析模式
  • Statistics(静态分析)

image

2.2.3)如何使用Allocations
(1)准备实例代码

  模拟一段占用内存代码,启动Allocations工具监测内存分配情况,找到高耗内存的代码:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    static NSString *identifier = @"id1";
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier];
    if (!cell) {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifier];
    }
    cell.textLabel.text = [NSString stringWithFormat:@"%ld",indexPath.row];
    
    // 模拟占用内存过高的代码
    for (int i = 0; i < 200; i++) {
        UIImageView *image = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"3.png"]];
        image.backgroundColor = [UIColor whiteColor];
        image.frame = CGRectMake(0, 0, 30, 30);
        [cell.contentView addSubview:image];
    }
    
    return cell;
}

  启动Instruments -> Allocations工具

image

(2)Statistics静态分析模式

  启动Allocations默认的数据是Statistics(静态分析)。

image

  为了更好的解决问题,有必要讲解下这里的内存相关概念:

  • All Heap Allocations:堆上malloc分配的内存,不包过虚拟内存区域;

  • All Anonymous VM:匿名的虚拟内存区域。何为匿名呢?就是Allocations不知道是你哪些代码创建的内存,也就是说这里的内存你无法直接控制。像memory mapped file,CALayer back store等都会出现在这里。这里的内存有些是你需要优化的,有些不是;

    具体数据库分析:

  • Graph:是否需要绘制出来内存曲线;

  • Category:内存类别;

  • Persistent:没有释放的内存大小;

  • # Persistent:没有释放的个数;

  • # Transient:已经释放的个数;

  • Transient Bytes:已经释放的内存大小;

  • Total Bytes:总内存大小(Persistent + Transient Bytes);

  • # Total:总数量(# Persistent + # Transient);

  点击Category小箭头,进入详情页。选择内存异常对象地址,在右侧我们可以看到这个对象是如何被创建的。以及定位到代码。

(3)Mark Generation

  利用Generation,我们可以对内存的增量进行分析,时间戳B相比时间戳A有那些内存增加了多少,对于暴增的地方我们可以很方便发现问题。

  在内存异常的地方做标记:Mark Generation

image

  设置Call Tree选项(跟检测内存泄漏是一样)

image

  切换导航到Generations,查看标记样本

  点击标记后面的箭头进一步分析内存分配,定位高内存代码块

image

image

(4)Call Tree

  按照类似的方式,这次我们选择Call Tree(调用树)来直接分析代码是如何创建内存的。

(5)Allocations List

  Allocations List提供了一种更纯粹的方式,让你看到内存的分配的列表,我们一般会选择内存从高到低,看看是不是有什么意外分配的大内存块

2.3)Leaks(动态分析)

2.3.1)Leaks简介

  Leaks可以实时看到APP的内存泄漏,我们经常会使用到这个工具。相关工具在XCode的Product->Profile->Instruments-> Leaks或者command+I。

参考:

https://www.jianshu.com/p/4ce4e7cf6a22

2.4)Zombies(僵尸对象分析)

2.4.1)Zombies简介

  Zombies动态分析内存中的僵尸对象,相关工具在XCode的Product->Profile->Instruments->Zombies。那什么是僵尸对象呢?在使用ARC之前,很多人遇到过EXC_BAD_ACCESS错误,这个错误可以理解为访问了已被释放的对象,苹果称之为僵尸对象。

  比如在不开启ARC下,下面这段代码:

NSString *str = [NSString stringWithFormat:@"HBZombie"];
NSLog(@"Go %@",str);
[str release];

  str对象不是手动分配,而是加入到自动释放池,由释放池负责释放,所以第三行调用release时就会产生EXC_BAD_ACCESS错误。

  在开启ARC后,可以很大程度上避免产生EXC_BAD_ACCESS错误,但也是有出现可能的,比如IOS里使用了C++代码,C++部分的对象是不会有ARC来管理的。

  EXC_BAD_ACCESS错误不像访问空指针一样容易定位,往往报错时很难查找到错误点,所以XCode在Instruments中提供了单独的Zombies工具来分析这类错误。

2.4.2)Zombies分析的原理

  和使用Instruments的其他工具一样,点击XCode的Product菜单Profile启动Instruments:

image

  可以看到Zombies工具下边的介绍,用于查找那些被过度释放的僵尸对象。

  Zombies工具的查找原理其实和设置NSZombieEnabled环境变量的调试方式是一样的,启动Zombies后在内部设置了NSZombieEnabled为True。

  启用了NSZombieEnabled的话,它会用一个僵尸来替换默认的dealloc实现,也就是在引用计数降到0时,该僵尸实现会将该对象转换成僵尸对象。僵尸对象的作用是在你向它发送消息时,就不会向之前那样Crash或者产生 一个难以理解的行为,而是放出一个错误消息,它会显示一段日志并自动跳入调试器, 因此我们就可以找到具体或者大概是哪个对象被错误的释放了。

2.4.3)使用Zombies分析的步骤

(1)启动Instruments;

(2)在模版选择器中,点击Zombies;

(3)选择app和目标设备;

(4)点击选择创建路径文档;

(5)点击工具栏红色圆形按钮或command+r开始记录;

(6)正常使用你的app;如果一个被过度释放的对象被访问了,在timeline窗口里会被插入一个标记同时僵尸对象被会话访问出现。这表示在某个内存地址上一个僵尸对象被访问了。你可以通过点击打开和关闭Zombie Messaged Dialog(僵尸对象访问会话);

(7)点击灰白色横向箭头到僵尸对象的内存地址并且显示僵尸对象详细内存历史的窗口,包括相对应的引用计数和方法调用。

(8)在详细窗口选择Zombie事件(或者是其它你想研究的事件);

(9)输入(Command+3)显示选择事件的栈轨迹的扩展详细区域;

(10)点击Collapse按钮在扩展详细区域隐藏栈轨迹,这样更容易看到你的应用的方法;通过用户icon标志Calls使你的app标记为黑色并置前;

(11)栈轨迹区双击方法显示它的代码在Instruments中;

(12)点击Xcode按钮在详细窗口顶部用于打开这代码在Xcode的编辑界面。

  虽然Instruments可以帮你发现“僵尸”对象,但是你仍然需要仔细检查关系内存历史来确定并解决问题。以下是常见导致僵尸对象的情况。前两个在ARC中应该不会出现,第三个倒是极有可能。

  • release一个已经被release或者autorelease的对象;

  • 对象需要被retain时没有被retain;

  • 一些调用发生在对象被release之后;

2.4.4)手动设置NSZombieEnabled环境变量

  XCode也提供了手动设置NSZombieEnabled环境变量的方法,不过设置NSZombieEnabled为True后,会导致内存占用的增长,同时会影响Leaks工具的调试,这是因为设置NSZombieEnabled会用僵尸对象来代替已释放对象。所以一般不建议进行进行手动设置,而应该使用Zombies工具进行调试。

  点击Product菜单Edit Scheme打开该页面,然后勾选Zombie Objects 复选框:

image

2.5)Time Profiler(查看时间占用)

  Time Profiler还有上面介绍过的Leaks、Allocations工具,被戏称为Instruments的救命三招,是当应用遇到问题时首先应当使用的三个工具。

  Time Profiler帮助我们分析代码的执行时间,找出导致程序变慢的原因,告诉我们“时间都去哪儿了?”。

  Time Profiler分析原理:它按照固定的时间间隔来跟踪每一个线程的堆栈信息,通过统计比较时间间隔之间的堆栈状态,来推算某个方法执行了多久,并获得一个近似值。其实从根本上来说与我们的原始分析方法异曲同工,只不过其将各个方法消耗的时间统计起来。

  选择Time Profiler工具开始测试,这时会自动启动模拟器和Time Profiler录制。

  先进行一些App的操作,让Time Profiler收集足够的数据,尤其是你觉得那些有性能瓶颈的地方。

二.第三方检测方法

MLeaksFinder

参考:

https://blog.csdn.net/Alpaca12/article/details/80157520

http://events.jianshu.io/p/b8d2f736ae6b

https://www.jianshu.com/p/70184408649a

posted @ 2021-10-13 16:28  背包の技术  阅读(2502)  评论(0编辑  收藏  举报