从 DisplayList 到 Transaction: SwiftUI 调试实战
注意:阅读本文需要你对 SwiftUI 和 AttributeGraph 的原理有一定了解,如 DisplayList / Animation / Attribute 等。
如无特殊说明,本文的代码环境均为 iPhone 16 Pro + iOS 18.5 + Xcode 16.4
问题说明
最近实现了 combineAnimation
后,发现 OpenSwiftUI 在动画过程中或者动画结束后,进行设备屏幕的旋转后,对应 View 会停留在容器的原相对位置,然后动画到新的正确位置,SwiftUI 中没有这个表现。
预期行为 / SwiftUI 表现
当前行为 / OpenSwiftUI 表现:
struct ColorAnimationExample: View {
@State private var showRed = false
var body: some View {
VStack {
Color(platformColor: showRed ? .red : .blue)
.frame(width: showRed ? 200 : 400, height: showRed ? 200 : 400)
}
.animation(.easeInOut(duration: 5), value: showRed)
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
showRed.toggle()
}
}
}
}
问题排查
DisplayList
首先设置 SWIFTUI_PRINT_TREE=1
观察在停止动画后的第一次 DisplayList 输出:
// SwiftUI & OpenSwiftUI old vertical DL
View 0x0000000107504a10 at Time(seconds: 6.0884439583460335):
(display-list
(item #:identity 1 #:version 900
(frame (101.0 370.3333333333333; 200.0 200.0))
(content-seed 1801)
(color #FF0000FF)))
// rotate screen
// SwiftUI new
(frame (337.0 112.66666666666666; 200.0 200.0))
// OpenSwiftUI new
(frame (101.0 370.33333333333326; 200.0 200.0))
...
(frame (337.0 112.66666666666666; 200.0 200.0))
DL 是通过 ViewGraph 的 rootDL 计算得到的,在这个例子下是由唯一的 Color 对应的 LeafDisplayList 计算得到。
LeafDisplayList
我们在 LeadDisplayList.updateValue
处断点 - 用 ColorView 泛型特化后的符号 $s7SwiftUI15LeafDisplayList33_65609C35608651F66D749EB1BD9D2226LLV11updateValueyyFAA9ColorViewV_Tg5
进行符号断点
我们即可来到此处堆栈:

SwiftUICore`generic specialization of SwiftUI.LeafDisplayList.updateValue() -> ():
-> 0x1d45e16b4 <+0>: sub sp, sp, #0x170
0x1d45e16b8 <+4>: stp d9, d8, [sp, #0x100]
0x1d45e16bc <+8>: stp x28, x27, [sp, #0x110]
0x1d45e16c0 <+12>: stp x26, x25, [sp, #0x120]
0x1d45e16c4 <+16>: stp x24, x23, [sp, #0x130]
0x1d45e16c8 <+20>: stp x22, x21, [sp, #0x140]
0x1d45e16cc <+24>: stp x20, x19, [sp, #0x150]
0x1d45e16d0 <+28>: stp x29, x30, [sp, #0x160]
0x1d45e16d4 <+32>: add x29, sp, #0x160
0x1d45e16d8 <+36>: add x23, sp, #0x60
通过 swift-section 工具,我们可以获取到相关定义
private struct LeafDisplayList<V>: StatefulRule, CustomStringConvertible where V: RendererLeafView {
let identity: DisplayList.Identity
@Attribute var view: V
@Attribute var position: ViewOrigin
@Attribute var size: CGSize
@Attribute var containerPosition: ViewOrigin
let options: DisplayList.Options
var contentSeed: DisplayList.Seed
}
使用 memory read -fx -s8 $x20
查看 x20 寄存器对应的地址,我们即可还原出当前的 LeafDisplayList 状态:
(lldb) mr $x20
0x10f431034: 0x000010c000000001 0x000011e9000011c9
0x10f431044: 0x06ff600000000358 0x00002a3d00000000
0x10f431054: 0x00001a4020000018 0x000019b800000082
0x10f431064: 0x000019b000000021 0x000010c000000000
// identity: 0x1
// _view: #0x10c0
// _position: #0x11c9
// _size: #0x11e9
// _containerPosition: #0x358
DisplayList.Item 的 frame.origin 是通过 position - containerPosition
计算得出
简单的 debug 可知 containerPosition 的 value 均为 .zero
,且 position 的求值结果不一致。
4553 % 1 = 1,因此这里的 _position
是一个 indirect 节点,通过 source 可以知道它的父节点,进而获取父节点的body类型:
(lldb) po AnyAttribute(rawValue: 0x11c9).source._bodyType
SwiftUI.AnimatableFrameAttribute
(lldb) po AnyAttribute(rawValue: 0x11c9).source.valueType
SwiftUI.ViewFrame
AnimatableFrameAttribute
重新准备环境,在动画静止后,添加 $s7SwiftUI24AnimatableFrameAttributeV11updateValueyyF
断点,并旋转设备屏幕,我们就可以断点到 AnimatableFrameAttribute 的求值函数中。
简单的进行分析后我们发现,在 sourceValue 处二者计算出来的结果是一致的,经过 helper.update
函数后,OpenSwiftUI 的 viewFrame 值发生了变化。
package struct AnimatableFrameAttribute: StatefulRule, AsyncAttribute, ObservedAttribute {
@Attribute
private var position: ViewOrigin
@Attribute
private var size: ViewSize
@Attribute
private var pixelLength: CGFloat
@Attribute
private var environment: EnvironmentValues
private var helper: AnimatableAttributeHelper<ViewFrame>
package mutating func updateValue() {
...
var sourceValue = (
value: viewFrame,
changed: anyChanged
)
if !animationsDisabled {
helper.update(
value: &sourceValue,
defaultAnimation: nil,
environment: $environment
)
}
guard sourceValue.changed || !hasValue else {
return
}
value = sourceValue.value
}
}
Transaction
继续进行下钻,二者的区分点在于 SwiftUI 中 transaction 中获取的 animation 为 nil
SwiftUICore`function signature specialization of closure #1 (A.AnimatableData, SwiftUI.Time) -> () in SwiftUI.AnimatableAttributeHelper.update(value: inout (value: A, changed: Swift.Bool), defaultAnimation: Swift.Optional, environment: AttributeGraph.Attribute) -> (), Argument Types : []> of generic specialization of SwiftUI.AnimatableAttributeHelper.update(value: inout (value: τ_0_0, changed: Swift.Bool), defaultAnimation: Swift.Optional, environment: AttributeGraph.Attribute, sampleCollector: (τ_0_0.AnimatableData, SwiftUI.Time) -> ()) -> ():
...
0x1d4afab24 <+384>: adrp x2, 108902
0x1d4afab28 <+388>: add x2, x2, #0x3b8 ; type metadata for SwiftUI.Transaction
0x1d4afab2c <+392>: mov x0, x21
-> 0x1d4afab30 <+396>: mov w1, #0x0 ; =0
0x1d4afab34 <+400>: bl 0x1d4d83d38 ; symbol stub for: AGGraphGetValue
0x1d4afab38 <+404>: ldr x26, [x0]
0x1d4afab3c <+408>: mov x0, x26
0x1d4afab40 <+412>: bl 0x1d4d85c4c ; symbol stub for: swift_retain
0x1d4afab44 <+416>: sub x0, x29, #0xf0
0x1d4afab48 <+420>: bl 0x1d4595bb0 ; outlined release of SwiftUI.AnimatableAttribute
0x1d4afab4c <+424>: mov x0, x20
0x1d4afab50 <+428>: bl 0x1d4d83e04 ; symbol stub for: AGGraphSetUpdate
0x1d4afab54 <+432>: mov x0, x26
0x1d4afab58 <+436>: bl 0x1d4d85c4c ; symbol stub for: swift_retain
0x1d4afab5c <+440>: bl 0x1d451feb0 ; generic specialization > of SwiftUI.find<τ_0_0 where τ_0_0: SwiftUI.PropertyKey>(_: Swift.Optional>, key: τ_0_0.Type) -> Swift.Optional>>
对应的函数实现大致如下:
let transaction: Transaction = Graph.withoutUpdate { self.transaction }
guard let animation = transaction.effectiveAnimation else {
return
}
对此处的 transaction 进行 debug 可知其 bodyType 是 AnimationModifier.swift 中的私有结构体 ChildTransaction
(lldb) reg read x21
x21 = 0x0000000000000e18
(lldb) po AnyAttribute(0xe18)._bodyType
SwiftUI.(unknown context at $1d4e8e3d4).ChildTransaction
有关 private discriminator 的更多信息可以参考 swift-pd-guess
ChildTransaction
同理我们可以直接对 ChildTransaction 进行简单的逆向,然后参考上面的步骤:
重新准备环境,在动画静止后,添加 $s7SwiftUI16ChildTransaction33_530459AF10BEFD7ED901D8CE93C1E289LLV5valueAA0D0Vvg
断点,并旋转设备屏幕,我们就可以断点到 ChildTransaction 的求值函数中。
SwiftUICore`SwiftUI.ChildTransaction.value.getter : SwiftUI.Transaction:
-> 0x1d4b40708 <+0>: sub sp, sp, #0x70
0x1d4b4070c <+4>: stp x26, x25, [sp, #0x20]
0x1d4b40710 <+8>: stp x24, x23, [sp, #0x30]
0x1d4b40714 <+12>: stp x22, x21, [sp, #0x40]
0x1d4b40718 <+16>: stp x20, x19, [sp, #0x50]
0x1d4b4071c <+20>: stp x29, x30, [sp, #0x60]
同理我们可以还原出相关实现和当前 ChildTransaction 的状态:
private struct ChildTransaction: Rule, AsyncAttribute {
@Attribute var valueTransactionSeed: UInt32
@Attribute var animation: Animation?
@Attribute var transaction: Transaction
@Attribute var transactionSeed: UInt32
var value: Transaction {
var transaction = transaction
guard !transaction.disablesAnimations else {
return transaction
}
let oldTransactionSeed = Graph.withoutUpdate { transactionSeed }
guard valueTransactionSeed == oldTransactionSeed else {
return transaction
}
transaction.animation = animation
Swift.assert(transactionSeed == oldTransactionSeed)
return transaction
}
}
(lldb) rr x0 x1
x0 = 0x00000df100000dc8
x1 = 0x00000300000002b8
; _valueTransactionSeed: 0xdc8
; _transactionSeed: 0x300
简单分析后我们可以知道逻辑不同点在于:
SwiftUI:
- oldTransactionSeed / w25 = 0x7c
- valueTransactionSeed / w8 = 0x6
OpenSwiftUI:
- oldTransactionSeed = 0x1
- valueTransactionSeed = 0x1
因此我们继续检查这两个 Attribute 的上游来源, 对 _AnimationModifier
进行简单分析后可知,ChildTransaction.transactionSeed 为 GraphHost.currentHost.data.$transactionSeed
public struct _AnimationModifier<Value>: ViewModifier, PrimitiveViewModifier where Value: Equatable {
public var animation: Animation?
public var value: Value
@inlinable
public init(animation: Animation?, value: Value) {
self.animation = animation
self.value = value
}
nonisolated static func _makeInputs(
modifier: _GraphValue<Self>,
inputs: inout _GraphInputs
) {
let transactionSeed = GraphHost.currentHost.data.$transactionSeed
let seed = Attribute(
ValueTransactionSeed(
value: modifier.value[offset: { .of(&$0.value) }],
transactionSeed: transactionSeed
)
)
seed.flags = .transactional
inputs.transaction = Attribute(
ChildTransaction(
valueTransactionSeed: seed,
animation: modifier.value[offset: { .of(&$0.animation) }],
transaction: inputs.transaction,
transactionSeed: transactionSeed
)
)
}
}
目前的几乎快看到问题的根因了,一个合理的猜测是 GraphHost data 上的 transactionSeed 没有正确触发更新导致的
GraphHost.data.transactionSeed
一个朴素的想法是给 transactionSeed 相关的 setter 添加断点,在转屏后查看哪些堆栈上有进行调用即可:
SwiftUI.GraphHost.Data.transactionSeed.setter
-$s7SwiftUI9GraphHostC4DataV15transactionSeeds6UInt32Vvs
SwiftUI.GraphHost.Data.$transactionSeed.setter
-$s7SwiftUI9GraphHostC4DataV16$transactionSeed09AttributeC00H0Vys6UInt32VGvs
但如果实际上这些函数都不会触发,因为 transactionSeed 的类型是 Attribute<UInt32>
,因此这里大概率是通过 Attribute.wrappedValue.setter
触发的(会被 inline 为 AGGraphSetValue 调用)
我们添加 AGGraphSetValue 符号断点,添加 action 为 reg read x0 x2
,并设置为自动继续模式:
View 0x000000011db04bc0 at Time(seconds: 27.712069541739766):
(display-list
(item #:identity 1 #:version 848
(frame (337.0 112.66666666666666; 200.0 200.0))
(content-seed 1679)
(color #FF0000FF)))
x0 = 0x0000000000000218
x2 = 0x00000001ef490c60 SwiftUICore`type metadata for SwiftUI.Time
x0 = 0x00000000000002e0
x2 = 0x00000001e83fce38 libswiftCore.dylib`type metadata for Swift.UInt32
x0 = 0x0000000000000300
x2 = 0x00000001e83fce38 libswiftCore.dylib`type metadata for Swift.UInt32
x0 = 0x0000000000000218
x2 = 0x00000001ef490c60 SwiftUICore`type metadata for SwiftUI.Time
x0 = 0x00000000000002e0
x2 = 0x00000001e83fce38 libswiftCore.dylib`type metadata for Swift.UInt32
x0 = 0x0000000000000300
x2 = 0x00000001e83fce38 libswiftCore.dylib`type metadata for Swift.UInt32
可以看到我们预期的 0x300 确实触发了 AGGraphSetValue 调用。
重新配置 AGGraphSetValue 符号断点,添加 condition为 $x0 == 0x300
,我们最终就得到了相关的更新上下文
查看当前 stack 的上一帧,最终定位到是 OpenSwiftUI 在 ViewGraph.updateOutputs(async: Swift.Bool)
遗漏了一行 data.transactionSeed &+= 1
调用导致
SwiftUICore`SwiftUI.ViewGraph.updateOutputs(async: Swift.Bool) -> ():
...
0x1d4d6f09c <+264>: mov x0, x20
0x1d4d6f0a0 <+268>: mov x2, x22
0x1d4d6f0a4 <+272>: bl 0x1d4d83e1c ; symbol stub for: AGGraphSetValue
-> 0x1d4d6f0a8 <+276>: sub x0, x29, #0x100
最终的修复 PR:OpenSwiftUI #464
Debug 技巧
在汇编模式下中使用 Graph/Attribute API
使用下面的调试代码进入 swift 语言模式并import OpenGraphShims (添加 OpenGraph package依赖)或者 AttributeGraph(添加 DarwinPrivateFrameworks package依赖),之后我们就可以通过相关 API 获取对应值了
- Eg.
AnyAttribute._valueType
/AnyAttribute.valuePointer
/AnyAttribute.bodyType
(lldb) settings set target.language swift
po import OpenGraphShims
(lldb) po AnyAttribute(rawValue: 0x11c9)
▿ #4553
- rawValue : 4553
AnyAttribute.debugDescription
OpenGraphShims 的最新版本为 AnyAttribute 提供了 debugDescription 实现,可以快速查看相关完整的 attribute 信息
相关 PR:OpenGraph #169
示例:
(lldb) po AnyAttribute(rawValue: 0x11c9).debugDescription
rawValue: 4553
graph:
(indirect attribute)
source attribute:
rawValue: 4432
graph:
(direct attribute)
valueType: ViewFrame
value: ViewFrame(origin: (1.0, 270.3333333333333), size: SwiftUI.ViewSize(value: (400.0, 400.0), _proposal: (400.0, 400.0)))
bodyType: AnimatableFrameAttribute
bodyValue:
AnimatableFrameAttribute(_position: Attribute {
rawValue: 4136
graph:
(direct attribute)
valueType: CGPoint
value: (1.0, 270.1666666666667)
bodyType: LayoutPositionQuery
bodyValue:
LayoutPositionQuery(_parentPosition: Attribute {
总结
通过这次深入的调试分析,我们成功定位并修复了 OpenSwiftUI 中的一个动画相关 bug。整个过程展示了如何:
- 使用 DisplayList 输出分析视图渲染问题
- 通过符号断点和汇编调试深入分析 SwiftUI 内部机制
- 使用 AttributeGraph 的调试工具进行问题定位
- 理解 SwiftUI 的 Transaction 和 Animation 系统
这种调试方法对于理解 SwiftUI 的内部工作原理和解决复杂的渲染问题非常有价值。