Swift如何正确向UnsafeMutableRawPointer?类型参数传递self
问题背景
- 设备情况:3D 4K投影仪采用DLP-Link技术,需要120fps左右交替视频流实现立体显示
- 开发场景:Mac端基于Swift + Core Video开发播放器,
CVDisplayLinkSetOutputCallback需要传入UnsafeMutableRawPointer?类型的上下文参数,尝试传入当前视图self实例 - 故障现象:回调触发时执行
mvview.video_render(inNow.pointee)抛出EXC_BAD_ACCESS(错误码1)
故障原因
- 原代码中
var me: MovieView = self是setup_displaylink函数内的局部变量,函数退出后该局部变量内存被回收,传入回调的&me是局部变量的栈地址,回调触发时该地址已经成为野指针 - Swift类是引用类型,直接对引用类型变量取地址传参、再在回调里用
load(as:)直接读取内存的方式,完全打破了Swift的引用计数内存管理规则,会导致内存访问异常 - 额外隐患:CVDisplayLink的输出回调运行在独立的渲染线程,直接在回调里调用NSView、NSGraphicsContext相关的UI操作,本身也会触发线程安全问题导致崩溃
修复方案
使用Unmanaged桥接Swift引用类型和C级别的裸指针,保证指针指向的实例在回调执行时有效,同时保证内存管理正确,另外把UI操作派发到主线程执行:
关键修改点
- 传入上下文参数时,用
Unmanaged.passUnretained(self).toOpaque()获取实例对应的裸指针,因为MovieView本身持有CVDisplayLink实例,用unretained不会产生循环引用 - 回调中读取实例时,用
Unmanaged<MovieView>.fromOpaque(rawPtr).takeUnretainedValue()拿回实例引用,不要直接调用load(as:)读取引用类型 - 回调内的渲染、UI相关逻辑全部派发到主线程执行,避免线程违规
修复后的核心代码
// setup_displaylink 方法修改部分 func setup_displaylink() -> Bool { var cvret: CVReturn cvret = CVDisplayLinkCreateWithActiveCGDisplays(&vlink) guard let activeLink = vlink else { return false } // 桥接self到C裸指针,不增加引用计数避免循环引用 let contextPtr = Unmanaged.passUnretained(self).toOpaque() cvret = CVDisplayLinkSetOutputCallback( activeLink, vlink_callback, contextPtr) return cvret == kCVReturnSuccess } // 全局回调函数修改部分 func vlink_callback( displayLink: CVDisplayLink, inNow: UnsafePointer<CVTimeStamp>, inOutputTime: UnsafePointer<CVTimeStamp>, flagsIn: CVOptionFlags, flagsOut: UnsafeMutablePointer<CVOptionFlags>, arg_mvview: UnsafeMutableRawPointer? ) -> CVReturn { guard let contextPtr = arg_mvview else { return kCVReturnInvalidArgument } // 从裸指针拿回MovieView实例,不修改引用计数 let mvview = Unmanaged<MovieView>.fromOpaque(contextPtr).takeUnretainedValue() // UI/渲染操作派发到主线程,符合AppKit线程规则 DispatchQueue.main.async { mvview.video_render(inNow.pointee) } return kCVReturnSuccess }
额外注意事项
- 视图销毁、播放器停止时记得调用
CVDisplayLinkStop(vlink)终止回调,避免回调触发时视图已经被销毁 - 如果业务场景中无法保证实例生命周期长于CVDisplayLink,就把传参时的
passUnretained换成passRetained,在停止displayLink的时候调用对应的takeRetainedValue()平衡引用计数,避免内存泄漏
内容的提问来源于stack exchange,提问作者DannyNiu
相关产品推荐
相关产品推荐

