[iOS][XCTest] XCDevice 真机内存快照(memgraph)采集:单轮 measure + 独立测试工程
场景:Unity 导出的 Xcode 工程(XPrj,App bundle id com.rm42.TrashDash),用 idevice 的 XCDevice.test() 在 物理真机 上跑一个与 App 工程解耦的独立 XCUITest,执行「启动游戏 → 延时 10s → 开始采集 → 延时 30s → 结束采集 → 结束游戏」,采集内存指标并导出 .memgraph 内存图。
总链路
独立测试工程 UITestsTemplate (UI Testing Bundle, 无 host app)
→ xcodebuild test (XCDevice.test) 针对真机
→ 测试内用 XCUIApplication(bundleIdentifier:) 启动已安装的 App
→ XCTMemoryMetric 采集 + -enablePerformanceTestsDiagnostics YES 产出 pre/post memgraph
→ xcresulttool export attachments 导出 memgraphset
方案一(推荐):测试工程固定、与频繁重导出的 App 工程完全解耦。测试用例通过 bundle id 启动「已安装」的 App,Unity 怎么重导出都不影响测试工程。
1. 独立测试工程的必要配置
UITestsTemplate.xcodeproj 是一个 UI Testing Bundle(无 target application),可驱动任意已安装 App。要在真机上跑通需配置两点:
1.1 签名团队(否则测试 runner 无法在真机签名)
project.pbxproj 里给测试 target 设置:
DEVELOPMENT_TEAM = TB8YV4RNJT; # 用 App 工程同一个 Team
CODE_SIGN_STYLE = Automatic;
1.2 被测 App 的 bundle id
测试用例通过环境变量 APP_BUNDLE_ID 决定启动哪个 App:
private let defaultBundleID = "com.example.app"
private var bundleID: String {
ProcessInfo.processInfo.environment["APP_BUNDLE_ID"] ?? defaultBundleID
}
let app = XCUIApplication(bundleIdentifier: bundleID)
在 UITests.xcscheme 的 TestAction 注入:
<TestAction
...
shouldUseLaunchSchemeArgsEnv = "NO"> <!-- 关键:必须为 NO -->
<EnvironmentVariables>
<EnvironmentVariable key = "APP_BUNDLE_ID" value = "com.rm42.TrashDash" isEnabled = "YES"/>
</EnvironmentVariables>
<Testables> ... </Testables>
</TestAction>
⚠️ 坑:shouldUseLaunchSchemeArgsEnv = "YES" 时,Test action 会继承 Run action 的(空)环境变量,忽略 TestAction 自己的 <EnvironmentVariables>,导致测试仍去启动默认的 com.example.app(设备上没装 → FBSApplicationLibrary returned nil)。改为 NO 即可。
2. 内存快照用例(单轮 + 可抓 memgraph)
import XCTest
final class GameplayUITests: XCTestCase {
private let defaultBundleID = "com.example.app"
private var bundleID: String {
ProcessInfo.processInfo.environment["APP_BUNDLE_ID"] ?? defaultBundleID
}
private let warmupSeconds: UInt32 = 10
private let captureSeconds: UInt32 = 30
override func setUpWithError() throws { continueAfterFailure = false }
// 结束游戏放在 tearDown,保证 post memgraph 抓取时 App 仍存活
override func tearDownWithError() throws {
XCUIApplication(bundleIdentifier: bundleID).terminate() // 结束游戏
}
func testGameplayMemory() throws {
let app = XCUIApplication(bundleIdentifier: bundleID)
let options = XCTMeasureOptions()
options.invocationOptions = [.manuallyStart, .manuallyStop]
options.iterationCount = 1 // 只测一轮(block 跑 iterationCount+1 次,首次 warmup 被忽略)
measure(metrics: [XCTMemoryMetric(application: app)], options: options) {
app.launch() // 启动游戏
XCTAssertTrue(app.wait(for: .runningForeground, timeout: 15))
sleep(warmupSeconds) // 延时 10s
startMeasuring() // 开始采集
sleep(captureSeconds) // 延时 30s
stopMeasuring() // 结束采集
}
}
}
两个关键点
- 只测一轮:
XCTMeasureOptions.iterationCount 控制测量次数;measure 实际跑 iterationCount+1 次(首次是被忽略的 warmup)。设 1 后指标 measurements 只有 1 个值;默认是 5。耗时从 ~5.4min 降到 ~2.5min。
- memgraph 与「结束游戏」的顺序冲突:
-enablePerformanceTestsDiagnostics YES 会额外追加一次迭代,在测量迭代开始/结束时分别抓 pre_* / post_* memgraph。post 图在迭代结束那一刻抓取,所以不能在 measure {} 块内提前 app.terminate(),否则 post 图抓不到存活进程。把「结束游戏」挪到 tearDownWithError()。
3. 用 idevice 运行(开启 memgraph 诊断)
XCDevice.test() 支持 enable_performance_diagnostics,内部追加 -enablePerformanceTestsDiagnostics YES:
from idevice.device import XCDevice
xc = XCDevice("/path/to/XPrj") # Unity 导出工程 / .xcodeproj 路径
result_bundle = xc.test(
test_project="examples/UITestsTemplate/UITestsTemplate.xcodeproj",
scheme="UITests",
udid="00008101-00161DAE14B8001E",
only_testing=["UITests/GameplayUITests/testGameplayMemory"],
enable_performance_diagnostics=True,
)
print(result_bundle) # .../build/TestResults.xcresult
等价的 xcodebuild 命令:
xcodebuild test \
-project examples/UITestsTemplate/UITestsTemplate.xcodeproj \
-scheme UITests \
-destination 'platform=iOS,id=00008101-00161DAE14B8001E' \
-only-testing:UITests/GameplayUITests/testGameplayMemory \
-enablePerformanceTestsDiagnostics YES \
-resultBundlePath ./TestResults.xcresult
设备 UDID 获取:xcrun devicectl device info details --device <coredevice-id> 看 hardwareProperties.udid。
4. 导出并查看 memgraph
xcrun xcresulttool export attachments \
--path TestResults.xcresult \
--output-path memgraph_output
导出的附件是一个 memgraphset 的 .tar.gz(文件名为 UUID .gz,真实名见 manifest.json 的 suggestedHumanReadableName),解包后得到两张图:
pre_TrashDash_<时间>.memgraph # 测量迭代开始
post_TrashDash_<时间>.memgraph # 测量迭代结束(相隔 ≈30s 采集窗口)
tar -xzf <uuid>.gz -C extracted
5. 分析
# pre/post 增长对比
heap -diffFrom pre_*.memgraph post_*.memgraph
# 泄漏 / 大对象 / 虚存摘要
leaks --fullContent post_*.memgraph
heap post_*.memgraph -sortBySize
vmmap --summary post_*.memgraph
也可 open *.memgraph 用 Xcode 可视化查看。
实测样例(iPhone 12 Pro / iOS 26.3.1,com.rm42.TrashDash):
| 项 |
pre |
post |
| Physical footprint |
411.1 MB |
413.0 MB |
| Physical footprint (peak) |
443.1 MB |
443.1 MB |
XCTMemoryMetric 指标:峰值物理内存 ≈456 MB,泄漏计数 84。
注意事项 / 踩坑总结
- memgraph 仅真机可靠,模拟器不生成。
- 必须加
-enablePerformanceTestsDiagnostics YES,否则只有内存数值、没有 .memgraph。
shouldUseLaunchSchemeArgsEnv 必须为 NO,否则 scheme 里 TestAction 的环境变量(APP_BUNDLE_ID)被忽略。
- 测试 runner 需
DEVELOPMENT_TEAM 才能在真机签名运行。
app.terminate() 不能放在 measure {} 块内(会破坏 post memgraph 抓取),放 tearDown。
XCDevice.test() 每次运行前会 rmtree 同名 TestResults.xcresult;要留存结果用 result_bundle_path= 指定不同路径,或及时复制 memgraphset。
iterationCount 最小有效值为 1(=1 warmup + 1 measured);设 0 无测量结果。
推荐组合
固定独立 UITest 工程(方案一)+ APP_BUNDLE_ID 环境变量切换被测 App + iterationCount=1 单轮 + enable_performance_diagnostics=True + xcresulttool export attachments 导出 → heap -diffFrom 分析增量。Unity 重导出 App 工程不影响测试工程,维护成本最低。
[iOS][XCTest] XCDevice 真机内存快照(memgraph)采集:单轮 measure + 独立测试工程
总链路
方案一(推荐):测试工程固定、与频繁重导出的 App 工程完全解耦。测试用例通过 bundle id 启动「已安装」的 App,Unity 怎么重导出都不影响测试工程。
1. 独立测试工程的必要配置
UITestsTemplate.xcodeproj是一个 UI Testing Bundle(无 target application),可驱动任意已安装 App。要在真机上跑通需配置两点:1.1 签名团队(否则测试 runner 无法在真机签名)
project.pbxproj里给测试 target 设置:1.2 被测 App 的 bundle id
测试用例通过环境变量
APP_BUNDLE_ID决定启动哪个 App:在
UITests.xcscheme的TestAction注入:2. 内存快照用例(单轮 + 可抓 memgraph)
两个关键点
XCTMeasureOptions.iterationCount控制测量次数;measure实际跑iterationCount+1次(首次是被忽略的 warmup)。设1后指标measurements只有 1 个值;默认是 5。耗时从 ~5.4min 降到 ~2.5min。-enablePerformanceTestsDiagnostics YES会额外追加一次迭代,在测量迭代开始/结束时分别抓pre_*/post_*memgraph。post 图在迭代结束那一刻抓取,所以不能在measure {}块内提前app.terminate(),否则 post 图抓不到存活进程。把「结束游戏」挪到tearDownWithError()。3. 用 idevice 运行(开启 memgraph 诊断)
XCDevice.test()支持enable_performance_diagnostics,内部追加-enablePerformanceTestsDiagnostics YES:等价的 xcodebuild 命令:
4. 导出并查看 memgraph
xcrun xcresulttool export attachments \ --path TestResults.xcresult \ --output-path memgraph_output导出的附件是一个 memgraphset 的
.tar.gz(文件名为 UUID.gz,真实名见manifest.json的suggestedHumanReadableName),解包后得到两张图:5. 分析
也可
open *.memgraph用 Xcode 可视化查看。实测样例(iPhone 12 Pro / iOS 26.3.1,com.rm42.TrashDash):
XCTMemoryMetric指标:峰值物理内存 ≈456 MB,泄漏计数 84。注意事项 / 踩坑总结
-enablePerformanceTestsDiagnostics YES,否则只有内存数值、没有.memgraph。shouldUseLaunchSchemeArgsEnv必须为NO,否则 scheme 里 TestAction 的环境变量(APP_BUNDLE_ID)被忽略。DEVELOPMENT_TEAM才能在真机签名运行。app.terminate()不能放在measure {}块内(会破坏 post memgraph 抓取),放tearDown。XCDevice.test()每次运行前会rmtree同名TestResults.xcresult;要留存结果用result_bundle_path=指定不同路径,或及时复制memgraphset。iterationCount最小有效值为1(=1 warmup + 1 measured);设0无测量结果。推荐组合
固定独立 UITest 工程(方案一)+
APP_BUNDLE_ID环境变量切换被测 App +iterationCount=1单轮 +enable_performance_diagnostics=True+xcresulttool export attachments导出 →heap -diffFrom分析增量。Unity 重导出 App 工程不影响测试工程,维护成本最低。