知阅百微 见微知著

从 DisplayList 到 Transaction: SwiftUI 调试实战

注意:阅读本文需要你对 SwiftUI 和 AttributeGraph 的原理有一定了解,如 DisplayList / Animation / Attribute 等。

如无特殊说明,本文的代码环境均为 iPhone 16 Pro + iOS 18.5 + Xcode 16.4

问题说明

最近实现了 combineAnimation 后,发现 OpenSwiftUI 在动画过程中或者动画结束后,进行设备屏幕的旋转后,对应 View 会停留在容器的原相对位置,然后动画到新的正确位置,SwiftUI 中没有这个表现。

相关问题:OpenSwiftUI Issue #461

预期行为 / SwiftUI 表现

Image

当前行为 / OpenSwiftUI 表现:

Image
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 进行符号断点

我们即可来到此处堆栈:

Xcode Screenshot

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。整个过程展示了如何:

  1. 使用 DisplayList 输出分析视图渲染问题
  2. 通过符号断点和汇编调试深入分析 SwiftUI 内部机制
  3. 使用 AttributeGraph 的调试工具进行问题定位
  4. 理解 SwiftUI 的 Transaction 和 Animation 系统

这种调试方法对于理解 SwiftUI 的内部工作原理和解决复杂的渲染问题非常有价值。