MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

使用Instruments工具进行Objective-C内存分析

2021-03-183.0k 阅读

1. 认识 Instruments 工具

1.1 Instruments 概述

Instruments 是 Xcode 自带的一款强大的性能分析工具集,它提供了一系列的分析模板(称为 Instruments),可以帮助开发者分析应用程序在运行时的各种性能指标,其中内存分析是其重要功能之一。在 Objective - C 开发中,有效地管理内存对于应用的稳定性、性能以及资源利用效率至关重要。Instruments 为我们提供了直观且深入的方式来检测内存泄漏、观察内存使用趋势等。

1.2 Instruments 的启动与界面

  1. 启动方式
    • 打开 Xcode,选择要分析的项目,然后点击菜单栏中的“Product”,再选择“Profile”。这将自动启动 Instruments 并使用默认的分析模板(通常是 Time Profiler)运行应用。
    • 也可以直接在“/Applications/Xcode.app/Contents/Applications/Instruments.app”路径下找到 Instruments 应用并独立启动,然后在启动界面选择要分析的目标应用(可以是模拟器中的应用或真机设备上的应用)。
  2. 界面组成
    • 模板选择窗口:启动 Instruments 时,会出现此窗口,列出各种分析模板,如 Time Profiler(时间剖析器,用于分析函数执行时间)、Leaks(内存泄漏检测)、Allocations(内存分配分析)等。
    • 时间轴:位于 Instruments 窗口的顶部,展示应用运行时间,不同的分析模板会在时间轴上以不同颜色表示其数据。
    • 详细信息区域:下方的大部分区域用于展示具体的分析数据,如函数调用栈、内存分配情况等,具体内容取决于所选的分析模板。

2. 内存分析相关的 Instruments

2.1 Leaks 工具

  1. 功能:Leaks 工具专门用于检测应用程序中的内存泄漏。内存泄漏是指应用程序分配了内存,但在不再使用这些内存时,没有将其释放,导致内存不断被占用,最终可能使应用程序耗尽系统内存,出现崩溃等问题。
  2. 工作原理:Leaks 工具通过跟踪应用程序的内存分配和释放情况来检测泄漏。它会记录每次内存分配的位置(代码中的行号等信息),当应用程序结束运行或进行手动触发分析时,Leaks 工具会检查是否存在已分配但未释放的内存块。如果存在,就会将这些内存块标记为泄漏,并提供相关的泄漏信息,如泄漏对象的类型、大小以及首次分配该对象的代码位置。
  3. 示例代码
#import <Foundation/Foundation.h>

@interface MemoryLeakObject : NSObject
@end

@implementation MemoryLeakObject
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        while (1) {
            MemoryLeakObject *leakObject = [[MemoryLeakObject alloc] init];
            // 这里没有释放 leakObject,会导致内存泄漏
        }
    }
    return 0;
}

在上述代码中,我们在一个无限循环中创建 MemoryLeakObject 对象,但没有释放它,这必然会导致内存泄漏。运行该应用并使用 Leaks 工具分析,就可以检测到这些泄漏。

2.2 Allocations 工具

  1. 功能:Allocations 工具主要用于观察应用程序的内存分配情况,包括对象的创建、销毁以及内存使用的增长趋势等。它可以帮助开发者了解应用在运行过程中哪些部分消耗了大量内存,从而针对性地进行优化。
  2. 工作原理:Allocations 工具在应用运行时实时跟踪内存分配操作。它记录每次内存分配的详细信息,如分配的大小、分配的对象类型、分配的时间等。通过这些数据,开发者可以绘制出内存使用的时间曲线,查看不同时间段内内存的增长和波动情况。
  3. 示例代码
#import <Foundation/Foundation.h>

@interface MemoryAnalysisObject : NSObject
@property (nonatomic, strong) NSString *largeString;
@end

@implementation MemoryAnalysisObject
- (instancetype)init {
    self = [super init];
    if (self) {
        self.largeString = [NSString stringWithFormat:@"%@", [NSString stringWithContentsOfFile:@"/usr/share/dict/words" encoding:NSUTF8StringEncoding error:nil]];
    }
    return self;
}
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSMutableArray *objectsArray = [NSMutableArray array];
        for (int i = 0; i < 1000; i++) {
            MemoryAnalysisObject *object = [[MemoryAnalysisObject alloc] init];
            [objectsArray addObject:object];
        }
        // 这里可以看到随着对象创建,内存不断增加
    }
    return 0;
}

在这个示例中,MemoryAnalysisObject 类初始化时会读取一个较大的文件内容到 largeString 属性中。通过 Allocations 工具分析这段代码的运行,可以清晰看到随着 MemoryAnalysisObject 对象的不断创建,内存使用量逐步上升的趋势。

3. 使用 Instruments 进行内存分析的实际操作

