XCTest自定义XCTMetric调用时出现未识别选择器崩溃问题
自定义XCTMetric崩溃问题:unrecognized selector sent to instance
问题场景
自定义了继承自NSObject并遵循XCTMetric协议的MyXCTApplicationLaunchMetric类,用于包装系统的XCTApplicationLaunchMetric,但在测试类中调用时,程序在myLaunchMetric.willBeginMeasuring()行崩溃,报错信息如下:
Thread 2: "-[XCTApplicationLaunchMetric willBeginMeasuring]: unrecognized selector sent to instance 0x60000026c620"
同时发现MyXCTApplicationLaunchMetric的init会被调用一次,之后copy(with:)方法也会被调用,不清楚为什么需要执行复制操作。
自定义指标类代码
class MyXCTApplicationLaunchMetric: NSObject, XCTMetric { let myLaunchMetric: XCTApplicationLaunchMetric override init() { myLaunchMetric = XCTApplicationLaunchMetric(waitUntilResponsive: true) super.init() } // XCTMetric conformance func willBeginMeasuring() { myLaunchMetric.willBeginMeasuring() } // XCTMetric conformance func didStopMeasuring() { myLaunchMetric.didStopMeasuring() } // XCTMetric conformance func reportMeasurements(from startTime: XCTPerformanceMeasurementTimestamp, to endTime: XCTPerformanceMeasurementTimestamp) throws -> [XCTPerformanceMeasurement] { try myLaunchMetric.reportMeasurements(from: startTime, to: endTime) } // NSObject conformance func copy(with zone: NSZone? = nil) -> Any { self // have also tried returning MyXCTApplicationLaunchMetric() // and MyXCTApplicationLaunchMetric(myLaunchMetric) when provided // an init that accepts that param. } }
测试类代码
final class PerformanceUITests: XCTestCase { func testLaunchPerformance() throws { if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) { let options = XCTMeasureOptions() options.iterationCount = 1 measure(metrics: [MyXCTApplicationLaunchMetric()], options: options) { XCUIApplication().launch() } } } }
原因分析
- 私有API调用问题:
XCTApplicationLaunchMetric虽然遵循XCTMetric协议,但willBeginMeasuring、didStopMeasuring这些方法属于框架内部私有方法,并未对外公开。直接调用这些方法会触发“未识别选择器”的崩溃。 - copy方法的必要性:XCTest框架在执行性能测试时,会对传入的
XCTMetric实例执行复制操作——因为测试可能会多次迭代,每次迭代需要独立的指标实例来维护状态,避免不同迭代间的状态干扰。之前的copy实现要么返回自身(导致同一实例被复用,状态混乱),要么初始化新实例但未正确处理内部逻辑,都会引发后续问题。
解决方案
方案1:直接使用系统指标类
如果只是需要使用启动性能测量功能,完全不需要自定义包装类,直接创建XCTApplicationLaunchMetric实例传入即可:
func testLaunchPerformance() throws { if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) { let options = XCTMeasureOptions() options.iterationCount = 1 let launchMetric = XCTApplicationLaunchMetric(waitUntilResponsive: true) measure(metrics: [launchMetric], options: options) { XCUIApplication().launch() } } }
方案2:正确实现自定义指标(避免调用私有方法)
如果确实需要扩展启动指标的功能,需要自己实现XCTMetric的逻辑,不要直接调用XCTApplicationLaunchMetric的私有方法,同时正确实现NSCopying协议:
class MyXCTApplicationLaunchMetric: NSObject, XCTMetric, NSCopying { private let waitUntilResponsive: Bool init(waitUntilResponsive: Bool = true) { self.waitUntilResponsive = waitUntilResponsive super.init() } // MARK: - NSCopying func copy(with zone: NSZone? = nil) -> Any { // 返回全新的实例,确保每次迭代使用独立状态 return MyXCTApplicationLaunchMetric(waitUntilResponsive: self.waitUntilResponsive) } // MARK: - XCTMetric func willBeginMeasuring() { // 在这里添加你的自定义前置逻辑,比如日志、环境准备等 } func didStopMeasuring() { // 在这里添加你的自定义后置逻辑,比如数据清理、日志收集等 } func reportMeasurements(from startTime: XCTPerformanceMeasurementTimestamp, to endTime: XCTPerformanceMeasurementTimestamp) throws -> [XCTPerformanceMeasurement] { // 如果需要复用系统的启动测量逻辑,可以在这里临时创建XCTApplicationLaunchMetric实例获取结果 // 注意:不要调用它的will/did方法,仅使用reportMeasurements获取数据 let systemMetric = XCTApplicationLaunchMetric(waitUntilResponsive: waitUntilResponsive) return try systemMetric.reportMeasurements(from: startTime, to: endTime) } }
内容的提问来源于stack exchange,提问作者fingia
相关产品推荐
相关产品推荐

