iOS 内存优化之自动导出内存图

语言: CN / TW / HK

0x1 前言

在上一篇文章iOS 内存优化之工具介绍中提到利用leaks工具进行排查内存问题的最佳实践方案,本篇文章是对其的补充,这里将讲述录制UITest和自动导出内存图过程的具体实践。

这里对导出内存图的方式提供两种方案

1.xcodebuild test + 性能测试(XCTMemoryMetric)

2.xcodebuild test + expect + lldb + leaks

简要的说一下这两种方案的区别:

第一种方案来自2021-WWDC中的Detect and diagnose memory issues的演示。其特点是对测试用例设置的平均内存值超出预设值时,触发测试用例执行失败,便会自动导出内存图。但是该方案使用xcodebuild运行时似乎存在bug,具体情况后面会细说。

第二种方案来自2021年时,刚研究leaks工具时产生的一个想法,是否可以通过录制UI测试用例,跑完业务场景后自动导出内存图?然后就一顿操作猛如虎,实现了方案2,但是当时遇到一个问题(导出的内存图无法包含堆栈)无法绕过,最近才找到解决方案。该方案特点是,可以任意时刻去导出内存图,不受测试用例执行结果影响。

Xcode 14.2

以下操作将以MemoryGraphDemo为例讲述

0x2 录制测试用例

1.首先,打开MemoryGraphDemo项目,在MemoryGraphDemoUITests.m中编写内存性能测试代码,如下: ``` - (void)testPerformanceExample { XCUIApplication app = [[XCUIApplication alloc] init]; XCTMeasureOptions options = [[XCTMeasureOptions alloc] init]; options.invocationOptions = XCTMeasurementInvocationManuallyStart;

[self measureWithMetrics:@[[[XCTMemoryMetric alloc] initWithApplication:app]] options:options block:^{
    [app launch];
    [self startMeasuring];
    //录制测试用例

}];

} ```

2.将光标移动至//录制测试用例下方,然后点击Xcode的录制按钮

record-begin.png

3.等待模拟器启动后,进行开始对页面进行交互。在模拟器中,每做一次UI交互(点击、滑动)Xcode都会在//录制测试用例下方光标位置插入相应的操作代码。

4.点击模拟器中按钮:Click Me -> Retain Cycles -> Indirect Retain Cycles -> Dynamic Indirect Retain Cycles -> Large Buffers。最后再次点击录制按钮停止录制,最终结果如下图

record-result.png

0x3 xcodebuild test

1.执行该测试用例,点击方法前面的执行按钮,如图

execute-test.png

2.第一次执行测试用例完成后是测试通过的状态,此时需要为其设置内存值的baseline。

pre-set-baseline.png

3.在本例中将baseline设置为32000kB。

set-baseline.png

4.由于Max STDDEV:10%,所以Average的值在32000kB (100% - 10%)到32000kB (100% + 10%)之间测试用例才会执行成功。重新执行测试用例,结果如图,从图中可以看到Xcode中执行时可以读取到步骤7中设置的baseline的值。

test-failure.png

5.xcodebuild test 参数 -enablePerformanceTestsDiagnostics YES来自Detect and diagnose memory issues中的介绍,按照视频中演示,使用了该参数后,当测试用例执行失败,会在xcresult中给出内存图的导出路径。

6.使用xcodebuild test -workspace MemoryGraphDemo.xcworkspace -scheme MemoryGraphDemo -destination platform="iOS Simulator",name="iPhone 14" -enablePerformanceTestsDiagnostics YES命令执行测试用例,结果如下图

export-mem-failure.png

在命令的执行结果里,测试用例是执行成功的,原因是xcodebuild运行的测试用例没有读取到baseline,所以无法触发失败流程,导出内存图。

这里查到些相关案例: - setting-baselines-for-performance-tests-using-xcodebuild-and-measure - Is it possible to record baseline for Performance tests with xcodebuild?

并且查找xcodebuild相关使用教程,也没有找到设置baseline相关的参数,再对比Detect and diagnose memory issues使用的xcodebuild命令,猜测是xcodebuild存在bug。

0x4 xcodebuild test + expect + lldb

本方案的原理:在xcodebuild执行测试用例期间,通过lldb进行吸附项目进程,并在用例结束的函数上添加结束断点,然后在该断点中添加leaks命令,当用例将要执行时,会触发断点,进而调用leaks命令导出*.memgraph内存。

1.新加一个测试用例,具体代码如下 ``` - (void)testLLDBExample { XCUIApplication *app = [[XCUIApplication alloc] init]; app.launchEnvironment = @{@"MallocStackLogging": @"YES"}; [app launch]; [app.staticTexts[@"Click Me"] tap];

XCUIElementQuery *tablesQuery = app.tables;
[tablesQuery.staticTexts[@"Large Buffers"] tap];
[tablesQuery.staticTexts[@"Retain Cycles"] tap];
[tablesQuery.staticTexts[@"Indirect Retain Cycles"] tap];
[tablesQuery.staticTexts[@"Dynamic Indirect Retain Cycles"] tap];

//以这个点击操作对应的方法作为触发leaks命令的入口
[app.tables.staticTexts[@"LLDB Export Memory Graph File"] tap];

} `` 这里以UITest触发点击LLDB Export Memory Graph File`这个cell时,作为触发导出内存图的条件。在真实业务场景中,可以以退出登陆的点击事件来作为一次内存回归的内存图导出节点,这样可以方便的观察到非登陆状态下,各个业务对象是否释放干净。

app.launchEnvironment = @{@"MallocStackLogging": @"YES"};这句启动环境的配置可以使导出的内存图包含堆栈。

2.cd /path/to/Scripts切换控制台工作路径到Scripts文件夹下

scripts.png

3.给两个脚本执行权限sudo chmod +x emg.sh uitest.sh

4.根据项目修改脚本uitest.sh中的配置

EXECUTE_NAME="MemoryGraphDemo" SCHEME="MemoryGraphDemoUITests" ROOT_PATH="../" TRIGER_CMD="trigerLLDBExportMemoryGraphFile" OUT_PUT="./"

  • EXECUTE_NAME: 项目进程名
  • SCHEME: xcodebuild test -scheme 的参数
  • ROOT_PATH: 项目根路径(.xcodeproj所在路径)
  • TRIGER_CMD: 触发leaks命令的函数(项目中存在+UITest最后的点击事件)
  • OUT_PUT: 内存图的输出路径

3.执行脚本./uitest.sh

export-lldb.png

MemoryGraphDemo的默认配置,最终会把内存图输出的Scripts路径下

mem-path.png

0x5 总结

本文介绍了两种自动导出内存图的方式,第一种方式目前存在问题,无法成功触发导出操作,可能是我使用姿势不对(缺少必要的参数配置)或者xcodebuild存在bug,功能正常的情况下,这种方式使用简单,仅在测试用例失败时才导出内存图进行分析,可以做到按需分析。第二种方式是利用expect命令给lldb吸附的进程发送交互式命令,实现自动调用leaks导出内存图的方式,该方式灵活性高,不仅仅可使用在这种场景。但是这种方式需要额外分配精力去分析每次回归的内存图,而第一种方式仅需关注测试用例失败的场景。

所以,对于刚开始关注内存问题的项目,使用第二种方式不断排查和修复相关问题,当项目中的内存问题趋于稳定时, 使用第一种方式来确保项目在迭代时的回归效率和避免再次劣化。

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 1 天,点击查看活动详情