3.1 配置 Instruments 会话

  1. 选择模板:启动 Instruments 后,在模板选择窗口中选择与内存分析相关的模板,如 Leaks 或 Allocations。如果要同时分析多种内存相关指标,可以选择 Allocations 模板,然后在分析过程中添加 Leaks 等其他工具。
  2. 选择目标设备:可以选择在模拟器或真机上运行应用进行分析。真机分析可以获取更真实的设备环境下的内存数据,但某些模拟器也能提供足够的分析信息,特别是在早期开发阶段。
  3. 设置分析选项:部分 Instruments 提供一些可配置选项,例如 Allocations 工具可以设置是否只跟踪特定类型的对象分配,或者设置采样频率等。这些选项可以根据具体的分析需求进行调整。

3.2 运行应用并收集数据

  1. 启动应用:选择好模板和目标设备后,点击 Instruments 窗口中的“Record”按钮,这将启动应用并开始收集分析数据。
  2. 模拟用户操作:在应用运行过程中,模拟真实用户的操作场景,例如点击按钮、滑动屏幕、切换视图等。这是因为不同的用户操作可能会触发不同的内存分配和释放行为,全面的操作模拟有助于发现更多潜在的内存问题。
  3. 暂停与继续记录:在分析过程中,可以根据需要暂停和继续记录数据。例如,当观察到某个特定操作导致内存异常变化时,可以暂停记录,详细查看当前的内存数据,然后继续操作和记录,以便进一步分析。

3.3 分析数据

  1. Leaks 工具数据分析
    • 泄漏列表:Leaks 工具会在检测到内存泄漏时,在详细信息区域显示泄漏列表。列表中会列出每个泄漏对象的相关信息,包括对象类型、大小、泄漏次数以及首次分配该对象的代码位置。点击泄漏对象可以查看其具体的调用栈信息,这对于定位内存泄漏的根源非常有帮助。
    • 时间轴显示:在时间轴上,Leaks 工具会以红色标记显示检测到内存泄漏的时间点。通过查看时间轴和对应的应用操作,可以确定哪些操作或功能模块容易引发内存泄漏。
  2. Allocations 工具数据分析
    • 内存分配统计:Allocations 工具会提供各种内存分配的统计信息,如总分配字节数、分配次数、不同类型对象的分配情况等。通过这些统计数据,可以快速了解应用中哪些对象类型占用了大量内存。
    • 时间曲线:时间轴上会显示内存使用量随时间的变化曲线。通过观察曲线的上升和下降趋势,可以判断应用在不同阶段的内存增长情况。例如,如果曲线持续上升且没有明显的下降趋势,可能意味着存在内存泄漏或内存使用不合理的情况。
    • 对象生命周期:Allocations 工具还可以展示对象的生命周期,即对象何时创建、何时销毁。这有助于分析对象的存活时间是否过长,是否存在不必要的对象持有导致内存无法及时释放的问题。

4. 解决 Objective - C 内存问题的策略

4.1 遵循内存管理规则

  1. 手动引用计数(MRC):在手动引用计数环境下,开发者需要严格遵循“谁创建,谁释放”的原则。例如,使用 allocnewcopy 方法创建的对象,需要使用 releaseautorelease 方法来释放其所有权。
NSObject *obj = [[NSObject alloc] init];
// 使用 obj
[obj release];
  1. 自动引用计数(ARC):ARC 大大简化了内存管理,编译器会自动插入适当的内存管理代码。但开发者仍需了解对象的所有权关系,避免循环引用等问题。例如:
@interface ClassA : NSObject
@property (nonatomic, strong) ClassB *classB;
@end

@interface ClassB : NSObject
@property (nonatomic, strong) ClassA *classA;
@end

// 在某个函数中
ClassA *a = [[ClassA alloc] init];
ClassB *b = [[ClassB alloc] init];
a.classB = b;
b.classA = a;
// 这里会形成循环引用,即使使用 ARC,a 和 b 也不会被释放

解决循环引用问题,可以将其中一个属性声明为 weakunsafe_unretained。例如:

@interface ClassA : NSObject
@property (nonatomic, strong) ClassB *classB;
@end

@interface ClassB : NSObject
@property (nonatomic, weak) ClassA *classA;
@end

4.2 优化对象创建与销毁

  1. 减少不必要的对象创建:避免在频繁调用的方法或循环中创建大量临时对象。例如,可以复用已有的对象,而不是每次都创建新的对象。
// 不优化的方式
for (int i = 0; i < 1000; i++) {
    NSString *tempString = [NSString stringWithFormat:@"%d", i];
    // 使用 tempString
}
// 优化的方式
NSMutableString *mutableString = [NSMutableString string];
for (int i = 0; i < 1000; i++) {
    [mutableString setString:@""];
    [mutableString appendFormat:@"%d", i];
    // 使用 mutableString
}
  1. 及时销毁不再使用的对象:确保对象在不再需要时能够及时被释放。在 ARC 环境下,只要没有强引用指向对象,对象就会被自动释放。但在 MRC 环境下,需要开发者手动释放对象。同时,要注意对象之间的引用关系,避免因错误的引用导致对象无法释放。

4.3 内存缓存与池

  1. 自动释放池(Autorelease Pool):合理使用自动释放池可以优化内存峰值。自动释放池会在其结束时释放池内所有调用了 autorelease 的对象。在循环中创建大量临时对象时,可以使用自动释放池来及时释放这些对象,避免内存峰值过高。
@autoreleasepool {
    for (int i = 0; i < 10000; i++) {
        NSString *tempString = [NSString stringWithFormat:@"%d", i];
        // 使用 tempString
    }
}
// 这里循环结束后,自动释放池内的 tempString 对象会被释放
  1. 内存缓存:对于一些频繁使用且创建开销较大的对象,可以考虑使用内存缓存。例如,对于图片加载,可以使用 NSCache 来缓存已加载的图片,避免重复从磁盘或网络加载,从而减少内存和性能开销。
NSCache *imageCache = [[NSCache alloc] init];
// 加载图片时先检查缓存
UIImage *image = [imageCache objectForKey:imageKey];
if (!image) {
    // 从文件或网络加载图片
    image = [UIImage imageWithContentsOfFile:imagePath];
    [imageCache setObject:image forKey:imageKey];
}

5. 高级内存分析技巧

5.1 深入分析调用栈

  1. Leaks 工具中的调用栈:当 Leaks 工具检测到内存泄漏时,详细查看泄漏对象的调用栈是定位问题根源的关键。调用栈会展示从应用程序入口到泄漏对象分配点的函数调用路径。通过分析调用栈,开发者可以了解在哪个函数、哪个模块中发生了内存分配但未释放的情况。例如,如果调用栈显示在某个自定义视图的初始化方法中分配了对象但未释放,那么问题可能就出在该视图的内存管理上。
  2. Allocations 工具中的调用栈:Allocations 工具中的调用栈同样重要。在观察内存分配情况时,调用栈可以帮助开发者了解哪些函数频繁地进行内存分配操作。这有助于发现潜在的性能瓶颈,例如某个函数在短时间内多次创建大型对象,导致内存快速增长。通过优化该函数的内存分配逻辑,可以提升应用的整体性能。

5.2 利用 Instruments 脚本自动化分析

  1. 脚本编写基础:Instruments 支持使用 AppleScript 或 JavaScript 编写脚本,以实现自动化分析任务。例如,可以编写一个脚本来自动启动应用、执行一系列预设的用户操作,然后停止分析并导出分析报告。这样可以在每次构建应用时自动进行内存分析,确保及时发现内存问题。
  2. 示例脚本(JavaScript 示例)
// 启动 Instruments 并选择 Allocations 模板
var app = Application("Instruments");
app.includeStandardAdditions = true;
app.launch();
app.activate();
app.windows[0].tables[0].rows[0].click(); // 选择 Allocations 模板
app.windows[0].buttons["Choose Target…"].click();
// 选择目标应用(假设目标应用已在模拟器或真机上安装并可识别)
app.windows[1].scrollAreas[0].tables[0].rows[0].click();
app.windows[1].buttons["Choose"].click();
// 开始记录
app.windows[0].buttons["Record"].click();
// 模拟用户操作(这里假设简单的点击操作,实际可根据应用功能编写更复杂操作)
app.keystroke("a", {using: ["command"]});
// 停止记录
app.windows[0].buttons["Stop"].click();
// 导出分析报告
app.windows[0].menuBars[0].menus[1].menuItems[21].click(); // 选择导出报告选项

通过编写这样的脚本,可以大大提高内存分析的效率,特别是在大型项目中,需要频繁进行内存分析时,自动化脚本可以节省大量的手动操作时间。

5.3 多线程环境下的内存分析

  1. 多线程内存问题特点:在多线程应用中,内存问题更加复杂。不同线程可能同时访问和修改共享内存,导致数据竞争和内存不一致等问题。例如,一个线程分配了内存,另一个线程在未正确同步的情况下访问该内存,可能导致内存访问错误或内存泄漏。此外,多线程环境下的对象生命周期管理也更加困难,因为对象可能在不同线程中被创建和销毁。
  2. Instruments 对多线程分析的支持:Instruments 提供了一些工具来帮助分析多线程环境下的内存问题。例如,Thread Sanitizer 工具可以检测多线程应用中的数据竞争问题。同时,Leaks 和 Allocations 工具在多线程应用中同样可以使用,但需要注意分析结果可能会受到线程并发执行的影响。在分析多线程应用时,需要结合线程同步机制(如锁、信号量等)来确保内存操作的正确性。例如,在使用 NSLock 来保护共享内存时,通过分析工具查看锁的使用是否正确,是否存在死锁等情况,以避免内存相关的错误。

通过以上对使用 Instruments 工具进行 Objective - C 内存分析的详细介绍,开发者可以更好地掌握内存分析技巧,优化应用程序的内存使用,提高应用的性能和稳定性。在实际开发中,应养成定期使用 Instruments 进行内存分析的习惯,及时发现并解决潜在的内存问题。